# Part 6 — sharing a graph

## With an existing member

Membership is your `Directory`'s answer. Once `isGroupMember('acme', 'alex')`
is true, Alex's devices can open `g:acme`, and the next sync from any key
holder grants them.

```ts
// on your server
await sql`INSERT INTO memberships (team_id, user_id) VALUES ('acme', 'alex')`;
// on Alex's device
await engine.openGraph('g:acme'); // 'waiting' → 'ok' after a holder syncs
```

## With somebody who is not a member yet — invites

The invite string is `<doorCode>.<keySecret>`. Only the door code reaches your
server; the keyring travels alongside it, sealed with a key derived via PBKDF2
from the half that never reaches the server.

```mermaid
sequenceDiagram
  participant S as Sam's device
  participant SRV as your server
  participant A as Alex's device
  S->>S: sealKeyringForInvite('g:acme') → {salt, sealedKeyring, keySecret}
  S->>SRV: create invite {doorCode, graphId, salt, sealedKeyring}
  SRV-->>S: doorCode
  S->>S: composeInvite(doorCode, keySecret)
  Note over S,A: the full string travels by whatever channel<br/>the two humans already trust
  A->>A: splitInvite(full) → {doorCode, keySecret}
  A->>SRV: redeem doorCode
  SRV->>SRV: add alex to the group
  SRV-->>A: {salt, sealedKeyring}
  A->>A: acceptInviteKeyring('g:acme', {salt, sealedKeyring, keySecret})
```

```ts
// Sam
const stash = await engine.sealKeyringForInvite('g:acme');
if (!stash) throw new Error('no key for that graph on this device');
const { salt, sealedKeyring, keySecret } = stash;
const doorCode = await myApi.createInvite({ graphId: 'g:acme', salt, sealedKeyring });
const inviteString = composeInvite(doorCode, keySecret); // from @argala/core

// Alex
const { doorCode, keySecret } = splitInvite(inviteString);
const { salt, sealedKeyring } = await myApi.redeemInvite(doorCode);
if (keySecret) {
  await engine.acceptInviteKeyring('g:acme', { salt, sealedKeyring, keySecret });
}
```

`acceptInviteKeyring` returns `false` on a wrong secret or a malformed keyring —
it never throws, so a mistyped invite is a message, not a crash.

`splitInvite` on a bare door code returns `keySecret: null`. That is the
"joined without the key" path: the member is in the group, so an existing
device will grant them normally on its next sync.

> Losing the whole invite string to an attacker is a real compromise. Losing
> your server's invite table is not: it holds the door code and a blob the
> server cannot open.

## Pairwise channels

```ts
await engine.openGraph('d:alex', { create: true });
await engine.put('d:alex', { kind: 'msg', data: { body: 'hey' } });
```

Alex opens `d:sam` and sees it. Both sides create with `{create: true}` — the
race is resolved and both end up on one key.

The relay learns that Sam and Alex have a channel; it never learns a word of
it. Hiding the _existence_ of a conversation needs sealed sender or a mixnet,
neither of which argala claims.

---
