@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.
- package/README.md +80 -8
- package/dist/doors/doors.d.ts +139 -0
- package/dist/doors/doors.d.ts.map +1 -0
- package/dist/doors/doors.js +210 -0
- package/dist/doors/doors.js.map +1 -0
- package/dist/doors/index.d.ts +9 -0
- package/dist/doors/index.d.ts.map +1 -0
- package/dist/doors/index.js +7 -0
- package/dist/doors/index.js.map +1 -0
- package/dist/identity/contact-key.d.ts +26 -0
- package/dist/identity/contact-key.d.ts.map +1 -1
- package/dist/identity/contact-key.js +52 -0
- package/dist/identity/contact-key.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/network/index.d.ts +2 -0
- package/dist/network/index.d.ts.map +1 -1
- package/dist/network/index.js +1 -0
- package/dist/network/index.js.map +1 -1
- package/dist/network/mailbox.d.ts +49 -0
- package/dist/network/mailbox.d.ts.map +1 -0
- package/dist/network/mailbox.js +125 -0
- package/dist/network/mailbox.js.map +1 -0
- package/dist/node/node.d.ts.map +1 -1
- package/dist/node/node.js +320 -2
- package/dist/node/node.js.map +1 -1
- package/dist/node/types.d.ts +107 -1
- package/dist/node/types.d.ts.map +1 -1
- package/dist/schemas/contacts.d.ts +137 -0
- package/dist/schemas/contacts.d.ts.map +1 -1
- package/dist/schemas/contacts.js +63 -0
- package/dist/schemas/contacts.js.map +1 -1
- package/dist/schemas/index.d.ts +2 -2
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/session/auth.d.ts.map +1 -1
- package/dist/session/auth.js +2 -1
- package/dist/session/auth.js.map +1 -1
- package/dist/session/hosting.d.ts +1 -1
- package/dist/space/notify.d.ts +1 -1
- 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
|
|
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 (
|
|
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
|
|
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
|
|
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/`
|
|
1412
|
-
core, one bundled file for the CLI), and only then
|
|
1413
|
-
|
|
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":"AAkCA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,UAAU,EAAE,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AACxJ,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEhG,uFAAuF;AACvF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC;AACjC,iFAAiF;AACjF,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAChD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAC7C,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,QAAQ,GAAG,IAAI,CAAC;AACtB,MAAM,UAAU,GAAG,IAAI,CAAC;AACxB,MAAM,SAAS,GAAG,IAAI,CAAC;AA+CvB,iGAAiG;AACjG,MAAM,UAAU,IAAI,CAAC,IAAY,EAAE,GAAW;IAC5C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,OAAO,KAAK,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,cAAc,CAAC,IAAyB;IACtD,MAAM,OAAO,GAAG,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;IACjD,IAAI,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IACtC,OAAO,eAAe,CACpB,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CACxI,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,kCAAkC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/D,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,OAAO,CAAC;IACxC,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,OAAO,EAAE,CAAC,CAAC;IACxE,MAAM,IAAI,GAAG,MAAkB,CAAC;IAChC,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;AACrJ,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,MAAM,IAAI,GAAG,KAAiC,CAAC;IAC/C,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC;QAAE,OAAO,4BAA4B,CAAC;IAC3F,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,mCAAmC,CAAC;IAC9E,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,GAAG;QAAE,OAAO,sDAAsD,CAAC;IAC5H,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,eAAe,EAAE,CAAC;QACpG,OAAO,cAAc,eAAe,SAAS,CAAC;IAChD,CAAC;IACD,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACxC,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC;QAAE,OAAO,+BAA+B,QAAQ,aAAa,CAAC;IACvK,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,IAAY;IAC1C,OAAO,eAAe,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAClF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAa,EAAE,KAAa,EAAE,GAAiC,EAAE,EAAE,CAC9F,UAAU,CAAC,uBAAuB,KAAK,IAAI,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;AAE/F,kEAAkE;AAClE,MAAM,UAAU,SAAS,CAAC,OAAmB,EAAE,KAAa,EAAE,KAAa,EAAE,GAAiC;IAC5G,OAAO,cAAc,CAAC,OAAO,EAAE,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC;AAClE,CAAC;AAED;;;;;GAKG;AACH,MAAM,aAAa,GAAG,CAAC,SAAiB,EAAE,GAAW,EAAE,EAAE,CAAC,UAAU,CAAC,yBAAyB,SAAS,IAAI,GAAG,EAAE,CAAC,CAAC;AAElH,MAAM,UAAU,UAAU,CAAC,OAAmB,EAAE,SAAiB,EAAE,GAAW;IAC5E,OAAO,cAAc,CAAC,OAAO,EAAE,aAAa,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;AAChE,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,SAAiB,EAAE,GAAW,EAAE,SAAiB;IACzF,OAAO,eAAe,CAAC,IAAI,EAAE,aAAa,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,SAAS,CAAC,CAAC;AACzE,CAAC;AAED,mEAAmE;AACnE,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAY;IACxC,OAAO,eAAe,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED,MAAM,YAAY,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,kBAAkB,IAAI,EAAE,CAAC;AAEhE;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,IAAY,EACZ,KAAwG,EACxG,OAAkF,EAClF,QAAwB;IAExB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACxE,MAAM,IAAI,GAAc;QACtB,CAAC,EAAE,CAAC;QACJ,IAAI;QACJ,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,QAAQ,CAAC,IAAI,SAAS;QACpD,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzB,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QACjC,OAAO,EAAE,OAAO,CAAC,GAAG;QACpB,KAAK,EAAE,OAAO,CAAC,KAAK;KACrB,CAAC;IACF,MAAM,GAAG,GAAG,eAAe,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9F,OAAO,OAAO,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAmB,EAAE,IAAY,EAAE,UAAkB,EAAE,QAAwB;IAC7G,MAAM,IAAI,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,MAAM,GAAG,CAAC,MAAM,UAAU,CAAC,CAAC,MAAM,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,EAAE,IAAI,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC,CAG9F,CAAC;IACT,MAAM,IAAI,GAAG,MAAM,EAAE,IAAI,CAAC;IAC1B,IAAI,CAAC,IAAI,IAAI,OAAO,MAAM,CAAC,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzD,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACpD,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACpH,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,QAAQ;QAAE,OAAO,IAAI,CAAC;IACzD,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,GAAG,UAAU;QAAE,OAAO,IAAI,CAAC;IACpF,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS;QAAE,OAAO,IAAI,CAAC;IACjF,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IACvH,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC;IAChF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAC1C,IAAI,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,uBAAuB,IAAI,IAAI,CAAC,EAAE,GAAG,GAAG,GAAG,iBAAiB;QAAE,OAAO,IAAI,CAAC;IAC9F,4EAA4E;IAC5E,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAC9C,IAAI,IAAI,CAAC,EAAE,GAAG,OAAO,GAAG,uBAAuB,IAAI,IAAI,CAAC,EAAE,GAAG,OAAO,GAAG,yBAAyB;QAAE,OAAO,IAAI,CAAC;IAE9G,sCAAsC;IACtC,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,eAAe,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,cAAc,CAAC,CAAC;QACxF,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,oFAAoF;IACpF,IAAI,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,KAAK,GAAG,MAAM,qBAAqB,CAAC,IAAI,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAC/G,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACjG,6EAA6E;IAC7E,4EAA4E;IAC5E,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,KAAK,cAAc,IAAI,UAAU,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpJ,iGAAiG;IACjG,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,OAAO,CAAC,KAAK,CAAC,OAAO,KAAK,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,UAAU,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IAC/G,IAAI,CAAC,MAAM,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAE5D,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,SAAS;QAC5C,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,SAAS,EAAE,OAAO,CAAC,KAAK,CAAC,EAAE;QAC3B,EAAE,EAAE,IAAI,CAAC,EAAE,GAAG,IAAI;KACnB,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -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":"
|
|
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));
|