@serfab/cadre-core 0.12.0 → 1.0.0
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 +23 -8
- package/dist/bootstrap-peer-store-file.js +2 -2
- package/dist/bootstrap-peer-store.d.ts +32 -12
- package/dist/bootstrap-peer-store.js +41 -15
- package/dist/bootstrap-peer-store.js.map +1 -1
- package/dist/cadre-node.d.ts +883 -102
- package/dist/cadre-node.js +1800 -209
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +2 -2
- package/dist/control-authorization.js +1 -0
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +13 -26
- package/dist/control-cohort.js +0 -26
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +318 -44
- package/dist/control-database.js +516 -74
- package/dist/control-database.js.map +1 -1
- package/dist/control-read-retry.d.ts +40 -11
- package/dist/control-read-retry.js +48 -15
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-retry.d.ts +58 -3
- package/dist/control-retry.js +58 -3
- package/dist/control-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +133 -21
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +80 -11
- package/dist/control-write-retry.js +168 -29
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store-file.d.ts +13 -0
- package/dist/enrolled-machine-store-file.js +41 -0
- package/dist/enrolled-machine-store-file.js.map +1 -0
- package/dist/enrolled-machine-store.d.ts +81 -0
- package/dist/enrolled-machine-store.js +225 -0
- package/dist/enrolled-machine-store.js.map +1 -0
- package/dist/file-durable-slot.js +5 -3
- package/dist/file-durable-slot.js.map +1 -1
- package/dist/index.d.ts +17 -8
- package/dist/index.js +54 -9
- package/dist/index.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +15 -1
- package/dist/membership-connection-gater.js +9 -3
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +21 -7
- package/dist/node-local-snapshot.js +30 -9
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +143 -0
- package/dist/peer-dial.js +176 -0
- package/dist/peer-dial.js.map +1 -0
- package/dist/{strand-backfill.d.ts → peer-join-backfill.d.ts} +49 -20
- package/dist/{strand-backfill.js → peer-join-backfill.js} +133 -52
- package/dist/peer-join-backfill.js.map +1 -0
- package/dist/relay-addrs.d.ts +160 -49
- package/dist/relay-addrs.js +287 -60
- package/dist/relay-addrs.js.map +1 -1
- package/dist/relay-reservation.d.ts +71 -3
- package/dist/relay-reservation.js +116 -8
- package/dist/relay-reservation.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +30 -1
- package/dist/seed-bootstrap.js +62 -17
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +62 -0
- package/dist/storage-scope.js +75 -0
- package/dist/storage-scope.js.map +1 -0
- package/dist/strand-database.d.ts +41 -11
- package/dist/strand-database.js +48 -18
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +152 -0
- package/dist/strand-first-sync-gate.js +208 -0
- package/dist/strand-first-sync-gate.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +99 -5
- package/dist/strand-formation-manager.js +119 -8
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +97 -3
- package/dist/strand-formation-protocol.js +108 -2
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +389 -19
- package/dist/strand-instance-manager.js +678 -65
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +20 -12
- package/dist/strand-member-key.js +20 -12
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.js +3 -0
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +361 -0
- package/dist/strand-membership-reconciler.js +548 -0
- package/dist/strand-membership-reconciler.js.map +1 -0
- package/dist/strand-membership-writer.d.ts +143 -11
- package/dist/strand-membership-writer.js +419 -135
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-config.d.ts +116 -0
- package/dist/strand-network-config.js +192 -0
- package/dist/strand-network-config.js.map +1 -0
- package/dist/strand-revocation-enforcer.d.ts +392 -0
- package/dist/strand-revocation-enforcer.js +533 -0
- package/dist/strand-revocation-enforcer.js.map +1 -0
- package/dist/strand-solicitation.d.ts +17 -1
- package/dist/strand-solicitation.js +13 -2
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +11 -7
- package/dist/strand-wake-protocol.js +20 -52
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.js +12 -1
- package/dist/strand-watcher.js.map +1 -1
- package/dist/timed-step.d.ts +12 -0
- package/dist/timed-step.js +28 -0
- package/dist/timed-step.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +19 -0
- package/dist/timeout-scheduler.js +22 -0
- package/dist/timeout-scheduler.js.map +1 -0
- package/dist/types.d.ts +472 -46
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/package.json +16 -8
- package/src/bootstrap-peer-store-file.ts +2 -2
- package/src/bootstrap-peer-store.ts +69 -27
- package/src/cadre-node.ts +1926 -228
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +122 -130
- package/src/control-database.ts +586 -74
- package/src/control-read-retry.ts +202 -167
- package/src/control-retry.ts +297 -190
- package/src/control-schema.ts +133 -21
- package/src/control-write-retry.ts +525 -383
- package/src/enrolled-machine-store-file.ts +43 -0
- package/src/enrolled-machine-store.ts +280 -0
- package/src/file-durable-slot.ts +5 -3
- package/src/index.ts +134 -15
- package/src/membership-connection-gater.ts +9 -3
- package/src/node-local-snapshot.ts +38 -9
- package/src/peer-addr-book.ts +251 -250
- package/src/peer-dial.ts +215 -0
- package/src/{strand-backfill.ts → peer-join-backfill.ts} +478 -389
- package/src/relay-addrs.ts +458 -218
- package/src/relay-reservation.ts +951 -828
- package/src/seed-bootstrap.ts +76 -18
- package/src/storage-scope.ts +78 -0
- package/src/strand-database.ts +61 -21
- package/src/strand-first-sync-gate.ts +258 -0
- package/src/strand-formation-manager.ts +162 -8
- package/src/strand-formation-protocol.ts +850 -692
- package/src/strand-instance-manager.ts +877 -74
- package/src/strand-member-key.ts +20 -12
- package/src/strand-member-registry.ts +3 -0
- package/src/strand-membership-reconciler.ts +680 -0
- package/src/strand-membership-writer.ts +550 -190
- package/src/strand-network-config.ts +250 -0
- package/src/strand-revocation-enforcer.ts +653 -0
- package/src/strand-solicitation.ts +499 -471
- package/src/strand-wake-protocol.ts +21 -66
- package/src/strand-watcher.ts +325 -314
- package/src/timed-step.ts +28 -0
- package/src/timeout-scheduler.ts +28 -0
- package/src/types.ts +487 -52
- package/dist/strand-backfill.js.map +0 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* storage-scope.ts — the scope keys `CadreNodeConfig.storage.provider` is called with.
|
|
3
|
+
*
|
|
4
|
+
* A provider is a factory cadre-core invokes once per **scope**: once for the control
|
|
5
|
+
* database and once for each strand. A strand's scope key is its strand id; the control
|
|
6
|
+
* database's scope key is what this module mints.
|
|
7
|
+
*
|
|
8
|
+
* WHY THE CONTROL SCOPE CARRIES THE PARTY ID. The control database holds the party's
|
|
9
|
+
* own records — its strands, owner keys, peers, invitations, revocations. Two parties
|
|
10
|
+
* on one device must therefore not share one control store, or a node started for
|
|
11
|
+
* party B reads party A's rows as if party A had written them for B. Every other
|
|
12
|
+
* party-scoped store in this codebase already knows this: `PersistentTrustedOwnerStore`,
|
|
13
|
+
* `PersistentBootstrapPeerStore` and `PersistentEnrolledMachineStore` each take a
|
|
14
|
+
* `partyId` and fail closed on a mismatch. Putting the party id in the key here means
|
|
15
|
+
* no embedder has to know it belongs there, and none can get it wrong.
|
|
16
|
+
*
|
|
17
|
+
* WHY IT IS ENCODED. A party id is arbitrary text — nothing in cadre-core validates
|
|
18
|
+
* its shape, and the React Native reference app lets a user type one into Settings.
|
|
19
|
+
* Scope keys reach real namespaces unescaped: cadre-cli builds `${config.path}/${scope}`,
|
|
20
|
+
* React Native a LevelDB filename, the browser an IndexedDB database name. A party id
|
|
21
|
+
* containing `/` or `..` would escape the CLI's storage directory. Base64url keeps every
|
|
22
|
+
* key inside `[A-Za-z0-9._-]` — the charset embedders already rely on, and the same
|
|
23
|
+
* encoding the React Native app's `secureStoreKeySegment` uses on a party id for the
|
|
24
|
+
* same reason.
|
|
25
|
+
*
|
|
26
|
+
* THE STRAND ARM OF THAT CHARSET RULE IS NOT YET ENFORCED. The strand scope key is
|
|
27
|
+
* `StrandRow.Id`, and the ids cadre-core itself mints are `strand-<digits>-<base36>`
|
|
28
|
+
* / `strand-<hex>`, which satisfy the charset. But a strand row replicated into the
|
|
29
|
+
* control database by another node in the party carries whatever id THAT node wrote,
|
|
30
|
+
* and nothing validates its shape before `resolveStrandStorage` hands it to the
|
|
31
|
+
* provider. See `tickets/backlog/bug-strand-scope-key-charset-unenforced`.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The storage scope key for a party's control database.
|
|
35
|
+
*
|
|
36
|
+
* Returns `control-` followed by the base64url encoding of `partyId`'s UTF-8 bytes,
|
|
37
|
+
* so the whole key stays within `[A-Za-z0-9._-]` and is safe to use directly as a
|
|
38
|
+
* file name, directory name or database name. See the module comment for why the
|
|
39
|
+
* encoding is load-bearing rather than decorative.
|
|
40
|
+
*
|
|
41
|
+
* To read a party id back off a device — from, say, a LevelDB file named
|
|
42
|
+
* `sereus-control-MTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1` — in a browser or
|
|
43
|
+
* Node console, with no dependency on this package:
|
|
44
|
+
* ```js
|
|
45
|
+
* const b64 = key.slice('control-'.length).replace(/-/g, '+').replace(/_/g, '/');
|
|
46
|
+
* new TextDecoder().decode(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)))
|
|
47
|
+
* ```
|
|
48
|
+
* The `TextDecoder` step is not optional: `atob` alone yields one character per BYTE,
|
|
49
|
+
* so a party id with any non-ASCII character in it decodes to mojibake.
|
|
50
|
+
*/
|
|
51
|
+
export declare function controlStorageScope(partyId: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Whether a scope key names a control database rather than a strand.
|
|
54
|
+
*
|
|
55
|
+
* A prefix test. Safe as one against the strand ids cadre-core mints, which are
|
|
56
|
+
* `strand-`-prefixed (`strand-formation-manager.ts`, `strand-solicitation.ts`,
|
|
57
|
+
* `control-formation-recorder.ts`) and so can never collide. It is NOT a security
|
|
58
|
+
* boundary: a strand id replicated in from another node is unvalidated text that
|
|
59
|
+
* could begin `control-`, which is one more reason to close the gap the module
|
|
60
|
+
* comment names.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isControlStorageScope(scope: string): boolean;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* storage-scope.ts — the scope keys `CadreNodeConfig.storage.provider` is called with.
|
|
3
|
+
*
|
|
4
|
+
* A provider is a factory cadre-core invokes once per **scope**: once for the control
|
|
5
|
+
* database and once for each strand. A strand's scope key is its strand id; the control
|
|
6
|
+
* database's scope key is what this module mints.
|
|
7
|
+
*
|
|
8
|
+
* WHY THE CONTROL SCOPE CARRIES THE PARTY ID. The control database holds the party's
|
|
9
|
+
* own records — its strands, owner keys, peers, invitations, revocations. Two parties
|
|
10
|
+
* on one device must therefore not share one control store, or a node started for
|
|
11
|
+
* party B reads party A's rows as if party A had written them for B. Every other
|
|
12
|
+
* party-scoped store in this codebase already knows this: `PersistentTrustedOwnerStore`,
|
|
13
|
+
* `PersistentBootstrapPeerStore` and `PersistentEnrolledMachineStore` each take a
|
|
14
|
+
* `partyId` and fail closed on a mismatch. Putting the party id in the key here means
|
|
15
|
+
* no embedder has to know it belongs there, and none can get it wrong.
|
|
16
|
+
*
|
|
17
|
+
* WHY IT IS ENCODED. A party id is arbitrary text — nothing in cadre-core validates
|
|
18
|
+
* its shape, and the React Native reference app lets a user type one into Settings.
|
|
19
|
+
* Scope keys reach real namespaces unescaped: cadre-cli builds `${config.path}/${scope}`,
|
|
20
|
+
* React Native a LevelDB filename, the browser an IndexedDB database name. A party id
|
|
21
|
+
* containing `/` or `..` would escape the CLI's storage directory. Base64url keeps every
|
|
22
|
+
* key inside `[A-Za-z0-9._-]` — the charset embedders already rely on, and the same
|
|
23
|
+
* encoding the React Native app's `secureStoreKeySegment` uses on a party id for the
|
|
24
|
+
* same reason.
|
|
25
|
+
*
|
|
26
|
+
* THE STRAND ARM OF THAT CHARSET RULE IS NOT YET ENFORCED. The strand scope key is
|
|
27
|
+
* `StrandRow.Id`, and the ids cadre-core itself mints are `strand-<digits>-<base36>`
|
|
28
|
+
* / `strand-<hex>`, which satisfy the charset. But a strand row replicated into the
|
|
29
|
+
* control database by another node in the party carries whatever id THAT node wrote,
|
|
30
|
+
* and nothing validates its shape before `resolveStrandStorage` hands it to the
|
|
31
|
+
* provider. See `tickets/backlog/bug-strand-scope-key-charset-unenforced`.
|
|
32
|
+
*/
|
|
33
|
+
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
34
|
+
/**
|
|
35
|
+
* Prefix marking a scope key as a control database rather than a strand.
|
|
36
|
+
*
|
|
37
|
+
* Not exported: `controlStorageScope` mints these keys and `isControlStorageScope`
|
|
38
|
+
* recognizes them, which is the whole surface a caller needs.
|
|
39
|
+
*/
|
|
40
|
+
const CONTROL_SCOPE_PREFIX = 'control-';
|
|
41
|
+
/**
|
|
42
|
+
* The storage scope key for a party's control database.
|
|
43
|
+
*
|
|
44
|
+
* Returns `control-` followed by the base64url encoding of `partyId`'s UTF-8 bytes,
|
|
45
|
+
* so the whole key stays within `[A-Za-z0-9._-]` and is safe to use directly as a
|
|
46
|
+
* file name, directory name or database name. See the module comment for why the
|
|
47
|
+
* encoding is load-bearing rather than decorative.
|
|
48
|
+
*
|
|
49
|
+
* To read a party id back off a device — from, say, a LevelDB file named
|
|
50
|
+
* `sereus-control-MTExMTExMTEtMjIyMi00MzMzLTg0NDQtNTU1NTU1NTU1NTU1` — in a browser or
|
|
51
|
+
* Node console, with no dependency on this package:
|
|
52
|
+
* ```js
|
|
53
|
+
* const b64 = key.slice('control-'.length).replace(/-/g, '+').replace(/_/g, '/');
|
|
54
|
+
* new TextDecoder().decode(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0)))
|
|
55
|
+
* ```
|
|
56
|
+
* The `TextDecoder` step is not optional: `atob` alone yields one character per BYTE,
|
|
57
|
+
* so a party id with any non-ASCII character in it decodes to mojibake.
|
|
58
|
+
*/
|
|
59
|
+
export function controlStorageScope(partyId) {
|
|
60
|
+
return CONTROL_SCOPE_PREFIX + uint8ArrayToString(uint8ArrayFromString(partyId, 'utf8'), 'base64url');
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether a scope key names a control database rather than a strand.
|
|
64
|
+
*
|
|
65
|
+
* A prefix test. Safe as one against the strand ids cadre-core mints, which are
|
|
66
|
+
* `strand-`-prefixed (`strand-formation-manager.ts`, `strand-solicitation.ts`,
|
|
67
|
+
* `control-formation-recorder.ts`) and so can never collide. It is NOT a security
|
|
68
|
+
* boundary: a strand id replicated in from another node is unvalidated text that
|
|
69
|
+
* could begin `control-`, which is one more reason to close the gap the module
|
|
70
|
+
* comment names.
|
|
71
|
+
*/
|
|
72
|
+
export function isControlStorageScope(scope) {
|
|
73
|
+
return scope.startsWith(CONTROL_SCOPE_PREFIX);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=storage-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-scope.js","sourceRoot":"","sources":["../src/storage-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,QAAQ,IAAI,kBAAkB,EAAE,UAAU,IAAI,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEjG;;;;;GAKG;AACH,MAAM,oBAAoB,GAAG,UAAU,CAAC;AAExC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe;IAClD,OAAO,oBAAoB,GAAG,kBAAkB,CAAC,oBAAoB,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,WAAW,CAAC,CAAC;AACtG,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAa;IAClD,OAAO,KAAK,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -19,11 +19,21 @@ export interface StrandDatabaseConfig {
|
|
|
19
19
|
*/
|
|
20
20
|
strandType: 'o' | 'c';
|
|
21
21
|
/**
|
|
22
|
-
* The closed-strand `MemberPrivateKey` (base64 protobuf) from the strand row
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* The closed-strand `MemberPrivateKey` (base64 protobuf) from the strand row —
|
|
23
|
+
* the strand-wide read secret every joining party receives. Carried for the
|
|
24
|
+
* strand's read-gating story only; it derives NOBODY's identity (that used to be
|
|
25
|
+
* this key's second job, which let any joiner sign as the founding manager —
|
|
26
|
+
* gotchoices/sereus#4). Absent for open strands.
|
|
25
27
|
*/
|
|
26
28
|
memberPrivateKey?: string;
|
|
29
|
+
/**
|
|
30
|
+
* THIS party's own strand membership private key (base64 protobuf), from the
|
|
31
|
+
* control-layer `StrandPartyKey` row (or supplied explicitly at attach). Required
|
|
32
|
+
* when `founder` is true and `strandType` is `'c'` — it derives the founding
|
|
33
|
+
* `Member.Key`/`Manager.MemberKey`. Never shared outside the party; absent for
|
|
34
|
+
* open strands and for joiners that have not yet persisted one.
|
|
35
|
+
*/
|
|
36
|
+
partyMemberPrivateKey?: string;
|
|
27
37
|
/**
|
|
28
38
|
* Whether this node founds the strand. When true, {@link initialize} runs the
|
|
29
39
|
* one-time founder membership bootstrap after the schema is applied. Joiners
|
|
@@ -61,19 +71,39 @@ export declare class StrandDatabase {
|
|
|
61
71
|
/**
|
|
62
72
|
* Run the founder membership bootstrap against the freshly-composed strand DB.
|
|
63
73
|
*
|
|
64
|
-
* Derives the founding keypair from
|
|
65
|
-
* `Member.Key`/`Manager.MemberKey` are its public key)
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
74
|
+
* Derives the founding keypair from this PARTY's own `partyMemberPrivateKey` (the
|
|
75
|
+
* `Member.Key`/`Manager.MemberKey` are its public key) — deliberately NOT from the
|
|
76
|
+
* strand row's shared `MemberPrivateKey`, which every joining party receives and could
|
|
77
|
+
* therefore use to forge the founder's identity. A closed strand with no
|
|
78
|
+
* `partyMemberPrivateKey` throws, because it could never seat a founding manager.
|
|
79
|
+
* The shared key's public half IS passed, but only so the bootstrap can refuse a strand
|
|
80
|
+
* founded under it before the split (`PreSplitStrandIdentityError`). Open strands derive
|
|
81
|
+
* no keypair (Header only). Idempotent — see {@link bootstrapFounderMembership}.
|
|
69
82
|
*/
|
|
70
83
|
private bootstrapFounder;
|
|
71
84
|
/**
|
|
72
|
-
* Derive the founding keypair for a closed strand from
|
|
73
|
-
* failing loudly when the key is absent (a closed strand
|
|
74
|
-
* Manager can never admit anyone).
|
|
85
|
+
* Derive the founding keypair for a closed strand from the party's own
|
|
86
|
+
* `partyMemberPrivateKey`, failing loudly when the key is absent (a closed strand
|
|
87
|
+
* with no founding Manager can never admit anyone).
|
|
75
88
|
*/
|
|
76
89
|
private deriveFounderKeyPair;
|
|
90
|
+
/**
|
|
91
|
+
* Run the founder membership bootstrap against an ALREADY-LIVE database — the
|
|
92
|
+
* seam {@link StrandInstanceManager.foundExistingStrand} uses when a founder
|
|
93
|
+
* request arrives for an instance that was first launched as a joiner.
|
|
94
|
+
* Idempotent: every bootstrap write is insert-if-absent
|
|
95
|
+
* ({@link bootstrapFounderMembership}), so calling it on an instance that
|
|
96
|
+
* already founded writes nothing. On success also flips the captured config's
|
|
97
|
+
* `founder`, so this object's own record of how it was launched stays coherent
|
|
98
|
+
* with what actually ran (a construction-time `founder: false` is a statement
|
|
99
|
+
* about the launch, not a permanent identity); a refused bootstrap leaves it a joiner.
|
|
100
|
+
*
|
|
101
|
+
* @param partyMemberPrivateKey - The party's own membership key, for a closed strand
|
|
102
|
+
* whose original (joiner) launch resolved none — e.g. the `StrandPartyKey` row had
|
|
103
|
+
* not been written or replicated yet. The captured config's key wins when both
|
|
104
|
+
* exist: it is the identity this instance launched under.
|
|
105
|
+
*/
|
|
106
|
+
ensureFounderBootstrap(partyMemberPrivateKey?: string): Promise<void>;
|
|
77
107
|
/**
|
|
78
108
|
* Get the underlying database for queries
|
|
79
109
|
*/
|
package/dist/strand-database.js
CHANGED
|
@@ -73,35 +73,65 @@ export class StrandDatabase {
|
|
|
73
73
|
/**
|
|
74
74
|
* Run the founder membership bootstrap against the freshly-composed strand DB.
|
|
75
75
|
*
|
|
76
|
-
* Derives the founding keypair from
|
|
77
|
-
* `Member.Key`/`Manager.MemberKey` are its public key)
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
76
|
+
* Derives the founding keypair from this PARTY's own `partyMemberPrivateKey` (the
|
|
77
|
+
* `Member.Key`/`Manager.MemberKey` are its public key) — deliberately NOT from the
|
|
78
|
+
* strand row's shared `MemberPrivateKey`, which every joining party receives and could
|
|
79
|
+
* therefore use to forge the founder's identity. A closed strand with no
|
|
80
|
+
* `partyMemberPrivateKey` throws, because it could never seat a founding manager.
|
|
81
|
+
* The shared key's public half IS passed, but only so the bootstrap can refuse a strand
|
|
82
|
+
* founded under it before the split (`PreSplitStrandIdentityError`). Open strands derive
|
|
83
|
+
* no keypair (Header only). Idempotent — see {@link bootstrapFounderMembership}.
|
|
81
84
|
*/
|
|
82
85
|
async bootstrapFounder() {
|
|
83
|
-
const { strandId, strandType, memberPrivateKey, sAppConfig } = this.config;
|
|
84
|
-
const
|
|
85
|
-
? this.deriveFounderKeyPair(strandId, memberPrivateKey)
|
|
86
|
-
: undefined;
|
|
86
|
+
const { strandId, strandType, memberPrivateKey, partyMemberPrivateKey, sAppConfig } = this.config;
|
|
87
|
+
const closed = strandType === 'c';
|
|
87
88
|
await bootstrapFounderMembership(this.db, {
|
|
88
89
|
strandId,
|
|
89
90
|
type: strandType,
|
|
90
91
|
sApp: sAppConfig,
|
|
91
|
-
founderKeyPair,
|
|
92
|
+
founderKeyPair: closed ? this.deriveFounderKeyPair(strandId, partyMemberPrivateKey) : undefined,
|
|
93
|
+
sharedMemberPublicKey: closed && memberPrivateKey
|
|
94
|
+
? strandMemberKeyPair(memberPrivateKey).publicKeyB64
|
|
95
|
+
: undefined,
|
|
92
96
|
});
|
|
93
97
|
}
|
|
94
98
|
/**
|
|
95
|
-
* Derive the founding keypair for a closed strand from
|
|
96
|
-
* failing loudly when the key is absent (a closed strand
|
|
97
|
-
* Manager can never admit anyone).
|
|
99
|
+
* Derive the founding keypair for a closed strand from the party's own
|
|
100
|
+
* `partyMemberPrivateKey`, failing loudly when the key is absent (a closed strand
|
|
101
|
+
* with no founding Manager can never admit anyone).
|
|
98
102
|
*/
|
|
99
|
-
deriveFounderKeyPair(strandId,
|
|
100
|
-
if (!
|
|
101
|
-
throw new Error(`Cannot found closed strand ${strandId}:
|
|
102
|
-
'A closed strand needs a founding Member/Manager derived from
|
|
103
|
+
deriveFounderKeyPair(strandId, partyMemberPrivateKey) {
|
|
104
|
+
if (!partyMemberPrivateKey) {
|
|
105
|
+
throw new Error(`Cannot found closed strand ${strandId}: this party has no StrandPartyKey for it. ` +
|
|
106
|
+
'A closed strand needs a founding Member/Manager derived from the party\'s own ' +
|
|
107
|
+
'membership key (minted at publishStrand, or at the founder launch that follows a ' +
|
|
108
|
+
'publish interrupted before its mint) — the shared MemberPrivateKey deliberately ' +
|
|
109
|
+
'no longer derives anyone\'s identity.');
|
|
103
110
|
}
|
|
104
|
-
return strandMemberKeyPair(
|
|
111
|
+
return strandMemberKeyPair(partyMemberPrivateKey);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Run the founder membership bootstrap against an ALREADY-LIVE database — the
|
|
115
|
+
* seam {@link StrandInstanceManager.foundExistingStrand} uses when a founder
|
|
116
|
+
* request arrives for an instance that was first launched as a joiner.
|
|
117
|
+
* Idempotent: every bootstrap write is insert-if-absent
|
|
118
|
+
* ({@link bootstrapFounderMembership}), so calling it on an instance that
|
|
119
|
+
* already founded writes nothing. On success also flips the captured config's
|
|
120
|
+
* `founder`, so this object's own record of how it was launched stays coherent
|
|
121
|
+
* with what actually ran (a construction-time `founder: false` is a statement
|
|
122
|
+
* about the launch, not a permanent identity); a refused bootstrap leaves it a joiner.
|
|
123
|
+
*
|
|
124
|
+
* @param partyMemberPrivateKey - The party's own membership key, for a closed strand
|
|
125
|
+
* whose original (joiner) launch resolved none — e.g. the `StrandPartyKey` row had
|
|
126
|
+
* not been written or replicated yet. The captured config's key wins when both
|
|
127
|
+
* exist: it is the identity this instance launched under.
|
|
128
|
+
*/
|
|
129
|
+
async ensureFounderBootstrap(partyMemberPrivateKey) {
|
|
130
|
+
var _a;
|
|
131
|
+
this.ensureInitialized();
|
|
132
|
+
(_a = this.config).partyMemberPrivateKey ?? (_a.partyMemberPrivateKey = partyMemberPrivateKey);
|
|
133
|
+
await this.bootstrapFounder();
|
|
134
|
+
this.config.founder = true;
|
|
105
135
|
}
|
|
106
136
|
/**
|
|
107
137
|
* Get the underlying database for queries
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strand-database.js","sourceRoot":"","sources":["../src/strand-database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAKhE,OAAO,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7D,MAAM,GAAG,GAAG,KAAK,CAAC,wBAAwB,CAAC,CAAC;AAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,qBAAqB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"strand-database.js","sourceRoot":"","sources":["../src/strand-database.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first-sync write gate for a JOINING machine.
|
|
3
|
+
*
|
|
4
|
+
* A machine that has never received anything from another member of a strand must not
|
|
5
|
+
* commit to it. With no strand peer connected, Optimystic's cohort for every block is the
|
|
6
|
+
* machine itself, so the first write to a table finds no collection locally and INVENTS
|
|
7
|
+
* one — exactly what a founder writing alone legitimately does. When the connection comes
|
|
8
|
+
* up, two independently created histories share one collection id: Optimystic keeps one and
|
|
9
|
+
* drops the other's commits, or never reconciles them at all. The joiner's own rows silently
|
|
10
|
+
* vanish (`joining-machine-writes-before-first-sync-fork-tables`). Optimystic cannot tell a
|
|
11
|
+
* founder from a joiner; sereus can, because the launch knows whether it is founding.
|
|
12
|
+
*
|
|
13
|
+
* The invariant this module enforces: **a machine that has never held this strand's
|
|
14
|
+
* `Strand.Header` row, and has not read every `App` table once, must not commit to it.**
|
|
15
|
+
* The founder writes the Header in its bootstrap; every other machine can only receive it
|
|
16
|
+
* from a peer, so "no local Header" means "never synced". The gate therefore holds the
|
|
17
|
+
* freshly initialized `StrandDatabase` back from the app — `StrandInstance.database` stays
|
|
18
|
+
* unset and the instance reports `'syncing'` — and probes on a cadence until the Header is
|
|
19
|
+
* readable AND a read of each app table settles, at which point the database is published
|
|
20
|
+
* and the instance goes `'active'`. A machine that already holds the Header (a restart, a
|
|
21
|
+
* hibernation resume, a founder) passes the probe on the first try and is never gated, so
|
|
22
|
+
* offline-first writes on a machine that has synced before keep working.
|
|
23
|
+
*
|
|
24
|
+
* Why the app tables too: the Header is ONE collection. It reaches a joiner ahead of the
|
|
25
|
+
* founder's app-table collections (peer-join backfill and pull-on-read deliver those
|
|
26
|
+
* separately), and a write to a table whose collection this machine has not yet fetched
|
|
27
|
+
* invents one exactly as a lone joiner did. Measured on the reference chat schema
|
|
28
|
+
* (2026-09-16, direct connections, Header-only probe): the joiner's participant + message
|
|
29
|
+
* written the instant `addStrand` resolved diverged in 3 of 4 runs. Reading each table once
|
|
30
|
+
* pulls its collection while the founder is reachable, so the write that follows appends.
|
|
31
|
+
*
|
|
32
|
+
* Every probe is a read (`Strand.Header`, then `select count(1)` on each `App` table). Reads
|
|
33
|
+
* never invent a collection (Optimystic's `Collection.open` resolves undefined on an
|
|
34
|
+
* authoritatively absent header, and throws on an unreachable one), so probing is safe to
|
|
35
|
+
* repeat and a throw is just "not yet". A table nobody has written yet reads as absent on
|
|
36
|
+
* every machine; the first write to it still creates its collection — see `docs/strands.md`
|
|
37
|
+
* ("Joining") for that residual.
|
|
38
|
+
*
|
|
39
|
+
* Owned by `StrandInstanceManager`: created in `buildStrandRuntime` for a non-founder launch
|
|
40
|
+
* whose first probe finds no Header, stopped and dropped in `releaseRuntime` (so a quiesce →
|
|
41
|
+
* resume rebuild re-probes over the same store), and force-opened when a founder request
|
|
42
|
+
* runs the bootstrap against the still-gated database (`foundExistingStrand`).
|
|
43
|
+
*
|
|
44
|
+
* NOTE: the gate covers app and membership tables, not the schema catalog. Every launch —
|
|
45
|
+
* joiner or founder — writes the catalog collection (`optimystic/schema` plus two
|
|
46
|
+
* hash-named blocks, measured 2026-09-16) alone, during `connectToStrand`'s schema apply,
|
|
47
|
+
* before any peer contact. That is the same "two histories under one id" shape this gate
|
|
48
|
+
* exists to prevent, and it is only safe because both sides derive byte-identical content
|
|
49
|
+
* from the same schema text. If the catalog ever carries per-machine or ordering-dependent
|
|
50
|
+
* content (optimystic's own `0.5-same-named-tables-in-two-schemas-share-storage` re-keys
|
|
51
|
+
* it), a joiner's catalog will fork against the founder's — gate the schema apply on the
|
|
52
|
+
* Header too, or have optimystic open the catalog read-only when a peer already holds it.
|
|
53
|
+
*/
|
|
54
|
+
import type { Database } from '@quereus/quereus';
|
|
55
|
+
import type { StrandDatabase } from './strand-database.js';
|
|
56
|
+
import { type TimeoutScheduler } from './timeout-scheduler.js';
|
|
57
|
+
/** How often a gated launch re-probes (`Strand.Header`, then every `App` table) while waiting for its first sync. */
|
|
58
|
+
export declare const DEFAULT_STRAND_FIRST_SYNC_POLL_MS = 500;
|
|
59
|
+
/**
|
|
60
|
+
* How long `CadreNode.addStrand` waits for a joining machine's first sync before rejecting
|
|
61
|
+
* with {@link StrandAwaitingFirstSyncError}. Measured: over a direct connection the
|
|
62
|
+
* joiner reads the founder's rows about 1.3 s after `addStrand` would previously have
|
|
63
|
+
* resolved; a relayed path is slower, and a phone that just redeemed an invitation has the
|
|
64
|
+
* host reachable moments ago, so 30 s is generous without leaving an app hanging.
|
|
65
|
+
*/
|
|
66
|
+
export declare const DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS = 30000;
|
|
67
|
+
/** Embedder-facing tuning for the gate, threaded from `CadreNodeConfig.strandFirstSync`. */
|
|
68
|
+
export interface StrandFirstSyncConfig {
|
|
69
|
+
/**
|
|
70
|
+
* Default wait for `CadreNode.addStrand` (and `whenStrandWritable`) before a joining
|
|
71
|
+
* machine that has reached no other member is reported as not yet writable. Default
|
|
72
|
+
* {@link DEFAULT_STRAND_FIRST_SYNC_TIMEOUT_MS}.
|
|
73
|
+
*/
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
/** Probe cadence while gated. Default {@link DEFAULT_STRAND_FIRST_SYNC_POLL_MS}. */
|
|
76
|
+
pollIntervalMs?: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Thrown by `CadreNode.addStrand` / `whenStrandWritable` when a joining machine has not
|
|
80
|
+
* received the strand's data from any other member within the wait budget. RETRYABLE: the
|
|
81
|
+
* strand stays launched and keeps probing, so a later `addStrand` (or `whenStrandWritable`)
|
|
82
|
+
* for the same strand completes the attach once another member is reachable; the
|
|
83
|
+
* `strand:writable` event fires at that moment too.
|
|
84
|
+
*/
|
|
85
|
+
export declare class StrandAwaitingFirstSyncError extends Error {
|
|
86
|
+
readonly strandId: string;
|
|
87
|
+
readonly waitedMs: number;
|
|
88
|
+
constructor(strandId: string, waitedMs: number);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether this machine holds the strand's `Strand.Header` row — the "has synced at least
|
|
92
|
+
* once (or founded)" signal. A read, never a write: it cannot invent the collection. A
|
|
93
|
+
* throwing read (an unreachable cohort mid-sync, a store still hydrating) reports `false`
|
|
94
|
+
* with a log, since the caller's only response to either answer is "probe again".
|
|
95
|
+
*/
|
|
96
|
+
export declare function strandHeaderHeld(db: Database, label: string): Promise<boolean>;
|
|
97
|
+
/**
|
|
98
|
+
* Whether a read of every `App` table settles on this machine — the "each app collection
|
|
99
|
+
* has been fetched from the cohort, or is authoritatively absent" signal. Only a throwing
|
|
100
|
+
* read (an unreachable cohort) reports `false`; an empty table is a settled read.
|
|
101
|
+
*/
|
|
102
|
+
export declare function appTablesReadable(db: Database, label: string): Promise<boolean>;
|
|
103
|
+
/**
|
|
104
|
+
* The gate's whole probe: the Header is held AND every app table has been read once. The
|
|
105
|
+
* app tables are read only once the Header is — a joiner with no peer would otherwise pay
|
|
106
|
+
* one failing network read per table per probe for nothing.
|
|
107
|
+
*/
|
|
108
|
+
export declare function strandFirstSyncComplete(db: Database, label: string): Promise<boolean>;
|
|
109
|
+
export interface StrandFirstSyncGateDeps {
|
|
110
|
+
/** Log tag naming which strand this gate holds (the strand id). */
|
|
111
|
+
label: string;
|
|
112
|
+
/** The initialized database being held back from the app until the Header is held. */
|
|
113
|
+
database: StrandDatabase;
|
|
114
|
+
/**
|
|
115
|
+
* Called exactly once, on the gate's own probe loop, when the first sync completes.
|
|
116
|
+
* NOT called by {@link StrandFirstSyncGate.open} — a caller that force-opens the gate
|
|
117
|
+
* (a founder bootstrap it just ran) publishes the database itself.
|
|
118
|
+
*/
|
|
119
|
+
onHeaderHeld: () => void;
|
|
120
|
+
/** Timer seam for the probe loop; omit for real (unref'd) timeouts. */
|
|
121
|
+
scheduler?: TimeoutScheduler;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Holds one launch's `StrandDatabase` until {@link strandFirstSyncComplete}. Probes are
|
|
125
|
+
* sequential (the next is scheduled only after the previous read settles), so a slow
|
|
126
|
+
* network read never stacks probes.
|
|
127
|
+
*/
|
|
128
|
+
export declare class StrandFirstSyncGate {
|
|
129
|
+
private readonly deps;
|
|
130
|
+
private readonly scheduler;
|
|
131
|
+
private readonly pollIntervalMs;
|
|
132
|
+
private timer;
|
|
133
|
+
private stopped;
|
|
134
|
+
private opened;
|
|
135
|
+
private probing;
|
|
136
|
+
constructor(deps: StrandFirstSyncGateDeps, config?: StrandFirstSyncConfig);
|
|
137
|
+
/** The database this gate is holding back. */
|
|
138
|
+
get database(): StrandDatabase;
|
|
139
|
+
/** True once the Header was seen (or the gate was force-opened); the loop is then done. */
|
|
140
|
+
get isOpen(): boolean;
|
|
141
|
+
/** Arm the probe loop. The first probe runs after one interval — the caller already probed once. */
|
|
142
|
+
start(): void;
|
|
143
|
+
/**
|
|
144
|
+
* Force-open: the caller has just written the Header itself (a founder bootstrap run
|
|
145
|
+
* against the gated database). Stops the loop; `onHeaderHeld` is NOT invoked.
|
|
146
|
+
*/
|
|
147
|
+
open(): void;
|
|
148
|
+
/** Disarm the loop; a probe already in flight completes and is then ignored. */
|
|
149
|
+
stop(): void;
|
|
150
|
+
private schedule;
|
|
151
|
+
private probe;
|
|
152
|
+
}
|