@weaveprotocol/core 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +80 -8
  2. package/dist/doors/doors.d.ts +139 -0
  3. package/dist/doors/doors.d.ts.map +1 -0
  4. package/dist/doors/doors.js +210 -0
  5. package/dist/doors/doors.js.map +1 -0
  6. package/dist/doors/index.d.ts +9 -0
  7. package/dist/doors/index.d.ts.map +1 -0
  8. package/dist/doors/index.js +7 -0
  9. package/dist/doors/index.js.map +1 -0
  10. package/dist/identity/contact-key.d.ts +26 -0
  11. package/dist/identity/contact-key.d.ts.map +1 -1
  12. package/dist/identity/contact-key.js +52 -0
  13. package/dist/identity/contact-key.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/network/index.d.ts +2 -0
  19. package/dist/network/index.d.ts.map +1 -1
  20. package/dist/network/index.js +1 -0
  21. package/dist/network/index.js.map +1 -1
  22. package/dist/network/mailbox.d.ts +49 -0
  23. package/dist/network/mailbox.d.ts.map +1 -0
  24. package/dist/network/mailbox.js +125 -0
  25. package/dist/network/mailbox.js.map +1 -0
  26. package/dist/node/node.d.ts.map +1 -1
  27. package/dist/node/node.js +320 -2
  28. package/dist/node/node.js.map +1 -1
  29. package/dist/node/types.d.ts +107 -1
  30. package/dist/node/types.d.ts.map +1 -1
  31. package/dist/schemas/contacts.d.ts +137 -0
  32. package/dist/schemas/contacts.d.ts.map +1 -1
  33. package/dist/schemas/contacts.js +63 -0
  34. package/dist/schemas/contacts.js.map +1 -1
  35. package/dist/schemas/index.d.ts +2 -2
  36. package/dist/schemas/index.d.ts.map +1 -1
  37. package/dist/schemas/index.js +1 -1
  38. package/dist/schemas/index.js.map +1 -1
  39. package/dist/session/auth.d.ts.map +1 -1
  40. package/dist/session/auth.js +2 -1
  41. package/dist/session/auth.js.map +1 -1
  42. package/dist/session/hosting.d.ts +1 -1
  43. package/dist/space/notify.d.ts +1 -1
  44. package/package.json +6 -2
package/README.md CHANGED
@@ -4,6 +4,11 @@ A peer-to-peer data protocol for the browser. You own your identity as a
4
4
  written-down code, keep your data in signed records that sync directly between
5
5
  devices, and every app is a view onto that data rather than its owner.
6
6
 
7
+ > **Building a client, or an agent that needs the architecture?** The
8
+ > protocol is specified in [docs/spec](docs/spec/README.md): wire formats,
9
+ > what is signed, and what every peer must check. The tests are its
10
+ > executable half.
11
+
7
12
  ## Architecture
8
13
 
9
14
  ```
@@ -229,7 +234,7 @@ it must not be enough to reach you. What lets two people reach each other is a
229
234
  space they share. So **a contact is a private space for two**: records you
230
235
  write there wait for the other person, and live messages reach them when
231
236
  they're online. There's no directory to look people up in, and no inbox
232
- strangers can knock on.
237
+ strangers can knock on — unless you open a door (below).
233
238
 
234
239
  ```typescript
235
240
  // In a space you share with Anna — the book club — ask her to add you.
@@ -277,6 +282,50 @@ contact key, which opens requests sent to you. Asking someone and accepting
277
282
  also make or join a space, so those need `scope: 'account'`, which includes
278
283
  the contacts. Agents never get them.
279
284
 
285
+ ### Doors
286
+
287
+ Someone you share no space with can still ask to become your contact — if you
288
+ give them a **door**. A door is a code you hand out on purpose (a link, a QR
289
+ code, a line in your bio) and can close. It names a key derived from your
290
+ contact key and the relays whose mailboxes hold knocks on it, and nothing
291
+ about who you are.
292
+
293
+ ```typescript
294
+ const door = await anna.doors.open(); // { id, code, relays, … }
295
+ share(`https://chat.example/#door=${door.code}`);
296
+
297
+ // Leif, who has never shared a space with Anna, pastes the link:
298
+ await leif.doors.knock(link, { note: 'We met at the gig' }); // a space for two, its invite sealed to the door
299
+
300
+ // Anna, whenever she's next online:
301
+ const [knock] = await anna.doors.knocks(); // { from, name, note, pairSpace, … } — `from` is proven
302
+ await anna.doors.accept(knock.id); // joins; Leif is a contact, and she is his once it syncs
303
+ await anna.doors.close(door.id); // the code leads nowhere now; contacts stay
304
+ ```
305
+
306
+ - **The relay keeps a mailbox**, the one thing it holds: a sealed blob under a
307
+ hash of the door's signing key, for up to 14 days. It sees addresses, as it
308
+ does for any socket, but not whose door it is, which account knocked, or what
309
+ they said. A door names up to three relays and a knock goes to all of them,
310
+ so no one relay can shut it.
311
+ - **A knock proves who knocked** before anyone joins anything: it's signed by
312
+ the knocker's session key under a note for their whole account, like a
313
+ record, bound to the door it was left at and to when the relay took it.
314
+ - **An answer proves who opened.** Accepting writes an answer in the space for
315
+ two, signed with the door's key: that, not joining, makes the owner the
316
+ knocker's contact, and the invite is closed behind them.
317
+ - **The door is not the account.** Its key is not your contact key, so nobody
318
+ can link a door to your profile in any space. Every device with the contact
319
+ key opens the same doors.
320
+ - **Spam** is capped at the mailbox (64 knocks a door; 4 a door and 30 in all
321
+ an hour from one address). `dismiss` lets one knock go without blocking,
322
+ blocking hides someone's knocks on every door, and a flooded door is cleared
323
+ by its owner (`clear`), so its code keeps working.
324
+
325
+ Next: handles that lead to a door, so `@anna.bsky.social` works where a code
326
+ does (planned in [07 — Doors](docs/spec/07-doors.md), Names). The full design is
327
+ [docs/spec/07-doors.md](docs/spec/07-doors.md).
328
+
280
329
  ## Signing in — the element, and React
281
330
 
282
331
  Getting to a node takes a sign-in flow: where the data lives (a pod or this
@@ -1001,7 +1050,7 @@ interface StorageAdapter {
1001
1050
  - `createIndexedDBAdapter(name)` — works in every browser. Origin-scoped.
1002
1051
  - `createFolderAdapter(directory, namespace)` — a directory the user picked, via the File System Access API. **Not** origin-scoped. Chrome, Edge and Opera on the desktop.
1003
1052
 
1004
- The always-on node (`weave run`) uses the folder adapter on disk, in the same layout. **Planned**: mirrors, which keep a space in storage the user already pays for (a Dropbox app folder, Drive, S3) and sync with it like a peer — see `docs/blocks/BLOCK-03-mirrors.md`. OPFS is not on the list: it is origin-private, so it would inherit exactly the limitation a data folder exists to avoid.
1053
+ The always-on node (`weave run`) uses the folder adapter on disk, in the same layout. **Planned**: mirrors, which keep a space in storage the user already pays for (a Dropbox app folder, Drive, S3) and sync with it like a peer — see [05 — Sync and storage](docs/spec/05-sync-and-storage.md). OPFS is not on the list: it is origin-private, so it would inherit exactly the limitation a data folder exists to avoid.
1005
1054
 
1006
1055
  ### Data folders — storage that outlives the origin
1007
1056
 
@@ -1247,7 +1296,7 @@ members pay for it. A subscription is a key the account makes and keeps in its
1247
1296
  registry (`sys.hosting`), so every device signs as it; `node.hosting.use(url)`
1248
1297
  starts one, and whichever device notices it is paid hands the host the carry
1249
1298
  space. Every call to the host is signed over method, path, time and body.
1250
- The home knows nothing about payment (BLOCK-23): a host describes itself at
1299
+ The home knows nothing about payment ([06 — Nodes, sessions and apps](docs/spec/06-nodes-and-sessions.md), Hosts): a host describes itself at
1251
1300
  `/.well-known/weave-host` (like a Nostr relay's NIP-11 document), signs every
1252
1301
  status it gives — the home keeps the latest in the registry as the person's
1253
1302
  proof — and takes payments on its own pay page, which `node.hosting.payPage(url)`
@@ -1395,7 +1444,23 @@ the agents it finds, and they start `weave mcp` themselves: a node of its own,
1395
1444
  over WebRTC (`node-datachannel`), that follows the account and keeps working
1396
1445
  with every tab closed. What it writes shows "via agent", and every peer
1397
1446
  ignores an agent changing collections, who may do what, or the account's own
1398
- list of spaces. See BLOCK-20.
1447
+ list of spaces. See [06 — Nodes, sessions and apps](docs/spec/06-nodes-and-sessions.md), Agents.
1448
+
1449
+ ## Still to do in the library
1450
+
1451
+ What the protocol still has planned is in [the spec](docs/spec/README.md), under
1452
+ **Planned** in each part. Library work that isn't protocol:
1453
+
1454
+ - **Typed queries, further.** Typed field paths and operator values in
1455
+ `where`, a misspelled collection name as a compile error, typed link roles,
1456
+ types generated from a space's stored definitions, and a dev-time warning
1457
+ when declared schemas differ from the space's catalogue.
1458
+ - **Typed collections.** One TypeScript builder that emits the schema, the
1459
+ rules and the types, and typed handles (`node.use(space, Poll)`).
1460
+ - **Definitions that update themselves.** `useSchemas` and `addApp` applying
1461
+ harmless changes, with `differences()` in `src/schemas/apps.ts` replaced by
1462
+ the planned `compare` ([02](docs/spec/02-records.md), compatible definitions).
1463
+ - **Web components** for the standard schemas.
1399
1464
 
1400
1465
  ## Releasing
1401
1466
 
@@ -1406,11 +1471,18 @@ workspace) are released together, always with the same version:
1406
1471
  npm run release
1407
1472
  ```
1408
1473
 
1409
- It typechecks and runs the tests, then [bumpp](https://github.com/antfu-collective/bumpp)
1474
+ It checks you're logged in to npm first (and runs `npm login` if not),
1475
+ typechecks and runs the tests, then [bumpp](https://github.com/antfu-collective/bumpp)
1410
1476
  asks for the next version, writes it to both packages, and commits and tags
1411
- it (`v0.1.2`). Both are published (each builds itself first: `dist/` for the
1412
- core, one bundled file for the CLI), and only then is the commit pushed. The
1413
- settings are in `bump.config.ts`.
1477
+ it (`v0.1.2`), locally. Both are published (each builds itself first: `dist/`
1478
+ for the core, one bundled file for the CLI), and only then are the commit and
1479
+ tag pushed.
1480
+
1481
+ If a release stops partway — npm login, a one-time password, the network —
1482
+ fix it and run `npm run release` again. It sees the version isn't fully on npm
1483
+ yet and publishes what's missing, instead of bumping again. `npm run release
1484
+ -- --dry-run` does everything but publish and push. The script is
1485
+ `scripts/release.mjs`; bumpp's settings are in `bump.config.ts`.
1414
1486
 
1415
1487
  ## Tests
1416
1488
 
@@ -0,0 +1,139 @@
1
+ /**
2
+ * @module doors
3
+ * Doors: how someone you share no space with can ask to become your contact.
4
+ *
5
+ * A DID is a name, not an address — knowing it must not be enough to reach
6
+ * you. A **door** is an address you hand out on purpose, and can close:
7
+ *
8
+ * - a **door key**, derived from your contact key and the door's id
9
+ * (`deriveDoorKeyBytes`), which says nothing about the account behind it,
10
+ * and a **signing key** beside it (`deriveDoorSignKeyBytes`) that proves
11
+ * ownership of the door — to a relay clearing its mailbox, and to a knocker
12
+ * when you answer;
13
+ * - the **relays** whose mailboxes hold knocks on it — two or three, chosen by
14
+ * you, so no one relay can shut it.
15
+ *
16
+ * Both travel in a **door code** (a link, a QR code, a line in a bio). Someone
17
+ * with it **knocks**: makes a private space for the two of you, and leaves its
18
+ * invite, sealed to the door key, in the door's mailboxes. The knock is signed
19
+ * by the knocker's session key under their account's note, like a record, so
20
+ * it proves who knocked before anyone joins anything. Opening the door is
21
+ * joining that space.
22
+ *
23
+ * ```
24
+ * code = base64url(JSON { v: 1, key, sign, relays, name? })
25
+ * topic = base64url(SHA-256("weave/door-topic/v1|" + sign))
26
+ * knock = sealFor(key, { body, sig }, "weave/knock/v1|" + key)
27
+ * body = { v: 1, door: key, from, name, invite, note?, at, session, proof }
28
+ * sig = session key signs canonical(body)
29
+ * ```
30
+ *
31
+ * The relay sees a topic and a sealed blob: not whose door it is, not who
32
+ * knocked, not what they said. See `docs/spec/07-doors.md`.
33
+ */
34
+ import type { CryptoProvider } from '../types.js';
35
+ /** A door names at most this many relays: enough that one going away doesn't matter */
36
+ export declare const MAX_DOOR_RELAYS = 3;
37
+ /** How long a knock waits in a mailbox, and so how old one may be when opened */
38
+ export declare const KNOCK_TTL_SECONDS: number;
39
+ /**
40
+ * How far a knock's own time may be from when the relay took it. A knock is
41
+ * dropped as soon as it is signed, so its time is checked against the relay's
42
+ * — which the knocker can't choose — and a note that ran out can't be used by
43
+ * dating a knock back to when it was good.
44
+ */
45
+ export declare const KNOCK_DROP_WINDOW_SECONDS = 600;
46
+ /** What a door code says: where to knock, and whose door the owner says it is */
47
+ export interface DoorCode {
48
+ readonly v: 1;
49
+ /** The door key's public half: a compressed P-256 point, base64url. Knocks are sealed to it. */
50
+ readonly key: string;
51
+ /** The door's signing key's public half, likewise. Its hash is the door's topic. */
52
+ readonly sign: string;
53
+ /** Relays whose mailboxes hold knocks on it, 1–3 */
54
+ readonly relays: ReadonlyArray<string>;
55
+ /** Who the owner says they are — shown to the knocker, and proves nothing */
56
+ readonly name?: string;
57
+ }
58
+ /** What a knock carries, signed by the knocker's session key */
59
+ export interface KnockBody {
60
+ readonly v: 1;
61
+ /** The door knocked on: its key. Binds the knock to this door. */
62
+ readonly door: string;
63
+ /** The knocker's account */
64
+ readonly from: string;
65
+ /** The name they give */
66
+ readonly name: string;
67
+ /** The invite to the space for two they made */
68
+ readonly invite: string;
69
+ readonly note?: string;
70
+ /** When it was signed, Unix seconds */
71
+ readonly at: number;
72
+ /** The session key that signed it */
73
+ readonly session: string;
74
+ /** The note from `from` to `session`: a UCAN whose chain ends at `from` */
75
+ readonly proof: string;
76
+ }
77
+ /** A knock that opened and checked out */
78
+ export interface OpenedKnock {
79
+ readonly from: string;
80
+ readonly name: string;
81
+ readonly note?: string;
82
+ readonly invite: string;
83
+ /** The id of the space for two */
84
+ readonly pairSpace: string;
85
+ /** When it was signed, ms */
86
+ readonly at: number;
87
+ }
88
+ /** Cuts text to at most `max` characters without splitting one (a surrogate pair stays whole) */
89
+ export declare function clip(text: string, max: number): string;
90
+ /** Encodes a door code: what goes in a link or a QR code */
91
+ export declare function encodeDoorCode(code: Omit<DoorCode, 'v'>): string;
92
+ /**
93
+ * Reads a door code, or a link carrying one after `#door=` or `door=`.
94
+ * @throws When it isn't one, saying why
95
+ */
96
+ export declare function parseDoorCode(text: string): DoorCode;
97
+ /** Why a value is not a door code, or null */
98
+ export declare function checkDoorCode(value: unknown): string | null;
99
+ /**
100
+ * The mailbox topic of a door: a hash of its signing key. The relay can't tell
101
+ * whose door it is, and can check that whoever clears it holds that key.
102
+ */
103
+ export declare function doorTopic(sign: string): Promise<string>;
104
+ /**
105
+ * What a door's owner signs to clear knocks from a relay's mailbox: the topic,
106
+ * the relay's one-time challenge, and which knocks (`*` for all of them).
107
+ */
108
+ export declare const purgeMessage: (topic: string, nonce: string, ids: ReadonlyArray<string> | null) => Uint8Array<ArrayBufferLike>;
109
+ /** Signs a relay's purge challenge with the door's signing key */
110
+ export declare function signPurge(signKey: Uint8Array, topic: string, nonce: string, ids: ReadonlyArray<string> | null): Promise<string>;
111
+ export declare function signAnswer(signKey: Uint8Array, pairSpace: string, did: string): Promise<string>;
112
+ export declare function checkAnswer(sign: string, pairSpace: string, did: string, signature: string): Promise<boolean>;
113
+ /** A knock's id: the hash of its sealed blob, as relays file it */
114
+ export declare function knockId(blob: string): Promise<string>;
115
+ /**
116
+ * Makes a knock: the body, signed by the session key, sealed to the door key.
117
+ * @returns The sealed blob, for relays' mailboxes
118
+ */
119
+ export declare function sealKnock(door: string, knock: {
120
+ readonly from: string;
121
+ readonly name: string;
122
+ readonly invite: string;
123
+ readonly note?: string;
124
+ }, session: {
125
+ readonly did: string;
126
+ readonly key: CryptoKey;
127
+ readonly proof: string;
128
+ }, provider: CryptoProvider): Promise<string>;
129
+ /**
130
+ * Opens a knock left on a door, and checks it through: sealed to this door;
131
+ * signed by a session key that its account's note vouches for, for the whole
132
+ * account and not by an agent; signed when the relay took it; recent; and
133
+ * carrying an invite to a private space that account made.
134
+ * @param doorKey The door key's private scalar (`deriveDoorKeyBytes`)
135
+ * @param receivedAt When the relay took it, ms, as `fetch` says
136
+ * @returns The knock, or null when it is anything less
137
+ */
138
+ export declare function openKnock(doorKey: Uint8Array, blob: string, receivedAt: number, provider: CryptoProvider): Promise<OpenedKnock | null>;
139
+ //# sourceMappingURL=doors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doors.d.ts","sourceRoot":"","sources":["../../src/doors/doors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,210 @@
1
+ import { contactKeyPair, contactPublicKey, isContactPublicKey, openSealed, sealFor, signWithScalar, verifyWithPoint } from '../identity/contact-key.js';
2
+ import { didToPublicKey } from '../identity/did.js';
3
+ import { resolveDelegationRoot, UCAN_CLOCK_SKEW_SECONDS } from '../identity/ucan.js';
4
+ import { isAgentNote } from '../identity/agent-note.js';
5
+ import { canonicalize } from '../schema/expression.js';
6
+ import { parseSpaceInvite } from '../space/space-manager.js';
7
+ import { checkSpace } from '../space/space-access.js';
8
+ import { checkRelays } from '../space/roles.js';
9
+ import { sha256 } from '../utils/hash.js';
10
+ import { base64UrlDecode, base64UrlEncode, utf8Decode, utf8Encode } from '../utils/encoding.js';
11
+ /** A door names at most this many relays: enough that one going away doesn't matter */
12
+ export const MAX_DOOR_RELAYS = 3;
13
+ /** How long a knock waits in a mailbox, and so how old one may be when opened */
14
+ export const KNOCK_TTL_SECONDS = 14 * 24 * 3600;
15
+ /**
16
+ * How far a knock's own time may be from when the relay took it. A knock is
17
+ * dropped as soon as it is signed, so its time is checked against the relay's
18
+ * — which the knocker can't choose — and a note that ran out can't be used by
19
+ * dating a knock back to when it was good.
20
+ */
21
+ export const KNOCK_DROP_WINDOW_SECONDS = 600;
22
+ const MAX_NAME = 64;
23
+ const MAX_NOTE = 2000;
24
+ const MAX_INVITE = 6000;
25
+ const MAX_PROOF = 4096;
26
+ /** Cuts text to at most `max` characters without splitting one (a surrogate pair stays whole) */
27
+ export function clip(text, max) {
28
+ const chars = Array.from(text);
29
+ return chars.length <= max ? text : chars.slice(0, max).join('');
30
+ }
31
+ /** Encodes a door code: what goes in a link or a QR code */
32
+ export function encodeDoorCode(code) {
33
+ const problem = checkDoorCode({ v: 1, ...code });
34
+ if (problem)
35
+ throw new Error(problem);
36
+ return base64UrlEncode(utf8Encode(canonicalize({ v: 1, key: code.key, sign: code.sign, relays: [...code.relays], ...(code.name ? { name: code.name } : {}) })));
37
+ }
38
+ /**
39
+ * Reads a door code, or a link carrying one after `#door=` or `door=`.
40
+ * @throws When it isn't one, saying why
41
+ */
42
+ export function parseDoorCode(text) {
43
+ const trimmed = text.trim();
44
+ const found = /(?:^|[#?&])door=([A-Za-z0-9_-]+)/.exec(trimmed);
45
+ const raw = found ? found[1] : trimmed;
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(utf8Decode(base64UrlDecode(raw)));
49
+ }
50
+ catch {
51
+ throw new Error('That is not a door code — it may be cut short.');
52
+ }
53
+ const problem = checkDoorCode(parsed);
54
+ if (problem)
55
+ throw new Error(`That door code doesn't work: ${problem}`);
56
+ const code = parsed;
57
+ return Object.freeze({ v: 1, key: code.key, sign: code.sign, relays: Object.freeze([...code.relays]), ...(code.name ? { name: code.name } : {}) });
58
+ }
59
+ /** Why a value is not a door code, or null */
60
+ export function checkDoorCode(value) {
61
+ const code = value;
62
+ if (!code || typeof code !== 'object' || code.v !== 1)
63
+ return 'it is not a version 1 door';
64
+ if (!isContactPublicKey(code.key))
65
+ return 'its key is not a P-256 public key';
66
+ if (!isContactPublicKey(code.sign) || code.sign === code.key)
67
+ return 'its signing key is not a P-256 public key of its own';
68
+ if (!Array.isArray(code.relays) || code.relays.length === 0 || code.relays.length > MAX_DOOR_RELAYS) {
69
+ return `it names 1–${MAX_DOOR_RELAYS} relays`;
70
+ }
71
+ const relays = checkRelays(code.relays);
72
+ if (relays)
73
+ return relays;
74
+ if (code.name !== undefined && (typeof code.name !== 'string' || Array.from(code.name).length > MAX_NAME))
75
+ return `its name is text of at most ${MAX_NAME} characters`;
76
+ return null;
77
+ }
78
+ /**
79
+ * The mailbox topic of a door: a hash of its signing key. The relay can't tell
80
+ * whose door it is, and can check that whoever clears it holds that key.
81
+ */
82
+ export async function doorTopic(sign) {
83
+ return base64UrlEncode(await sha256(utf8Encode(`weave/door-topic/v1|${sign}`)));
84
+ }
85
+ /**
86
+ * What a door's owner signs to clear knocks from a relay's mailbox: the topic,
87
+ * the relay's one-time challenge, and which knocks (`*` for all of them).
88
+ */
89
+ export const purgeMessage = (topic, nonce, ids) => utf8Encode(`weave/door-purge/v1|${topic}|${nonce}|${ids ? [...ids].sort().join(',') : '*'}`);
90
+ /** Signs a relay's purge challenge with the door's signing key */
91
+ export function signPurge(signKey, topic, nonce, ids) {
92
+ return signWithScalar(signKey, purgeMessage(topic, nonce, ids));
93
+ }
94
+ /**
95
+ * The answer to a knock: the door's owner, signing with the door's signing key
96
+ * that the account which joined the space for two is theirs. Without it,
97
+ * whoever joined first — someone the invite was passed on to — would be taken
98
+ * for the person behind the door.
99
+ */
100
+ const answerMessage = (pairSpace, did) => utf8Encode(`weave/knock-answer/v1|${pairSpace}|${did}`);
101
+ export function signAnswer(signKey, pairSpace, did) {
102
+ return signWithScalar(signKey, answerMessage(pairSpace, did));
103
+ }
104
+ export function checkAnswer(sign, pairSpace, did, signature) {
105
+ return verifyWithPoint(sign, answerMessage(pairSpace, did), signature);
106
+ }
107
+ /** A knock's id: the hash of its sealed blob, as relays file it */
108
+ export async function knockId(blob) {
109
+ return base64UrlEncode(await sha256(utf8Encode(blob)));
110
+ }
111
+ const knockContext = (door) => `weave/knock/v1|${door}`;
112
+ /**
113
+ * Makes a knock: the body, signed by the session key, sealed to the door key.
114
+ * @returns The sealed blob, for relays' mailboxes
115
+ */
116
+ export async function sealKnock(door, knock, session, provider) {
117
+ const note = knock.note ? clip(knock.note.trim(), MAX_NOTE) : undefined;
118
+ const body = {
119
+ v: 1,
120
+ door,
121
+ from: knock.from,
122
+ name: clip(knock.name.trim(), MAX_NAME) || 'Someone',
123
+ invite: knock.invite,
124
+ ...(note ? { note } : {}),
125
+ at: Math.floor(Date.now() / 1000),
126
+ session: session.did,
127
+ proof: session.proof,
128
+ };
129
+ const sig = base64UrlEncode(await provider.sign(session.key, utf8Encode(canonicalize(body))));
130
+ return sealFor(door, { body, sig }, knockContext(door));
131
+ }
132
+ /**
133
+ * Opens a knock left on a door, and checks it through: sealed to this door;
134
+ * signed by a session key that its account's note vouches for, for the whole
135
+ * account and not by an agent; signed when the relay took it; recent; and
136
+ * carrying an invite to a private space that account made.
137
+ * @param doorKey The door key's private scalar (`deriveDoorKeyBytes`)
138
+ * @param receivedAt When the relay took it, ms, as `fetch` says
139
+ * @returns The knock, or null when it is anything less
140
+ */
141
+ export async function openKnock(doorKey, blob, receivedAt, provider) {
142
+ const door = contactPublicKey(doorKey);
143
+ const opened = (await openSealed((await contactKeyPair(doorKey)).privateKey, blob, knockContext(door)));
144
+ const body = opened?.body;
145
+ if (!body || typeof opened.sig !== 'string')
146
+ return null;
147
+ if (body.v !== 1 || body.door !== door)
148
+ return null;
149
+ if (typeof body.from !== 'string' || typeof body.session !== 'string' || typeof body.name !== 'string')
150
+ return null;
151
+ if (Array.from(body.name).length > MAX_NAME)
152
+ return null;
153
+ if (typeof body.invite !== 'string' || body.invite.length > MAX_INVITE)
154
+ return null;
155
+ if (typeof body.proof !== 'string' || body.proof.length > MAX_PROOF)
156
+ return null;
157
+ if (body.note !== undefined && (typeof body.note !== 'string' || Array.from(body.note).length > MAX_NOTE))
158
+ return null;
159
+ if (!Number.isSafeInteger(body.at) || !Number.isFinite(receivedAt))
160
+ return null;
161
+ const now = Math.floor(Date.now() / 1000);
162
+ if (body.at > now + UCAN_CLOCK_SKEW_SECONDS || body.at < now - KNOCK_TTL_SECONDS)
163
+ return null;
164
+ // Signed when it was dropped, not dated back to when a note was still good.
165
+ const dropped = Math.floor(receivedAt / 1000);
166
+ if (body.at > dropped + UCAN_CLOCK_SKEW_SECONDS || body.at < dropped - KNOCK_DROP_WINDOW_SECONDS)
167
+ return null;
168
+ // Signed by the session key it names…
169
+ let signed = false;
170
+ try {
171
+ const key = await provider.importPublicKey(didToPublicKey(body.session).publicKeyBytes);
172
+ signed = await provider.verify(key, base64UrlDecode(opened.sig), utf8Encode(canonicalize(body)));
173
+ }
174
+ catch {
175
+ return null;
176
+ }
177
+ if (!signed)
178
+ return null;
179
+ // …which the account it claims had delegated to when it signed. Agents never knock.
180
+ if (isAgentNote(body.proof))
181
+ return null;
182
+ const chain = await resolveDelegationRoot(body.proof, () => null, provider, { at: body.at }).catch(() => null);
183
+ if (!chain?.valid || chain.audience !== body.session || chain.rootDid !== body.from)
184
+ return null;
185
+ // Knocking makes a space and hands out its invite: only a note for the whole
186
+ // account, to write, may. An app given one space, or only to read, may not.
187
+ if (!chain.capabilities.some((capability) => capability.with === '*' && (capability.can === 'expression/*' || capability.can === '*')))
188
+ return null;
189
+ // A private space the knocker made, with its key: anything else isn't a space for two from them.
190
+ let invited;
191
+ try {
192
+ invited = parseSpaceInvite(body.invite);
193
+ }
194
+ catch {
195
+ return null;
196
+ }
197
+ if (invited.space.creator !== body.from || invited.space.visibility !== 'private' || !invited.key)
198
+ return null;
199
+ if ((await checkSpace(invited.space)) !== null)
200
+ return null;
201
+ return Object.freeze({
202
+ from: body.from,
203
+ name: clip(body.name, MAX_NAME) || 'Someone',
204
+ ...(body.note ? { note: body.note } : {}),
205
+ invite: body.invite,
206
+ pairSpace: invited.space.id,
207
+ at: body.at * 1000,
208
+ });
209
+ }
210
+ //# sourceMappingURL=doors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doors.js","sourceRoot":"","sources":["../../src/doors/doors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @module doors
3
+ * Doors and knocks — see `doors.ts`, and `node.doors` for the API an app uses.
4
+ */
5
+ export { encodeDoorCode, parseDoorCode, checkDoorCode, doorTopic, knockId, sealKnock, openKnock, signPurge, purgeMessage, signAnswer, checkAnswer, clip, KNOCK_DROP_WINDOW_SECONDS, MAX_DOOR_RELAYS, KNOCK_TTL_SECONDS, } from './doors.js';
6
+ export type { DoorCode, KnockBody, OpenedKnock } from './doors.js';
7
+ export { createMailboxClient } from '../network/mailbox.js';
8
+ export type { MailboxClient, MailboxOptions, MailItem } from '../network/mailbox.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/doors/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,cAAc,EACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,OAAO,EACP,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,UAAU,EACV,WAAW,EACX,IAAI,EACJ,yBAAyB,EACzB,eAAe,EACf,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @module doors
3
+ * Doors and knocks — see `doors.ts`, and `node.doors` for the API an app uses.
4
+ */
5
+ export { encodeDoorCode, parseDoorCode, checkDoorCode, doorTopic, knockId, sealKnock, openKnock, signPurge, purgeMessage, signAnswer, checkAnswer, clip, KNOCK_DROP_WINDOW_SECONDS, MAX_DOOR_RELAYS, KNOCK_TTL_SECONDS, } from './doors.js';
6
+ export { createMailboxClient } from '../network/mailbox.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/doors/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EACL,cAAc,EACd,aAAa,EACb,aAAa,EACb,SAAS,EACT,OAAO,EACP,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAY,EACZ,UAAU,EACV,WAAW,EACX,IAAI,EACJ,yBAAyB,EACzB,eAAe,EACf,iBAAiB,GAClB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -20,6 +20,32 @@ export declare function deriveContactKeyBytes(seed: Uint8Array): Promise<Uint8Ar
20
20
  * @param accountKey The vault key bytes (`deriveVaultKeyBytes(seed)`)
21
21
  */
22
22
  export declare function deriveMemberKeyBytes(accountKey: Uint8Array, spaceId: string): Promise<Uint8Array>;
23
+ /**
24
+ * A **door key**: the key a door's knocks are sealed to (`doors/doors.ts`).
25
+ * Derived from the contact key and the door's id, so every device and app
26
+ * holding the contact key opens the same doors, and a new door is a new id.
27
+ *
28
+ * Not the contact key itself: that one's public half is on your profile in
29
+ * every space you write in, and a door is handed to people who may not know
30
+ * who you are yet. A door key says nothing about the account behind it.
31
+ * @param contactKey The contact key's private scalar (`deriveContactKeyBytes`)
32
+ * @param doorId The door's id, as its `std.door` record names it
33
+ */
34
+ export declare function deriveDoorKeyBytes(contactKey: Uint8Array, doorId: string): Promise<Uint8Array>;
35
+ /**
36
+ * A door's **signing key**: what proves you own a door, without saying whose
37
+ * it is — to a relay, when clearing the door's mailbox, and to someone who
38
+ * knocked, when you answer. Separate from the door key, which only opens
39
+ * knocks: one key should not both sign and decrypt.
40
+ */
41
+ export declare function deriveDoorSignKeyBytes(contactKey: Uint8Array, doorId: string): Promise<Uint8Array>;
42
+ /**
43
+ * Signs with a P-256 private scalar: ECDSA over SHA-256, 64 bytes r ‖ s,
44
+ * base64url — for door signing keys.
45
+ */
46
+ export declare function signWithScalar(secret: Uint8Array, data: Uint8Array): Promise<string>;
47
+ /** Checks what `signWithScalar` signed, against a compressed public key (base64url) */
48
+ export declare function verifyWithPoint(publicKey: string, data: Uint8Array, signature: string): Promise<boolean>;
23
49
  /** The public half, from the private scalar */
24
50
  export declare function contactPublicKey(secret: Uint8Array): string;
25
51
  /** Whether a string is a contact key's public half: a point on P-256 */
@@ -1 +1 @@
1
- {"version":3,"file":"contact-key.d.ts","sourceRoot":"","sources":["../../src/identity/contact-key.ts"],"names":[],"mappings":"AAkCA,MAAM,WAAW,cAAc;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;CAChC;AAYD;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAEjF;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAEvG;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAE3D;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAQlE;AAED,sEAAsE;AACtE,wBAAsB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAiBhF;AASD;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBjG;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA2BzG"}
1
+ {"version":3,"file":"contact-key.d.ts","sourceRoot":"","sources":["../../src/identity/contact-key.ts"],"names":[],"mappings":"AAoCA,MAAM,WAAW,cAAc;IAC7B,oFAAoF;IACpF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;CAChC;AAYD;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAEjF;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAEvG;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAEpG;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAExG;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAiB1F;AAED,uFAAuF;AACvF,wBAAsB,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ9G;AAED,+CAA+C;AAC/C,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAE3D;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAQlE;AAED,sEAAsE;AACtE,wBAAsB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,cAAc,CAAC,CAiBhF;AASD;;;;;;GAMG;AACH,wBAAsB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBjG;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAAC,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA2BzG"}
@@ -25,6 +25,8 @@ import { base64UrlDecode, base64UrlEncode, utf8Decode, utf8Encode } from '../uti
25
25
  const CONTACT_KEY_INFO = 'weave/p256-contact-key/v1';
26
26
  const SEAL_INFO = 'weave/contact-seal/v1';
27
27
  const MEMBER_KEY_INFO = 'weave/p256-member-key/v1';
28
+ const DOOR_KEY_INFO = 'weave/p256-door-key/v1';
29
+ const DOOR_SIGN_KEY_INFO = 'weave/p256-door-sign-key/v1';
28
30
  /** 48 bytes reduce to a P-256 scalar without bias, as for the root key (`crypto-p256.ts`) */
29
31
  const P256_SEED_BYTES = 48;
30
32
  const POINT_BYTES = 65;
@@ -54,6 +56,56 @@ export async function deriveContactKeyBytes(seed) {
54
56
  export async function deriveMemberKeyBytes(accountKey, spaceId) {
55
57
  return p256.utils.randomSecretKey(await hkdf(accountKey, `${MEMBER_KEY_INFO}|${spaceId}`, P256_SEED_BYTES));
56
58
  }
59
+ /**
60
+ * A **door key**: the key a door's knocks are sealed to (`doors/doors.ts`).
61
+ * Derived from the contact key and the door's id, so every device and app
62
+ * holding the contact key opens the same doors, and a new door is a new id.
63
+ *
64
+ * Not the contact key itself: that one's public half is on your profile in
65
+ * every space you write in, and a door is handed to people who may not know
66
+ * who you are yet. A door key says nothing about the account behind it.
67
+ * @param contactKey The contact key's private scalar (`deriveContactKeyBytes`)
68
+ * @param doorId The door's id, as its `std.door` record names it
69
+ */
70
+ export async function deriveDoorKeyBytes(contactKey, doorId) {
71
+ return p256.utils.randomSecretKey(await hkdf(contactKey, `${DOOR_KEY_INFO}|${doorId}`, P256_SEED_BYTES));
72
+ }
73
+ /**
74
+ * A door's **signing key**: what proves you own a door, without saying whose
75
+ * it is — to a relay, when clearing the door's mailbox, and to someone who
76
+ * knocked, when you answer. Separate from the door key, which only opens
77
+ * knocks: one key should not both sign and decrypt.
78
+ */
79
+ export async function deriveDoorSignKeyBytes(contactKey, doorId) {
80
+ return p256.utils.randomSecretKey(await hkdf(contactKey, `${DOOR_SIGN_KEY_INFO}|${doorId}`, P256_SEED_BYTES));
81
+ }
82
+ /**
83
+ * Signs with a P-256 private scalar: ECDSA over SHA-256, 64 bytes r ‖ s,
84
+ * base64url — for door signing keys.
85
+ */
86
+ export async function signWithScalar(secret, data) {
87
+ const point = p256.getPublicKey(secret, false);
88
+ const key = await globalThis.crypto.subtle.importKey('jwk', {
89
+ kty: 'EC',
90
+ crv: 'P-256',
91
+ x: base64UrlEncode(point.subarray(1, 33)),
92
+ y: base64UrlEncode(point.subarray(33, 65)),
93
+ d: base64UrlEncode(secret),
94
+ ext: false,
95
+ }, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']);
96
+ return base64UrlEncode(new Uint8Array(await globalThis.crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, key, data)));
97
+ }
98
+ /** Checks what `signWithScalar` signed, against a compressed public key (base64url) */
99
+ export async function verifyWithPoint(publicKey, data, signature) {
100
+ try {
101
+ const point = p256.Point.fromBytes(base64UrlDecode(publicKey)).toBytes(false);
102
+ const key = await globalThis.crypto.subtle.importKey('raw', point, { name: 'ECDSA', namedCurve: 'P-256' }, false, ['verify']);
103
+ return await globalThis.crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, base64UrlDecode(signature), data);
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
57
109
  /** The public half, from the private scalar */
58
110
  export function contactPublicKey(secret) {
59
111
  return base64UrlEncode(p256.getPublicKey(secret, true));