@weaveprotocol/core 0.1.2 → 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 +322 -45
- 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 +68 -0
- package/dist/identity/contact-key.d.ts.map +1 -0
- package/dist/identity/contact-key.js +185 -0
- package/dist/identity/contact-key.js.map +1 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- 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/network/mesh.d.ts +6 -0
- package/dist/network/mesh.d.ts.map +1 -1
- package/dist/network/mesh.js +11 -1
- package/dist/network/mesh.js.map +1 -1
- package/dist/network/multi-signaling.d.ts +12 -2
- package/dist/network/multi-signaling.d.ts.map +1 -1
- package/dist/network/multi-signaling.js +86 -14
- package/dist/network/multi-signaling.js.map +1 -1
- package/dist/network/peer-auth.d.ts +50 -16
- package/dist/network/peer-auth.d.ts.map +1 -1
- package/dist/network/peer-auth.js +52 -15
- package/dist/network/peer-auth.js.map +1 -1
- package/dist/node/carrier.d.ts +87 -0
- package/dist/node/carrier.d.ts.map +1 -1
- package/dist/node/carrier.js +180 -59
- package/dist/node/carrier.js.map +1 -1
- package/dist/node/copy.d.ts.map +1 -1
- package/dist/node/copy.js +6 -3
- package/dist/node/copy.js.map +1 -1
- package/dist/node/host.d.ts +117 -0
- package/dist/node/host.d.ts.map +1 -0
- package/dist/node/host.js +192 -0
- package/dist/node/host.js.map +1 -0
- package/dist/node/index.d.ts +6 -2
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -0
- package/dist/node/index.js.map +1 -1
- package/dist/node/node.d.ts.map +1 -1
- package/dist/node/node.js +958 -25
- package/dist/node/node.js.map +1 -1
- package/dist/node/space-runtime.d.ts +42 -25
- package/dist/node/space-runtime.d.ts.map +1 -1
- package/dist/node/space-runtime.js +734 -35
- package/dist/node/space-runtime.js.map +1 -1
- package/dist/node/types.d.ts +375 -3
- package/dist/node/types.d.ts.map +1 -1
- package/dist/privacy/space-encryption.d.ts +16 -0
- package/dist/privacy/space-encryption.d.ts.map +1 -1
- package/dist/privacy/space-encryption.js +40 -0
- package/dist/privacy/space-encryption.js.map +1 -1
- package/dist/query/engine.js +1 -1
- package/dist/query/engine.js.map +1 -1
- package/dist/query/types.d.ts +7 -0
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query/types.js.map +1 -1
- package/dist/react/use-query.d.ts +1 -1
- package/dist/react/use-query.d.ts.map +1 -1
- package/dist/records/topics.d.ts +21 -0
- package/dist/records/topics.d.ts.map +1 -0
- package/dist/records/topics.js +86 -0
- package/dist/records/topics.js.map +1 -0
- package/dist/schema/collection-def.d.ts +7 -0
- package/dist/schema/collection-def.d.ts.map +1 -1
- package/dist/schema/collection-def.js +4 -0
- package/dist/schema/collection-def.js.map +1 -1
- package/dist/schema/expression.d.ts +2 -0
- package/dist/schema/expression.d.ts.map +1 -1
- package/dist/schema/expression.js +1 -0
- package/dist/schema/expression.js.map +1 -1
- package/dist/schemas/contacts.d.ts +214 -0
- package/dist/schemas/contacts.d.ts.map +1 -0
- package/dist/schemas/contacts.js +98 -0
- package/dist/schemas/contacts.js.map +1 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +1 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/session/auth.d.ts.map +1 -1
- package/dist/session/auth.js +15 -3
- package/dist/session/auth.js.map +1 -1
- package/dist/session/connect.d.ts +25 -1
- package/dist/session/connect.d.ts.map +1 -1
- package/dist/session/connect.js +9 -2
- package/dist/session/connect.js.map +1 -1
- package/dist/session/hosting.d.ts +155 -0
- package/dist/session/hosting.d.ts.map +1 -0
- package/dist/session/hosting.js +160 -0
- package/dist/session/hosting.js.map +1 -0
- package/dist/space/account-registry.d.ts +6 -0
- package/dist/space/account-registry.d.ts.map +1 -1
- package/dist/space/account-registry.js +16 -4
- package/dist/space/account-registry.js.map +1 -1
- package/dist/space/notify.d.ts +88 -0
- package/dist/space/notify.d.ts.map +1 -0
- package/dist/space/notify.js +127 -0
- package/dist/space/notify.js.map +1 -0
- package/dist/space/pass.d.ts +6 -0
- package/dist/space/pass.d.ts.map +1 -1
- package/dist/space/pass.js +5 -2
- package/dist/space/pass.js.map +1 -1
- package/dist/space/roles.d.ts +71 -0
- package/dist/space/roles.d.ts.map +1 -1
- package/dist/space/roles.js +114 -0
- package/dist/space/roles.js.map +1 -1
- package/dist/space/space-access.d.ts +24 -0
- package/dist/space/space-access.d.ts.map +1 -1
- package/dist/space/space-access.js +24 -0
- package/dist/space/space-access.js.map +1 -1
- package/dist/space/space-manager.d.ts +35 -1
- package/dist/space/space-manager.d.ts.map +1 -1
- package/dist/space/space-manager.js +72 -25
- package/dist/space/space-manager.js.map +1 -1
- package/dist/storage/blob/memory.d.ts +9 -0
- package/dist/storage/blob/memory.d.ts.map +1 -0
- package/dist/storage/blob/memory.js +20 -0
- package/dist/storage/blob/memory.js.map +1 -0
- package/dist/storage/blob/s3.d.ts +16 -0
- package/dist/storage/blob/s3.d.ts.map +1 -0
- package/dist/storage/blob/s3.js +86 -0
- package/dist/storage/blob/s3.js.map +1 -0
- package/dist/storage/blob-store.d.ts +26 -0
- package/dist/storage/blob-store.d.ts.map +1 -0
- package/dist/storage/blob-store.js +2 -0
- package/dist/storage/blob-store.js.map +1 -0
- package/dist/storage/encrypted-adapter.d.ts +3 -3
- package/dist/storage/encrypted-adapter.js +3 -3
- package/dist/storage/folder-adapter.d.ts +8 -8
- package/dist/storage/folder-adapter.d.ts.map +1 -1
- package/dist/storage/folder-adapter.js +7 -7
- package/dist/storage/folder-reconcile.d.ts +12 -14
- package/dist/storage/folder-reconcile.d.ts.map +1 -1
- package/dist/storage/folder-reconcile.js +20 -14
- package/dist/storage/folder-reconcile.js.map +1 -1
- package/dist/storage/index.d.ts +0 -1
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +0 -1
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/indexeddb-adapter.d.ts.map +1 -1
- package/dist/storage/indexeddb-adapter.js +8 -9
- package/dist/storage/indexeddb-adapter.js.map +1 -1
- package/dist/storage/mirror.d.ts +68 -0
- package/dist/storage/mirror.d.ts.map +1 -0
- package/dist/storage/mirror.js +174 -0
- package/dist/storage/mirror.js.map +1 -0
- package/dist/storage/segment.d.ts +20 -0
- package/dist/storage/segment.d.ts.map +1 -0
- package/dist/storage/segment.js +27 -0
- package/dist/storage/segment.js.map +1 -0
- package/dist/storage/storage-provider.d.ts +36 -40
- package/dist/storage/storage-provider.d.ts.map +1 -1
- package/dist/storage/storage-provider.js +208 -117
- package/dist/storage/storage-provider.js.map +1 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/negentropy.d.ts +68 -0
- package/dist/sync/negentropy.d.ts.map +1 -0
- package/dist/sync/negentropy.js +379 -0
- package/dist/sync/negentropy.js.map +1 -0
- package/dist/sync/sync-engine.d.ts +45 -13
- package/dist/sync/sync-engine.d.ts.map +1 -1
- package/dist/sync/sync-engine.js +290 -207
- package/dist/sync/sync-engine.js.map +1 -1
- package/dist/sync/sync-messages.d.ts +42 -32
- package/dist/sync/sync-messages.d.ts.map +1 -1
- package/dist/sync/sync-messages.js +1 -1
- package/dist/sync/sync-messages.js.map +1 -1
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/hash.d.ts +9 -0
- package/dist/utils/hash.d.ts.map +1 -1
- package/dist/utils/hash.js +33 -0
- package/dist/utils/hash.js.map +1 -1
- package/package.json +8 -3
- package/dist/storage/mst.d.ts +0 -121
- package/dist/storage/mst.d.ts.map +0 -1
- package/dist/storage/mst.js +0 -402
- package/dist/storage/mst.js.map +0 -1
- package/dist/sync/anti-entropy.d.ts +0 -49
- package/dist/sync/anti-entropy.d.ts.map +0 -1
- package/dist/sync/anti-entropy.js +0 -62
- package/dist/sync/anti-entropy.js.map +0 -1
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
|
```
|
|
@@ -11,11 +16,11 @@ devices, and every app is a view onto that data rather than its owner.
|
|
|
11
16
|
│ Applications │
|
|
12
17
|
├──────────────┬───────────┬────────────┬──────────┬───────────┤
|
|
13
18
|
│ Accounts │ Spaces │ Validation │ Privacy │ Sync │
|
|
14
|
-
│ seed, vault, │ roles, │ crypto → │ AES-GCM │
|
|
15
|
-
│ root signer, │ members × │ structural │ per │
|
|
16
|
-
│ UCAN, pairing│ pub/priv │ → UCAN │ space │
|
|
19
|
+
│ seed, vault, │ roles, │ crypto → │ AES-GCM │ Negentropy│
|
|
20
|
+
│ root signer, │ members × │ structural │ per │ per │
|
|
21
|
+
│ UCAN, pairing│ pub/priv │ → UCAN │ space │ collection│
|
|
17
22
|
├──────────────┴───────────┴────────────┴──────────┴───────────┤
|
|
18
|
-
│ Storage:
|
|
23
|
+
│ Storage: signed versions + plain index over an adapter │
|
|
19
24
|
│ IndexedDB (per origin) · data folder (shared by origins) │
|
|
20
25
|
├──────────────────────────────────────────────────────────────┤
|
|
21
26
|
│ Network: WebRTC data channels │
|
|
@@ -71,7 +76,7 @@ const ucan = await root.delegate({
|
|
|
71
76
|
const spaces = createSpaceManager(await createIndexedDBAdapter('my-app/registry'));
|
|
72
77
|
const { space } = await spaces.create({ name: 'Notes', visibility: 'public', creator: me.did });
|
|
73
78
|
|
|
74
|
-
// 4. A signed record, stored in that space's own
|
|
79
|
+
// 4. A signed record, stored in that space's own store
|
|
75
80
|
const storage = createStorageProvider(await createIndexedDBAdapter(`my-app/space/${space.id}`));
|
|
76
81
|
const signed = await createSigner(provider).sign(
|
|
77
82
|
createExpression({
|
|
@@ -222,6 +227,105 @@ await calls.answer(ringing.id); calls.setMuted(true); await calls.shareScreen(
|
|
|
222
227
|
|
|
223
228
|
The messages, and the reasoning, are at the top of `src/calls/calls.ts`.
|
|
224
229
|
|
|
230
|
+
### Contacts
|
|
231
|
+
|
|
232
|
+
A DID is a **name, not an address**: it's on everything you sign, so knowing
|
|
233
|
+
it must not be enough to reach you. What lets two people reach each other is a
|
|
234
|
+
space they share. So **a contact is a private space for two**: records you
|
|
235
|
+
write there wait for the other person, and live messages reach them when
|
|
236
|
+
they're online. There's no directory to look people up in, and no inbox
|
|
237
|
+
strangers can knock on — unless you open a door (below).
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// In a space you share with Anna — the book club — ask her to add you.
|
|
241
|
+
const { space } = await node.contacts.ask(club.id, anna, { note: "it's Leif from book club" });
|
|
242
|
+
|
|
243
|
+
// On Anna's side:
|
|
244
|
+
const [request] = await node.contacts.requests(club.id); // { from, name, note, pairSpace, … }
|
|
245
|
+
await node.contacts.accept(club.id, request.key); // joins the space for two, adds Leif
|
|
246
|
+
|
|
247
|
+
await node.contacts.list(); // [{ did, name, space, note, blocked }]
|
|
248
|
+
await node.contacts.put({ did, name: 'Anna K' }); // what you call them — only you see it
|
|
249
|
+
await node.contacts.remove(anna); // off the list, and out of your space with her
|
|
250
|
+
await node.contacts.block(anna); // and her requests are hidden in every space
|
|
251
|
+
await node.contacts.others(anna); // anyone else in your space with her: [] unless someone was let in
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
- **The list** is one `std.contact` per person, in a **contacts space**
|
|
255
|
+
derived from the account key the way the account registry is: every device
|
|
256
|
+
of the account has it, nobody else can find it, and it's hidden from
|
|
257
|
+
`spaces.list`. `contacts.space()` gives its id. A carrier carries it, so a
|
|
258
|
+
restore brings the list back.
|
|
259
|
+
- **Asking** (`ask`) makes a private space for the two of you, puts them on
|
|
260
|
+
your list with it, and posts a `std.contact-request` in the shared space:
|
|
261
|
+
the new space's invite, **sealed with their contact key**. The other members
|
|
262
|
+
can see that you asked, not what. The seal is bound to the space it was
|
|
263
|
+
posted in and to who asked whom, so a request copied into another space, or
|
|
264
|
+
re-posted by someone else, doesn't open. Joining is the answer: there's no
|
|
265
|
+
reply record.
|
|
266
|
+
- **The contact key** is a P-256 key for encryption, derived from the seed
|
|
267
|
+
under its own label (`deriveContactKeyBytes`), so it's the same on every
|
|
268
|
+
device and comes back with the recovery code. Its public half goes on your
|
|
269
|
+
profile in every space you write in (`spaces.profiles(id)[n].contactKey`),
|
|
270
|
+
and only counts on a profile signed by your own account. An app without the
|
|
271
|
+
key never takes it off: a profile keeps the newest key it was given.
|
|
272
|
+
- **Conversation or group** is decided by how the space was made, not by how
|
|
273
|
+
many people are in it: a space is your conversation with Anna because your
|
|
274
|
+
list says so. A space made with "New space" and shared with one person is
|
|
275
|
+
never one. If a third account turns up in a space for two, `others` says so,
|
|
276
|
+
and an app can offer to start a new space with just the two of you, or a new
|
|
277
|
+
group with everyone.
|
|
278
|
+
|
|
279
|
+
**What an app gets.** A home gives the contacts to an app that asks with
|
|
280
|
+
`contacts: true`: the contacts space as one more space in its grant, and the
|
|
281
|
+
contact key, which opens requests sent to you. Asking someone and accepting
|
|
282
|
+
also make or join a space, so those need `scope: 'account'`, which includes
|
|
283
|
+
the contacts. Agents never get them.
|
|
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
|
+
|
|
225
329
|
## Signing in — the element, and React
|
|
226
330
|
|
|
227
331
|
Getting to a node takes a sign-in flow: where the data lives (a pod or this
|
|
@@ -362,12 +466,51 @@ the account's list on every device.
|
|
|
362
466
|
An app that is a view onto *everything* — like the example — asks for
|
|
363
467
|
`scope: 'account'`: a note for every space, plus the key the account's space
|
|
364
468
|
list is derived from, so it sees every space and can make and join them. It
|
|
469
|
+
gets the contacts too (see Contacts, above). It
|
|
365
470
|
still never holds the seed: it cannot sign in anywhere as the account, change
|
|
366
471
|
its password or passkeys, or keep access past the note's date.
|
|
367
472
|
|
|
368
473
|
The home side is `receiveConnectRequest()` and `auth.grant(…)`; see
|
|
369
474
|
[home/README.md](home/README.md).
|
|
370
475
|
|
|
476
|
+
### Apps hold what they use — keepers hold the rest
|
|
477
|
+
|
|
478
|
+
A space can name its **keepers**: nodes that hold every record of it, like a
|
|
479
|
+
host or the extension (`node.spaces.setKeepers`, for someone who manages the
|
|
480
|
+
space; the account's carriers are named by themselves in every space it
|
|
481
|
+
manages). Once it does, an app connected to the account home holds only the
|
|
482
|
+
collections it uses, besides the space's own (`sys.*`):
|
|
483
|
+
|
|
484
|
+
- a query says what it needs — its collection and every `include … from` —
|
|
485
|
+
and those start syncing from any peer that has them;
|
|
486
|
+
- `result.complete` is false until they have caught up with a node holding
|
|
487
|
+
the whole space: show "Loading…", not "Nothing here";
|
|
488
|
+
- the app's own writes stay **pending** until enough keepers say they have
|
|
489
|
+
them (`stored`): the space's `copies`, or 2, never more than it names.
|
|
490
|
+
Nothing pending is ever dropped;
|
|
491
|
+
- a collection no query has touched for `unusedAfterDays` (30) is dropped,
|
|
492
|
+
unless the app declared it (`cache.collections`). It syncs back when needed.
|
|
493
|
+
|
|
494
|
+
A collection can also name **topic fields** (`topics: ['channel', 'mentions']`).
|
|
495
|
+
Each record then carries, on its outside, a keyed hash of each of those values
|
|
496
|
+
(`tags`), so a keeper that can't read the record can still match "in
|
|
497
|
+
#design" or "mentions me" — learning which records share a topic, never which
|
|
498
|
+
topic. `node.collections.tag(space, collection, field, value)` gives the tag to
|
|
499
|
+
match; anyone who can read a record checks its tags and refuses a mismatch.
|
|
500
|
+
|
|
501
|
+
**"Notify me when…"** (`node.notifications`, and the home's section of that
|
|
502
|
+
name): new records in a collection, in every space or some, perhaps only with
|
|
503
|
+
a topic value ("mentions me"), perhaps only other people's. The account
|
|
504
|
+
registry keeps each one sealed; every carrier gets it with the value replaced
|
|
505
|
+
by each space's tag, so the Weave extension notices matching records as they
|
|
506
|
+
arrive and shows a notification — the space, your label, the time — without
|
|
507
|
+
being able to read them, or what you asked for.
|
|
508
|
+
|
|
509
|
+
A space that names no keeper is held whole, as before: then the app may be one
|
|
510
|
+
of its copies. How much to hold is each node's choice (`NodeConfig.cache`;
|
|
511
|
+
`startConnectedNode` turns it on unless given `cache: false`); only how
|
|
512
|
+
keepers confirm is protocol. `spaces.status(id)` shows `holds` and `pending`.
|
|
513
|
+
|
|
371
514
|
## Modules
|
|
372
515
|
|
|
373
516
|
### Identity (`@weaveprotocol/core/identity`)
|
|
@@ -484,16 +627,27 @@ Typed, signed data expressions using [Standard Schema](https://standardschema.de
|
|
|
484
627
|
|
|
485
628
|
### Storage (`@weaveprotocol/core/storage`)
|
|
486
629
|
|
|
487
|
-
Local-first storage
|
|
630
|
+
Local-first storage: signed versions, and plain entries saying which is current
|
|
631
|
+
and which versions each collection keeps — the set sync compares.
|
|
488
632
|
|
|
489
633
|
| Export | Description |
|
|
490
634
|
|--------|-------------|
|
|
491
|
-
| `createStorageProvider()` |
|
|
635
|
+
| `createStorageProvider()` | Expression storage: current, first and retained versions (`r/`, `g/`, `h/`), and every kept version by collection (`i/`) for sync; `fingerprint()` is equal on two stores keeping the same versions |
|
|
492
636
|
| `createIndexedDBAdapter()` | IndexedDB storage adapter, scoped to this origin |
|
|
493
637
|
| `createFolderAdapter()` | A user-picked directory, shared by every origin given access |
|
|
494
638
|
| `createEncryptedAdapter()` | Seals chosen keys (space records, space keys) at rest |
|
|
495
|
-
| `reconcileFolder()` |
|
|
496
|
-
| `
|
|
639
|
+
| `reconcileFolder()` | Places the files another writer added to a folder, and drops entries whose file is gone |
|
|
640
|
+
| `createMirror()` | Keeps a space in a dumb file store too, synced like a peer that never runs code |
|
|
641
|
+
| `createS3BlobStore()` / `createMemoryBlobStore()` | File stores a mirror can use: any S3-compatible bucket (R2, B2, MinIO, AWS), or memory |
|
|
642
|
+
|
|
643
|
+
**Mirrors.** A bucket or an app folder can hold a space: each writer (one store
|
|
644
|
+
on one device, with a random id) only ever adds immutable segments in its own
|
|
645
|
+
folder, named by a counter and the hash of their bytes — so nothing is written
|
|
646
|
+
twice and nothing needs a lock. A segment holds versions exactly as they travel,
|
|
647
|
+
private bodies still sealed, and everything read back passes the same gates as a
|
|
648
|
+
peer's records: the store can hide things, not forge them. What a writer knows
|
|
649
|
+
the store holds, it never uploads again; a writer compacts its own segments
|
|
650
|
+
into fewer. Merging is the protocol's own — a set of versions that only grows.
|
|
497
651
|
|
|
498
652
|
### Spaces
|
|
499
653
|
|
|
@@ -523,7 +677,7 @@ const { space, key } = await spaces.create({
|
|
|
523
677
|
});
|
|
524
678
|
```
|
|
525
679
|
|
|
526
|
-
Give each space its own storage and its own
|
|
680
|
+
Give each space its own storage and its own sync and a peer you share one list
|
|
527
681
|
with learns nothing about the others.
|
|
528
682
|
|
|
529
683
|
#### Who may write: roles, and a history every peer replays
|
|
@@ -543,8 +697,9 @@ and roles ranked below you, and give out roles up to your own rank. Two people
|
|
|
543
697
|
at the same rank can never remove each other, only themselves — so the creator
|
|
544
698
|
**hands over** by giving someone their role, then leaving, and the space goes on.
|
|
545
699
|
|
|
546
|
-
Roles, members, invites, revoked notes
|
|
547
|
-
(`sys.role`, `sys.member`, `sys.invite`,
|
|
700
|
+
Roles, members, invites, revoked notes, collection definitions and changes of a
|
|
701
|
+
private space's key are records (`sys.role`, `sys.member`, `sys.invite`,
|
|
702
|
+
`sys.revoke`, `sys.collection`, `sys.key`), and
|
|
548
703
|
every record written anywhere names the latest of them its writer knew, as
|
|
549
704
|
`seen`. That makes the **access history** a small graph, which every peer
|
|
550
705
|
replays the same way (`space/roles.ts`): a change comes after what it saw;
|
|
@@ -586,15 +741,35 @@ connect under someone else's name, and in a private space with the read key
|
|
|
586
741
|
too, checked against its public half. A stranger who learns a space's id, or a
|
|
587
742
|
relay that sees its room, gets no ciphertext. A peer-to-peer handshake also
|
|
588
743
|
signs both ends' DTLS fingerprints, so a relay that swapped in its own offer to
|
|
589
|
-
sit in the middle is caught.
|
|
590
|
-
|
|
744
|
+
sit in the middle is caught.
|
|
745
|
+
|
|
746
|
+
**Removing someone from a private space changes its key**, the way Keybase
|
|
747
|
+
changes a team's key. Whoever manages the space, seeing someone lose their
|
|
748
|
+
place (removed, left, or their role deleted), makes a new key by itself and
|
|
749
|
+
writes a `sys.key` record: the new key's id and public read key, and every
|
|
750
|
+
earlier key sealed under the new one. It is part of the access history, so two
|
|
751
|
+
new keys made apart resolve like any other change, and only `manage` may make
|
|
752
|
+
one. Each member's copy goes in a `sys.box`, sealed to their **member key**: a
|
|
753
|
+
key pair per account per space, derived from the account's vault key, whose
|
|
754
|
+
public half each member publishes in the clear (`sys.memberkey`). An account
|
|
755
|
+
home hands an app the member keys of exactly the spaces it grants.
|
|
756
|
+
|
|
757
|
+
From then on new records are sealed with the new key. Someone removed keeps what
|
|
758
|
+
they could already read, and nothing after it. Holding the current key opens
|
|
759
|
+
every earlier one, so a newcomer reads the space's past. A member who was away
|
|
760
|
+
when the key changed still holds only the older read key: they prove that, and
|
|
761
|
+
send their note sealed under the older key, and a peer holding it lets them in
|
|
762
|
+
if they are still a member. A host with no key takes the current read key only.
|
|
763
|
+
View-only links made before the change stop working; share a new one.
|
|
764
|
+
`node.spaces.changeKey(space)` does the same by hand, for a lost device.
|
|
591
765
|
|
|
592
766
|
A space's **id is the hash of what is fixed at creation**: creator, visibility,
|
|
593
767
|
starting roles and which one the creator holds, time, a random nonce and the
|
|
594
|
-
read key (the name is left out, so it can change). `join` refuses an
|
|
595
|
-
whose space does not hash to its id
|
|
596
|
-
|
|
597
|
-
|
|
768
|
+
first read key (the name is left out, so it can change). `join` refuses an
|
|
769
|
+
invite whose space does not hash to its id — so whoever passes an invite on
|
|
770
|
+
cannot change who started the space, or with which roles. The key an invite
|
|
771
|
+
carries may be a later one; its id is its hash, so it is either the key the
|
|
772
|
+
history names or one that opens nothing.
|
|
598
773
|
|
|
599
774
|
**Spaces describe themselves.** A space stores its collections' definitions —
|
|
600
775
|
name, title, description and a JSON Schema — as signed records in
|
|
@@ -774,23 +949,36 @@ tell a Weave peer from anyone else, so set coturn's own quotas (`user-quota`,
|
|
|
774
949
|
`total-quota`, `max-bps`). TURN only forwards encrypted packets; it can't
|
|
775
950
|
see or hear a call.
|
|
776
951
|
|
|
952
|
+
The relay's Docker image (`server/`, deployed to Fly) runs coturn beside it when
|
|
953
|
+
`TURN_SECRET` is set, already capped that way: `server/fly.toml` says how.
|
|
954
|
+
|
|
777
955
|
Only peers already in a room hear about a newcomer, so exactly one side creates
|
|
778
956
|
the offer and the two never collide.
|
|
779
957
|
|
|
780
958
|
### Sync (`@weaveprotocol/core/sync`)
|
|
781
959
|
|
|
782
|
-
|
|
960
|
+
Range-based set reconciliation (Negentropy, as Nostr's NIP-77), one collection at a time.
|
|
783
961
|
|
|
784
962
|
| Export | Description |
|
|
785
963
|
|--------|-------------|
|
|
786
|
-
| `createSyncEngine()` |
|
|
787
|
-
| `
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
964
|
+
| `createSyncEngine()` | Reconciles a space with its peers, with a heartbeat |
|
|
965
|
+
| `createReconciler()` / `ItemSet` / `fingerprintOf()` | Negentropy itself, wire-compatible with the reference implementation |
|
|
966
|
+
|
|
967
|
+
A peer says hello with a fingerprint of each collection it keeps — in essence
|
|
968
|
+
the sum of its version ids. Equal fingerprints mean the same versions, and that
|
|
969
|
+
collection is done. For each one that differs, the peer whose id sorts first
|
|
970
|
+
compares fingerprints of ever smaller ranges with the other until both know
|
|
971
|
+
exactly which versions each lacks; then it asks for what it lacks and sends what
|
|
972
|
+
the other does. Cost follows the difference, not the size: one new version
|
|
973
|
+
among 2,000 syncs in under 8 KB, all told. Nothing is stored for sync beyond
|
|
974
|
+
the versions' own index entries: the sets and sums live in memory, updated by
|
|
975
|
+
every change and read again when another writer (a tab, a folder) changed the
|
|
976
|
+
store.
|
|
977
|
+
|
|
978
|
+
Each hello also says what the peer holds — `all`, or some collections — and
|
|
979
|
+
two peers reconcile only the collections both hold. A version for a collection
|
|
980
|
+
a node doesn't hold is passed over; one it takes in, it tells the sender it has
|
|
981
|
+
(`stored`). One peer's messages are handled in the order they came.
|
|
794
982
|
|
|
795
983
|
The engine's `validate` hook is the seam where the validation engine sits.
|
|
796
984
|
Expressions a peer sends are only committed if it accepts them; the rest are
|
|
@@ -862,7 +1050,7 @@ interface StorageAdapter {
|
|
|
862
1050
|
- `createIndexedDBAdapter(name)` — works in every browser. Origin-scoped.
|
|
863
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.
|
|
864
1052
|
|
|
865
|
-
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.
|
|
866
1054
|
|
|
867
1055
|
### Data folders — storage that outlives the origin
|
|
868
1056
|
|
|
@@ -877,7 +1065,7 @@ A directory handle is the exception. Each origin asks for permission once, and b
|
|
|
877
1065
|
accounts.json name, DID and id of each account (readable without unlocking)
|
|
878
1066
|
accounts/<id>/account.json that account's seed, encrypted once per way of unlocking it
|
|
879
1067
|
accounts/<id>/stores/<namespace>/
|
|
880
|
-
kv/<key>
|
|
1068
|
+
kv/<key> index entries (current, first, retained, by collection), space records (sealed)
|
|
881
1069
|
expressions/<cid>.json one signed record per file
|
|
882
1070
|
```
|
|
883
1071
|
|
|
@@ -910,11 +1098,11 @@ const registry = createEncryptedAdapter(
|
|
|
910
1098
|
);
|
|
911
1099
|
```
|
|
912
1100
|
|
|
913
|
-
**Expressions are the truth; the
|
|
1101
|
+
**Expressions are the truth; the entries are an index over them.** That inversion is what lets several writers share one folder without taking a lock. Every expression file is named by its own content hash, so concurrent writers can only ever add files that agree; the one thing both can overwrite, which version a record's current entry names, is derived state either side can put right. `reconcileFolder()` does — it places every file that appeared, which corrects an entry a race left wrong — so call it on an interval, on window focus, or after a sync round, since the web has no filesystem change notification.
|
|
914
1102
|
|
|
915
1103
|
Two consequences worth having:
|
|
916
1104
|
|
|
917
|
-
- **The folder is the account.** Copy it to a USB stick and it is your whole identity. Put it in iCloud, Dropbox or Syncthing and several devices converge with no relay at all — the folder becomes a second transport alongside WebRTC, and both meet in the same
|
|
1105
|
+
- **The folder is the account.** Copy it to a USB stick and it is your whole identity. Put it in iCloud, Dropbox or Syncthing and several devices converge with no relay at all — the folder becomes a second transport alongside WebRTC, and both meet in the same merge: the set of versions only grows, and the version rule picks the same current one everywhere.
|
|
918
1106
|
- **It changes nothing about the mesh.** A folder-backed node is an ordinary peer that happens to be durable and readable by several origins — an availability role, never an authority one. Where there is no folder (Safari, Firefox, mobile) a node keeps an origin-scoped replica and gossips exactly as before.
|
|
919
1107
|
|
|
920
1108
|
### Locking the folder
|
|
@@ -939,7 +1127,7 @@ await accounts.write(account, withWrap(vault, wrap));
|
|
|
939
1127
|
|
|
940
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 `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.
|
|
941
1129
|
|
|
942
|
-
`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
|
|
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.
|
|
943
1131
|
|
|
944
1132
|
### Where the root key lives
|
|
945
1133
|
|
|
@@ -991,6 +1179,18 @@ space with is a peer in that one only.
|
|
|
991
1179
|
**Several relays**, so there is no single phone book — a peer announced by two
|
|
992
1180
|
of them is announced upward once, and replies go back the way they arrived.
|
|
993
1181
|
|
|
1182
|
+
**The space says where it meets**, like a Nostr relay list, but the space's
|
|
1183
|
+
own. `sys.relays` is part of the access history, so only someone who manages
|
|
1184
|
+
the space changes it (`node.spaces.setRelays`), and a new space names its
|
|
1185
|
+
creator's relays by itself. Invites carry the list, so a joiner whose app uses
|
|
1186
|
+
other relays reaches the space before anything has synced. Every member then
|
|
1187
|
+
joins the space's room on those relays as well as their own
|
|
1188
|
+
(`mesh.useRelays(room, relays)`); relays only one space names are joined for
|
|
1189
|
+
that space's room alone, and dropped once no open space names them. Moving a
|
|
1190
|
+
space to another relay, a self-hosted one say, is one change, and everyone
|
|
1191
|
+
follows. No DHT: browsers can't be DHT nodes, and would need a relay to reach
|
|
1192
|
+
one anyway.
|
|
1193
|
+
|
|
994
1194
|
**Peers introduce peers**, so a relay is only needed for the *first* connection.
|
|
995
1195
|
Once you are connected to someone, their data channel carries signalling for the
|
|
996
1196
|
peers you have not met: `__peers` says who I can see, `__signal` carries
|
|
@@ -1089,6 +1289,27 @@ it doubles as a relay), and `weave mcp` to hand the same operations to an agent.
|
|
|
1089
1289
|
It reads and writes the same data folder layout a browser does. See
|
|
1090
1290
|
[cli/README.md](cli/README.md).
|
|
1091
1291
|
|
|
1292
|
+
**Hosting.** `weave host` keeps many accounts' spaces online without being able
|
|
1293
|
+
to read them: it is the extension's carrier (`createCarrierNode`) with one carry
|
|
1294
|
+
space per paying account (`createHostNode`), each space held once however many
|
|
1295
|
+
members pay for it. A subscription is a key the account makes and keeps in its
|
|
1296
|
+
registry (`sys.hosting`), so every device signs as it; `node.hosting.use(url)`
|
|
1297
|
+
starts one, and whichever device notices it is paid hands the host the carry
|
|
1298
|
+
space. Every call to the host is signed over method, path, time and body.
|
|
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
|
|
1300
|
+
`/.well-known/weave-host` (like a Nostr relay's NIP-11 document), signs every
|
|
1301
|
+
status it gives — the home keeps the latest in the registry as the person's
|
|
1302
|
+
proof — and takes payments on its own pay page, which `node.hosting.payPage(url)`
|
|
1303
|
+
links to with a signature by the subscription key, valid for an hour at that
|
|
1304
|
+
host only. On the page: Stripe Checkout and the Customer Portal (card, Apple
|
|
1305
|
+
Pay, Google Pay), whose webhook moves a paid-until date taken from Stripe's own
|
|
1306
|
+
billing period; and USDC from a crypto wallet straight to the host's address,
|
|
1307
|
+
checked on the network by the host. Past the date: a grace period, then the host drops
|
|
1308
|
+
the spaces and deletes its copy. With a bucket (`WEAVE_S3_*`) the host's disk
|
|
1309
|
+
is only a cache — every carried space and the subscription list live in the
|
|
1310
|
+
bucket, sealed, and a new machine starts from it. The account home's Settings
|
|
1311
|
+
has **Keep my spaces online**.
|
|
1312
|
+
|
|
1092
1313
|
## Example app
|
|
1093
1314
|
|
|
1094
1315
|
`example/` is the Weave website — a landing page for developers at `/`, and
|
|
@@ -1101,11 +1322,44 @@ npm install && (cd example && npm install) && (cd home && npm install) # the C
|
|
|
1101
1322
|
npm run dev
|
|
1102
1323
|
```
|
|
1103
1324
|
|
|
1104
|
-
That starts
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1325
|
+
That starts everything, with coloured output per part, and Ctrl-C stops it all:
|
|
1326
|
+
|
|
1327
|
+
| Part | Where | What |
|
|
1328
|
+
|---|---|---|
|
|
1329
|
+
| app | http://localhost:5173 | The example app |
|
|
1330
|
+
| home | http://localhost:5174 | The account home it connects to |
|
|
1331
|
+
| 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/`) |
|
|
1332
|
+
| host | http://localhost:8788 | `weave host`, what "Keep my spaces online" uses, with its pay page at `/pay`; settings in `cli/.env.host.dev` |
|
|
1333
|
+
| stripe | — | Only when you add a Stripe test key (below): forwards Stripe's webhooks to the host |
|
|
1334
|
+
|
|
1335
|
+
`example/.env.development` and `home/.env.development` point the app and home
|
|
1336
|
+
at the rest. Override any of them in a `.env.local`, and the host in
|
|
1337
|
+
`cli/.env.host.local`.
|
|
1338
|
+
|
|
1339
|
+
**Trying hosting and payments.** In the home: Settings, **Keep my spaces
|
|
1340
|
+
online**, **Keep online** (the dev host is filled in), then **Payment**, which
|
|
1341
|
+
opens the host's pay page in a new tab. What you can pay with there:
|
|
1342
|
+
|
|
1343
|
+
- **A browser wallet, on by default.** Payments go to Base Sepolia, a test
|
|
1344
|
+
network: test money only. In MetaMask (or any browser wallet), get test ETH
|
|
1345
|
+
for the fee from a Base Sepolia faucet (Coinbase's, or Alchemy's) and test
|
|
1346
|
+
USDC from faucet.circle.com (choose Base Sepolia). Pay, and the pay page
|
|
1347
|
+
says "Payment received"; back in the home's tab, the host shows "paid until"
|
|
1348
|
+
and takes your spaces. To see payments arrive, set your own address as
|
|
1349
|
+
`WEAVE_WALLET_ADDRESS` in `cli/.env.host.local`.
|
|
1350
|
+
- **A card, in Stripe's test mode.** In `cli/.env.host.local`, add
|
|
1351
|
+
`STRIPE_SECRET_KEY=sk_test_…` and the price ids of a monthly and a yearly
|
|
1352
|
+
recurring test price (`STRIPE_PRICE_MONTHLY`, `STRIPE_PRICE_YEARLY`, from the Stripe
|
|
1353
|
+
dashboard in test mode). With the Stripe CLI installed, `npm run dev`
|
|
1354
|
+
forwards Stripe's webhooks to the host by itself. Pay with card
|
|
1355
|
+
4242 4242 4242 4242, any future date, any CVC.
|
|
1356
|
+
- **Phone wallets, by QR code.** Add `WEAVE_WALLETCONNECT_PROJECT_ID` (free at
|
|
1357
|
+
dashboard.reown.com, with localhost allowed in the project) to
|
|
1358
|
+
`cli/.env.host.local`; `npm run dev` builds what the pay page needs. The
|
|
1359
|
+
wallet has to be on Base Sepolia too.
|
|
1360
|
+
|
|
1361
|
+
`npm test` covers the same paths without any of this: a fake network, fake
|
|
1362
|
+
Stripe calls, and the home's side against a real host.
|
|
1109
1363
|
|
|
1110
1364
|
The example never signs anyone in: "Connect with Weave" opens the home, where
|
|
1111
1365
|
you make an account or sign in, and allow the example your whole account. Its
|
|
@@ -1129,7 +1383,7 @@ between pods, pair a phone by QR code, and see which apps you connected. In the
|
|
|
1129
1383
|
example: make private or public spaces, just yours or with people you invite, and share one with a
|
|
1130
1384
|
friend via an invite link. Everyone in a space is shown by the name
|
|
1131
1385
|
they gave. Every record is signed by a delegated session key, stored in that
|
|
1132
|
-
space's
|
|
1386
|
+
space's own store, encrypted first if the space is private, and gossiped to peers over
|
|
1133
1387
|
WebRTC; a record says *verified* once its signature and its delegation chain
|
|
1134
1388
|
check out here, and *encrypted* when it arrived encrypted.
|
|
1135
1389
|
|
|
@@ -1190,7 +1444,23 @@ the agents it finds, and they start `weave mcp` themselves: a node of its own,
|
|
|
1190
1444
|
over WebRTC (`node-datachannel`), that follows the account and keeps working
|
|
1191
1445
|
with every tab closed. What it writes shows "via agent", and every peer
|
|
1192
1446
|
ignores an agent changing collections, who may do what, or the account's own
|
|
1193
|
-
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.
|
|
1194
1464
|
|
|
1195
1465
|
## Releasing
|
|
1196
1466
|
|
|
@@ -1201,11 +1471,18 @@ workspace) are released together, always with the same version:
|
|
|
1201
1471
|
npm run release
|
|
1202
1472
|
```
|
|
1203
1473
|
|
|
1204
|
-
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)
|
|
1205
1476
|
asks for the next version, writes it to both packages, and commits and tags
|
|
1206
|
-
it (`v0.1.2`). Both are published (each builds itself first: `dist/`
|
|
1207
|
-
core, one bundled file for the CLI), and only then
|
|
1208
|
-
|
|
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`.
|
|
1209
1486
|
|
|
1210
1487
|
## Tests
|
|
1211
1488
|
|
|
@@ -1218,8 +1495,8 @@ for the same private scalars, and pinned to recorded DIDs so an accidental
|
|
|
1218
1495
|
change cannot slip through; recovery codes; account vaults, wraps and account
|
|
1219
1496
|
stores; data folders with several writers; spaces, invites and
|
|
1220
1497
|
encrypt-then-sign; UCAN issuing, attenuation and chain validation; phone
|
|
1221
|
-
pairing; peer introductions;
|
|
1222
|
-
|
|
1498
|
+
pairing; peer introductions; Negentropy, checked against the reference
|
|
1499
|
+
implementation; the validation gates; and two peers reconciling, including the forged, stolen,
|
|
1223
1500
|
unauthorized and malformed expressions their gatekeepers reject. Above those:
|
|
1224
1501
|
the node API — versioned records, links, queries, collection definitions,
|
|
1225
1502
|
profiles, the account registry, moving and merging accounts — and the CLI.
|