@serfab/cadre-core 0.8.1 → 0.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.
Files changed (93) hide show
  1. package/README.md +2 -2
  2. package/dist/cadre-node.d.ts +101 -61
  3. package/dist/cadre-node.js +165 -124
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/control-cohort.d.ts +15 -15
  6. package/dist/control-cohort.js +14 -14
  7. package/dist/control-cohort.js.map +1 -1
  8. package/dist/control-database.d.ts +51 -34
  9. package/dist/control-database.js +72 -56
  10. package/dist/control-database.js.map +1 -1
  11. package/dist/control-formation-recorder.d.ts +2 -2
  12. package/dist/control-formation-recorder.js +2 -2
  13. package/dist/control-schema.d.ts +1 -1
  14. package/dist/control-schema.js +57 -33
  15. package/dist/control-schema.js.map +1 -1
  16. package/dist/device-token.d.ts +1 -1
  17. package/dist/device-token.js +1 -1
  18. package/dist/{authority-key.d.ts → ed25519-key.d.ts} +11 -10
  19. package/dist/{authority-key.js → ed25519-key.js} +9 -10
  20. package/dist/ed25519-key.js.map +1 -0
  21. package/dist/index.d.ts +3 -3
  22. package/dist/index.js +8 -8
  23. package/dist/index.js.map +1 -1
  24. package/dist/key-store.d.ts +1 -1
  25. package/dist/key-store.js +1 -1
  26. package/dist/peer-authorization.d.ts +28 -6
  27. package/dist/peer-authorization.js +33 -7
  28. package/dist/peer-authorization.js.map +1 -1
  29. package/dist/peer-record.d.ts +3 -3
  30. package/dist/peer-record.js +3 -3
  31. package/dist/push-node.d.ts +38 -0
  32. package/dist/push-node.js +48 -0
  33. package/dist/push-node.js.map +1 -0
  34. package/dist/push-notifier-apns.d.ts +10 -2
  35. package/dist/push-notifier-apns.js +14 -10
  36. package/dist/push-notifier-apns.js.map +1 -1
  37. package/dist/push-notifier-fcm.d.ts +7 -1
  38. package/dist/push-notifier-fcm.js +9 -10
  39. package/dist/push-notifier-fcm.js.map +1 -1
  40. package/dist/push-notifier.d.ts +16 -35
  41. package/dist/push-notifier.js +16 -44
  42. package/dist/push-notifier.js.map +1 -1
  43. package/dist/seed-bootstrap.d.ts +70 -51
  44. package/dist/seed-bootstrap.js +168 -106
  45. package/dist/seed-bootstrap.js.map +1 -1
  46. package/dist/seed-trust-policy.d.ts +11 -11
  47. package/dist/seed-trust-policy.js +11 -11
  48. package/dist/seed-trust-policy.js.map +1 -1
  49. package/dist/strand-database.d.ts +5 -5
  50. package/dist/strand-database.js +4 -4
  51. package/dist/strand-database.js.map +1 -1
  52. package/dist/strand-formation-protocol.d.ts +1 -1
  53. package/dist/strand-formation-protocol.js +1 -1
  54. package/dist/strand-instance-manager.d.ts +1 -1
  55. package/dist/strand-instance-manager.js +1 -1
  56. package/dist/strand-instance-manager.js.map +1 -1
  57. package/dist/strand-member-key.d.ts +6 -6
  58. package/dist/strand-member-key.js +6 -6
  59. package/dist/strand-member-key.js.map +1 -1
  60. package/dist/strand-member-registry.d.ts +7 -7
  61. package/dist/strand-member-registry.js +5 -5
  62. package/dist/strand-member-registry.js.map +1 -1
  63. package/dist/strand-membership-writer.d.ts +84 -84
  64. package/dist/strand-membership-writer.js +90 -90
  65. package/dist/strand-membership-writer.js.map +1 -1
  66. package/dist/types.d.ts +52 -32
  67. package/dist/types.js.map +1 -1
  68. package/package.json +7 -3
  69. package/src/cadre-node.ts +170 -126
  70. package/src/control-cohort.ts +17 -17
  71. package/src/control-database.ts +76 -59
  72. package/src/control-formation-recorder.ts +2 -2
  73. package/src/control-schema.ts +57 -33
  74. package/src/device-token.ts +1 -1
  75. package/src/{authority-key.ts → ed25519-key.ts} +12 -11
  76. package/src/index.ts +21 -19
  77. package/src/key-store.ts +1 -1
  78. package/src/peer-authorization.ts +35 -7
  79. package/src/peer-record.ts +3 -3
  80. package/src/push-node.ts +64 -0
  81. package/src/push-notifier-apns.ts +16 -12
  82. package/src/push-notifier-fcm.ts +9 -10
  83. package/src/push-notifier.ts +16 -57
  84. package/src/seed-bootstrap.ts +179 -115
  85. package/src/seed-trust-policy.ts +18 -18
  86. package/src/strand-database.ts +6 -6
  87. package/src/strand-formation-protocol.ts +1 -1
  88. package/src/strand-instance-manager.ts +2 -2
  89. package/src/strand-member-key.ts +7 -7
  90. package/src/strand-member-registry.ts +10 -10
  91. package/src/strand-membership-writer.ts +127 -127
  92. package/src/types.ts +52 -32
  93. package/dist/authority-key.js.map +0 -1
@@ -5,14 +5,14 @@
5
5
  * connected (and thus replicating), without devolving into an N² mesh.
6
6
  *
7
7
  * FRET only needs a *connected* graph with the block clusters reachable, not a
8
- * full mesh. Storage/authority nodes are the stable, publicly-dialable backbone
8
+ * full mesh. Storage/owner nodes are the stable, publicly-dialable backbone
9
9
  * that hold the control blocks, so the policy is **backbone-preferential with a
10
10
  * bounded out-degree**:
11
11
  *
12
- * - **Always dial every backbone (authority) member** — the authority set is
12
+ * - **Always dial every backbone (owner) member** — the owner set is
13
13
  * small by design, and routing cohort formation through publicly-dialable
14
- * authority nodes keeps the cohort ≥2 where authority-signed writes originate.
15
- * - **Fill the remainder** up to `targetDegree` with non-authority members,
14
+ * owner nodes keeps the cohort ≥2 where owner-signed writes originate.
15
+ * - **Fill the remainder** up to `targetDegree` with non-owner members,
16
16
  * ordered deterministically (peerId sort) so every node makes the same stable
17
17
  * choice across passes and load spreads predictably.
18
18
  *
@@ -32,8 +32,8 @@ import type { CohortPeerRow } from './strand-cohort.js';
32
32
  export const DEFAULT_CONTROL_COHORT_RECONCILE_MS = 15_000;
33
33
 
34
34
  /**
35
- * Default cap on the number of **non-authority** members a single reconcile pass
36
- * proactively dials. Backbone (authority) members are always dialed and do NOT
35
+ * Default cap on the number of **non-owner** members a single reconcile pass
36
+ * proactively dials. Backbone (owner) members are always dialed and do NOT
37
37
  * count against this cap.
38
38
  */
39
39
  export const DEFAULT_CONTROL_COHORT_TARGET_DEGREE = 6;
@@ -41,14 +41,14 @@ export const DEFAULT_CONTROL_COHORT_TARGET_DEGREE = 6;
41
41
  /** Outcome of {@link selectControlCohortDials}: who to dial + what the cap dropped. */
42
42
  export interface ControlCohortSelection {
43
43
  /**
44
- * Siblings to dial this pass, backbone first then the bounded non-authority
44
+ * Siblings to dial this pass, backbone first then the bounded non-owner
45
45
  * fill. Already self-excluded (the caller must pass self-excluded `siblings`).
46
46
  * Still includes peers that may turn out to be already-connected — the caller
47
47
  * diffs against live connections separately.
48
48
  */
49
49
  dials: CohortPeerRow[];
50
- /** Non-authority members dropped by the `targetDegree` cap (0 when none). */
51
- cappedNonAuthority: number;
50
+ /** Non-owner members dropped by the `targetDegree` cap (0 when none). */
51
+ cappedNonOwner: number;
52
52
  }
53
53
 
54
54
  /** Stable peerId ordering so the bounded fill is identical across passes/nodes. */
@@ -63,19 +63,19 @@ function byPeerId(a: CohortPeerRow, b: CohortPeerRow): number {
63
63
  * to dial.
64
64
  *
65
65
  * A member is **backbone** iff the ed25519 key derived from its peerId
66
- * (see {@link ed25519PublicKeyB64FromPeerId}) is in `authorityKeys`. All backbone
67
- * members are selected; non-authority members fill up to `targetDegree` in
66
+ * (see {@link ed25519PublicKeyB64FromPeerId}) is in `ownerKeys`. All backbone
67
+ * members are selected; non-owner members fill up to `targetDegree` in
68
68
  * deterministic peerId order.
69
69
  *
70
70
  * @param siblings - cadre members EXCLUDING self (the caller filters self out).
71
- * @param authorityKeys - the converged `AuthorityKey` set; may be empty/partial
72
- * before authority convergence, in which case no member classifies as backbone
71
+ * @param ownerKeys - the converged `OwnerKey` set; may be empty/partial
72
+ * before owner convergence, in which case no member classifies as backbone
73
73
  * yet and the bounded fill still makes progress (preference sharpens later).
74
- * @param targetDegree - cap on non-authority dials (negative is treated as 0).
74
+ * @param targetDegree - cap on non-owner dials (negative is treated as 0).
75
75
  */
76
76
  export function selectControlCohortDials(
77
77
  siblings: CohortPeerRow[],
78
- authorityKeys: ReadonlySet<string>,
78
+ ownerKeys: ReadonlySet<string>,
79
79
  targetDegree: number,
80
80
  ): ControlCohortSelection {
81
81
  const backbone: CohortPeerRow[] = [];
@@ -83,7 +83,7 @@ export function selectControlCohortDials(
83
83
 
84
84
  for (const peer of siblings) {
85
85
  const pubKey = ed25519PublicKeyB64FromPeerId(peer.peerId);
86
- if (pubKey !== null && authorityKeys.has(pubKey)) {
86
+ if (pubKey !== null && ownerKeys.has(pubKey)) {
87
87
  backbone.push(peer);
88
88
  } else {
89
89
  rest.push(peer);
@@ -98,6 +98,6 @@ export function selectControlCohortDials(
98
98
 
99
99
  return {
100
100
  dials: [...backbone, ...fill],
101
- cappedNonAuthority: rest.length - fill.length,
101
+ cappedNonOwner: rest.length - fill.length,
102
102
  };
103
103
  }
@@ -20,7 +20,7 @@ const timing = debug('sereus:cadre:timing');
20
20
  * - First 16 bytes: SHA-256 hash of peer ID (for distributed uniqueness)
21
21
  * - Last 16 bytes: Random bytes (for collision resistance)
22
22
  */
23
- function generateStampId(peerId: string): string {
23
+ export function generateStampId(peerId: string): string {
24
24
  // Hash the peer ID and get first 16 bytes (128 bits). A purely local ID
25
25
  // generator: never signed/verified against SQL, so the framed single-field
26
26
  // digest is fine (the changed framing is not cross-checked anywhere).
@@ -54,7 +54,7 @@ function parseStoredDatetimeMs(value: string | number): number {
54
54
  }
55
55
 
56
56
  /**
57
- * Build the canonical authorization message that authority signatures are bound to.
57
+ * Build the canonical authorization message that owner signatures are bound to.
58
58
  *
59
59
  * The message is a SINGLE framed SHA-256 digest over the ordered tuple of fields (the
60
60
  * crypto plugin's injective multi-field encoding), in a fixed field order, with the
@@ -111,7 +111,7 @@ interface OptimysticPluginResult {
111
111
  * `waitForControlSync`) a typo-proof table argument.
112
112
  */
113
113
  export type ControlTable =
114
- | 'AuthorityKey'
114
+ | 'OwnerKey'
115
115
  | 'ValidationKey'
116
116
  | 'Strand'
117
117
  | 'CadrePeer'
@@ -121,7 +121,7 @@ export type ControlTable =
121
121
 
122
122
  /** Runtime guard mirroring {@link ControlTable} for the dynamic-`from` count. */
123
123
  const CONTROL_TABLES: ReadonlySet<ControlTable> = new Set<ControlTable>([
124
- 'AuthorityKey',
124
+ 'OwnerKey',
125
125
  'ValidationKey',
126
126
  'Strand',
127
127
  'CadrePeer',
@@ -329,7 +329,7 @@ export class ControlDatabase {
329
329
  * into the `from` clause: the names are not user input, but the check keeps the
330
330
  * dynamic query off the injection surface and fails loudly on a typo instead of
331
331
  * emitting a malformed statement. The count reflects only the rows this node's
332
- * control DB has converged on — in the integration harness that is the authority
332
+ * control DB has converged on — in the integration harness that is the owner
333
333
  * node (one ControlDatabase per party), i.e. the authoritative control-network
334
334
  * view, not a per-drone convergence guarantee.
335
335
  */
@@ -353,44 +353,44 @@ export class ControlDatabase {
353
353
  }
354
354
 
355
355
  /**
356
- * Check whether any authority key exists in the control database.
356
+ * Check whether any owner key exists in the control database.
357
357
  * Used to decide whether a fresh-party genesis insert is required.
358
358
  */
359
- async hasAuthorityKey(): Promise<boolean> {
359
+ async hasOwnerKey(): Promise<boolean> {
360
360
  this.ensureInitialized();
361
- for await (const row of this.db!.eval('select count(1) as Count from CadreControl.AuthorityKey')) {
361
+ for await (const row of this.db!.eval('select count(1) as Count from CadreControl.OwnerKey')) {
362
362
  return (row.Count as number) > 0;
363
363
  }
364
364
  return false;
365
365
  }
366
366
 
367
367
  /**
368
- * Idempotent genesis: insert `key` as the founding authority key only when
369
- * the party has none yet. Returns true if it inserted, false if an authority
370
- * key already existed (so a repeat `--authority` start is a no-op).
368
+ * Idempotent genesis: insert `key` as the founding owner key only when
369
+ * the party has none yet. Returns true if it inserted, false if an owner
370
+ * key already existed (so a repeat `--owner` start is a no-op).
371
371
  */
372
- async ensureAuthorityKey(key: string): Promise<boolean> {
372
+ async ensureOwnerKey(key: string): Promise<boolean> {
373
373
  this.ensureInitialized();
374
- if (await this.hasAuthorityKey()) {
375
- log('Authority key already present; skipping genesis insert');
374
+ if (await this.hasOwnerKey()) {
375
+ log('Owner key already present; skipping genesis insert');
376
376
  return false;
377
377
  }
378
- await this.insertAuthorityKey(key);
378
+ await this.insertOwnerKey(key);
379
379
  return true;
380
380
  }
381
381
 
382
382
  /**
383
- * Collect every authority key (`CadreControl.AuthorityKey.Key`) as a set.
383
+ * Collect every owner key (`CadreControl.OwnerKey.Key`) as a set.
384
384
  *
385
385
  * This is the steady-state trust anchor for seeds: a seed's signer key is
386
386
  * trusted only if it is already enrolled here (see `SeedTrustPolicy`). It is
387
- * also the authority-identity source for `queryPeers`, decoupling authority
387
+ * also the owner-identity source for `queryPeers`, decoupling owner
388
388
  * status from the libp2p transport peer ID.
389
389
  */
390
- async getAuthorityKeys(): Promise<Set<string>> {
390
+ async getOwnerKeys(): Promise<Set<string>> {
391
391
  this.ensureInitialized();
392
392
  const keys = new Set<string>();
393
- for await (const row of this.db!.eval('select Key from CadreControl.AuthorityKey')) {
393
+ for await (const row of this.db!.eval('select Key from CadreControl.OwnerKey')) {
394
394
  keys.add(row.Key as string);
395
395
  }
396
396
  return keys;
@@ -399,18 +399,35 @@ export class ControlDatabase {
399
399
  /**
400
400
  * Enumerate the CadrePeer rows (cadre membership) for admin/membership reads.
401
401
  */
402
- async queryCadrePeers(): Promise<Array<{ peerId: string; multiaddr: string | null }>> {
402
+ async queryCadrePeers(): Promise<Array<{ peerId: string; multiaddr: string | null; stampId: string | null; vouchOwner: string | null; vouchSig: string | null }>> {
403
403
  this.ensureInitialized();
404
- const rows: Array<{ peerId: string; multiaddr: string | null }> = [];
405
- for await (const row of this.db!.eval('select PeerId, Multiaddr from CadreControl.CadrePeer')) {
404
+ const rows: Array<{ peerId: string; multiaddr: string | null; stampId: string | null; vouchOwner: string | null; vouchSig: string | null }> = [];
405
+ for await (const row of this.db!.eval('select PeerId, Multiaddr, StampId, VouchOwner, VouchSig from CadreControl.CadrePeer')) {
406
406
  rows.push({
407
407
  peerId: row.PeerId as string,
408
408
  multiaddr: (row.Multiaddr as string | null) ?? null,
409
+ stampId: (row.StampId as string | null) ?? null,
410
+ vouchOwner: (row.VouchOwner as string | null) ?? null,
411
+ vouchSig: (row.VouchSig as string | null) ?? null,
409
412
  });
410
413
  }
411
414
  return rows;
412
415
  }
413
416
 
417
+ /**
418
+ * Read a single `CadrePeer` row's single-use `StampId` nonce (null when the row
419
+ * does not exist). Used by the owner delete / re-touch paths, which must bind
420
+ * their signature to the row's CURRENT nonce ({@link cadrePeerRemoveDigest} /
421
+ * {@link cadrePeerVoucherDigest}).
422
+ */
423
+ async queryCadrePeerStampId(peerId: string): Promise<string | null> {
424
+ this.ensureInitialized();
425
+ for await (const row of this.db!.eval('select StampId from CadreControl.CadrePeer where PeerId = ?', [peerId])) {
426
+ return (row.StampId as string | null) ?? null;
427
+ }
428
+ return null;
429
+ }
430
+
414
431
  /**
415
432
  * Read a single peer's address record (the full `CadrePeer` row) by PeerId.
416
433
  *
@@ -444,8 +461,8 @@ export class ControlDatabase {
444
461
  *
445
462
  * Authorization is carried entirely by the record: the `Sig` column (verified
446
463
  * by the `AuthorizedUpdate` self-branch against the stored `PublicKey`) plus
447
- * the strictly-increasing `UpdatedAt`. No authority key is involved, so this
448
- * is the refresh path for any member — authority or drone — once its row
464
+ * the strictly-increasing `UpdatedAt`. No owner key is involved, so this
465
+ * is the refresh path for any member — owner or drone — once its row
449
466
  * exists. `PublicKey` is intentionally not in the SET list (it is immutable on
450
467
  * self-update and the constraint enforces `new.PublicKey = old.PublicKey`).
451
468
  */
@@ -454,7 +471,7 @@ export class ControlDatabase {
454
471
  const multiaddr = record.addrs.join(',');
455
472
  await this.db!.exec(`
456
473
  update CadreControl.CadrePeer
457
- with context AuthorityKey = null, Signature = ?
474
+ with context OwnerKey = null, Signature = ?
458
475
  set Multiaddr = ?, UpdatedAt = ?, Sig = ?
459
476
  where PeerId = ?
460
477
  `, [record.sig, multiaddr, record.updatedAt, record.sig, record.peerId]);
@@ -492,7 +509,7 @@ export class ControlDatabase {
492
509
  *
493
510
  * Authorization is carried entirely by the record: the `Sig` column (verified by
494
511
  * the `DeviceToken.AuthorizedUpdate` self-branch against the stored
495
- * `CadrePeer.PublicKey`) plus the strictly-increasing `UpdatedAt`. No authority key
512
+ * `CadrePeer.PublicKey`) plus the strictly-increasing `UpdatedAt`. No owner key
496
513
  * is involved, so this is the refresh / rotation path for any member once both its
497
514
  * `CadrePeer` row (for the PublicKey) and its `DeviceToken` row exist. `PeerId` is
498
515
  * intentionally not in the SET list (immutable; the constraint enforces
@@ -502,7 +519,7 @@ export class ControlDatabase {
502
519
  this.ensureInitialized();
503
520
  await this.db!.exec(`
504
521
  update CadreControl.DeviceToken
505
- with context AuthorityKey = null, Signature = ?
522
+ with context OwnerKey = null, Signature = ?
506
523
  set Platform = ?, Token = ?, UpdatedAt = ?, Sig = ?
507
524
  where PeerId = ?
508
525
  `, [record.sig, record.platform, record.token, record.updatedAt, record.sig, record.peerId]);
@@ -510,44 +527,44 @@ export class ControlDatabase {
510
527
  }
511
528
 
512
529
  /**
513
- * Insert the initial authority key (bootstrap - no existing authorities required)
530
+ * Insert the initial owner key (bootstrap - no existing owners required)
514
531
  */
515
- async insertAuthorityKey(key: string): Promise<void> {
532
+ async insertOwnerKey(key: string): Promise<void> {
516
533
  this.ensureInitialized();
517
- log('Inserting authority key: %s', key);
534
+ log('Inserting owner key: %s', key);
518
535
 
519
- // Bootstrap is authorized by the schema's `(select count(1) from AuthorityKey) <= 1`
536
+ // Bootstrap is authorized by the schema's `(select count(1) from OwnerKey) <= 1`
520
537
  // branch, so no signature is needed. We still persist a fresh, unique StampId in the
521
538
  // row's own column to satisfy the not-null/unique anti-replay constraint — the StampId
522
539
  // is now a real column value, not the optimystic `StampId()` SQL function.
523
540
  const stampId = generateStampId(this.config.libp2pNode.peerId.toString());
524
541
  await this.db!.exec(`
525
- insert into CadreControl.AuthorityKey (Key, StampId)
526
- with context AuthorityKey = null, Signature = null
542
+ insert into CadreControl.OwnerKey (Key, StampId)
543
+ with context OwnerKey = null, Signature = null
527
544
  values (?, ?)
528
545
  `, [key, stampId]);
529
- log('Authority key inserted');
546
+ log('Owner key inserted');
530
547
  }
531
548
 
532
549
  /**
533
- * Insert a strand into the control database using an authority signature.
550
+ * Insert a strand into the control database using an owner signature.
534
551
  *
535
- * The authority signs the canonical row-bound authorization message (see
552
+ * The owner signs the canonical row-bound authorization message (see
536
553
  * {@link buildAuthorizationMessage}) — NOT a bare stamp — so the signature is bound to
537
554
  * this strand's contents and cannot be transplanted onto an attacker-chosen row. The
538
555
  * StampId is persisted as a unique column for single-use anti-replay.
539
556
  *
540
557
  * @param strandId - Unique identifier for the strand
541
558
  * @param type - Strand type: 'o' for open, 'c' for closed
542
- * @param authorityKey - Public key of the authorizing authority
559
+ * @param ownerKey - Public key of the authorizing owner
543
560
  * @param signMessage - Function that ed25519-signs the raw message bytes (no pre-hash)
544
- * with the authority's private key, returning a base64url signature
561
+ * with the owner's private key, returning a base64url signature
545
562
  * @param memberPrivateKey - Optional private key for membership in closed strands
546
563
  */
547
564
  async insertStrand(
548
565
  strandId: string,
549
566
  type: 'o' | 'c',
550
- authorityKey: string,
567
+ ownerKey: string,
551
568
  signMessage: (message: Uint8Array) => string,
552
569
  memberPrivateKey?: string
553
570
  ): Promise<void> {
@@ -566,29 +583,29 @@ export class ControlDatabase {
566
583
  // StampId is a real, unique column (single-use anti-replay), no longer a context value.
567
584
  await this.db!.exec(`
568
585
  insert into CadreControl.Strand (Id, Type, MemberPrivateKey, StampId)
569
- with context AuthorityKey = ?, Signature = ?
586
+ with context OwnerKey = ?, Signature = ?
570
587
  values (?, ?, ?, ?)
571
- `, [authorityKey, signature, strandId, type, memberPrivateKey ?? null, stampId]);
588
+ `, [ownerKey, signature, strandId, type, memberPrivateKey ?? null, stampId]);
572
589
 
573
590
  log('Strand inserted: %s', strandId);
574
591
  }
575
592
 
576
593
  /**
577
- * Insert a validation key into the control database using an authority signature.
594
+ * Insert a validation key into the control database using an owner signature.
578
595
  *
579
- * Mirrors {@link insertStrand}: the authority signs the canonical row-bound
596
+ * Mirrors {@link insertStrand}: the owner signs the canonical row-bound
580
597
  * authorization message over (Key, StampId), and the StampId is persisted as a unique
581
598
  * column for single-use anti-replay. A `ValidationKey` authorizes verifying strand
582
599
  * formation disclosures.
583
600
  *
584
601
  * @param key - The validation public key to enroll
585
- * @param authorityKey - Public key of the authorizing authority
602
+ * @param ownerKey - Public key of the authorizing owner
586
603
  * @param signMessage - Function that ed25519-signs the raw message bytes (no pre-hash)
587
- * with the authority's private key, returning a base64url signature
604
+ * with the owner's private key, returning a base64url signature
588
605
  */
589
606
  async insertValidationKey(
590
607
  key: string,
591
- authorityKey: string,
608
+ ownerKey: string,
592
609
  signMessage: (message: Uint8Array) => string
593
610
  ): Promise<void> {
594
611
  this.ensureInitialized();
@@ -603,22 +620,22 @@ export class ControlDatabase {
603
620
 
604
621
  await this.db!.exec(`
605
622
  insert into CadreControl.ValidationKey (Key, StampId)
606
- with context AuthorityKey = ?, Signature = ?
623
+ with context OwnerKey = ?, Signature = ?
607
624
  values (?, ?)
608
- `, [authorityKey, signature, key, stampId]);
625
+ `, [ownerKey, signature, key, stampId]);
609
626
 
610
627
  log('Validation key inserted: %s', key);
611
628
  }
612
629
 
613
630
  /**
614
- * Insert an authority-signed `FormationInvite` (open invitation token).
631
+ * Insert an owner-signed `FormationInvite` (open invitation token).
615
632
  *
616
633
  * The invite is the on-network record that later authorizes an
617
- * authority-signature-FREE `Strand` creation: an invited cadre peer redeems it
634
+ * owner-signature-FREE `Strand` creation: an invited cadre peer redeems it
618
635
  * by inserting a matching `FormationUsage` row (see {@link redeemInvitation}),
619
636
  * which satisfies the consent branch of `Strand.Authorized`.
620
637
  *
621
- * Like {@link insertStrand}/{@link insertValidationKey}, the authority signs the
638
+ * Like {@link insertStrand}/{@link insertValidationKey}, the owner signs the
622
639
  * canonical row-bound authorization message (see {@link buildAuthorizationMessage})
623
640
  * over (Token, sAppId, ExpiresAt, TotalUses, ValidationUrl, StrandId, StampId) — NOT a
624
641
  * bare stamp — so the signature is bound to this invite's contents and cannot be
@@ -643,7 +660,7 @@ export class ControlDatabase {
643
660
  *
644
661
  * @param token - Invitation token (the `FormationInvite` primary key)
645
662
  * @param sAppId - The sApp a redeemed strand will use
646
- * @param authorityKey - Public key of the authorizing authority
663
+ * @param ownerKey - Public key of the authorizing owner
647
664
  * @param signMessage - ed25519-signs the raw message bytes (no pre-hash),
648
665
  * returning a base64url signature — the same callback shape {@link insertStrand} uses
649
666
  * @param options - Optional `expiresAtMs` (epoch ms), `totalUses`, `validationUrl`,
@@ -652,7 +669,7 @@ export class ControlDatabase {
652
669
  async insertFormationInvite(
653
670
  token: string,
654
671
  sAppId: string,
655
- authorityKey: string,
672
+ ownerKey: string,
656
673
  signMessage: (message: Uint8Array) => string,
657
674
  options: { expiresAtMs?: number; totalUses?: number; validationUrl?: string; strandId?: string } = {}
658
675
  ): Promise<void> {
@@ -687,10 +704,10 @@ export class ControlDatabase {
687
704
  // unique column (single-use anti-replay), no longer a context value.
688
705
  await this.db!.exec(`
689
706
  insert into CadreControl.FormationInvite (Token, sAppId, ExpiresAt, TotalUses, ValidationUrl, StrandId, StampId)
690
- with context AuthorityKey = ?, Signature = ?
707
+ with context OwnerKey = ?, Signature = ?
691
708
  values (?, ?, ?, ?, ?, ?, ?)
692
709
  `, [
693
- authorityKey, signature,
710
+ ownerKey, signature,
694
711
  token, sAppId,
695
712
  expiresAtCanonical,
696
713
  options.totalUses ?? null,
@@ -711,7 +728,7 @@ export class ControlDatabase {
711
728
  * `FormationUsage.StrandExists` requires the `Strand` row. Both CHECKs contain
712
729
  * subqueries, so Quereus auto-defers them to transaction commit — wrapping both
713
730
  * inserts in a single explicit `begin … commit` lets both deferred CHECKs see
714
- * both rows at commit. The strand is authorised WITHOUT an authority signature
731
+ * both rows at commit. The strand is authorised WITHOUT an owner signature
715
732
  * (the `FormationUsage` branch of `Strand.Authorized`) but still gets a fresh,
716
733
  * unique `StampId` column to satisfy the not-null/unique anti-replay column.
717
734
  *
@@ -746,11 +763,11 @@ export class ControlDatabase {
746
763
 
747
764
  await this.db!.beginTransaction();
748
765
  try {
749
- // 1. Strand row — authorised by the FormationUsage branch (no authority sig),
766
+ // 1. Strand row — authorised by the FormationUsage branch (no owner sig),
750
767
  // still carrying a fresh unique StampId for the anti-replay column.
751
768
  await this.db!.exec(`
752
769
  insert into CadreControl.Strand (Id, Type, MemberPrivateKey, StampId)
753
- with context AuthorityKey = null, Signature = null
770
+ with context OwnerKey = null, Signature = null
754
771
  values (?, ?, ?, ?)
755
772
  `, [strandId, type, memberPrivateKey ?? null, strandStampId]);
756
773
 
@@ -778,7 +795,7 @@ export class ControlDatabase {
778
795
  /**
779
796
  * Record a `FormationUsage` against an **already-existing** `Strand` (no strand
780
797
  * insert). This is the redemption path when the strand was provisioned
781
- * separately (e.g. authority-signed) and the consent record is added after the
798
+ * separately (e.g. owner-signed) and the consent record is added after the
782
799
  * fact: the single insert auto-commits, and the deferred `StrandExists` CHECK
783
800
  * is satisfied by the pre-existing committed strand row. Returns the assigned
784
801
  * `UseNumber`.
@@ -16,7 +16,7 @@ const NEVER_EXPIRES = new Date(8640000000000000);
16
16
  * usage questions, and writes the consent row that records a redemption. Two
17
17
  * provisioning shapes are supported, keyed on whether the invite binds a host strand:
18
18
  *
19
- * - **Bound (provision-then-record):** the host strand already exists (authority-signed
19
+ * - **Bound (provision-then-record):** the host strand already exists (owner-signed
20
20
  * up front and named by the invite's `StrandId`), so {@link resolveStrand} reports it
21
21
  * and {@link recordUsage} writes the consent row against that pre-existing strand
22
22
  * (record-only) rather than inserting a new `Strand`.
@@ -74,7 +74,7 @@ export class ControlFormationUsageRecorder implements FormationUsageRecorder {
74
74
  * Record consent against an **already-existing** host strand (record-only): the
75
75
  * single `FormationUsage` insert auto-commits and the deferred `StrandExists`
76
76
  * CHECK is satisfied by the pre-existing strand. This is the provision-then-record
77
- * commitment — the strand was minted authority-signed up front, so we do NOT
77
+ * commitment — the strand was minted owner-signed up front, so we do NOT
78
78
  * re-insert it (which would double-insert the same PK). `initiatorKey` is carried
79
79
  * as the usage `PeerId` (advisory). Use {@link ControlDatabase.redeemInvitation}
80
80
  * for the consent-creates-strand path instead.
@@ -12,30 +12,30 @@
12
12
  export const CONTROL_SCHEMA = `-- This manages a Sereus party's cadre, or set of nodes, and their participation in strands (networks)
13
13
  declare schema CadreControl {
14
14
  -- A key that can authorize various control changes
15
- table AuthorityKey (
15
+ table OwnerKey (
16
16
  Key text primary key,
17
17
  StampId text not null unique, -- single-use authorization nonce (anti-replay)
18
18
  constraint Authorized check (
19
- -- Bootstrap: first authority key needs no existing authorization
20
- (select count(1) from AuthorityKey) <= 1
19
+ -- Bootstrap: first owner key needs no existing authorization
20
+ (select count(1) from OwnerKey) <= 1
21
21
 
22
- -- Old authority authorizes by signing over THIS row (Key, StampId); single-use via unique StampId
23
- or (old.Key is not null and old.Key = context.AuthorityKey and verify(digest(new.Key, new.StampId), context.Signature, old.Key, 'ed25519'))
22
+ -- Old owner authorizes by signing over THIS row (Key, StampId); single-use via unique StampId
23
+ or (old.Key is not null and old.Key = context.OwnerKey and verify(digest(new.Key, new.StampId), context.Signature, old.Key, 'ed25519'))
24
24
 
25
- -- or other authorities authorize by signing over THIS row (Key, StampId); single-use via unique StampId
26
- or exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(new.Key, new.StampId), context.Signature, A.Key, 'ed25519'))
25
+ -- or other owners authorize by signing over THIS row (Key, StampId); single-use via unique StampId
26
+ or exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest(new.Key, new.StampId), context.Signature, A.Key, 'ed25519'))
27
27
  )
28
- ) with context (AuthorityKey text null, Signature text null);
28
+ ) with context (OwnerKey text null, Signature text null);
29
29
 
30
30
  -- A key that can validate a strand formation disclosure
31
31
  table ValidationKey (
32
32
  Key text primary key,
33
33
  StampId text not null unique, -- single-use authorization nonce (anti-replay)
34
34
  constraint Authorized check (
35
- -- Authorities authorize by signing over THIS row (Key, StampId); single-use via unique StampId
36
- exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(new.Key, new.StampId), context.Signature, A.Key, 'ed25519'))
35
+ -- Owners authorize by signing over THIS row (Key, StampId); single-use via unique StampId
36
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest(new.Key, new.StampId), context.Signature, A.Key, 'ed25519'))
37
37
  )
38
- ) with context (AuthorityKey text, Signature text);
38
+ ) with context (OwnerKey text, Signature text);
39
39
 
40
40
  -- A network of members sharing an sApp database, each contributing peer nodes (their cadre) to the overall cohort
41
41
  -- Cadre peers should participate in each of these strands
@@ -45,8 +45,8 @@ declare schema CadreControl {
45
45
  Type text, -- Types: 'o' = Open, 'c' = Closed -- Open can still control writes in the sApp, but only Closed controls reads
46
46
  StampId text not null unique, -- single-use authorization nonce (anti-replay)
47
47
  constraint Authorized check (
48
- -- Authorized by an authority signing over THIS row (Id, Type, MemberPrivateKey, StampId); single-use via unique StampId
49
- exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(new.Id, new.Type, coalesce(new.MemberPrivateKey, ''), new.StampId), context.Signature, A.Key, 'ed25519'))
48
+ -- Authorized by an owner signing over THIS row (Id, Type, MemberPrivateKey, StampId); single-use via unique StampId
49
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest(new.Id, new.Type, coalesce(new.MemberPrivateKey, ''), new.StampId), context.Signature, A.Key, 'ed25519'))
50
50
 
51
51
  -- or authorized by a cadre peer who has received a valid formation invitation.
52
52
  -- That path has no signed writer yet; when one is built it must still supply a
@@ -58,7 +58,7 @@ declare schema CadreControl {
58
58
  -- member key. A non-null MemberPrivateKey requires a closed strand ('c').
59
59
  new.MemberPrivateKey is null or new.Type = 'c'
60
60
  )
61
- ) with context (AuthorityKey text, Signature text);
61
+ ) with context (OwnerKey text, Signature text);
62
62
 
63
63
  -- A peer (node) that is part of the cadre, carrying a self-published,
64
64
  -- freshness-stamped, self-signed address record (see PeerAddressRecord in cadre-core).
@@ -70,31 +70,55 @@ declare schema CadreControl {
70
70
  Multiaddr text, -- comma-joined current addrs (signaling / p2p-circuit first)
71
71
  UpdatedAt int null, -- epoch ms; strictly increases per self-update (replay/rollback guard)
72
72
  Sig text null, -- ed25519 self-signature over the signed payload (base64url); null until self-published
73
- constraint AuthorizedInsert check on insert, delete (
74
- -- Authorized by an authority key, which vouches both membership and the
73
+ StampId text not null unique, -- single-use authorization nonce (anti-replay), bound into the voucher/remove digests; rotates on (re)insert
74
+ VouchOwner text null, -- ed25519 (base64url) owner key that vouched this membership (== insert context.OwnerKey)
75
+ VouchSig text null, -- that owner's signature over digest(PeerId, StampId) (== insert context.Signature). A reader checks VouchOwner against its NODE-LOCAL trusted-owner anchor (not this replicated table, which a self-owner can pollute) to decide authorized membership.
76
+ constraint AuthorizedInsert check on insert (
77
+ -- Authorized by an owner key, which vouches both membership and the
75
78
  -- PublicKey<->PeerId binding (cadre-core derives PublicKey from PeerId before insert).
76
- -- The single-field digest frames the base58btc PeerId as TEXT, matching
77
- -- cadre-core peer-authorization.ts:peerAuthorizationDigest.
78
- exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(coalesce(new.PeerId, old.PeerId)), context.Signature, A.Key, 'ed25519'))
79
+ -- The digest binds PeerId + the single-use StampId nonce (two TEXT fields),
80
+ -- matching cadre-core peer-authorization.ts:cadrePeerVoucherDigest.
81
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest(new.PeerId, new.StampId), context.Signature, A.Key, 'ed25519'))
82
+ -- Persist the vouching (owner, signature) onto the row so a reader can later
83
+ -- re-check VouchOwner against its node-local anchor. The stored pair MUST equal
84
+ -- the pair the verify above validated, so a writer cannot store a voucher it did
85
+ -- not actually receive a signature for.
86
+ and new.VouchOwner = context.OwnerKey
87
+ and new.VouchSig = context.Signature
88
+ ),
89
+ constraint AuthorizedDelete check on delete (
90
+ -- Delete is authorized by a signature over a DISTINCT 'remove'-scoped digest bound
91
+ -- to the row's StampId (cadre-core peer-authorization.ts:cadrePeerRemoveDigest), so
92
+ -- the stored voucher (a signature over digest(PeerId, StampId)) can NEVER be replayed
93
+ -- to authorize a delete. The remove signature rides in context and is never stored.
94
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest(old.PeerId, old.StampId, 'remove'), context.Signature, A.Key, 'ed25519'))
79
95
  ),
80
96
  constraint AuthorizedUpdate check on update (
81
97
  -- Peer self-updates its own addrs + freshness, signing with its OWN ed25519 key
82
- -- (the key behind PeerId). PeerId/PublicKey are immutable on self-update, UpdatedAt
83
- -- must strictly increase (replay guard), and Sig is verified against the stored
84
- -- PublicKey over the same payload the publish path signs
98
+ -- (the key behind PeerId). PeerId/PublicKey/StampId/voucher are immutable on
99
+ -- self-update, UpdatedAt must strictly increase (replay guard), and Sig is verified
100
+ -- against the stored PublicKey over the same payload the publish path signs
85
101
  -- (cadre-core peer-record.ts:peerRecordSignedPayload).
86
102
  (
87
103
  new.PeerId = old.PeerId
88
104
  and new.PublicKey = old.PublicKey
105
+ and new.StampId = old.StampId
106
+ and new.VouchOwner = old.VouchOwner
107
+ and new.VouchSig = old.VouchSig
89
108
  and new.UpdatedAt > coalesce(old.UpdatedAt, 0)
90
109
  and verify(
91
110
  digest(new.PeerId || '|' || new.Multiaddr || '|' || cast(new.UpdatedAt as text)),
92
111
  new.Sig, new.PublicKey, 'ed25519')
93
112
  )
94
- -- or an authority re-authorizes (rotation / correction)
95
- or exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(new.PeerId), context.Signature, A.Key, 'ed25519'))
113
+ -- or an owner re-authorizes (rotation / correction): re-vouches the row over
114
+ -- its current StampId, so the stored voucher is re-bound to the re-authorizing owner.
115
+ or (
116
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest(new.PeerId, new.StampId), context.Signature, A.Key, 'ed25519'))
117
+ and new.VouchOwner = context.OwnerKey
118
+ and new.VouchSig = context.Signature
119
+ )
96
120
  )
97
- ) with context (AuthorityKey text null, Signature text);
121
+ ) with context (OwnerKey text null, Signature text);
98
122
 
99
123
  -- A mobile cadre peer's platform push token (FCM/APNs), self-published so a
100
124
  -- server peer can deliver a push-wake to a suspended app over the platform push
@@ -109,9 +133,9 @@ declare schema CadreControl {
109
133
  UpdatedAt int null, -- epoch ms; strictly increases per self-update (replay guard)
110
134
  Sig text null, -- ed25519 self-sig over (PeerId|Platform|Token|UpdatedAt)
111
135
  constraint AuthorizedInsert check on insert, delete (
112
- -- Authorized by an authority key (membership vouch), exactly like CadrePeer.
136
+ -- Authorized by an owner key (membership vouch), exactly like CadrePeer.
113
137
  -- The single-field digest frames the base58btc PeerId as TEXT.
114
- exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(coalesce(new.PeerId, old.PeerId)), context.Signature, A.Key, 'ed25519'))
138
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest(coalesce(new.PeerId, old.PeerId)), context.Signature, A.Key, 'ed25519'))
115
139
  ),
116
140
  constraint AuthorizedUpdate check on update (
117
141
  -- Peer self-updates its own token, signing with its OWN ed25519 key (the key
@@ -126,10 +150,10 @@ declare schema CadreControl {
126
150
  digest(new.PeerId || '|' || new.Platform || '|' || new.Token || '|' || cast(new.UpdatedAt as text)),
127
151
  new.Sig, P.PublicKey, 'ed25519'))
128
152
  )
129
- -- or an authority re-authorizes (rotation / correction)
130
- or exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(new.PeerId), context.Signature, A.Key, 'ed25519'))
153
+ -- or an owner re-authorizes (rotation / correction)
154
+ or exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(digest(new.PeerId), context.Signature, A.Key, 'ed25519'))
131
155
  )
132
- ) with context (AuthorityKey text null, Signature text);
156
+ ) with context (OwnerKey text null, Signature text);
133
157
 
134
158
  -- An open invitation to form a strand with this party
135
159
  table FormationInvite (
@@ -141,11 +165,11 @@ declare schema CadreControl {
141
165
  StrandId text null, -- Host strand this invite binds to (provision-then-record). Non-null => the responder records consent against this pre-existing strand; null => legacy responder-provisions path.
142
166
  StampId text not null unique, -- single-use authorization nonce (anti-replay)
143
167
  constraint AuthorizedAddOrRemove check on insert, delete (
144
- -- Authorized by an authority signing over THIS row
168
+ -- Authorized by an owner signing over THIS row
145
169
  -- (Token, sAppId, ExpiresAt, TotalUses, ValidationUrl, StrandId, StampId); single-use via unique StampId.
146
170
  -- coalesce(new.F, old.F) binds the NEW row on insert and the OLD row on delete (cf. CadrePeer).
147
171
  -- StrandId is a nullable bound field: it signs as '' when absent (cf. ValidationUrl).
148
- exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(
172
+ exists (select 1 from OwnerKey A where A.Key = context.OwnerKey and verify(
149
173
  digest(
150
174
  coalesce(new.Token, old.Token),
151
175
  coalesce(new.sAppId, old.sAppId),
@@ -163,7 +187,7 @@ declare schema CadreControl {
163
187
  -- defaults to insert+update; the check on insert, delete above excludes update,
164
188
  -- so this guard is required. Mirrors FormationUsage.InsertOnly.
165
189
  constraint Immutable check on update (false)
166
- ) with context (AuthorityKey text, Signature text);
190
+ ) with context (OwnerKey text, Signature text);
167
191
 
168
192
  table FormationUsage (
169
193
  Token text,