@serfab/cadre-core 1.17.0 → 1.19.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 +3 -2
- package/dist/cadre-invite-protocol.d.ts +35 -13
- package/dist/cadre-invite-protocol.js +90 -25
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-node.d.ts +586 -255
- package/dist/cadre-node.js +1182 -566
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-cohort.d.ts +66 -9
- package/dist/control-cohort.js +67 -9
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +451 -193
- package/dist/control-database.js +701 -359
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +46 -43
- package/dist/control-formation-recorder.js +77 -78
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +6 -0
- package/dist/control-protocol-guard.js +14 -2
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +48 -31
- package/dist/control-schema-version.js +67 -45
- package/dist/control-schema-version.js.map +1 -1
- package/dist/control-schema.d.ts +8 -8
- package/dist/control-schema.js +16 -16
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +42 -7
- package/dist/control-write-retry.js +87 -19
- package/dist/control-write-retry.js.map +1 -1
- package/dist/enrolled-machine-store.d.ts +3 -3
- package/dist/enrolled-machine-store.js +3 -3
- package/dist/formation-token.d.ts +48 -0
- package/dist/formation-token.js +26 -0
- package/dist/formation-token.js.map +1 -0
- package/dist/index.d.ts +15 -14
- package/dist/index.js +17 -14
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +26 -9
- package/dist/joined-strand-store.js +63 -24
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/link-budget.d.ts +9 -7
- package/dist/link-budget.js +9 -7
- package/dist/link-budget.js.map +1 -1
- package/dist/member-redemption.d.ts +15 -9
- package/dist/member-redemption.js +13 -8
- package/dist/member-redemption.js.map +1 -1
- package/dist/membership-view.d.ts +91 -0
- package/dist/membership-view.js +110 -0
- package/dist/membership-view.js.map +1 -0
- package/dist/owner-grant-protocol.d.ts +11 -7
- package/dist/owner-grant-protocol.js +31 -17
- package/dist/owner-grant-protocol.js.map +1 -1
- package/dist/peer-addr-book.d.ts +10 -1
- package/dist/peer-addr-book.js +4 -3
- package/dist/peer-addr-book.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +96 -30
- package/dist/peer-join-backfill.js +272 -128
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/pending-join-runner.d.ts +48 -18
- package/dist/pending-join-runner.js +74 -56
- package/dist/pending-join-runner.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +47 -40
- package/dist/seed-bootstrap.js +66 -61
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/self-address-update.d.ts +13 -0
- package/dist/self-address-update.js +19 -0
- package/dist/self-address-update.js.map +1 -0
- package/dist/strand-database.d.ts +23 -2
- package/dist/strand-database.js +26 -6
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +176 -89
- package/dist/strand-first-sync-gate.js +288 -122
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +9 -9
- package/dist/strand-formation-deadlines.js +9 -9
- package/dist/strand-formation-manager.d.ts +23 -17
- package/dist/strand-formation-manager.js +46 -44
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +5 -8
- package/dist/strand-formation-protocol.js +101 -16
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +26 -17
- package/dist/strand-instance-manager.js +119 -50
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-reconciler.d.ts +16 -13
- package/dist/strand-membership-reconciler.js +28 -22
- package/dist/strand-membership-reconciler.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +40 -28
- package/dist/strand-membership-writer.js +212 -153
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.js +5 -2
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-revocation-enforcer.d.ts +134 -60
- package/dist/strand-revocation-enforcer.js +144 -71
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +50 -39
- package/dist/strand-solicitation.js +10 -3
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-watcher.d.ts +80 -13
- package/dist/strand-watcher.js +152 -48
- package/dist/strand-watcher.js.map +1 -1
- package/dist/table-change-watch.d.ts +63 -0
- package/dist/table-change-watch.js +209 -0
- package/dist/table-change-watch.js.map +1 -0
- package/dist/timeout-scheduler.d.ts +4 -3
- package/dist/timeout-scheduler.js +4 -3
- package/dist/timeout-scheduler.js.map +1 -1
- package/dist/types.d.ts +90 -49
- package/dist/types.js +4 -4
- package/dist/types.js.map +1 -1
- package/package.json +8 -8
- package/src/cadre-invite-protocol.ts +119 -31
- package/src/cadre-node.ts +1289 -588
- package/src/control-cohort.ts +86 -9
- package/src/control-database.ts +848 -403
- package/src/control-formation-recorder.ts +359 -353
- package/src/control-protocol-guard.ts +16 -2
- package/src/control-schema-version.ts +101 -51
- package/src/control-schema.ts +16 -16
- package/src/control-write-retry.ts +91 -19
- package/src/delegate-admission.ts +351 -351
- package/src/enrolled-machine-store.ts +3 -3
- package/src/formation-token.ts +63 -0
- package/src/index.ts +30 -18
- package/src/joined-strand-store.ts +69 -24
- package/src/link-budget.ts +9 -7
- package/src/member-redemption.ts +20 -13
- package/src/membership-view.ts +144 -0
- package/src/owner-grant-protocol.ts +33 -18
- package/src/peer-addr-book.ts +4 -3
- package/src/peer-join-backfill.ts +961 -761
- package/src/pending-join-runner.ts +76 -53
- package/src/seed-bootstrap.ts +72 -67
- package/src/self-address-update.ts +19 -0
- package/src/strand-database.ts +352 -322
- package/src/strand-first-sync-gate.ts +334 -134
- package/src/strand-formation-deadlines.ts +227 -227
- package/src/strand-formation-manager.ts +731 -725
- package/src/strand-formation-protocol.ts +122 -24
- package/src/strand-instance-manager.ts +2092 -2026
- package/src/strand-membership-reconciler.ts +31 -25
- package/src/strand-membership-writer.ts +2093 -2019
- package/src/strand-move.ts +5 -2
- package/src/strand-revocation-enforcer.ts +740 -654
- package/src/strand-solicitation.ts +61 -34
- package/src/strand-watcher.ts +175 -56
- package/src/table-change-watch.ts +285 -0
- package/src/timeout-scheduler.ts +29 -28
- package/src/types.ts +89 -51
|
@@ -219,8 +219,8 @@ async function execStrandTransaction(db, statements, options) {
|
|
|
219
219
|
*/
|
|
220
220
|
function revocationStatement(tableName, stampId, retiree) {
|
|
221
221
|
return {
|
|
222
|
-
sql: `insert into Strand.Revocation (TableName, StampId)
|
|
223
|
-
with context MemberKey = :revocationSignerKey, Signature = :revocationSignature
|
|
222
|
+
sql: `insert into Strand.Revocation (TableName, StampId)
|
|
223
|
+
with context MemberKey = :revocationSignerKey, Signature = :revocationSignature
|
|
224
224
|
values (:revocationTable, :revocationStampId);`,
|
|
225
225
|
params: {
|
|
226
226
|
revocationSignerKey: retiree.publicKeyB64,
|
|
@@ -286,47 +286,37 @@ async function strandHasManagerRevocation(db) {
|
|
|
286
286
|
return false;
|
|
287
287
|
}
|
|
288
288
|
/**
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Commit an empty collection for every table the strand declares, so a read of a table
|
|
303
|
-
* nobody has written is answered from local storage instead of asking the block's cohort
|
|
304
|
-
* to prove the collection absent, and still answers with the other members offline.
|
|
305
|
-
* Peer-join backfill hands these collections to every joiner.
|
|
306
|
-
*
|
|
307
|
-
* Only while the Header is absent, and before it is written: a machine cannot write to a
|
|
308
|
-
* strand before it holds the Header (`strand-first-sync-gate.ts`), so no other machine can
|
|
309
|
-
* yet have created any of these collections and this cannot fork one. On a founder restart
|
|
310
|
-
* the Header is present and this does not run; running it there could commit an empty
|
|
311
|
-
* second history beside a table a joiner wrote while the founder was away. A crash between
|
|
312
|
-
* this and the Header repeats it on the next run, which commits only the collections still
|
|
289
|
+
* Commit an empty collection for every table the strand declares except `writtenTables`, so a
|
|
290
|
+
* read of a table nobody has written is answered from local storage instead of asking the
|
|
291
|
+
* block's cohort to prove the collection absent, and still answers with the other members
|
|
292
|
+
* offline. Peer-join backfill hands these collections to every joiner. The founding
|
|
293
|
+
* transaction's own tables are left out because its inserts create their collections anyway.
|
|
294
|
+
*
|
|
295
|
+
* Only while the Header is absent, and before the founding transaction writes it: a machine
|
|
296
|
+
* cannot write to a strand before it holds the Header (`strand-first-sync-gate.ts`), so no other
|
|
297
|
+
* machine can yet have created any of these collections and this cannot fork one. On a founder
|
|
298
|
+
* restart the Header is present and this does not run; running it there could commit an empty
|
|
299
|
+
* second history beside a table a joiner wrote while the founder was away. A crash between this
|
|
300
|
+
* and the founding commit repeats it on the next run, which commits only the collections still
|
|
313
301
|
* missing.
|
|
314
302
|
*
|
|
315
303
|
* NOTE: one `sync()` per collection, in sequence. Measured 2026-10-17 on a lone node over the
|
|
316
304
|
* network transactor (`connectToStrand`, then the step alone, 3 runs each): a nine-table app
|
|
317
305
|
* schema (20 collections with the strand's own) took 32–82 ms in memory and 465–486 ms on
|
|
318
306
|
* `FileRawStorage`; the reference chat schema (13 collections) 19–30 ms and 298–350 ms. About
|
|
319
|
-
* 23 ms per collection on disk, once per strand, before `foundStrand` returns.
|
|
320
|
-
*
|
|
321
|
-
*
|
|
307
|
+
* 23 ms per collection on disk, once per strand, before `foundStrand` returns. Leaving out the
|
|
308
|
+
* founding transaction's tables brings the chat schema to 6 collections on a closed strand whose
|
|
309
|
+
* founder binds its peer, and 12 on an open one. If founding latency starts to matter (slower
|
|
310
|
+
* phone storage, much larger schemas), commit them as one batch through a coordinator, the way
|
|
311
|
+
* the transaction bridge's `commitBatchLegacy` does.
|
|
322
312
|
*/
|
|
323
|
-
async function createDeclaredCollections(params) {
|
|
313
|
+
async function createDeclaredCollections(params, writtenTables) {
|
|
324
314
|
const t0 = performance.now();
|
|
325
|
-
const committed = await params.createEmptyCollections();
|
|
315
|
+
const committed = await params.createEmptyCollections(writtenTables);
|
|
326
316
|
log('Committed %d empty collections for strand %s in %dms', committed, params.strandId, Math.round(performance.now() - t0));
|
|
327
317
|
}
|
|
328
318
|
/**
|
|
329
|
-
*
|
|
319
|
+
* The singleton `Strand.Header` insert.
|
|
330
320
|
*
|
|
331
321
|
* Every `Header` column is NOT NULL (Quereus defaults unqualified columns to NOT
|
|
332
322
|
* NULL; the table declares no `null` columns), so all eight are supplied non-null.
|
|
@@ -334,84 +324,127 @@ async function createDeclaredCollections(params) {
|
|
|
334
324
|
* rather than inserting null. `Header` carries only `InsertOnly` + a singleton PK,
|
|
335
325
|
* so the insert needs no `with context`.
|
|
336
326
|
*/
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
327
|
+
function headerRow(params) {
|
|
328
|
+
return {
|
|
329
|
+
table: 'Strand.Header',
|
|
330
|
+
statements: {
|
|
331
|
+
sql: `insert into Strand.Header
|
|
332
|
+
(Id, Type, sAppId, sAppVersion, sAppSchema, sAppSignature, Engine, EngineVersion)
|
|
333
|
+
values (:headerId, :headerType, :headerSAppId, :headerSAppVersion, :headerSAppSchema,
|
|
334
|
+
:headerSAppSignature, :headerEngine, :headerEngineVersion);`,
|
|
335
|
+
params: {
|
|
336
|
+
headerId: params.strandId,
|
|
337
|
+
headerType: params.type,
|
|
338
|
+
headerSAppId: params.sApp.id,
|
|
339
|
+
headerSAppVersion: params.sApp.version,
|
|
340
|
+
headerSAppSchema: params.sApp.schema,
|
|
341
|
+
headerSAppSignature: params.sApp.signature ?? '',
|
|
342
|
+
headerEngine: STRAND_ENGINE,
|
|
343
|
+
headerEngineVersion: STRAND_ENGINE_VERSION,
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
};
|
|
351
347
|
}
|
|
352
348
|
/**
|
|
353
|
-
*
|
|
349
|
+
* The founding `Strand.Member` insert.
|
|
354
350
|
*
|
|
355
351
|
* The empty PRE-transaction member set (`count(1) from committed.Member = 0`)
|
|
356
352
|
* satisfies the bootstrap branch of `Member.Authorized`, so no signature is
|
|
357
353
|
* needed — the context fields are explicit nulls. The branch also caps the
|
|
358
354
|
* POST-image at one member, so it waives authorization for exactly this single
|
|
359
355
|
* seat, never for a batch. The row still mints a fresh StampId — the column is
|
|
360
|
-
* NOT NULL on every path; bootstrap only waives the SIGNATURE.
|
|
361
|
-
* count makes this idempotent: a re-run (the founder re-`addStrand`/
|
|
362
|
-
* `resumeStrand`) finds the member present and skips.
|
|
356
|
+
* NOT NULL on every path; bootstrap only waives the SIGNATURE.
|
|
363
357
|
*/
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
358
|
+
function founderMemberRow(memberKey) {
|
|
359
|
+
return {
|
|
360
|
+
table: 'Strand.Member',
|
|
361
|
+
statements: {
|
|
362
|
+
sql: `insert into Strand.Member (Key, StampId)
|
|
363
|
+
with context ManagerKey = null, ManagerSignature = null, MemberSignature = null
|
|
364
|
+
values (:founderMemberKey, :founderMemberStampId);`,
|
|
365
|
+
params: { founderMemberKey: memberKey, founderMemberStampId: generateStrandStampId() },
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* The founding `Strand.Manager` insert, at `Generation = 0` — the root of the manager
|
|
371
|
+
* lineage; the bootstrap branch of `Manager.Authorized` requires exactly that value, and
|
|
372
|
+
* every later manager is seated at a strictly greater generation (see {@link addManager}).
|
|
373
|
+
* No signature is needed (a fresh StampId is still minted — the column is NOT NULL).
|
|
374
|
+
*
|
|
375
|
+
* The bootstrap branch, `Manager.MemberExists` and `Manager.OnlyClosed` all have subqueries,
|
|
376
|
+
* so they defer to commit and read the live `Member` and `Header` tables: a founding
|
|
377
|
+
* `Manager` whose `Member` and `Header` are neither committed already nor in its own
|
|
378
|
+
* transaction is rejected.
|
|
379
|
+
*/
|
|
380
|
+
function founderManagerRow(memberKey) {
|
|
381
|
+
return {
|
|
382
|
+
table: 'Strand.Manager',
|
|
383
|
+
statements: {
|
|
384
|
+
sql: `insert into Strand.Manager (MemberKey, Generation, StampId)
|
|
385
|
+
with context ManagerKey = null, Signature = null
|
|
386
|
+
values (:founderManagerKey, 0, :founderManagerStampId);`,
|
|
387
|
+
params: { founderManagerKey: memberKey, founderManagerStampId: generateStrandStampId() },
|
|
388
|
+
},
|
|
389
|
+
};
|
|
373
390
|
}
|
|
374
391
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
* no
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
* Idempotent via the same count guard.
|
|
383
|
-
*
|
|
384
|
-
* ORDERING IS LOAD-BEARING, not merely convenient. The bootstrap branch is gated to
|
|
385
|
-
* the founding state and now reads `exists (select 1 from Member M where M.Key =
|
|
386
|
-
* new.MemberKey)` alongside `count(Manager) <= 1` and `count(Member) <= 1`, so the
|
|
387
|
-
* founding `Member` row MUST already exist when this commits — a Manager-first
|
|
388
|
-
* seeding path is rejected. It must also run AFTER the Header insert:
|
|
389
|
-
* `Manager.OnlyClosed` is a deferred (subquery) check evaluated at commit. The
|
|
390
|
-
* sequential auto-commit Header→Member→Manager order in
|
|
391
|
-
* {@link bootstrapFounderMembership} satisfies both.
|
|
392
|
-
*
|
|
393
|
-
* SEALED strands are skipped, not re-founded: a strand whose sole manager has
|
|
394
|
-
* {@link sealStrand}ed also has zero `Manager` rows, but its founding branch is
|
|
395
|
-
* closed for good (the schema's bootstrap branch requires no `Manager`-table
|
|
396
|
-
* `Revocation` row to exist). Attempting the founding insert here would be
|
|
397
|
-
* schema-rejected and turn every founder restart of a sealed strand into a
|
|
398
|
-
* loud failure — so this mirrors the schema's own gate and returns quietly.
|
|
399
|
-
* A crash BETWEEN the founding Member and Manager inserts leaves no Revocation
|
|
400
|
-
* rows at all, so that restart still completes the founding.
|
|
392
|
+
* Whether the founding `Strand.Manager` still has to be seated: no manager exists and the
|
|
393
|
+
* strand is not SEALED. A strand whose sole manager has {@link sealStrand}ed also has zero
|
|
394
|
+
* `Manager` rows, but its founding branch is closed for good (the schema's bootstrap branch
|
|
395
|
+
* requires no `Manager`-table `Revocation` row to exist). Seating it there would be
|
|
396
|
+
* schema-rejected and turn every founder restart of a sealed strand into a loud failure — so
|
|
397
|
+
* this mirrors the schema's own gate. A founding that never committed its Manager leaves no
|
|
398
|
+
* Revocation rows at all, so that restart still completes the founding.
|
|
401
399
|
*/
|
|
402
|
-
async function
|
|
400
|
+
async function founderManagerMissing(db, strandId) {
|
|
403
401
|
if (await strandTableCount(db, 'Manager') > 0) {
|
|
404
402
|
log('Manager already present for strand %s; skipping founder Manager', strandId);
|
|
405
|
-
return;
|
|
403
|
+
return false;
|
|
406
404
|
}
|
|
407
405
|
if (await strandHasManagerRevocation(db)) {
|
|
408
406
|
log('Strand %s has retired a manager seat (sealed); skipping founder Manager', strandId);
|
|
409
|
-
return;
|
|
407
|
+
return false;
|
|
410
408
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* This machine's `Strand.MemberPeer` binding, unless there is no peer id to bind or the
|
|
413
|
+
* binding is already present (a re-run after a torn founding commit).
|
|
414
|
+
*/
|
|
415
|
+
async function founderBindingIfAbsent(db, founderKeyPair, peerId) {
|
|
416
|
+
if (peerId === undefined) {
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
if (await memberPeerStampId(db, founderKeyPair.publicKeyB64, peerId) !== undefined) {
|
|
420
|
+
log('MemberPeer (%s, %s) already present; skipping founder binding', founderKeyPair.publicKeyB64, peerId);
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
return [{ table: 'Strand.MemberPeer', statements: memberPeerInsertStatement(founderKeyPair, peerId) }];
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The closed strand's founding rows that are still missing, in Member → Manager → MemberPeer
|
|
427
|
+
* order. Each is behind an insert-if-absent guard, so a re-run after a crash or a torn commit
|
|
428
|
+
* writes only what is missing.
|
|
429
|
+
*
|
|
430
|
+
* The binding joins only a run that seats the founding `Member`. With a member row already
|
|
431
|
+
* present the founder may since have left the strand or been removed, and
|
|
432
|
+
* `MemberPeer.MemberExists` would then refuse the whole founding transaction; the membership
|
|
433
|
+
* reconciler, which binds only once it sees this party's member row, writes it instead.
|
|
434
|
+
*/
|
|
435
|
+
async function missingClosedFoundingRows(db, founderKeyPair, params) {
|
|
436
|
+
const memberKey = founderKeyPair.publicKeyB64;
|
|
437
|
+
const seatsMember = await strandTableCount(db, 'Member') === 0;
|
|
438
|
+
if (!seatsMember) {
|
|
439
|
+
log('Member already present for strand %s; skipping founder Member', params.strandId);
|
|
440
|
+
}
|
|
441
|
+
const seatsManager = await founderManagerMissing(db, params.strandId);
|
|
442
|
+
const binding = seatsMember ? await founderBindingIfAbsent(db, founderKeyPair, params.founderPeerId) : [];
|
|
443
|
+
return [
|
|
444
|
+
...(seatsMember ? [founderMemberRow(memberKey)] : []),
|
|
445
|
+
...(seatsManager ? [founderManagerRow(memberKey)] : []),
|
|
446
|
+
...binding,
|
|
447
|
+
];
|
|
415
448
|
}
|
|
416
449
|
/**
|
|
417
450
|
* Refuse a closed strand whose membership still holds the key derived from the shared
|
|
@@ -437,21 +470,23 @@ export async function assertNotPreSplitStrand(db, strandId, sharedMemberPublicKe
|
|
|
437
470
|
*
|
|
438
471
|
* Runs once at bring-up on the strand's FOUNDER (the party that provisioned and
|
|
439
472
|
* published the strand). A joiner never calls this — it receives these rows via
|
|
440
|
-
* Optimystic sync.
|
|
441
|
-
*
|
|
473
|
+
* Optimystic sync. It reads which founding rows are missing, then writes all of them in
|
|
474
|
+
* ONE strand transaction. Every row is insert-if-absent, so a restart / founder
|
|
475
|
+
* re-`addStrand` writes nothing, and a re-run after a failed or torn founding commit
|
|
476
|
+
* writes only what is missing.
|
|
442
477
|
*
|
|
443
478
|
* On the run that finds no `Header`, it first commits an empty collection for every
|
|
444
|
-
* table the `Strand` and `App` schemas declare
|
|
445
|
-
* needs another member to prove it empty
|
|
479
|
+
* table the `Strand` and `App` schemas declare except the ones the founding transaction
|
|
480
|
+
* writes, so no read of a declared table ever needs another member to prove it empty
|
|
481
|
+
* ({@link createDeclaredCollections}).
|
|
446
482
|
*
|
|
447
483
|
* Behavior by strand type:
|
|
448
|
-
* - **Open (`'o'`)**:
|
|
449
|
-
*
|
|
450
|
-
* - **Closed (`'c'`)**:
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
* evaluate at commit.
|
|
484
|
+
* - **Open (`'o'`)**: `Header` only. `Member`/`Manager`/`Invite` are `OnlyClosed` and
|
|
485
|
+
* would trip that constraint — they are skipped entirely.
|
|
486
|
+
* - **Closed (`'c'`)**: `Header(Type='c')`, the founding `Member` and `Manager` (both
|
|
487
|
+
* `founderKeyPair.publicKeyB64`) and, when `founderPeerId` is supplied, this machine's
|
|
488
|
+
* `MemberPeer` binding. Every check that reads another founding row has a subquery, so
|
|
489
|
+
* it defers to commit and sees the whole transaction.
|
|
455
490
|
*
|
|
456
491
|
* A closed strand with no `founderKeyPair` throws: a closed strand with no founding
|
|
457
492
|
* Manager could never admit anyone, so failing loudly here (which propagates out
|
|
@@ -460,12 +495,14 @@ export async function assertNotPreSplitStrand(db, strandId, sharedMemberPublicKe
|
|
|
460
495
|
* A closed strand whose membership holds `sharedMemberPublicKey` throws
|
|
461
496
|
* {@link PreSplitStrandIdentityError} before writing anything: the insert-if-absent
|
|
462
497
|
* guards would otherwise skip over its pre-split founding rows and leave the shared
|
|
463
|
-
* key seated.
|
|
498
|
+
* key seated. Checked only when the `Header` is already present: a strand founded before
|
|
499
|
+
* the key split committed its Header first, alone, so a strand with no Header has none
|
|
500
|
+
* of those rows.
|
|
464
501
|
*
|
|
465
502
|
* @param db - The strand's Quereus database (schema already applied).
|
|
466
503
|
* @param params - Strand id/type, the sApp config for the Header, the composition's
|
|
467
|
-
* empty-collection step, and (closed only) the derived founder keypair
|
|
468
|
-
* shared-derived public key to refuse.
|
|
504
|
+
* empty-collection step, and (closed only) the derived founder keypair, the
|
|
505
|
+
* shared-derived public key to refuse and this machine's peer id to bind.
|
|
469
506
|
* @throws If `type === 'c'` and no `founderKeyPair` is supplied.
|
|
470
507
|
* @throws {PreSplitStrandIdentityError} If `type === 'c'` and a `Strand.Member` or
|
|
471
508
|
* `Strand.Manager` row is `sharedMemberPublicKey`.
|
|
@@ -480,20 +517,35 @@ export async function bootstrapFounderMembership(db, params) {
|
|
|
480
517
|
throw new Error(`Cannot bootstrap closed strand ${strandId}: no founder key pair derived from this party's ` +
|
|
481
518
|
'StrandPartyKey. A closed strand needs a founding Member + Manager or it can never admit members.');
|
|
482
519
|
}
|
|
483
|
-
|
|
520
|
+
const headerPresent = await strandTableCount(db, 'Header') > 0;
|
|
521
|
+
if (headerPresent && type === 'c' && sharedMemberPublicKey !== undefined) {
|
|
484
522
|
await assertNotPreSplitStrand(db, strandId, sharedMemberPublicKey);
|
|
485
523
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
524
|
+
const rows = [
|
|
525
|
+
...(headerPresent ? [] : [headerRow(params)]),
|
|
526
|
+
...(type === 'c' ? await missingClosedFoundingRows(db, founderKeyPair, params) : []),
|
|
527
|
+
];
|
|
528
|
+
if (!headerPresent) {
|
|
529
|
+
await createDeclaredCollections(params, rows.map((row) => row.table));
|
|
530
|
+
}
|
|
531
|
+
if (rows.length === 0) {
|
|
532
|
+
log('Strand %s is already founded; nothing to write', strandId);
|
|
491
533
|
return;
|
|
492
534
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
535
|
+
// NOTE: a torn founding commit (some of the batch's collections saved, others not) can leave
|
|
536
|
+
// the Header committed beside a founding table with no collection, which committing every
|
|
537
|
+
// table's empty collection first used to rule out. A sibling machine of this party could then
|
|
538
|
+
// create that collection itself (its reconciler binding its own peer into MemberPeer). The
|
|
539
|
+
// re-run's guards, and this machine's reconciler, read each table before writing it, and a
|
|
540
|
+
// networked read asks the cohort, so the healing write appends to the sibling's collection
|
|
541
|
+
// unless the sibling is unreachable at that moment. A torn founding that saves this
|
|
542
|
+
// machine's MemberPeer without its Member also leaves an orphaned binding, which every
|
|
543
|
+
// machine's revocation enforcer (this one's and any joiner's) reads as a removed founder
|
|
544
|
+
// until a later run of this bootstrap seats the Member. If torn founding commits are ever
|
|
545
|
+
// observed, commit the founding tables a sibling writes (MemberPeer) as empty collections
|
|
546
|
+
// before this batch again.
|
|
547
|
+
await execStrandTransaction(db, combineStatements(...rows.map((row) => row.statements)));
|
|
548
|
+
log('Founder bootstrap for strand %s wrote %s', strandId, rows.map((row) => row.table).join(', '));
|
|
497
549
|
}
|
|
498
550
|
/**
|
|
499
551
|
* Issue a single-use invitation to join a closed strand.
|
|
@@ -525,8 +577,8 @@ export async function issueInvite(db, params) {
|
|
|
525
577
|
const payload = `${inviteKey}|${expirationSegment}`;
|
|
526
578
|
const managerSignature = signStrandPayload(payload, managerKeyPair.privateKeyB64);
|
|
527
579
|
const inviteSignature = signStrandPayload(payload, invitePrivateKey);
|
|
528
|
-
await db.exec(`insert into Strand.Invite (Key, Expiration)
|
|
529
|
-
with context ManagerKey = ?, ManagerSignature = ?, InviteSignature = ?
|
|
580
|
+
await db.exec(`insert into Strand.Invite (Key, Expiration)
|
|
581
|
+
with context ManagerKey = ?, ManagerSignature = ?, InviteSignature = ?
|
|
530
582
|
values (?, ?)`, [managerKeyPair.publicKeyB64, managerSignature, inviteSignature, inviteKey, expirationColumn]);
|
|
531
583
|
log('Issued invite %s (expires=%s)', inviteKey, expirationColumn ?? 'never');
|
|
532
584
|
return { inviteKey, invitePrivateKey };
|
|
@@ -583,8 +635,8 @@ export async function consumeInvite(db, params, options) {
|
|
|
583
635
|
// Member first — admitted by the deferred invite branch (the matching ConsumedInvite
|
|
584
636
|
// after it), so no manager signature is supplied.
|
|
585
637
|
const member = {
|
|
586
|
-
sql: `insert into Strand.Member (Key, StampId)
|
|
587
|
-
with context ManagerKey = null, ManagerSignature = null, MemberSignature = null
|
|
638
|
+
sql: `insert into Strand.Member (Key, StampId)
|
|
639
|
+
with context ManagerKey = null, ManagerSignature = null, MemberSignature = null
|
|
588
640
|
values (:memberKey, :memberStampId);`,
|
|
589
641
|
params: { memberKey, memberStampId: generateStrandStampId() },
|
|
590
642
|
};
|
|
@@ -605,8 +657,8 @@ export async function consumeInvite(db, params, options) {
|
|
|
605
657
|
async function consumedInviteStatement(db, params) {
|
|
606
658
|
const { inviteKey, invitePrivateKey, memberKey, nowMs } = params;
|
|
607
659
|
return {
|
|
608
|
-
sql: `insert into Strand.ConsumedInvite (InviteKey, MemberKey)
|
|
609
|
-
with context InviteSignature = :consumedSignature, Now = :consumedNow
|
|
660
|
+
sql: `insert into Strand.ConsumedInvite (InviteKey, MemberKey)
|
|
661
|
+
with context InviteSignature = :consumedSignature, Now = :consumedNow
|
|
610
662
|
values (:consumedInviteKey, :consumedMemberKey);`,
|
|
611
663
|
params: {
|
|
612
664
|
consumedSignature: signStrandPayload(`${inviteKey}|${memberKey}`, invitePrivateKey),
|
|
@@ -712,8 +764,8 @@ export async function cancelInvite(db, params) {
|
|
|
712
764
|
return;
|
|
713
765
|
}
|
|
714
766
|
const signature = signStrandApproval(['Strand.CancelledInvite', 'cancel', inviteKey], managerKeyPair.privateKeyB64);
|
|
715
|
-
await db.exec(`insert into Strand.CancelledInvite (InviteKey)
|
|
716
|
-
with context ManagerKey = ?, ManagerSignature = ?
|
|
767
|
+
await db.exec(`insert into Strand.CancelledInvite (InviteKey)
|
|
768
|
+
with context ManagerKey = ?, ManagerSignature = ?
|
|
717
769
|
values (?)`, [managerKeyPair.publicKeyB64, signature, inviteKey]);
|
|
718
770
|
log('Cancelled invite %s by manager %s', inviteKey, managerKeyPair.publicKeyB64);
|
|
719
771
|
}
|
|
@@ -826,8 +878,8 @@ function memberAddByManagerStatement(params) {
|
|
|
826
878
|
const { managerKeyPair, memberKey } = params;
|
|
827
879
|
const stampId = generateStrandStampId();
|
|
828
880
|
return {
|
|
829
|
-
sql: `insert into Strand.Member (Key, StampId)
|
|
830
|
-
with context ManagerKey = :memberAdmitterKey, ManagerSignature = :memberAdmitterSignature, MemberSignature = null
|
|
881
|
+
sql: `insert into Strand.Member (Key, StampId)
|
|
882
|
+
with context ManagerKey = :memberAdmitterKey, ManagerSignature = :memberAdmitterSignature, MemberSignature = null
|
|
831
883
|
values (:memberKey, :memberStampId);`,
|
|
832
884
|
params: {
|
|
833
885
|
memberAdmitterKey: managerKeyPair.publicKeyB64,
|
|
@@ -873,12 +925,11 @@ function memberAddByManagerStatement(params) {
|
|
|
873
925
|
* (`Member.MinOneMember`, a local-count floor with the cross-node caveat its
|
|
874
926
|
* schema NOTE states).
|
|
875
927
|
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
* that call the cut lands up to one poll interval (default 30 s) later.
|
|
928
|
+
* Inside a cadre runtime the removed party's machines are then refused at the
|
|
929
|
+
* network layer by the strand's revoked-peer gate (`strand-revocation-enforcer.ts`),
|
|
930
|
+
* which re-reads membership when this write commits on the runtime's own
|
|
931
|
+
* `Database`. A caller that wants to await the cut follows the write with
|
|
932
|
+
* `CadreNode.refreshRevocationEnforcement(strandId)`, which resolves after it.
|
|
882
933
|
*
|
|
883
934
|
* @param db - The closed strand's database.
|
|
884
935
|
* @param params - The revoking manager keypair and the target member key.
|
|
@@ -896,8 +947,8 @@ export async function revokeMember(db, params, options) {
|
|
|
896
947
|
return;
|
|
897
948
|
}
|
|
898
949
|
const removal = {
|
|
899
|
-
sql: `delete from Strand.Member
|
|
900
|
-
with context ManagerKey = :memberRemoverKey, ManagerSignature = :memberRemoverSignature, MemberSignature = null
|
|
950
|
+
sql: `delete from Strand.Member
|
|
951
|
+
with context ManagerKey = :memberRemoverKey, ManagerSignature = :memberRemoverSignature, MemberSignature = null
|
|
901
952
|
where Key = :memberKey;`,
|
|
902
953
|
params: {
|
|
903
954
|
memberRemoverKey: managerKeyPair.publicKeyB64,
|
|
@@ -922,10 +973,10 @@ export async function revokeMember(db, params, options) {
|
|
|
922
973
|
* The same floors as {@link revokeMember} apply: a manager must resign first
|
|
923
974
|
* (`NotAManager`) and the last member cannot leave (`MinOneMember`).
|
|
924
975
|
*
|
|
925
|
-
* Like {@link revokeMember},
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
*
|
|
976
|
+
* Like {@link revokeMember}, inside a cadre runtime the write wakes the strand's
|
|
977
|
+
* revoked-peer gate, which emits `strand:revoked` when it finds this node's own
|
|
978
|
+
* peer id revoked; `CadreNode.refreshRevocationEnforcement(strandId)` awaits that
|
|
979
|
+
* refresh.
|
|
929
980
|
*
|
|
930
981
|
* @param db - The closed strand's database.
|
|
931
982
|
* @param params - The departing member's own keypair.
|
|
@@ -942,8 +993,8 @@ export async function leaveStrand(db, params, options) {
|
|
|
942
993
|
return;
|
|
943
994
|
}
|
|
944
995
|
const departure = {
|
|
945
|
-
sql: `delete from Strand.Member
|
|
946
|
-
with context ManagerKey = null, ManagerSignature = null, MemberSignature = :memberSignature
|
|
996
|
+
sql: `delete from Strand.Member
|
|
997
|
+
with context ManagerKey = null, ManagerSignature = null, MemberSignature = :memberSignature
|
|
947
998
|
where Key = :memberKey;`,
|
|
948
999
|
params: {
|
|
949
1000
|
memberSignature: signStrandApproval(['Strand.Member', 'leave', memberKey, stampId], memberKeyPair.privateKeyB64),
|
|
@@ -996,10 +1047,19 @@ export async function registerMemberPeer(db, params, options) {
|
|
|
996
1047
|
log('MemberPeer (%s, %s) already present; skipping', memberKey, peerId);
|
|
997
1048
|
return;
|
|
998
1049
|
}
|
|
1050
|
+
await execStrandTransaction(db, memberPeerInsertStatement(memberKeyPair, peerId), options);
|
|
1051
|
+
log('Registered MemberPeer (%s, %s)', memberKey, peerId);
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* The self-signed `Strand.MemberPeer` insert binding `peerId` to the member, under a fresh
|
|
1055
|
+
* stamp bound into the add-tagged digest (see {@link registerMemberPeer}).
|
|
1056
|
+
*/
|
|
1057
|
+
function memberPeerInsertStatement(memberKeyPair, peerId) {
|
|
1058
|
+
const memberKey = memberKeyPair.publicKeyB64;
|
|
999
1059
|
const stampId = generateStrandStampId();
|
|
1000
|
-
|
|
1001
|
-
sql: `insert into Strand.MemberPeer (MemberKey, PeerId, StampId)
|
|
1002
|
-
with context Signature = :peerSignature, ManagerKey = null, ManagerSignature = null
|
|
1060
|
+
return {
|
|
1061
|
+
sql: `insert into Strand.MemberPeer (MemberKey, PeerId, StampId)
|
|
1062
|
+
with context Signature = :peerSignature, ManagerKey = null, ManagerSignature = null
|
|
1003
1063
|
values (:peerMemberKey, :peerId, :peerStampId);`,
|
|
1004
1064
|
params: {
|
|
1005
1065
|
peerSignature: signStrandApproval(['Strand.MemberPeer', 'add', memberKey, peerId, stampId], memberKeyPair.privateKeyB64),
|
|
@@ -1007,8 +1067,7 @@ export async function registerMemberPeer(db, params, options) {
|
|
|
1007
1067
|
peerId,
|
|
1008
1068
|
peerStampId: stampId,
|
|
1009
1069
|
},
|
|
1010
|
-
}
|
|
1011
|
-
log('Registered MemberPeer (%s, %s)', memberKey, peerId);
|
|
1070
|
+
};
|
|
1012
1071
|
}
|
|
1013
1072
|
/**
|
|
1014
1073
|
* Yield every `(PeerId, StampId)` bound to `memberKey`, re-comparing the member key
|
|
@@ -1150,8 +1209,8 @@ function ownMemberPeerRemovalStatements(params, stampId) {
|
|
|
1150
1209
|
const { memberKeyPair, peerId } = params;
|
|
1151
1210
|
const memberKey = memberKeyPair.publicKeyB64;
|
|
1152
1211
|
const removal = {
|
|
1153
|
-
sql: `delete from Strand.MemberPeer
|
|
1154
|
-
with context Signature = :peerSignature, ManagerKey = null, ManagerSignature = null
|
|
1212
|
+
sql: `delete from Strand.MemberPeer
|
|
1213
|
+
with context Signature = :peerSignature, ManagerKey = null, ManagerSignature = null
|
|
1155
1214
|
where MemberKey = :peerMemberKey and PeerId = :peerId;`,
|
|
1156
1215
|
params: {
|
|
1157
1216
|
peerSignature: signStrandApproval(['Strand.MemberPeer', 'remove', memberKey, peerId, stampId], memberKeyPair.privateKeyB64),
|
|
@@ -1175,8 +1234,8 @@ function ownMemberPeerRemovalStatements(params, stampId) {
|
|
|
1175
1234
|
function memberPeerRemovalByManagerStatements(params, stampId) {
|
|
1176
1235
|
const { managerKeyPair, memberKey, peerId } = params;
|
|
1177
1236
|
const removal = {
|
|
1178
|
-
sql: `delete from Strand.MemberPeer
|
|
1179
|
-
with context Signature = null, ManagerKey = :peerManagerKey, ManagerSignature = :peerManagerSignature
|
|
1237
|
+
sql: `delete from Strand.MemberPeer
|
|
1238
|
+
with context Signature = null, ManagerKey = :peerManagerKey, ManagerSignature = :peerManagerSignature
|
|
1180
1239
|
where MemberKey = :peerMemberKey and PeerId = :peerId;`,
|
|
1181
1240
|
params: {
|
|
1182
1241
|
peerManagerKey: managerKeyPair.publicKeyB64,
|
|
@@ -1262,8 +1321,8 @@ async function managerAddStatement(db, params) {
|
|
|
1262
1321
|
const generation = authorizer == null ? 1 : authorizer.generation + 1;
|
|
1263
1322
|
const stampId = generateStrandStampId();
|
|
1264
1323
|
return {
|
|
1265
|
-
sql: `insert into Strand.Manager (MemberKey, Generation, StampId)
|
|
1266
|
-
with context ManagerKey = :managerAuthorizerKey, Signature = :managerSignature
|
|
1324
|
+
sql: `insert into Strand.Manager (MemberKey, Generation, StampId)
|
|
1325
|
+
with context ManagerKey = :managerAuthorizerKey, Signature = :managerSignature
|
|
1267
1326
|
values (:managerMemberKey, :managerGeneration, :managerStampId);`,
|
|
1268
1327
|
params: {
|
|
1269
1328
|
managerAuthorizerKey: byManagerKeyPair.publicKeyB64,
|
|
@@ -1407,8 +1466,8 @@ export async function removeManager(db, params, options) {
|
|
|
1407
1466
|
*/
|
|
1408
1467
|
function managerRemovalStatements(signer, signature, targetManagerKey, targetStampId) {
|
|
1409
1468
|
const removal = {
|
|
1410
|
-
sql: `delete from Strand.Manager
|
|
1411
|
-
with context ManagerKey = :managerRemoverKey, Signature = :managerRemoverSignature
|
|
1469
|
+
sql: `delete from Strand.Manager
|
|
1470
|
+
with context ManagerKey = :managerRemoverKey, Signature = :managerRemoverSignature
|
|
1412
1471
|
where MemberKey = :managerMemberKey;`,
|
|
1413
1472
|
params: {
|
|
1414
1473
|
managerRemoverKey: signer.publicKeyB64,
|