@serfab/cadre-core 1.7.0 → 1.9.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 +8 -6
- package/dist/cadre-node.d.ts +527 -206
- package/dist/cadre-node.js +1084 -528
- 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 +6 -0
- package/dist/control-cohort.js +6 -0
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +46 -8
- package/dist/control-database.js +97 -18
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +37 -14
- package/dist/control-formation-recorder.js +60 -10
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-read-retry.d.ts +13 -12
- package/dist/control-read-retry.js +14 -12
- package/dist/control-read-retry.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +67 -5
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +9 -1
- package/dist/control-stream.js +24 -1
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +12 -2
- package/dist/control-write-retry.js +13 -2
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +11 -8
- package/dist/delegate-admission.js +19 -11
- package/dist/delegate-admission.js.map +1 -1
- package/dist/diagnostics/connection-path.d.ts +0 -7
- package/dist/diagnostics/connection-path.js +0 -7
- package/dist/diagnostics/connection-path.js.map +1 -1
- package/dist/formation-approval.d.ts +7 -0
- package/dist/formation-approval.js +9 -3
- package/dist/formation-approval.js.map +1 -1
- package/dist/fs-atomic.d.ts +11 -2
- package/dist/fs-atomic.js +13 -3
- package/dist/fs-atomic.js.map +1 -1
- package/dist/hibernation-manager.d.ts +111 -8
- package/dist/hibernation-manager.js +234 -49
- package/dist/hibernation-manager.js.map +1 -1
- package/dist/index.d.ts +10 -10
- package/dist/index.js +25 -20
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +90 -25
- package/dist/joined-strand-store.js +223 -55
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store-file.js +14 -5
- package/dist/key-store-file.js.map +1 -1
- package/dist/link-budget.d.ts +367 -69
- package/dist/link-budget.js +375 -72
- package/dist/link-budget.js.map +1 -1
- package/dist/membership-connection-gater.d.ts +30 -43
- package/dist/membership-connection-gater.js +31 -42
- package/dist/membership-connection-gater.js.map +1 -1
- package/dist/node-local-snapshot.d.ts +6 -4
- package/dist/node-local-snapshot.js +7 -3
- package/dist/node-local-snapshot.js.map +1 -1
- package/dist/peer-dial.d.ts +24 -4
- package/dist/peer-dial.js +74 -5
- package/dist/peer-dial.js.map +1 -1
- package/dist/peer-join-backfill.d.ts +2 -10
- package/dist/peer-join-backfill.js +8 -8
- package/dist/peer-join-backfill.js.map +1 -1
- package/dist/push-fanout.d.ts +6 -0
- package/dist/push-fanout.js +6 -0
- package/dist/push-fanout.js.map +1 -1
- package/dist/relay-reservation.d.ts +3 -1
- package/dist/relay-reservation.js +3 -1
- package/dist/relay-reservation.js.map +1 -1
- package/dist/relay-server.d.ts +79 -0
- package/dist/relay-server.js +74 -0
- package/dist/relay-server.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +57 -4
- package/dist/seed-bootstrap.js +107 -83
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/storage-scope.d.ts +33 -17
- package/dist/storage-scope.js +44 -23
- package/dist/storage-scope.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +58 -21
- package/dist/strand-addr-protocol.js +133 -85
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-database.d.ts +20 -2
- package/dist/strand-database.js +42 -5
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +59 -21
- package/dist/strand-first-sync-gate.js +58 -19
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-formation-deadlines.d.ts +151 -0
- package/dist/strand-formation-deadlines.js +171 -0
- package/dist/strand-formation-deadlines.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +63 -30
- package/dist/strand-formation-manager.js +67 -41
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +45 -46
- package/dist/strand-formation-protocol.js +62 -126
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-fret-addrs.d.ts +54 -0
- package/dist/strand-fret-addrs.js +108 -0
- package/dist/strand-fret-addrs.js.map +1 -0
- package/dist/strand-instance-manager.d.ts +122 -44
- package/dist/strand-instance-manager.js +270 -114
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +6 -0
- package/dist/strand-membership-writer.js +11 -0
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-network-state-file.d.ts +12 -0
- package/dist/strand-network-state-file.js +38 -0
- package/dist/strand-network-state-file.js.map +1 -0
- package/dist/strand-network-state.d.ts +111 -0
- package/dist/strand-network-state.js +191 -0
- package/dist/strand-network-state.js.map +1 -0
- package/dist/strand-revocation-enforcer.js +2 -1
- package/dist/strand-revocation-enforcer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +36 -20
- package/dist/strand-solicitation.js +4 -4
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-wake-protocol.d.ts +80 -23
- package/dist/strand-wake-protocol.js +89 -64
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/types.d.ts +148 -61
- package/dist/types.js +4 -3
- package/dist/types.js.map +1 -1
- package/package.json +11 -10
- package/src/cadre-node.ts +8400 -7776
- package/src/control-authorization.ts +2 -1
- package/src/control-cohort.ts +6 -0
- package/src/control-database.ts +122 -18
- package/src/control-formation-recorder.ts +354 -305
- package/src/control-read-retry.ts +14 -12
- package/src/control-schema.ts +67 -5
- package/src/control-stream.ts +25 -1
- package/src/control-write-retry.ts +13 -2
- package/src/delegate-admission.ts +23 -13
- package/src/diagnostics/connection-path.ts +0 -7
- package/src/formation-approval.ts +610 -604
- package/src/fs-atomic.ts +14 -4
- package/src/hibernation-manager.ts +638 -437
- package/src/index.ts +43 -65
- package/src/joined-strand-store.ts +247 -57
- package/src/key-store-file.ts +14 -5
- package/src/link-budget.ts +393 -74
- package/src/membership-connection-gater.ts +31 -45
- package/src/node-local-snapshot.ts +12 -7
- package/src/peer-dial.ts +81 -7
- package/src/peer-join-backfill.ts +10 -9
- package/src/push-fanout.ts +6 -0
- package/src/relay-reservation.ts +3 -1
- package/src/relay-server.ts +98 -0
- package/src/seed-bootstrap.ts +133 -91
- package/src/storage-scope.ts +44 -23
- package/src/strand-addr-protocol.ts +547 -458
- package/src/strand-database.ts +287 -242
- package/src/strand-first-sync-gate.ts +58 -19
- package/src/strand-formation-deadlines.ts +227 -0
- package/src/strand-formation-manager.ts +700 -647
- package/src/strand-formation-protocol.ts +106 -154
- package/src/strand-fret-addrs.ts +145 -0
- package/src/strand-instance-manager.ts +307 -140
- package/src/strand-membership-writer.ts +12 -0
- package/src/strand-network-state-file.ts +40 -0
- package/src/strand-network-state.ts +243 -0
- package/src/strand-revocation-enforcer.ts +2 -1
- package/src/strand-solicitation.ts +519 -500
- package/src/strand-wake-protocol.ts +401 -355
- package/src/types.ts +150 -61
- package/dist/strand-peer-book-file.d.ts +0 -12
- package/dist/strand-peer-book-file.js +0 -37
- package/dist/strand-peer-book-file.js.map +0 -1
- package/dist/strand-peer-book-protocol.d.ts +0 -159
- package/dist/strand-peer-book-protocol.js +0 -360
- package/dist/strand-peer-book-protocol.js.map +0 -1
- package/dist/strand-peer-book-swap.d.ts +0 -104
- package/dist/strand-peer-book-swap.js +0 -284
- package/dist/strand-peer-book-swap.js.map +0 -1
- package/dist/strand-peer-book.d.ts +0 -159
- package/dist/strand-peer-book.js +0 -354
- package/dist/strand-peer-book.js.map +0 -1
- package/dist/strand-peer-observer.d.ts +0 -88
- package/dist/strand-peer-observer.js +0 -180
- package/dist/strand-peer-observer.js.map +0 -1
- package/src/strand-peer-book-file.ts +0 -39
- package/src/strand-peer-book-protocol.ts +0 -463
- package/src/strand-peer-book-swap.ts +0 -338
- package/src/strand-peer-book.ts +0 -468
- package/src/strand-peer-observer.ts +0 -235
|
@@ -39,6 +39,7 @@ export const CONTROL_TABLES = [
|
|
|
39
39
|
'ValidationKey',
|
|
40
40
|
'Strand',
|
|
41
41
|
'StrandPartyKey',
|
|
42
|
+
'JoinedStrand',
|
|
42
43
|
'CadrePeer',
|
|
43
44
|
'DeviceToken',
|
|
44
45
|
'FormationInvite',
|
|
@@ -64,7 +65,7 @@ export type ControlTable = typeof CONTROL_TABLES[number];
|
|
|
64
65
|
* (`control-database.ts`) so the lightweight signers — `peer-authorization.ts`'s
|
|
65
66
|
* `revocationDigest` — can type against it without pulling in the runtime.
|
|
66
67
|
*/
|
|
67
|
-
export type RevocableTable = Extract<ControlTable, 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'DeviceToken'>;
|
|
68
|
+
export type RevocableTable = Extract<ControlTable, 'OwnerKey' | 'CadrePeer' | 'ValidationKey' | 'Strand' | 'StrandPartyKey' | 'JoinedStrand' | 'DeviceToken'>;
|
|
68
69
|
|
|
69
70
|
/**
|
|
70
71
|
* What a signature authorizes, scoped to one table rule — or, for
|
package/src/control-cohort.ts
CHANGED
|
@@ -28,6 +28,12 @@ import type { CohortPeerRow } from './strand-cohort.js';
|
|
|
28
28
|
* Default recurring cadence for {@link CadreNode.reconcileControlCohort} (ms).
|
|
29
29
|
* In the same spirit as the 5s strand-watch poll but lighter, since a control
|
|
30
30
|
* cohort changes far less often than strand activity.
|
|
31
|
+
*
|
|
32
|
+
* An interval between pass starts, not a deadline on a pass. Passes are
|
|
33
|
+
* single-flight, so a pass that runs longer than this is joined rather than
|
|
34
|
+
* stacked, and the effective cadence becomes the pass length: one unreachable
|
|
35
|
+
* sibling's dial alone can take the whole per-peer dial budget (64 s at the
|
|
36
|
+
* default declared link).
|
|
31
37
|
*/
|
|
32
38
|
export const DEFAULT_CONTROL_COHORT_RECONCILE_MS = 15_000;
|
|
33
39
|
|
package/src/control-database.ts
CHANGED
|
@@ -100,7 +100,9 @@ export class FormationAbortedError extends Error {
|
|
|
100
100
|
*
|
|
101
101
|
* Raised off the committed-count check ({@link ControlDatabase.assertSeatRemains}) ahead of
|
|
102
102
|
* the write, inside {@link ControlDatabase.redeemInvitation} /
|
|
103
|
-
* {@link ControlDatabase.recordFormationUsage}
|
|
103
|
+
* {@link ControlDatabase.recordFormationUsage}, and off the unlocked pre-check
|
|
104
|
+
* `ControlFormationUsageRecorder.authorizeUsage` runs before the manager issues a membership
|
|
105
|
+
* pass — and off nothing else. It exists because
|
|
104
106
|
* without it the refusal surfaces as a generic `CHECK constraint failed: Authorized` from the
|
|
105
107
|
* schema's own count-based cap clause, which the manager reports as a retryable
|
|
106
108
|
* `Formation conflict, retry` — telling the joiner to retry something that can never succeed.
|
|
@@ -293,26 +295,33 @@ const GUARDED_KEY_COLUMN: Readonly<Record<RevocableTable, GuardedKeyColumn>> = {
|
|
|
293
295
|
ValidationKey: 'Key',
|
|
294
296
|
Strand: 'Id',
|
|
295
297
|
StrandPartyKey: 'Id',
|
|
298
|
+
JoinedStrand: 'Id',
|
|
296
299
|
DeviceToken: 'PeerId',
|
|
297
300
|
};
|
|
298
301
|
|
|
302
|
+
/** The control tables keyed by a strand id whose repeat insert a caller may treat as idempotent. */
|
|
303
|
+
type StrandIdTable = Extract<RevocableTable, 'Strand' | 'JoinedStrand'>;
|
|
304
|
+
|
|
299
305
|
/**
|
|
300
|
-
* The rejection a second insert of an already-seated strand id produces, as
|
|
301
|
-
* vtab words it (`uniqueConstraintMessage`, qualified by table name only —
|
|
302
|
-
* prefix, matching `test/control-constraint-helpers.ts`'s `expectUniqueViolation`).
|
|
306
|
+
* The rejection a second insert of an already-seated strand id into `table` produces, as
|
|
307
|
+
* the optimystic vtab words it (`uniqueConstraintMessage`, qualified by table name only —
|
|
308
|
+
* no schema prefix, matching `test/control-constraint-helpers.ts`'s `expectUniqueViolation`).
|
|
303
309
|
*
|
|
304
|
-
* `
|
|
305
|
-
* the primary key can collide on a repeat publish. Matching the column explicitly
|
|
306
|
-
* any OTHER uniqueness failure out of the idempotency branch.
|
|
310
|
+
* Each table's `StampId` is unique too, but a fresh stamp is minted per insert attempt, so
|
|
311
|
+
* only the primary key can collide on a repeat publish. Matching the column explicitly
|
|
312
|
+
* keeps any OTHER uniqueness failure out of the idempotency branch.
|
|
307
313
|
*/
|
|
308
|
-
|
|
314
|
+
function strandIdConflictPattern(table: StrandIdTable): RegExp {
|
|
315
|
+
return new RegExp(`UNIQUE constraint failed: ${table}\\.Id\\b`, 'i');
|
|
316
|
+
}
|
|
309
317
|
|
|
310
318
|
/**
|
|
311
|
-
* Did this write fail because the strand id is already seated?
|
|
319
|
+
* Did this write to `table` fail because the strand id is already seated there?
|
|
312
320
|
*
|
|
313
321
|
* {@link CadreNode.publishStrand} uses this to tell "my own earlier publish already
|
|
314
322
|
* landed" (re-read, and no-op when the row matches) apart from every other rejection —
|
|
315
|
-
* an unauthorized signer, a retired stamp — which must keep surfacing.
|
|
323
|
+
* an unauthorized signer, a retired stamp — which must keep surfacing. For `JoinedStrand`
|
|
324
|
+
* the same test tells "another machine of this party already published this join" apart.
|
|
316
325
|
*
|
|
317
326
|
* Matched by TEXT, not by type: the typed engine error does not survive the trip out of
|
|
318
327
|
* optimystic (same constraint the retry classifiers in `control-write-retry.ts` document).
|
|
@@ -321,8 +330,8 @@ const STRAND_ID_CONFLICT = /UNIQUE constraint failed: Strand\.Id\b/i;
|
|
|
321
330
|
* `publish-strand.spec.ts` repeat-publish cases assert against the live engine error, so a
|
|
322
331
|
* reword reddens there.
|
|
323
332
|
*/
|
|
324
|
-
export function isStrandIdConflict(error: unknown): boolean {
|
|
325
|
-
return errorChainMatches(error,
|
|
333
|
+
export function isStrandIdConflict(error: unknown, table: StrandIdTable): boolean {
|
|
334
|
+
return errorChainMatches(error, strandIdConflictPattern(table));
|
|
326
335
|
}
|
|
327
336
|
|
|
328
337
|
/**
|
|
@@ -339,7 +348,7 @@ export const REVOCATION_LEDGER_MARKER = {
|
|
|
339
348
|
|
|
340
349
|
/**
|
|
341
350
|
* The rejection a second insert of the ledger marker produces: the optimystic vtab's
|
|
342
|
-
* primary-key wording (see {@link
|
|
351
|
+
* primary-key wording (see {@link strandIdConflictPattern}), naming both columns of
|
|
343
352
|
* `Revocation`'s composite key. Applied only to the marker's own insert, whose key
|
|
344
353
|
* ('Revocation', 'opened') no row but the marker can hold (`RowIsGone`), and `Revocation`
|
|
345
354
|
* has no other unique constraint.
|
|
@@ -362,10 +371,12 @@ function errorChainMatches(error: unknown, pattern: RegExp): boolean {
|
|
|
362
371
|
* `CadrePeer.AuthorizedDelete` in the schema). `Strand` and `StrandPartyKey` are
|
|
363
372
|
* deliberately absent: their rows carry `MemberPrivateKey` / `PrivateKey` — party
|
|
364
373
|
* secrets stored nowhere else (tickets/backlog/debt-strand-tombstone-reap.md owns any
|
|
365
|
-
* future change). `
|
|
366
|
-
*
|
|
374
|
+
* future change). `JoinedStrand` also carries a `MemberPrivateKey` but IS reapable: that
|
|
375
|
+
* secret is the joined strand's shared read key, which its founding party and every member
|
|
376
|
+
* hold, so a reaped row is recoverable by re-forming. `OwnerKey` has no production removal
|
|
377
|
+
* path and `MinOneOwner` makes an automated owner-key reap a party-bricking hazard.
|
|
367
378
|
*/
|
|
368
|
-
export const REAPABLE_TABLES = ['CadrePeer', 'DeviceToken', 'ValidationKey'] as const satisfies readonly RevocableTable[];
|
|
379
|
+
export const REAPABLE_TABLES = ['CadrePeer', 'DeviceToken', 'ValidationKey', 'JoinedStrand'] as const satisfies readonly RevocableTable[];
|
|
369
380
|
export type ReapableTable = (typeof REAPABLE_TABLES)[number];
|
|
370
381
|
|
|
371
382
|
/**
|
|
@@ -378,6 +389,16 @@ export type ReapableTable = (typeof REAPABLE_TABLES)[number];
|
|
|
378
389
|
const REAPABLE_TABLE_SET: ReadonlySet<RevocableTable> = new Set<RevocableTable>(REAPABLE_TABLES);
|
|
379
390
|
const isReapableTable = (table: RevocableTable): table is ReapableTable => REAPABLE_TABLE_SET.has(table);
|
|
380
391
|
|
|
392
|
+
/** A `JoinedStrand` row as a {@link StrandRow}; the schema's `KnownType` check backs the `Type` cast. */
|
|
393
|
+
function joinedStrandRow(row: Record<string, SqlValue>): StrandRow {
|
|
394
|
+
return {
|
|
395
|
+
Id: row.Id as string,
|
|
396
|
+
MemberPrivateKey: row.MemberPrivateKey as string | null,
|
|
397
|
+
Type: row.Type as 'o' | 'c',
|
|
398
|
+
FounderOwnerKey: null,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
381
402
|
/**
|
|
382
403
|
* Notified after a `CadreControl.CadrePeer` row write has COMMITTED.
|
|
383
404
|
*
|
|
@@ -559,7 +580,7 @@ export class ControlDatabase {
|
|
|
559
580
|
// NOTE: this is where a slow launch is felt. Duration is (raw-storage operations
|
|
560
581
|
// issued) × per-operation storage latency: ~1ms/op on an idle machine, but
|
|
561
582
|
// 50-90ms/op on a loaded disk or a phone's flash under launch contention. A cold
|
|
562
|
-
// start now reaches the backend
|
|
583
|
+
// start now reaches the backend 48 times (10 tables + 1 index, 23 distinct blocks
|
|
563
584
|
// — dominated by its genuine writes), a warm restart 13, because cadre-core
|
|
564
585
|
// wraps every embedder storage in `@optimystic/db-p2p`'s write-through cache
|
|
565
586
|
// (@serfab/quereus-plugin-sereus's cached-storage.ts). Uncached the same start issued ~2000 operations — the
|
|
@@ -629,6 +650,11 @@ export class ControlDatabase {
|
|
|
629
650
|
// exactly why `initialize` hydrates persisted optimystic schemas BEFORE getting here.
|
|
630
651
|
// A table the live catalog already lists generates no statements at all, which is what
|
|
631
652
|
// makes an apply over an already-complete schema (a warm start) a no-op.
|
|
653
|
+
// NOTE: a table the catalog lists with DIFFERENT constraint text diffs to
|
|
654
|
+
// `alter table ... drop constraint`, which the optimystic module refuses, so a store
|
|
655
|
+
// written by a build with older constraint text fails here on every start (policy:
|
|
656
|
+
// recreate, docs/architecture.md after the Control Network table). If backwards
|
|
657
|
+
// compatibility becomes a requirement, this apply is the migration site.
|
|
632
658
|
// - a failed apply is taken back WHOLE (Quereus 4.20.0). The migration loop keeps an
|
|
633
659
|
// undo journal; when a step fails it runs that journal in reverse, re-renders the
|
|
634
660
|
// catalog, compares it against a fingerprint taken BEFORE the apply, and only then
|
|
@@ -879,6 +905,29 @@ export class ControlDatabase {
|
|
|
879
905
|
return null;
|
|
880
906
|
}
|
|
881
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Every strand this party joined from another party (`CadreControl.JoinedStrand`), shaped
|
|
910
|
+
* as a {@link StrandRow} so a reader can treat it like one of the party's own. A joiner
|
|
911
|
+
* never founds, so `FounderOwnerKey` is always null. Read raw, like {@link queryStrands}:
|
|
912
|
+
* no retired-stamp filter.
|
|
913
|
+
*/
|
|
914
|
+
async queryJoinedStrands(): Promise<StrandRow[]> {
|
|
915
|
+
this.ensureInitialized();
|
|
916
|
+
const rows = await this.readRows('select Id, Type, MemberPrivateKey from CadreControl.JoinedStrand', undefined, 'joined-strands');
|
|
917
|
+
return rows.map(joinedStrandRow);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Single-row sibling of {@link queryJoinedStrands}: the joined strand with this id, or null. */
|
|
921
|
+
async queryJoinedStrand(strandId: string): Promise<StrandRow | null> {
|
|
922
|
+
this.ensureInitialized();
|
|
923
|
+
const rows = await this.readRows(
|
|
924
|
+
'select Id, Type, MemberPrivateKey from CadreControl.JoinedStrand where Id = ?',
|
|
925
|
+
[strandId],
|
|
926
|
+
'joined-strand'
|
|
927
|
+
);
|
|
928
|
+
return rows.length === 0 ? null : joinedStrandRow(rows[0]!);
|
|
929
|
+
}
|
|
930
|
+
|
|
882
931
|
/**
|
|
883
932
|
* Count rows in a CadreControl table as seen by THIS database instance.
|
|
884
933
|
*
|
|
@@ -1095,6 +1144,11 @@ export class ControlDatabase {
|
|
|
1095
1144
|
return this.queryStampId('StrandPartyKey', strandId);
|
|
1096
1145
|
}
|
|
1097
1146
|
|
|
1147
|
+
/** `JoinedStrand` stamp nonce — bound into {@link deleteJoinedStrand}'s remove digest. */
|
|
1148
|
+
queryJoinedStrandStampId(strandId: string): Promise<string | null> {
|
|
1149
|
+
return this.queryStampId('JoinedStrand', strandId);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1098
1152
|
/**
|
|
1099
1153
|
* Read THIS party's own strand membership private key (base64 protobuf; decode with
|
|
1100
1154
|
* `strandMemberKeyPair`) for one strand, or null when no `StrandPartyKey` row exists.
|
|
@@ -1606,6 +1660,56 @@ export class ControlDatabase {
|
|
|
1606
1660
|
return this.lockedWithRetry(() => this.deleteGuardedRow('StrandPartyKey', strandId, ownerKey, signMessage), {}, 'strand-party-key-delete');
|
|
1607
1661
|
}
|
|
1608
1662
|
|
|
1663
|
+
/**
|
|
1664
|
+
* Record, party-wide, a strand this party joined from another party (`JoinedStrand` row),
|
|
1665
|
+
* using an owner signature.
|
|
1666
|
+
*
|
|
1667
|
+
* Mirrors {@link insertStrand}: the owner signs the canonical row-bound authorization
|
|
1668
|
+
* message over (Id, Type, MemberPrivateKey, StampId) — binding the read secret, so a
|
|
1669
|
+
* captured approval can only reproduce the row it approved — and the StampId is persisted
|
|
1670
|
+
* as a unique column for single-use anti-replay.
|
|
1671
|
+
*
|
|
1672
|
+
* Fails with a `JoinedStrand.Id` uniqueness violation when the id is already recorded;
|
|
1673
|
+
* `isStrandIdConflict(error, 'JoinedStrand')` identifies that case.
|
|
1674
|
+
*/
|
|
1675
|
+
async insertJoinedStrand(
|
|
1676
|
+
row: Pick<StrandRow, 'Id' | 'Type' | 'MemberPrivateKey'>,
|
|
1677
|
+
ownerKey: string,
|
|
1678
|
+
signMessage: (message: Uint8Array) => string
|
|
1679
|
+
): Promise<void> {
|
|
1680
|
+
this.ensureInitialized();
|
|
1681
|
+
log('Inserting joined strand: %s (type: %s)', row.Id, row.Type);
|
|
1682
|
+
|
|
1683
|
+
const stampId = generateStampId(this.config.libp2pNode.peerId.toString());
|
|
1684
|
+
|
|
1685
|
+
// Field order MUST match the schema's JoinedStrand `AuthorizedInsert` verify:
|
|
1686
|
+
// Id, Type, MemberPrivateKey ('' when null), StampId.
|
|
1687
|
+
const message = buildAuthorizationMessage('CadreControl.JoinedStrand', 'add', [row.Id, row.Type, row.MemberPrivateKey ?? '', stampId]);
|
|
1688
|
+
const signature = signMessage(message);
|
|
1689
|
+
|
|
1690
|
+
await this.execWrite(`
|
|
1691
|
+
insert into CadreControl.JoinedStrand (Id, Type, MemberPrivateKey, StampId)
|
|
1692
|
+
with context OwnerKey = ?, Signature = ?
|
|
1693
|
+
values (?, ?, ?, ?)
|
|
1694
|
+
`, [ownerKey, signature, row.Id, row.Type, row.MemberPrivateKey, stampId], 'joined-strand-insert');
|
|
1695
|
+
|
|
1696
|
+
log('Joined strand inserted: %s', row.Id);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Owner-signed removal of one `JoinedStrand` row. Mirrors {@link deleteStrandPartyKey}:
|
|
1701
|
+
* `'remove'`-tagged digest over (Id, StampId), `Revocation` tombstone in the same
|
|
1702
|
+
* transaction. A no-op (no throw, no tombstone) when the row does not exist — `false`
|
|
1703
|
+
* then, `true` when a row was actually removed.
|
|
1704
|
+
*/
|
|
1705
|
+
deleteJoinedStrand(
|
|
1706
|
+
strandId: string,
|
|
1707
|
+
ownerKey: string,
|
|
1708
|
+
signMessage: (message: Uint8Array) => string
|
|
1709
|
+
): Promise<boolean> {
|
|
1710
|
+
return this.lockedWithRetry(() => this.deleteGuardedRow('JoinedStrand', strandId, ownerKey, signMessage), {}, 'joined-strand-delete');
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1609
1713
|
/**
|
|
1610
1714
|
* Insert a validation key into the control database using an owner signature.
|
|
1611
1715
|
*
|
|
@@ -2082,7 +2186,7 @@ export class ControlDatabase {
|
|
|
2082
2186
|
* reads from this node (every other node already filters it by tombstone). A revoked
|
|
2083
2187
|
* node therefore keeps its own copy of its own row; "a node that learns it has been
|
|
2084
2188
|
* revoked should shut itself down" is a distinct behaviour and its own ticket.
|
|
2085
|
-
* `ValidationKey`
|
|
2189
|
+
* `ValidationKey` and `JoinedStrand` have no self notion and are not special-cased.
|
|
2086
2190
|
*
|
|
2087
2191
|
* A per-row failure is logged and skipped rather than aborting the sweep: an abort would
|
|
2088
2192
|
* starve every tombstone after the failing one on every subsequent pass, and each row is
|