@weaveprotocol/core 0.2.0 → 0.2.2

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 (71) hide show
  1. package/README.md +137 -44
  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/elements/auth-styles.d.ts +1 -1
  11. package/dist/elements/auth-styles.d.ts.map +1 -1
  12. package/dist/elements/auth-styles.js +13 -0
  13. package/dist/elements/auth-styles.js.map +1 -1
  14. package/dist/elements/weave-auth.d.ts +2 -2
  15. package/dist/elements/weave-auth.d.ts.map +1 -1
  16. package/dist/elements/weave-auth.js +251 -119
  17. package/dist/elements/weave-auth.js.map +1 -1
  18. package/dist/identity/account-vault.d.ts +10 -3
  19. package/dist/identity/account-vault.d.ts.map +1 -1
  20. package/dist/identity/account-vault.js +10 -3
  21. package/dist/identity/account-vault.js.map +1 -1
  22. package/dist/identity/contact-key.d.ts +26 -0
  23. package/dist/identity/contact-key.d.ts.map +1 -1
  24. package/dist/identity/contact-key.js +52 -0
  25. package/dist/identity/contact-key.js.map +1 -1
  26. package/dist/index.d.ts +3 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/network/index.d.ts +2 -0
  31. package/dist/network/index.d.ts.map +1 -1
  32. package/dist/network/index.js +1 -0
  33. package/dist/network/index.js.map +1 -1
  34. package/dist/network/mailbox.d.ts +49 -0
  35. package/dist/network/mailbox.d.ts.map +1 -0
  36. package/dist/network/mailbox.js +125 -0
  37. package/dist/network/mailbox.js.map +1 -0
  38. package/dist/node/host.d.ts +2 -2
  39. package/dist/node/host.d.ts.map +1 -1
  40. package/dist/node/index.d.ts +1 -1
  41. package/dist/node/index.d.ts.map +1 -1
  42. package/dist/node/node.d.ts.map +1 -1
  43. package/dist/node/node.js +320 -2
  44. package/dist/node/node.js.map +1 -1
  45. package/dist/node/types.d.ts +107 -1
  46. package/dist/node/types.d.ts.map +1 -1
  47. package/dist/schemas/contacts.d.ts +137 -0
  48. package/dist/schemas/contacts.d.ts.map +1 -1
  49. package/dist/schemas/contacts.js +63 -0
  50. package/dist/schemas/contacts.js.map +1 -1
  51. package/dist/schemas/index.d.ts +2 -2
  52. package/dist/schemas/index.d.ts.map +1 -1
  53. package/dist/schemas/index.js +1 -1
  54. package/dist/schemas/index.js.map +1 -1
  55. package/dist/session/auth.d.ts +39 -9
  56. package/dist/session/auth.d.ts.map +1 -1
  57. package/dist/session/auth.js +177 -70
  58. package/dist/session/auth.js.map +1 -1
  59. package/dist/session/connect.js +2 -2
  60. package/dist/session/credentials.d.ts +22 -6
  61. package/dist/session/credentials.d.ts.map +1 -1
  62. package/dist/session/credentials.js +35 -6
  63. package/dist/session/credentials.js.map +1 -1
  64. package/dist/session/hosting.d.ts +1 -1
  65. package/dist/session/index.d.ts +3 -3
  66. package/dist/session/index.d.ts.map +1 -1
  67. package/dist/session/index.js +2 -2
  68. package/dist/session/index.js.map +1 -1
  69. package/dist/space/notify.d.ts +1 -1
  70. package/dist/storage/blob/s3.js +1 -1
  71. package/package.json +54 -40
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 [spec](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
  ```
@@ -35,7 +40,7 @@ devices, and every app is a view onto that data rather than its owner.
35
40
  - **Local-first.** Works offline, syncs when peers are reachable.
36
41
  - **Few, boring dependencies.** Native browser APIs first. Where a problem is
37
42
  hard and already solved — elliptic-curve arithmetic, for one — a very stable,
38
- widely used library instead of our own. See [docs/DEPENDENCIES.md](docs/DEPENDENCIES.md).
43
+ widely used library instead of our own. See [CLAUDE.md](CLAUDE.md#dependencies).
39
44
  - **Isomorphic.** Runs in browsers, Node and Bun via `globalThis`.
40
45
  - **Functional.** Plain functions and frozen data, no class hierarchies.
41
46
  - **Standard Schema.** Bring your own validator (Zod, Valibot, ArkType, …).
@@ -220,7 +225,7 @@ await calls.answer(ringing.id); calls.setMuted(true); await calls.shareScreen(
220
225
  servers a relay hands out (see the relay, below), with passwords fresh for
221
226
  a while. A call asks for them when it starts.
222
227
 
223
- The messages, and the reasoning, are at the top of `src/calls/calls.ts`.
228
+ The messages, and the reasoning, are at the top of `packages/core/src/calls/calls.ts`.
224
229
 
225
230
  ### Contacts
226
231
 
@@ -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](spec/07-doors.md), Names). The full design is
327
+ [spec/07-doors.md](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
@@ -298,7 +347,7 @@ The protocol ships it, so an app does not write it:
298
347
  The element fits whatever it is put in — a page, a modal, a side panel — by
299
348
  sizing to its container, and draws nothing once someone is in. It renders into
300
349
  the page rather than a shadow root, because password managers fill forms there
301
- reliably and the account password living in one is the point. Colours, font and
350
+ reliably and the everyday password living in one is much of the point. Colours, font and
302
351
  radius are custom properties (`--weave-accent`, `--weave-font`, …).
303
352
 
304
353
  Underneath it is `createWeaveAuth` (`@weaveprotocol/core/session`): the same flow as
@@ -354,10 +403,10 @@ An app connected to an account home passes its node instead:
354
403
 
355
404
  An app does not have to sign anyone in at all. It can ask an **account home** —
356
405
  a page, at an address the person chose, that holds their account — for access,
357
- and never see the seed. `home/` is one, ready to deploy as your own:
406
+ and never see the seed. `apps/home/` is one, ready to deploy as your own:
358
407
 
359
- [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/leifriksheim/weave&base=home)
360
- [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/leifriksheim/weave&root-directory=home)
408
+ [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/leifriksheim/weave&base=apps/home)
409
+ [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/leifriksheim/weave&root-directory=apps/home)
361
410
 
362
411
  An app connects with `createWeaveConnection` — the twin of `createWeaveAuth`,
363
412
  for apps:
@@ -398,8 +447,8 @@ relays still meet. Underneath are
398
447
 
399
448
  1. The app makes its own key, kept in its own site's storage and never
400
449
  exportable (`appKey()`).
401
- 2. The home opens in a popup. The person unlocks there — the account password
402
- from their password manager, or a passkey — and picks which spaces the app
450
+ 2. The home opens in a popup. The person unlocks there — a passkey, or the
451
+ password their password manager keeps — and picks which spaces the app
403
452
  gets.
404
453
  3. The home signs a note from the account to the app's key: these spaces, read
405
454
  or change, for seven days. It hands the note back with invites for those
@@ -422,7 +471,7 @@ still never holds the seed: it cannot sign in anywhere as the account, change
422
471
  its password or passkeys, or keep access past the note's date.
423
472
 
424
473
  The home side is `receiveConnectRequest()` and `auth.grant(…)`; see
425
- [home/README.md](home/README.md).
474
+ [apps/home/README.md](apps/home/README.md).
426
475
 
427
476
  ### Apps hold what they use — keepers hold the rest
428
477
 
@@ -865,8 +914,8 @@ Browser-to-browser communication via WebRTC.
865
914
 
866
915
  #### Signaling relay
867
916
 
868
- `server/relay.mjs` is a dumb relay in a couple hundred lines of Node with no
869
- dependencies of its own. It runs on its own as `server/signaling-server.mjs`
917
+ `packages/relay/relay.mjs` is a dumb relay in a couple hundred lines of Node with no
918
+ dependencies of its own. It runs on its own as `packages/relay/signaling-server.mjs`
870
919
  (an HTTP server and the `ws` library around it), and inside every always-on
871
920
  node (`weave serve`), so the two are the same relay. A peer holds one socket and joins a room on it for
872
921
  each space, and the relay passes join notices and WebRTC offers, answers and
@@ -900,8 +949,8 @@ tell a Weave peer from anyone else, so set coturn's own quotas (`user-quota`,
900
949
  `total-quota`, `max-bps`). TURN only forwards encrypted packets; it can't
901
950
  see or hear a call.
902
951
 
903
- The relay's Docker image (`server/`, deployed to Fly) runs coturn beside it when
904
- `TURN_SECRET` is set, already capped that way: `server/fly.toml` says how.
952
+ The relay's Docker image (`packages/relay/`, deployed to Fly) runs coturn beside it when
953
+ `TURN_SECRET` is set, already capped that way: `packages/relay/fly.toml` says how.
905
954
 
906
955
  Only peers already in a room hear about a newcomer, so exactly one side creates
907
956
  the offer and the two never collide.
@@ -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](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
 
@@ -1076,7 +1125,7 @@ const wrap = await wrapSeedWithDeviceKey(seed, deviceKey, { rpId, credentialId }
1076
1125
  await accounts.write(account, withWrap(vault, wrap));
1077
1126
  ```
1078
1127
 
1079
- `deviceWrapsFor(vault, rpId)` says which wraps this origin can even attempt; the rest name keys it cannot reach. The gate is enforced in application code rather than by cryptography — see `src/identity/device-key.ts` for what that does and does not protect against. The recovery code needs no wrap, because it *is* the seed in printable form — it opens the folder anywhere, including on a phone or in a browser with no File System Access API, and it is shown once and stored nowhere.
1128
+ `deviceWrapsFor(vault, rpId)` says which wraps this origin can even attempt; the rest name keys it cannot reach. The gate is enforced in application code rather than by cryptography — see `packages/core/src/identity/device-key.ts` for what that does and does not protect against. The recovery code needs no wrap, because it *is* the seed in printable form — it opens the folder anywhere, including on a phone or in a browser with no File System Access API, and it is shown once and stored nowhere.
1080
1129
 
1081
1130
  `createEncryptedAdapter` seals `space:`, `spacekey:`, `spaceinvite:` and `spacerole:` values under the vault key, which is what makes a private space genuinely unreadable to someone holding the folder. It is scoped deliberately narrowly: expressions and index entries pass through, so what stays legible is each record's author, timestamp and collection, plus anything in a space its owner made public. Sealing those too would mean an opaque blob store, which would cost the property that makes a folder worth having.
1082
1131
 
@@ -1150,7 +1199,7 @@ Both sides of a new pair learn of each other at once, so the lower identifier
1150
1199
  offers and the other waits — otherwise every introduction would open two
1151
1200
  connections. After that the mesh introduces itself and the relay can go away.
1152
1201
 
1153
- `server/` has a Dockerfile and a `fly.toml` for running one.
1202
+ `packages/relay/` has a Dockerfile and a `fly.toml` for running one.
1154
1203
 
1155
1204
  ### Pairing a phone
1156
1205
 
@@ -1234,11 +1283,11 @@ schema.registerCollection({ name: 'app.example.post', schema: PostSchema });
1234
1283
 
1235
1284
  ## Command line, always-on node, and agents
1236
1285
 
1237
- `cli/` is `weave`: every node operation from a terminal, `weave run` to keep an
1286
+ `packages/cli/` is `weave`: every node operation from a terminal, `weave run` to keep an
1238
1287
  account's spaces syncing on a server (browsers connect to it over WebSocket, and
1239
1288
  it doubles as a relay), and `weave mcp` to hand the same operations to an agent.
1240
1289
  It reads and writes the same data folder layout a browser does. See
1241
- [cli/README.md](cli/README.md).
1290
+ [packages/cli/README.md](packages/cli/README.md).
1242
1291
 
1243
1292
  **Hosting.** `weave host` keeps many accounts' spaces online without being able
1244
1293
  to read them: it is the extension's carrier (`createCarrierNode`) with one carry
@@ -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](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)`
@@ -1263,13 +1312,13 @@ has **Keep my spaces online**.
1263
1312
 
1264
1313
  ## Example app
1265
1314
 
1266
- `example/` is the Weave website — a landing page for developers at `/`, and
1315
+ `apps/example/` is the Weave website — a landing page for developers at `/`, and
1267
1316
  why Weave, for people, at `/why` — and, at `/app`, a general-purpose app for your spaces — Vite + React, consuming
1268
- the protocol straight from `src/`. It knows no kinds of data in advance: every
1317
+ the protocol straight from `packages/core/src/`. It knows no kinds of data in advance: every
1269
1318
  screen is worked out from what a space says about itself (see *Derived UI* below):
1270
1319
 
1271
1320
  ```bash
1272
- npm install && (cd example && npm install) && (cd home && npm install) # the CLI is a workspace: the first one installs it
1321
+ npm install # one install for the whole workspace: core, CLI, relay and the apps
1273
1322
  npm run dev
1274
1323
  ```
1275
1324
 
@@ -1279,13 +1328,13 @@ That starts everything, with coloured output per part, and Ctrl-C stops it all:
1279
1328
  |---|---|---|
1280
1329
  | app | http://localhost:5173 | The example app |
1281
1330
  | home | http://localhost:5174 | The account home it connects to |
1282
- | node | port 8787 | An always-on node that is also the relay; a throwaway identity on first run (`cli/.env.dev`, data in `.weave-dev/`) |
1283
- | host | http://localhost:8788 | `weave host`, what "Keep my spaces online" uses, with its pay page at `/pay`; settings in `cli/.env.host.dev` |
1331
+ | node | port 8787 | An always-on node that is also the relay; a throwaway identity on first run (`packages/cli/.env.dev`, data in `.weave-dev/`) |
1332
+ | host | http://localhost:8788 | `weave host`, what "Keep my spaces online" uses, with its pay page at `/pay`; settings in `packages/cli/.env.host.dev` |
1284
1333
  | stripe | — | Only when you add a Stripe test key (below): forwards Stripe's webhooks to the host |
1285
1334
 
1286
- `example/.env.development` and `home/.env.development` point the app and home
1335
+ `apps/example/.env.development` and `apps/home/.env.development` point the app and home
1287
1336
  at the rest. Override any of them in a `.env.local`, and the host in
1288
- `cli/.env.host.local`.
1337
+ `packages/cli/.env.host.local`.
1289
1338
 
1290
1339
  **Trying hosting and payments.** In the home: Settings, **Keep my spaces
1291
1340
  online**, **Keep online** (the dev host is filled in), then **Payment**, which
@@ -1297,8 +1346,8 @@ opens the host's pay page in a new tab. What you can pay with there:
1297
1346
  USDC from faucet.circle.com (choose Base Sepolia). Pay, and the pay page
1298
1347
  says "Payment received"; back in the home's tab, the host shows "paid until"
1299
1348
  and takes your spaces. To see payments arrive, set your own address as
1300
- `WEAVE_WALLET_ADDRESS` in `cli/.env.host.local`.
1301
- - **A card, in Stripe's test mode.** In `cli/.env.host.local`, add
1349
+ `WEAVE_WALLET_ADDRESS` in `packages/cli/.env.host.local`.
1350
+ - **A card, in Stripe's test mode.** In `packages/cli/.env.host.local`, add
1302
1351
  `STRIPE_SECRET_KEY=sk_test_…` and the price ids of a monthly and a yearly
1303
1352
  recurring test price (`STRIPE_PRICE_MONTHLY`, `STRIPE_PRICE_YEARLY`, from the Stripe
1304
1353
  dashboard in test mode). With the Stripe CLI installed, `npm run dev`
@@ -1306,7 +1355,7 @@ opens the host's pay page in a new tab. What you can pay with there:
1306
1355
  4242 4242 4242 4242, any future date, any CVC.
1307
1356
  - **Phone wallets, by QR code.** Add `WEAVE_WALLETCONNECT_PROJECT_ID` (free at
1308
1357
  dashboard.reown.com, with localhost allowed in the project) to
1309
- `cli/.env.host.local`; `npm run dev` builds what the pay page needs. The
1358
+ `packages/cli/.env.host.local`; `npm run dev` builds what the pay page needs. The
1310
1359
  wallet has to be on Base Sepolia too.
1311
1360
 
1312
1361
  `npm test` covers the same paths without any of this: a fake network, fake
@@ -1325,7 +1374,7 @@ npm run weave -- records list --space <id>
1325
1374
 
1326
1375
  Close every browser holding the space, open the link somewhere else, and the
1327
1376
  records come from the node. Or make the node your own account's — see
1328
- [cli/README.md](cli/README.md) — and it serves every space you make, unasked.
1377
+ [packages/cli/README.md](packages/cli/README.md) — and it serves every space you make, unasked.
1329
1378
 
1330
1379
  Between them they exercise the stack end to end. At the home: choose where
1331
1380
  your data lives (a pod, or this browser), create an account (a password your
@@ -1353,7 +1402,7 @@ for fixed ones, and `x-choicesFrom: { rel: 'about', field: 'options' }` for a
1353
1402
  field that picks from a list in the linked record — so a vote stored as `1`
1354
1403
  shows as "Lisbon", its form offers the poll's options, and the poll shows a
1355
1404
  tally. The helpers that work this out are pure functions
1356
- (`example/src/derive/schema-ui.ts`), with nothing DOM-specific in them. An
1405
+ (`apps/example/src/derive/schema-ui.ts`), with nothing DOM-specific in them. An
1357
1406
  empty space offers a small "define a collection" form; an agent can do the
1358
1407
  same over WebMCP.
1359
1408
 
@@ -1370,7 +1419,7 @@ above everything that changes as you move around.
1370
1419
 
1371
1420
  **Agents in the browser (WebMCP).** When `/app` loads, it registers
1372
1421
  every node operation as a WebMCP tool on `document.modelContext`
1373
- (`example/src/webmcp.ts`, with `@mcp-b/webmcp-polyfill`: Chrome's own WebMCP
1422
+ (`apps/example/src/webmcp.ts`, with `@mcp-b/webmcp-polyfill`: Chrome's own WebMCP
1374
1423
  when present, a polyfill otherwise). A browser agent or extension sees the same
1375
1424
  tools as the CLI and `weave mcp` — `spaces_list`, `records_query`,
1376
1425
  `records_put`, `apps_propose`… — and works as the person, with nothing to
@@ -1381,8 +1430,7 @@ them. Anything that changes a space's people, or hands out its key, asks the
1381
1430
  person first. An app may bring its own screen: a collection definition's
1382
1431
  `screen`, one HTML document, which the example runs in a sandboxed frame with
1383
1432
  no network, talking to the space only through a message port
1384
- (`createScreenBridge`, `apps_screen_guide`). `docs/screens/chess.html` is one
1385
- an agent wrote.
1433
+ (`createScreenBridge`, `apps_screen_guide`).
1386
1434
 
1387
1435
  **Agents on your computer (Claude Code, Claude Desktop, Cursor).** "Connect an
1388
1436
  agent", in the account menu, shows one command:
@@ -1390,32 +1438,77 @@ agent", in the account menu, shows one command:
1390
1438
  the tab through the relay, and the person allows it at their account home,
1391
1439
  which signs an agent's note for the whole account, for as long as they chose.
1392
1440
  Everything said on the way is sealed with a key from the code, so the relay
1393
- learns nothing (`src/session/agent-link.ts`). The command then adds `weave` to
1441
+ learns nothing (`packages/core/src/session/agent-link.ts`). The command then adds `weave` to
1394
1442
  the agents it finds, and they start `weave mcp` themselves: a node of its own,
1395
1443
  over WebRTC (`node-datachannel`), that follows the account and keeps working
1396
1444
  with every tab closed. What it writes shows "via agent", and every peer
1397
1445
  ignores an agent changing collections, who may do what, or the account's own
1398
- list of spaces. See BLOCK-20.
1446
+ list of spaces. See [06 — Nodes, sessions and apps](spec/06-nodes-and-sessions.md), Agents.
1447
+
1448
+ ## Still to do in the library
1449
+
1450
+ What the protocol still has planned is in [the spec](spec/README.md), under
1451
+ **Planned** in each part. Library work that isn't protocol:
1452
+
1453
+ - **Typed queries, further.** Typed field paths and operator values in
1454
+ `where`, a misspelled collection name as a compile error, typed link roles,
1455
+ types generated from a space's stored definitions, and a dev-time warning
1456
+ when declared schemas differ from the space's catalogue.
1457
+ - **Typed collections.** One TypeScript builder that emits the schema, the
1458
+ rules and the types, and typed handles (`node.use(space, Poll)`).
1459
+ - **Definitions that update themselves.** `useSchemas` and `addApp` applying
1460
+ harmless changes, with `differences()` in `packages/core/src/schemas/apps.ts` replaced by
1461
+ the planned `compare` ([02](spec/02-records.md), compatible definitions).
1462
+ - **Web components** for the standard schemas.
1463
+
1464
+ ## Repository layout
1465
+
1466
+ One npm workspace, installed once at the root (`npm install`):
1467
+
1468
+ | Folder | Package | |
1469
+ |---|---|---|
1470
+ | `packages/core` | `@weaveprotocol/core` (published) | The protocol library, and its tests |
1471
+ | `packages/cli` | `@weaveprotocol/cli` (published) | `weave`: the always-on node, hosting, agents, MCP |
1472
+ | `packages/relay` | `@weaveprotocol/relay` (private) | The signaling relay and its mailbox, run alone on Fly and inside every node |
1473
+ | `apps/home` | — | The account home |
1474
+ | `apps/example` | — | The website and the example app |
1475
+ | `apps/extension` | — | The Chrome extension |
1476
+ | `spec` | — | The protocol specification |
1477
+
1478
+ Everything imports the protocol by name, `@weaveprotocol/core`, and only
1479
+ through what it exports. Inside the workspace, the `@weaveprotocol/source`
1480
+ export condition resolves those imports to `packages/core/src`, so the apps,
1481
+ the CLI and the tests run on the source with no build step; published copies
1482
+ use `dist`. The CLI bundles the protocol and the relay into one file, so
1483
+ `npx @weaveprotocol/cli` and the single-file binaries need nothing else.
1399
1484
 
1400
1485
  ## Releasing
1401
1486
 
1402
- `@weaveprotocol/core` (this folder) and `@weaveprotocol/cli` (`cli/`, an npm
1403
- workspace) are released together, always with the same version:
1487
+ `@weaveprotocol/core` (`packages/core/`) and `@weaveprotocol/cli`
1488
+ (`packages/cli/`) are released together, always with the same version:
1404
1489
 
1405
1490
  ```bash
1406
1491
  npm run release
1407
1492
  ```
1408
1493
 
1409
- It typechecks and runs the tests, then [bumpp](https://github.com/antfu-collective/bumpp)
1494
+ It checks you're logged in to npm first (and runs `npm login` if not),
1495
+ typechecks and runs the tests, then [bumpp](https://github.com/antfu-collective/bumpp)
1410
1496
  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`.
1497
+ it (`v0.1.2`), locally. Both are published (each builds itself first: `dist/`
1498
+ for the core, one bundled file for the CLI), and only then are the commit and
1499
+ tag pushed.
1500
+
1501
+ If a release stops partway — npm login, a one-time password, the network —
1502
+ fix it and run `npm run release` again. It sees the version isn't fully on npm
1503
+ yet and publishes what's missing, instead of bumping again. `npm run release
1504
+ -- --dry-run` does everything but publish and push. The script is
1505
+ `scripts/release.mjs`; bumpp's settings are in `bump.config.ts`.
1414
1506
 
1415
1507
  ## Tests
1416
1508
 
1417
1509
  ```bash
1418
- npm test
1510
+ npm test # every workspace: core's tests, then the CLI's
1511
+ npm run typecheck
1419
1512
  ```
1420
1513
 
1421
1514
  Covers key derivation — checked against the public keys Web Crypto generates
@@ -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 `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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"}