> ## Documentation Index
> Fetch the complete documentation index at: https://docs.oximail.ch/llms.txt
> Use this file to discover all available pages before exploring further.

# Sharing

> The JMAP Sharing surface (RFC 9670): Principal (the directory), shareWith grants, ShareNotification, the shared: account indirection, and the cross-organization federation grants.

Sharing implements [RFC 9670](https://www.rfc-editor.org/rfc/rfc9670) (`urn:ietf:params:jmap:principals`): a **Principal** directory of the people, teams, resources, and locations of the organization, and a uniform grant model every collection type reuses.

## Objects and methods

| Object              | Methods                                          | Notes                                                                   |
| ------------------- | ------------------------------------------------ | ----------------------------------------------------------------------- |
| `Principal`         | `get`, `set`, `query`, `queryChanges`, `changes` | The directory: `individual`, `group`, `resource`, `location`, `other`.  |
| `ShareNotification` | `get`, `set`, `query`, `queryChanges`, `changes` | "X shared Y with you" events, newest first, deletable by the recipient. |

## The grant model

Every shareable collection (Mailbox, Calendar, TaskList, AddressBook, FileNode, Label) carries the same `shareWith` map: principal id → rights object (`mayRead`, `mayWrite`, `mayAdmin`, plus domain-specific rights like `mayReadFreeBusy`). Grants are:

* **Written by the owner** on the collection object; each grant change produces a `ShareNotification` for the grantee and a recorded change for resync.
* **Exercised through the `shared:` indirection**: a grantee addresses the owner's data with the granted account id in the standard JMAP `accountId` slot; the session's `accounts` object lists what is reachable. Capabilities resolve on the **authenticated caller**, data residency on the **owner** — usurpation is not representable in the request shape.
* **Fail-closed.** Shared-account dispatch denies by default; a rights lookup error refuses rather than granting.

## Directory semantics worth knowing

* **Principal types are honest.** A shared team account's principal is `type: "other"` (non-loginnable), never `individual` — people-pickers can rely on the type.
* **Grantees must be human-backed.** A grant to a principal backed by a non-loginnable account is rejected at write time on every domain.
* **Destroying a principal purges its grants.** A `Principal/set` destroy removes the grantee's keys from every `shareWith` map — no dangling ACL entries — and warns or aborts if it would leave a shared drive without an eligible custodian.
* **Erasure leaves tombstones.** An erased person's principal is PII-scrubbed but kept as a graph anchor, and a tombstoned principal is refused as a new grantee ([compliance](../operator/compliance)).
* **`Principal/getAvailability`** (free-busy) is documented on the [calendar page](./jmap-calendar).

`ShareNotification/query` returns a total order (newest first). Strictness rules are identical to the [mail surface](./jmap-mail).

## Across organizations: federation

Everything above shares data *inside* one organization. Sharing with a **different** organization is a separate surface, `urn:oximail:params:jmap:federation`, and it is deliberately not the same mechanism: a federated grant is neither a `shareWith` entry nor an account-access row. At v0.30.0 what it carries is **availability** (free-busy) — the grant widens *who* may ask, never *what* they may see.

The capability object advertises the vocabulary rather than making clients discover it from rejections:

```json theme={null}
"urn:oximail:params:jmap:federation": {
  "supportedGranteeKinds": ["realm"],
  "revocationIsForwardOnly": true
}
```

| Object                 | Methods               | Notes                                                                                |
| ---------------------- | --------------------- | ------------------------------------------------------------------------------------ |
| `FederationPeer`       | `get`, `query`        | Read-only. The paired organizations, in realm order — what fills a grantee selector. |
| `FederationGrant`      | `get`, `set`, `query` | The subject's own consent records.                                                   |
| `FederationDisclosure` | `get`, `query`        | Read-only. What each peer was actually told about this subject.                      |

### The grantee is an organization, not a person

`supportedGranteeKinds` is `["realm"]`, and the reason is not schedule. The identity a federated request proves today is the sending **node**, not which of its principals acted — so a grant naming one person at the far end would not actually be narrower than a grant naming their organization; it would only *look* narrower in an interface. The list exists so that the day a per-principal identity layer lands, `"principal"` is an addition a client can detect rather than a behaviour change it has to guess.

`granteeValue` must be a realm you have actually paired with. Pairing itself is a bilateral, out-of-band operator ceremony (it is not a JMAP method), which is exactly why `FederationPeer` exists: without a listing, a client had either a free-text field where almost every entry is rejected, or a selector with nothing to fill it.

`FederationPeer` is **tenant-scoped and readable by any authenticated account** of the organization, not administrators only — a peering is a relationship of the organization, with no per-subject view. It carries `realm` (the value to send as `granteeValue`), `jurisdiction`, `state`, and `pairedAt`. It deliberately does *not* expose the pinned key, the pairing anchor, the key epoch, or the suspension cause: those are operator bookkeeping, and a public key on every consent screen is security surface bought for no feature.

<Note>
  `FederationPeer` has no `changes` method and its `state` is **not** a change-log counter — it is a digest of the served list. Pairing and suspension happen in operator ceremonies outside any JMAP request, so nothing writes a change-log row for them; a change-log state here would be a string that never moves while the data under it does.
</Note>

### Consent is not delegable

* **Only the subject may create or modify a grant**, acting on their own account. An administrator may **revoke** — revocation removes access, so an operator who must cut a leak does not wait for the subject — but never grant on someone's behalf. A grant an administrator can create for you is not consent. The check is stricter than a role test: a request carrying shared access is somebody acting for someone else, whatever level it holds.
* **Each write is one transaction**: the grant row, the `ShareNotification` that tells the subject, and the audit row naming the *writer* commit together. The audit records who actually wrote it, not the account the request targeted, so an administrator's revocation is never attributed to the subject.
* **An unsupported `granteeKind` is refused loudly.** `"principal"` parses as a well-formed request and would look accepted; the row it created would be a realm grant wearing a person's name.

Most of a grant is server-decided and read-only on the wire — only `granteeKind`, `granteeValue` and `expiresAt` are settable. `rights` in particular is not: a client that could widen its own consent record past what the resolver honours would be a permission wearing a setting's clothes. `expiresAt: null` means the subject explicitly chose no expiry, and `jurisdiction` is **frozen at grant time** — consent was given to a counterparty in a place, and a mutable field would let the place change without new consent.

### Why a grant may stop working, and how the subject learns it

`FederationGrant.state` answers "why can that organization no longer see me?" directly: `active`, `unpaired`, `expired`, `dormancy_exhausted`, `dormant`, `lapsed` or `reanchored`. The store derives it from the very predicate the authorization read applies, so it cannot drift from the decision. `FederationPeer.state` (`active`, `dormant`, `lapsed`) uses the same words for the same failures, and only `active` accepts a grant — again derived from the predicate the write refuses on, so a realm the selector offers is a realm the write takes.

Two consequences worth designing around:

* **Revocation is unilateral and instant, and forward-only.** A peer never reads or writes your grants; it learns of a revocation by being refused. That is what keeps revocation out of distributed-consistency territory — and it is also why `revocationIsForwardOnly` is advertised as data: revoking removes the read path going forward and undoes nothing already disclosed.
* **The disclosure log is the other half of that sentence.** `FederationDisclosure` is what makes "revoking undoes nothing already disclosed" checkable instead of merely reassuring: it survives the grant, and there is **no** `FederationDisclosure/set` at all, not even destroy-only. Retention bounds the log; the subject cannot erase it. It records **disclosures, never attempts**, so a peer that probes five hundred addresses creates no rows about the four hundred and ninety-nine people who never consented. Each row names the peer realm, the identity layer that was verified, the jurisdiction the grant stood under at that moment, and when it happened.

<Note>
  Filters and sorts on the two `query` methods are **refused** (`unsupportedFilter` / `unsupportedSort`) rather than silently ignored. A client that filtered peers on `state: "active"` and quietly received the dormant ones too would offer its user a grantee the write then rejects.
</Note>
