@serfab/cadre-core 1.16.0 → 1.17.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 +1 -1
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +55 -138
- package/dist/cadre-invite-protocol.js +94 -449
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +530 -247
- package/dist/cadre-node.js +1046 -525
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +22 -23
- package/dist/control-cohort.js +24 -24
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +416 -136
- package/dist/control-database.js +757 -378
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +5 -4
- package/dist/control-protocol-guard.js +6 -3
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +80 -0
- package/dist/control-schema-version.js +203 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +254 -70
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +5 -5
- package/dist/control-write-retry.js +10 -8
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +26 -7
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +3 -3
- package/dist/joined-strand-store.js +3 -3
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +250 -0
- package/dist/member-redemption.js +485 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +247 -0
- package/dist/owner-grant-protocol.js +330 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +131 -109
- package/dist/seed-bootstrap.js +260 -248
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +1 -0
- package/dist/strand-database.js +3 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +4 -3
- package/dist/strand-first-sync-gate.js +4 -3
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +1 -1
- package/dist/strand-membership-writer.d.ts +13 -2
- package/dist/strand-membership-writer.js +52 -16
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +31 -30
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/types.d.ts +286 -103
- package/dist/types.js.map +1 -1
- package/package.json +11 -2
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +652 -1052
- package/src/cadre-join-request.ts +24 -3
- package/src/cadre-node.ts +1166 -541
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +127 -128
- package/src/control-database.ts +940 -456
- package/src/control-protocol-guard.ts +7 -4
- package/src/control-schema-version.ts +251 -0
- package/src/control-schema.ts +265 -70
- package/src/control-write-retry.ts +569 -567
- package/src/delegate-admission.ts +37 -0
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/index.ts +886 -789
- package/src/joined-strand-store.ts +4 -4
- package/src/key-store.ts +10 -4
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +642 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +532 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-authorization.ts +558 -488
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +301 -312
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +3 -0
- package/src/strand-first-sync-gate.ts +4 -3
- package/src/strand-instance-manager.ts +1 -1
- package/src/strand-membership-writer.ts +61 -16
- package/src/strand-move.ts +45 -36
- package/src/strand-peer-redial.ts +2 -3
- package/src/types.ts +304 -103
package/src/strand-cohort.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { NodeProfile } from './types.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* A single CadrePeer membership row, as returned by
|
|
3
5
|
* `ControlDatabase.queryCadrePeers()`.
|
|
@@ -6,6 +8,8 @@ export interface CohortPeerRow {
|
|
|
6
8
|
peerId: string;
|
|
7
9
|
/** Comma-joined list of multiaddr strings; `''` when no addrs were recorded. */
|
|
8
10
|
multiaddr: string | null;
|
|
11
|
+
/** The machine's self-declared profile; null or absent until it self-publishes. */
|
|
12
|
+
profile?: NodeProfile | null;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
/**
|
package/src/strand-database.ts
CHANGED
|
@@ -76,6 +76,7 @@ export class StrandDatabase {
|
|
|
76
76
|
private db: Database | null = null;
|
|
77
77
|
private shutdownStrand: SereusPluginResult['shutdown'] | null = null;
|
|
78
78
|
private resolvedTransactor: SereusPluginResult['transactor'] | null = null;
|
|
79
|
+
private createEmptyCollections: SereusPluginResult['createEmptyCollections'] | null = null;
|
|
79
80
|
private readonly config: StrandDatabaseConfig;
|
|
80
81
|
private initialized = false;
|
|
81
82
|
private readonly commits: StrandCommitRecord;
|
|
@@ -131,6 +132,7 @@ export class StrandDatabase {
|
|
|
131
132
|
});
|
|
132
133
|
this.shutdownStrand = result.shutdown;
|
|
133
134
|
this.resolvedTransactor = result.transactor;
|
|
135
|
+
this.createEmptyCollections = result.createEmptyCollections;
|
|
134
136
|
// Before the founder bootstrap, so its commits are reported like any other.
|
|
135
137
|
this.subscribeToCommits(result);
|
|
136
138
|
timing('[strandDb:%s] connectToStrand: %dms (hydrated tables=%d, indexes=%d)',
|
|
@@ -181,6 +183,7 @@ export class StrandDatabase {
|
|
|
181
183
|
sharedMemberPublicKey: closed && memberPrivateKey
|
|
182
184
|
? strandMemberKeyPair(memberPrivateKey).publicKeyB64
|
|
183
185
|
: undefined,
|
|
186
|
+
createEmptyCollections: this.createEmptyCollections!,
|
|
184
187
|
});
|
|
185
188
|
}
|
|
186
189
|
|
|
@@ -40,9 +40,10 @@
|
|
|
40
40
|
* Every probe is a read (`Strand.Header`, then `select count(1)` on each `App` table). Reads
|
|
41
41
|
* never invent a collection (Optimystic's `Collection.open` resolves undefined on an
|
|
42
42
|
* authoritatively absent header, and throws on an unreachable one), so probing is safe to
|
|
43
|
-
* repeat and a throw is just "not yet".
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* repeat and a throw is just "not yet". Every table declared at founding has a collection
|
|
44
|
+
* from the start (the founder bootstrap commits them all before the Header), so a probe read
|
|
45
|
+
* fetches it; a table a later schema version adds reads as absent until its first write
|
|
46
|
+
* creates the collection — see `docs/strands.md` ("Joining") for that residual.
|
|
46
47
|
*
|
|
47
48
|
* Owned by `StrandInstanceManager`: created in `buildStrandRuntime` for every launch, armed
|
|
48
49
|
* only for a non-founder launch whose first probe fails, stopped and dropped in
|
|
@@ -142,7 +142,7 @@ export interface StartStrandConfig {
|
|
|
142
142
|
* StrandDatabase so the founder bootstrap (Header, founding Member/Manager)
|
|
143
143
|
* runs once at bring-up. Joiners leave this unset and write nothing. Callers
|
|
144
144
|
* resolve it BEFORE calling `startStrand` (`CadreNode.launchStrand` derives it
|
|
145
|
-
* from the row's `
|
|
145
|
+
* from the row's `FounderPeerId` when no explicit flag is given — see
|
|
146
146
|
* {@link StrandConfig.founder}); a founder request that arrives while the
|
|
147
147
|
* instance is already tracked goes through {@link StrandInstanceManager.foundExistingStrand},
|
|
148
148
|
* never through a repeat `startStrand`.
|
|
@@ -305,6 +305,12 @@ export interface FounderBootstrapParams {
|
|
|
305
305
|
* holds no shared key has nothing to compare against). Ignored for an open strand.
|
|
306
306
|
*/
|
|
307
307
|
sharedMemberPublicKey?: string;
|
|
308
|
+
/**
|
|
309
|
+
* Commit an empty collection for every table the strand declares, resolving to how many
|
|
310
|
+
* it committed: the composition's `createEmptyCollections`. Run once, on the founding run
|
|
311
|
+
* that finds no `Header` — see {@link createDeclaredCollections}.
|
|
312
|
+
*/
|
|
313
|
+
createEmptyCollections: () => Promise<number>;
|
|
308
314
|
}
|
|
309
315
|
|
|
310
316
|
/**
|
|
@@ -356,12 +362,6 @@ async function strandTableCount(db: Database, table: 'Header' | 'Member' | 'Mana
|
|
|
356
362
|
* `Revocation` schema comment already flags, tracked by `debt-strand-tombstone-reap`
|
|
357
363
|
* — this wants a stored "founding closed" marker rather than a scan per call.
|
|
358
364
|
*
|
|
359
|
-
* NOTE: on a strand that has never revoked anyone, `Strand.Revocation` has never been
|
|
360
|
-
* written, so this node does not hold its block, and the storage layer consults the
|
|
361
|
-
* block's cohort on every read of a block it does not hold — this scan and every
|
|
362
|
-
* `NotRevoked` insert check pay that per call. Not measured, and cheap while strand
|
|
363
|
-
* membership writes are rare; if they become frequent, file a singleton marker row in
|
|
364
|
-
* the strand schema the way the control schema does (`ControlDatabase.openRevocationLedger`).
|
|
365
365
|
*/
|
|
366
366
|
async function strandHasManagerRevocation(db: Database): Promise<boolean> {
|
|
367
367
|
for await (const row of db.eval('select TableName from Strand.Revocation')) {
|
|
@@ -373,7 +373,50 @@ async function strandHasManagerRevocation(db: Database): Promise<boolean> {
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
/**
|
|
376
|
-
*
|
|
376
|
+
* Found the strand if its `Strand.Header` is absent: commit every declared table's
|
|
377
|
+
* collection ({@link createDeclaredCollections}), then insert the Header. A Header
|
|
378
|
+
* already present means the strand was founded before, so both are skipped.
|
|
379
|
+
*/
|
|
380
|
+
async function foundIfHeaderAbsent(db: Database, params: FounderBootstrapParams): Promise<void> {
|
|
381
|
+
if (await strandTableCount(db, 'Header') > 0) {
|
|
382
|
+
log('Header already present for strand %s; skipping', params.strandId);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
await createDeclaredCollections(params);
|
|
386
|
+
await insertHeader(db, params);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Commit an empty collection for every table the strand declares, so a read of a table
|
|
391
|
+
* nobody has written is answered from local storage instead of asking the block's cohort
|
|
392
|
+
* to prove the collection absent, and still answers with the other members offline.
|
|
393
|
+
* Peer-join backfill hands these collections to every joiner.
|
|
394
|
+
*
|
|
395
|
+
* Only while the Header is absent, and before it is written: a machine cannot write to a
|
|
396
|
+
* strand before it holds the Header (`strand-first-sync-gate.ts`), so no other machine can
|
|
397
|
+
* yet have created any of these collections and this cannot fork one. On a founder restart
|
|
398
|
+
* the Header is present and this does not run; running it there could commit an empty
|
|
399
|
+
* second history beside a table a joiner wrote while the founder was away. A crash between
|
|
400
|
+
* this and the Header repeats it on the next run, which commits only the collections still
|
|
401
|
+
* missing.
|
|
402
|
+
*
|
|
403
|
+
* NOTE: one `sync()` per collection, in sequence. Measured 2026-10-17 on a lone node over the
|
|
404
|
+
* network transactor (`connectToStrand`, then the step alone, 3 runs each): a nine-table app
|
|
405
|
+
* schema (20 collections with the strand's own) took 32–82 ms in memory and 465–486 ms on
|
|
406
|
+
* `FileRawStorage`; the reference chat schema (13 collections) 19–30 ms and 298–350 ms. About
|
|
407
|
+
* 23 ms per collection on disk, once per strand, before `foundStrand` returns. If founding
|
|
408
|
+
* latency starts to matter (slower phone storage, much larger schemas), commit them as one
|
|
409
|
+
* batch through a coordinator, the way the transaction bridge's `commitBatchLegacy` does.
|
|
410
|
+
*/
|
|
411
|
+
async function createDeclaredCollections(params: FounderBootstrapParams): Promise<void> {
|
|
412
|
+
const t0 = performance.now();
|
|
413
|
+
const committed = await params.createEmptyCollections();
|
|
414
|
+
log('Committed %d empty collections for strand %s in %dms',
|
|
415
|
+
committed, params.strandId, Math.round(performance.now() - t0));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Insert the singleton `Strand.Header`.
|
|
377
420
|
*
|
|
378
421
|
* Every `Header` column is NOT NULL (Quereus defaults unqualified columns to NOT
|
|
379
422
|
* NULL; the table declares no `null` columns), so all eight are supplied non-null.
|
|
@@ -381,11 +424,7 @@ async function strandHasManagerRevocation(db: Database): Promise<boolean> {
|
|
|
381
424
|
* rather than inserting null. `Header` carries only `InsertOnly` + a singleton PK,
|
|
382
425
|
* so the insert needs no `with context`.
|
|
383
426
|
*/
|
|
384
|
-
async function
|
|
385
|
-
if (await strandTableCount(db, 'Header') > 0) {
|
|
386
|
-
log('Header already present for strand %s; skipping', params.strandId);
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
427
|
+
async function insertHeader(db: Database, params: FounderBootstrapParams): Promise<void> {
|
|
389
428
|
await db.exec(
|
|
390
429
|
`insert into Strand.Header
|
|
391
430
|
(Id, Type, sAppId, sAppVersion, sAppSchema, sAppSignature, Engine, EngineVersion)
|
|
@@ -504,6 +543,10 @@ export async function assertNotPreSplitStrand(db: Database, strandId: string, sh
|
|
|
504
543
|
* Optimystic sync. Every write is insert-if-absent (guarded by a row count) so a
|
|
505
544
|
* restart / founder re-`addStrand` is a no-op and never double-inserts.
|
|
506
545
|
*
|
|
546
|
+
* On the run that finds no `Header`, it first commits an empty collection for every
|
|
547
|
+
* table the `Strand` and `App` schemas declare, so no read of a declared table ever
|
|
548
|
+
* needs another member to prove it empty ({@link createDeclaredCollections}).
|
|
549
|
+
*
|
|
507
550
|
* Behavior by strand type:
|
|
508
551
|
* - **Open (`'o'`)**: insert `Header` only. `Member`/`Manager`/`Invite` are
|
|
509
552
|
* `OnlyClosed` and would trip that constraint — they are skipped entirely.
|
|
@@ -523,8 +566,9 @@ export async function assertNotPreSplitStrand(db: Database, strandId: string, sh
|
|
|
523
566
|
* key seated.
|
|
524
567
|
*
|
|
525
568
|
* @param db - The strand's Quereus database (schema already applied).
|
|
526
|
-
* @param params - Strand id/type, the sApp config for the Header,
|
|
527
|
-
*
|
|
569
|
+
* @param params - Strand id/type, the sApp config for the Header, the composition's
|
|
570
|
+
* empty-collection step, and (closed only) the derived founder keypair and the
|
|
571
|
+
* shared-derived public key to refuse.
|
|
528
572
|
* @throws If `type === 'c'` and no `founderKeyPair` is supplied.
|
|
529
573
|
* @throws {PreSplitStrandIdentityError} If `type === 'c'` and a `Strand.Member` or
|
|
530
574
|
* `Strand.Manager` row is `sharedMemberPublicKey`.
|
|
@@ -546,8 +590,9 @@ export async function bootstrapFounderMembership(db: Database, params: FounderBo
|
|
|
546
590
|
await assertNotPreSplitStrand(db, strandId, sharedMemberPublicKey);
|
|
547
591
|
}
|
|
548
592
|
|
|
549
|
-
// Header
|
|
550
|
-
|
|
593
|
+
// Every table's collection and the Header are written for every strand; Member/Manager
|
|
594
|
+
// are closed-only.
|
|
595
|
+
await foundIfHeaderAbsent(db, params);
|
|
551
596
|
|
|
552
597
|
if (type === 'o') {
|
|
553
598
|
// Open strand: Member/Manager/Invite are OnlyClosed — nothing else to write.
|
package/src/strand-move.ts
CHANGED
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
* Built over `CadreNode`'s public surface, so `cadre-node.ts` does not grow by the feature.
|
|
22
22
|
*/
|
|
23
23
|
import debug from 'debug';
|
|
24
|
-
import { sign } from '@optimystic/quereus-plugin-crypto';
|
|
25
24
|
import type { PersistedNetworkState } from '@optimystic/db-p2p';
|
|
26
25
|
import type { CadreNode } from './cadre-node.js';
|
|
26
|
+
import type { OwnerSign } from './owner-signer.js';
|
|
27
27
|
import {
|
|
28
28
|
isPendingJoinConflict,
|
|
29
29
|
isStrandIdConflict,
|
|
@@ -98,10 +98,10 @@ export interface StrandMoveReport {
|
|
|
98
98
|
pendingJoinsMoved: number;
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
/** {@link adoptStrandBundle} refused before any write: the running node's key is not an owner of its party. */
|
|
101
|
+
/** {@link adoptStrandBundle} refused before any write: the running node's owner key is not an owner of its party. */
|
|
102
102
|
export class StrandMoveNotOwnerError extends Error {
|
|
103
103
|
constructor(readonly partyId: string) {
|
|
104
|
-
super(`Cannot adopt strands into party ${partyId}: this node's key is not one of its owners, and every re-seated row is owner-signed`);
|
|
104
|
+
super(`Cannot adopt strands into party ${partyId}: this node's owner key is not one of its owners, and every re-seated row is owner-signed`);
|
|
105
105
|
this.name = 'StrandMoveNotOwnerError';
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -201,10 +201,11 @@ async function exportFormationInvites(database: ControlDatabase, now: number): P
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
|
-
* Seat `bundle` in the running node's party, every row owner-signed
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* ({@link
|
|
204
|
+
* Seat `bundle` in the running node's party, every row owner-signed through the node's owner
|
|
205
|
+
* signer (`CadreNode.getOwnerSigner`, wired here by `wireOwnKeyIfOwner` when it is not yet),
|
|
206
|
+
* in one approval for the whole bundle. Refuses up front, with a typed error and no write, when
|
|
207
|
+
* that signer's key is not an owner of the party ({@link StrandMoveNotOwnerError}) or the
|
|
208
|
+
* bundle came from this very party ({@link StrandMoveSamePartyError}).
|
|
208
209
|
*
|
|
209
210
|
* Strands first, then the invitations, then the pending joins. Every write is idempotent (a row
|
|
210
211
|
* already present with the same content is skipped and counted as moved), so the same bundle
|
|
@@ -221,11 +222,28 @@ export async function adoptStrandBundle(node: CadreNode, bundle: StrandBundle):
|
|
|
221
222
|
if (bundle.partyId === node.partyId) {
|
|
222
223
|
throw new StrandMoveSamePartyError(node.partyId);
|
|
223
224
|
}
|
|
224
|
-
|
|
225
|
+
const owner = (await node.wireOwnKeyIfOwner()) ? node.getOwnerSigner() : null;
|
|
226
|
+
if (owner === null) {
|
|
225
227
|
throw new StrandMoveNotOwnerError(node.partyId);
|
|
226
228
|
}
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
+
const action = `Move ${bundle.strands.length} strand(s) from the cadre ${bundle.partyId} into this one`;
|
|
230
|
+
return await owner.approve({ partyId: node.partyId, action }, (sign) =>
|
|
231
|
+
adoptUnderApproval(node, database, bundle, { ownerKey: owner.publicKey, sign }));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** An open owner approval: the key every row is written under, and the approval's `sign`. */
|
|
235
|
+
interface OwnerApproval {
|
|
236
|
+
ownerKey: string;
|
|
237
|
+
sign: OwnerSign;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** {@link adoptStrandBundle}'s writes, every one signed inside the one approval `approval`. */
|
|
241
|
+
async function adoptUnderApproval(
|
|
242
|
+
node: CadreNode,
|
|
243
|
+
database: ControlDatabase,
|
|
244
|
+
bundle: StrandBundle,
|
|
245
|
+
approval: OwnerApproval
|
|
246
|
+
): Promise<StrandMoveReport> {
|
|
229
247
|
const report: StrandMoveReport = {
|
|
230
248
|
fromPartyId: bundle.partyId,
|
|
231
249
|
moved: [],
|
|
@@ -236,15 +254,15 @@ export async function adoptStrandBundle(node: CadreNode, bundle: StrandBundle):
|
|
|
236
254
|
pendingJoinsMoved: 0
|
|
237
255
|
};
|
|
238
256
|
for (const entry of bundle.strands) {
|
|
239
|
-
await adoptStrand(node, database,
|
|
257
|
+
await adoptStrand(node, database, approval, entry, report);
|
|
240
258
|
}
|
|
241
259
|
const settled = settledStrands(report);
|
|
242
260
|
const now = Date.now();
|
|
243
261
|
for (const invite of bundle.formationInvites) {
|
|
244
|
-
await adoptFormationInvite(node, database, invite, settled, report, now);
|
|
262
|
+
await adoptFormationInvite(node, database, approval.sign, invite, settled, report, now);
|
|
245
263
|
}
|
|
246
264
|
for (const join of bundle.pendingJoins) {
|
|
247
|
-
await adoptPendingJoin(database,
|
|
265
|
+
await adoptPendingJoin(database, approval, join, report);
|
|
248
266
|
}
|
|
249
267
|
log('adopt(%s ← %s): %d moved, %d refused, %d failed; %d invitation(s) moved, %d dropped; %d pending join(s)',
|
|
250
268
|
node.partyId, bundle.partyId, report.moved.length, report.refused.length, report.failed.length,
|
|
@@ -252,11 +270,6 @@ export async function adoptStrandBundle(node: CadreNode, bundle: StrandBundle):
|
|
|
252
270
|
return report;
|
|
253
271
|
}
|
|
254
272
|
|
|
255
|
-
interface OwnerSigner {
|
|
256
|
-
ownerKey: string;
|
|
257
|
-
signMessage: (message: Uint8Array) => string;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
273
|
/** The strands an adoption has decided on, by outcome, for the invitations bound to them. */
|
|
261
274
|
interface SettledStrands {
|
|
262
275
|
moved: Set<string>;
|
|
@@ -282,7 +295,7 @@ function settledStrands(report: StrandMoveReport): SettledStrands {
|
|
|
282
295
|
async function adoptStrand(
|
|
283
296
|
node: CadreNode,
|
|
284
297
|
database: ControlDatabase,
|
|
285
|
-
|
|
298
|
+
approval: OwnerApproval,
|
|
286
299
|
entry: StrandBundleEntry,
|
|
287
300
|
report: StrandMoveReport
|
|
288
301
|
): Promise<void> {
|
|
@@ -297,12 +310,12 @@ async function adoptStrand(
|
|
|
297
310
|
}
|
|
298
311
|
await seatStrandPeers(node, entry);
|
|
299
312
|
if (entry.partyKey !== null) {
|
|
300
|
-
await node.ensureStrandPartyKey(strandId, entry.partyKey);
|
|
313
|
+
await node.ensureStrandPartyKey(strandId, entry.partyKey, approval.sign);
|
|
301
314
|
}
|
|
302
315
|
if (entry.kind === 'own') {
|
|
303
|
-
await node.publishStrand(strandId, entry.row.Type, entry.row.MemberPrivateKey ?? undefined);
|
|
316
|
+
await node.publishStrand(strandId, entry.row.Type, entry.row.MemberPrivateKey ?? undefined, approval.sign);
|
|
304
317
|
} else {
|
|
305
|
-
await seatJoinedStrand(database,
|
|
318
|
+
await seatJoinedStrand(database, approval, entry.row, live.joined);
|
|
306
319
|
}
|
|
307
320
|
report.moved.push(strandId);
|
|
308
321
|
} catch (error) {
|
|
@@ -377,7 +390,7 @@ async function seatStrandPeers(node: CadreNode, entry: StrandBundleEntry): Promi
|
|
|
377
390
|
*/
|
|
378
391
|
async function seatJoinedStrand(
|
|
379
392
|
database: ControlDatabase,
|
|
380
|
-
|
|
393
|
+
approval: OwnerApproval,
|
|
381
394
|
row: StrandBundleEntry['row'],
|
|
382
395
|
existing: StrandRow | null
|
|
383
396
|
): Promise<void> {
|
|
@@ -385,7 +398,7 @@ async function seatJoinedStrand(
|
|
|
385
398
|
return;
|
|
386
399
|
}
|
|
387
400
|
try {
|
|
388
|
-
await database.insertJoinedStrand(row,
|
|
401
|
+
await database.insertJoinedStrand(row, approval.ownerKey, approval.sign);
|
|
389
402
|
} catch (error) {
|
|
390
403
|
if (!isStrandIdConflict(error, 'JoinedStrand')) {
|
|
391
404
|
throw error;
|
|
@@ -412,6 +425,7 @@ async function seatJoinedStrand(
|
|
|
412
425
|
async function adoptFormationInvite(
|
|
413
426
|
node: CadreNode,
|
|
414
427
|
database: ControlDatabase,
|
|
428
|
+
sign: OwnerSign,
|
|
415
429
|
invite: FormationInviteEntry,
|
|
416
430
|
settled: SettledStrands,
|
|
417
431
|
report: StrandMoveReport,
|
|
@@ -434,7 +448,7 @@ async function adoptFormationInvite(
|
|
|
434
448
|
expiresAtMs: invite.expiresAtMs ?? undefined,
|
|
435
449
|
totalUses: invite.usesLeft ?? undefined,
|
|
436
450
|
strandId: invite.strandId ?? undefined
|
|
437
|
-
});
|
|
451
|
+
}, sign);
|
|
438
452
|
}
|
|
439
453
|
report.invitationsMoved += 1;
|
|
440
454
|
} catch (error) {
|
|
@@ -464,15 +478,15 @@ async function adoptFormationInvite(
|
|
|
464
478
|
*/
|
|
465
479
|
async function adoptPendingJoin(
|
|
466
480
|
database: ControlDatabase,
|
|
467
|
-
|
|
481
|
+
approval: OwnerApproval,
|
|
468
482
|
join: PendingJoin,
|
|
469
483
|
report: StrandMoveReport
|
|
470
484
|
): Promise<void> {
|
|
471
485
|
const { StampId: _stampId, outcome, ...request } = join;
|
|
472
486
|
try {
|
|
473
|
-
const live = await seatJoinRequest(database,
|
|
487
|
+
const live = await seatJoinRequest(database, approval, request);
|
|
474
488
|
if (live !== null && outcome !== null && live.outcome === null) {
|
|
475
|
-
await database.recordJoinOutcome(live, outcome,
|
|
489
|
+
await database.recordJoinOutcome(live, outcome, approval.ownerKey, approval.sign);
|
|
476
490
|
}
|
|
477
491
|
report.pendingJoinsMoved += 1;
|
|
478
492
|
} catch (error) {
|
|
@@ -487,9 +501,9 @@ async function adoptPendingJoin(
|
|
|
487
501
|
* own ask, left alone) or a dismissed one whose retired stamp is still held until the reap
|
|
488
502
|
* pass (the party chose not to have it).
|
|
489
503
|
*/
|
|
490
|
-
async function seatJoinRequest(database: ControlDatabase,
|
|
504
|
+
async function seatJoinRequest(database: ControlDatabase, approval: OwnerApproval, request: JoinRequestFields): Promise<PendingJoin | null> {
|
|
491
505
|
try {
|
|
492
|
-
return await database.insertJoinRequest(request,
|
|
506
|
+
return await database.insertJoinRequest(request, approval.ownerKey, approval.sign);
|
|
493
507
|
} catch (error) {
|
|
494
508
|
if (!isPendingJoinConflict(error)) {
|
|
495
509
|
throw error;
|
|
@@ -557,7 +571,7 @@ function isOpenJoin(join: PendingJoin, nowMs: number): boolean {
|
|
|
557
571
|
return join.outcome === null && nowMs < join.ExpiresAt;
|
|
558
572
|
}
|
|
559
573
|
|
|
560
|
-
/** Content only: `Type` and the read secret.
|
|
574
|
+
/** Content only: `Type` and the read secret. The founder columns are provenance, and a moved strand's change. */
|
|
561
575
|
function sameStrandContent(live: StrandRow, desired: StrandBundleEntry['row']): boolean {
|
|
562
576
|
return live.Type === desired.Type && (live.MemberPrivateKey ?? null) === (desired.MemberPrivateKey ?? null);
|
|
563
577
|
}
|
|
@@ -575,11 +589,6 @@ function requireControlDatabase(node: CadreNode, action: string): ControlDatabas
|
|
|
575
589
|
return database;
|
|
576
590
|
}
|
|
577
591
|
|
|
578
|
-
function signMessageWith(privateKeyB64: string): (message: Uint8Array) => string {
|
|
579
|
-
return (message: Uint8Array): string =>
|
|
580
|
-
sign(message, privateKeyB64, 'ed25519', 'bytes', 'base64url', 'base64url') as string;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
592
|
function errorText(error: unknown): string {
|
|
584
593
|
return error instanceof Error ? error.message : String(error);
|
|
585
594
|
}
|
|
@@ -234,9 +234,8 @@ class StrandPeerRedialLoop implements StrandPeerRedial {
|
|
|
234
234
|
*
|
|
235
235
|
* NOTE: a round whose dials failed within milliseconds (a relay connection that is still
|
|
236
236
|
* closing, the member's relay reservation not back yet) waits as long as one whose dials ran
|
|
237
|
-
* out their budget.
|
|
238
|
-
*
|
|
239
|
-
* ever matters, retry a round that failed that fast after a short delay instead.
|
|
237
|
+
* out their budget. If reconnecting within seconds of a cut ever matters, retry a round that
|
|
238
|
+
* failed that fast after a short delay instead.
|
|
240
239
|
*
|
|
241
240
|
* NOTE: one such fast failure is `ConnectionClosedError: The connection muxer is "closed"`. A
|
|
242
241
|
* relay that aborts its connection to this node cannot tell it so (`@libp2p/websockets`'
|