@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.
- package/README.md +137 -44
- 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/elements/auth-styles.d.ts +1 -1
- package/dist/elements/auth-styles.d.ts.map +1 -1
- package/dist/elements/auth-styles.js +13 -0
- package/dist/elements/auth-styles.js.map +1 -1
- package/dist/elements/weave-auth.d.ts +2 -2
- package/dist/elements/weave-auth.d.ts.map +1 -1
- package/dist/elements/weave-auth.js +251 -119
- package/dist/elements/weave-auth.js.map +1 -1
- package/dist/identity/account-vault.d.ts +10 -3
- package/dist/identity/account-vault.d.ts.map +1 -1
- package/dist/identity/account-vault.js +10 -3
- package/dist/identity/account-vault.js.map +1 -1
- 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 +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- 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/host.d.ts +2 -2
- package/dist/node/host.d.ts.map +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.d.ts.map +1 -1
- 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 +39 -9
- package/dist/session/auth.d.ts.map +1 -1
- package/dist/session/auth.js +177 -70
- package/dist/session/auth.js.map +1 -1
- package/dist/session/connect.js +2 -2
- package/dist/session/credentials.d.ts +22 -6
- package/dist/session/credentials.d.ts.map +1 -1
- package/dist/session/credentials.js +35 -6
- package/dist/session/credentials.js.map +1 -1
- package/dist/session/hosting.d.ts +1 -1
- package/dist/session/index.d.ts +3 -3
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/index.js +2 -2
- package/dist/session/index.js.map +1 -1
- package/dist/space/notify.d.ts +1 -1
- package/dist/storage/blob/s3.js +1 -1
- 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 [
|
|
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
|
|
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
|
-
[](https://app.netlify.com/start/deploy?repository=https://github.com/leifriksheim/weave&base=home)
|
|
360
|
-
[](https://vercel.com/new/clone?repository-url=https://github.com/leifriksheim/weave&root-directory=home)
|
|
408
|
+
[](https://app.netlify.com/start/deploy?repository=https://github.com/leifriksheim/weave&base=apps/home)
|
|
409
|
+
[](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 —
|
|
402
|
-
|
|
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
|
-
`
|
|
869
|
-
dependencies of its own. It runs on its own as `
|
|
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 (`
|
|
904
|
-
`TURN_SECRET` is set, already capped that way: `
|
|
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
|
|
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
|
-
`
|
|
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 (
|
|
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
|
|
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`).
|
|
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
|
|
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` (
|
|
1403
|
-
|
|
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
|
|
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/`
|
|
1412
|
-
core, one bundled file for the CLI), and only then
|
|
1413
|
-
|
|
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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"}
|