@serfab/cadre-core 0.10.0 → 0.11.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 (105) hide show
  1. package/README.md +255 -255
  2. package/dist/cadre-node.d.ts +203 -28
  3. package/dist/cadre-node.js +445 -84
  4. package/dist/cadre-node.js.map +1 -1
  5. package/dist/control-cohort.d.ts +26 -0
  6. package/dist/control-cohort.js +26 -0
  7. package/dist/control-cohort.js.map +1 -1
  8. package/dist/control-database.d.ts +82 -117
  9. package/dist/control-database.js +217 -295
  10. package/dist/control-database.js.map +1 -1
  11. package/dist/control-formation-recorder.d.ts +8 -7
  12. package/dist/control-formation-recorder.js +8 -7
  13. package/dist/control-formation-recorder.js.map +1 -1
  14. package/dist/control-schema.d.ts +1 -1
  15. package/dist/control-schema.js +789 -774
  16. package/dist/control-schema.js.map +1 -1
  17. package/dist/control-write-retry.d.ts +15 -5
  18. package/dist/control-write-retry.js +19 -18
  19. package/dist/control-write-retry.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/peer-addr-book.d.ts +92 -0
  24. package/dist/peer-addr-book.js +220 -0
  25. package/dist/peer-addr-book.js.map +1 -0
  26. package/dist/seed-bootstrap.d.ts +7 -0
  27. package/dist/seed-bootstrap.js +15 -13
  28. package/dist/seed-bootstrap.js.map +1 -1
  29. package/dist/strand-cohort.d.ts +4 -37
  30. package/dist/strand-cohort.js +4 -15
  31. package/dist/strand-cohort.js.map +1 -1
  32. package/dist/strand-database.d.ts +11 -16
  33. package/dist/strand-database.js +19 -7
  34. package/dist/strand-database.js.map +1 -1
  35. package/dist/strand-formation-manager.d.ts +8 -8
  36. package/dist/strand-formation-manager.js +15 -18
  37. package/dist/strand-formation-manager.js.map +1 -1
  38. package/dist/strand-instance-manager.d.ts +52 -21
  39. package/dist/strand-instance-manager.js +100 -34
  40. package/dist/strand-instance-manager.js.map +1 -1
  41. package/dist/strand-membership-writer.d.ts +6 -4
  42. package/dist/strand-membership-writer.js +38 -36
  43. package/dist/strand-membership-writer.js.map +1 -1
  44. package/dist/types.d.ts +39 -31
  45. package/dist/types.js.map +1 -1
  46. package/package.json +8 -7
  47. package/src/arachnode-stub.ts +159 -159
  48. package/src/bootstrap-peer-store-file.ts +44 -44
  49. package/src/bootstrap-peer-store.ts +172 -172
  50. package/src/cadre-node.ts +5158 -4771
  51. package/src/canonical-json.ts +30 -30
  52. package/src/control-authorization.ts +114 -114
  53. package/src/control-cohort.ts +130 -103
  54. package/src/control-database.ts +2406 -2506
  55. package/src/control-formation-recorder.ts +305 -304
  56. package/src/control-schema.ts +801 -786
  57. package/src/control-stream.ts +205 -205
  58. package/src/control-write-retry.ts +483 -473
  59. package/src/delegate-admission.ts +302 -302
  60. package/src/device-token.ts +94 -94
  61. package/src/diagnostics/connection-path.ts +277 -277
  62. package/src/diagnostics/webrtc-turn-tracker.ts +222 -222
  63. package/src/ed25519-key.ts +134 -134
  64. package/src/enrollment.ts +199 -199
  65. package/src/file-durable-slot.ts +61 -61
  66. package/src/formation-approval.ts +604 -604
  67. package/src/fs-atomic.ts +107 -107
  68. package/src/hibernation-manager.ts +437 -437
  69. package/src/identity-key.ts +109 -109
  70. package/src/index.ts +1 -0
  71. package/src/key-store-file.ts +132 -132
  72. package/src/key-store.ts +99 -99
  73. package/src/membership-connection-gater.ts +204 -204
  74. package/src/node-local-snapshot.ts +260 -260
  75. package/src/peer-addr-book.ts +250 -0
  76. package/src/peer-authorization.ts +259 -259
  77. package/src/peer-record.ts +164 -164
  78. package/src/push-credentials.ts +84 -84
  79. package/src/push-node.ts +64 -64
  80. package/src/push-notifier-apns.ts +247 -247
  81. package/src/push-notifier-fcm.ts +220 -220
  82. package/src/push-notifier-shared.ts +28 -28
  83. package/src/push-notifier.ts +52 -52
  84. package/src/relay-addrs.ts +111 -111
  85. package/src/relay-reservation.ts +823 -823
  86. package/src/seed-bootstrap.ts +15 -15
  87. package/src/seed-trust-policy.ts +140 -140
  88. package/src/strand-addr-protocol.ts +441 -441
  89. package/src/strand-backfill.ts +366 -366
  90. package/src/strand-cohort.ts +38 -77
  91. package/src/strand-database.ts +202 -204
  92. package/src/strand-formation-manager.ts +491 -494
  93. package/src/strand-formation-protocol.ts +692 -692
  94. package/src/strand-instance-manager.ts +108 -50
  95. package/src/strand-member-key.ts +39 -39
  96. package/src/strand-member-registry.ts +182 -182
  97. package/src/strand-membership-writer.ts +1432 -1430
  98. package/src/strand-solicitation.ts +472 -472
  99. package/src/strand-transport-key.ts +58 -58
  100. package/src/strand-wake-payload.ts +32 -32
  101. package/src/strand-wake-protocol.ts +314 -314
  102. package/src/strand-watcher.ts +314 -314
  103. package/src/trusted-owner-store-file.ts +41 -41
  104. package/src/trusted-owner-store.ts +171 -171
  105. package/src/types.ts +1309 -1303
@@ -404,7 +404,7 @@ export class SeedBootstrapService {
404
404
  insert into CadreControl.DeviceToken (PeerId, Platform, Token, UpdatedAt, Sig, StampId)
405
405
  with context OwnerKey = ?, Signature = ?
406
406
  values (?, ?, ?, ?, ?, ?)
407
- `, [this.ownerPublicKey, signature, record.peerId, record.platform, record.token, record.updatedAt, record.sig, stampId]);
407
+ `, [this.ownerPublicKey, signature, record.peerId, record.platform, record.token, record.updatedAt, record.sig, stampId], 'device-token-insert');
408
408
  log('Device token inserted (owner-signed): %s', record.peerId);
409
409
  }
410
410
 
@@ -751,8 +751,8 @@ export class SeedBootstrapService {
751
751
  // `ApplySeedResult.ownerDialsFailed`). Recovery is not this loop's job —
752
752
  // `CadreNode.dialColdStartBootstrap` retries these same addresses on every
753
753
  // control-cohort reconcile pass until the control database has siblings.
754
- // `createSeed` projects EVERY CadrePeer row, so an owner applying a seed
755
- // minted after it joined finds ITSELF in the owner list. Dialing self always
754
+ // `createSeed` projects every non-revoked CadrePeer row, so an owner applying
755
+ // a seed minted after it joined finds ITSELF in the owner list. Dialing self always
756
756
  // throws, which would report a healthy owner as "seeded but stranded".
757
757
  // Optional-chained: partial libp2p handles (unit-test doubles) omit `peerId`,
758
758
  // and an undefined self simply matches nothing.
@@ -966,6 +966,13 @@ export class SeedBootstrapService {
966
966
  * This makes any owner node markable — not just the local one — and ties
967
967
  * `isOwner` to the control table rather than to `peerId === self`.
968
968
  *
969
+ * Read through {@link ControlDatabase.queryCadrePeers}, not a raw `CadrePeer`
970
+ * select: that reader drops any row whose `StampId` is retired in
971
+ * `CadreControl.Revocation`, so a removed member's addresses are never packed
972
+ * into a seed and pushed into a joiner's peerstore (`applySeed` adds every
973
+ * seed peer's addrs and dials the owner-flagged ones). A revoked peer is off
974
+ * the addressable surface everywhere, and this is one of its exits.
975
+ *
969
976
  * NOTE: this is the one owner lookup deliberately left on the REPLICATED
970
977
  * table rather than the node-local anchor. `SeedPeer.isOwner` is a dial hint
971
978
  * — the receiver dials owner-flagged peers first — not a trust decision, and
@@ -980,29 +987,22 @@ export class SeedBootstrapService {
980
987
  }
981
988
 
982
989
  const ownerKeys = await this.controlDatabase.getOwnerKeys();
983
- const db = this.controlDatabase.getDatabase();
984
- const peers: SeedPeer[] = [];
985
-
986
- // Query CadrePeer table
987
- for await (const row of db.eval('select PeerId, Multiaddr from CadreControl.CadrePeer')) {
988
- const peerId = row.PeerId as string;
989
- const multiaddr = row.Multiaddr as string | null;
990
+ const rows = await this.controlDatabase.queryCadrePeers();
990
991
 
992
+ return rows.map(({ peerId, multiaddr }) => {
991
993
  // Derive the peer's ed25519 key from its PeerId; a non-Ed25519 peer or an
992
994
  // unparsable id yields null and is treated as a non-owner rather than
993
995
  // failing the whole seed creation.
994
996
  const pubKeyB64 = ed25519PublicKeyB64FromPeerId(peerId);
995
997
  const isOwner = pubKeyB64 !== null && ownerKeys.has(pubKeyB64);
996
998
 
997
- peers.push({
999
+ return {
998
1000
  peerId,
999
1001
  multiaddrs: multiaddr ? multiaddr.split(',') : [],
1000
1002
  isOwner,
1001
1003
  ...(isOwner ? { publicKey: pubKeyB64 } : {}),
1002
- });
1003
- }
1004
-
1005
- return peers;
1004
+ };
1005
+ });
1006
1006
  }
1007
1007
 
1008
1008
  /**
@@ -1,140 +1,140 @@
1
- /**
2
- * Trust-anchor policy for control-network seeds.
3
- *
4
- * A seed carries an ed25519 signature over its body and the `signerKey` that
5
- * produced it. Verifying that signature only proves the seed is internally
6
- * consistent — it does NOT prove the signer is an owner, because both the
7
- * signer key and the seed's own `isOwner` peer flags are attacker-supplied.
8
- * The trust decision must therefore rest on an anchor that does not come from
9
- * the seed body:
10
- *
11
- * 1. Anchored — keys in the receiver's NODE-LOCAL `TrustedOwnerStore`: the
12
- * non-replicated, per-party record of owner keys established out of band
13
- * (founding the party, the `CadreInvite` that enrolled this node, an
14
- * operator pin). Deliberately NOT the replicated `CadreControl.OwnerKey`
15
- * table — any connecting node can genesis-insert its own key there and let
16
- * it replicate, so that table can be made to say "yes" by a stranger.
17
- * 2. Pinned out-of-band — keys handed to the node for this seed only (carried
18
- * by a `CadreInvite.ownerKeys`, or pinned by an operator) without having
19
- * been anchored yet.
20
- * 3. TOFU (opt-in) — an explicit confirmation callback invoked on first sight
21
- * of an unknown signer key. Interactive hosts only; off by default.
22
- *
23
- * Secure default (`anchoredTrustPolicy`): a node with an empty anchor and no
24
- * pinned keys rejects the seed — a seed can no longer vouch for its own signer,
25
- * and neither can a polluted replicated table.
26
- *
27
- * A key accepted via anchor 2 or 3 is reported back through
28
- * {@link SeedTrustDecision.anchorAs} so `applySeed` can persist it into the
29
- * node-local anchor, and later seeds from the same owner are accepted without
30
- * re-supplying the invite/confirmation.
31
- */
32
- import type { TrustSource } from './trusted-owner-store.js';
33
-
34
- export interface SeedTrustContext {
35
- /** Party the seed claims to belong to. */
36
- partyId: string;
37
- /** ed25519 base64url signer key — already signature-verified by `applySeed`. */
38
- signerKey: string;
39
- /**
40
- * The receiver's anchored owner keys, sourced from its node-local
41
- * `TrustedOwnerStore` — NOT from the seed, and NOT from the replicated
42
- * `OwnerKey` table. Empty for a node whose anchor was never seeded.
43
- */
44
- knownOwnerKeys: ReadonlySet<string>;
45
- }
46
-
47
- export interface SeedTrustDecision {
48
- /** Whether the signer key is trusted. */
49
- trusted: boolean;
50
- /** Human-readable reason, surfaced as the seed-apply error when not trusted. */
51
- reason?: string;
52
- /**
53
- * Set when the key was trusted via an anchor OUTSIDE the node-local store (a
54
- * pinned key, a TOFU confirmation): the provenance to record it under, so
55
- * `applySeed` persists it and the next seed from that owner is anchored
56
- * without re-supplying the pin. Omitted when the key was already anchored
57
- * (nothing to persist) or not trusted at all.
58
- */
59
- anchorAs?: Exclude<TrustSource, 'genesis'>;
60
- }
61
-
62
- /**
63
- * Decides whether a signature-verified seed's signer key should be trusted.
64
- * May be synchronous (anchored/pinned) or asynchronous (TOFU confirmation).
65
- */
66
- export interface SeedTrustPolicy {
67
- evaluate(ctx: SeedTrustContext): Promise<SeedTrustDecision> | SeedTrustDecision;
68
- }
69
-
70
- /**
71
- * Default policy: trust only keys already in the receiver's node-local
72
- * trusted-owner anchor. A node whose anchor was never seeded (no genesis, no
73
- * invite pin, no operator pin) rejects every seed.
74
- */
75
- export function anchoredTrustPolicy(): SeedTrustPolicy {
76
- return {
77
- evaluate({ signerKey, knownOwnerKeys }) {
78
- if (knownOwnerKeys.has(signerKey)) {
79
- return { trusted: true };
80
- }
81
- return {
82
- trusted: false,
83
- reason: 'Signer key is not an anchored owner (anchored trust policy)',
84
- };
85
- },
86
- };
87
- }
88
-
89
- /**
90
- * Cold-start policy: trust anchored keys plus a set pinned out-of-band
91
- * (typically `CadreInvite.ownerKeys` or operator config). Lets an unenrolled
92
- * invitee accept its first seed without the seed vouching for itself.
93
- *
94
- * @param anchorAs - provenance under which a pin-only acceptance is persisted
95
- * into the node-local anchor ('invite' by default — the invite-redemption
96
- * case; pass 'operator' for an operator-supplied pin).
97
- */
98
- export function pinnedKeyTrustPolicy(
99
- pinned: Iterable<string>,
100
- anchorAs: Exclude<TrustSource, 'genesis'> = 'invite'
101
- ): SeedTrustPolicy {
102
- const pinnedSet = new Set(pinned);
103
- return {
104
- evaluate({ signerKey, knownOwnerKeys }) {
105
- if (knownOwnerKeys.has(signerKey)) {
106
- return { trusted: true };
107
- }
108
- if (pinnedSet.has(signerKey)) {
109
- return { trusted: true, anchorAs };
110
- }
111
- return {
112
- trusted: false,
113
- reason: 'Signer key is neither an anchored nor a pinned owner (pinned-key trust policy)',
114
- };
115
- },
116
- };
117
- }
118
-
119
- /**
120
- * Opt-in interactive policy: trust keys already in the node-local anchor, and
121
- * on an unknown key invoke `confirm` (e.g. a trust-circle UI prompt). The key is
122
- * trusted iff `confirm` resolves true, and a confirmed key is persisted into the
123
- * anchor as an 'operator' pin (a human at the console is the same provenance as
124
- * an explicit operator pin) so the prompt is not repeated. Not enabled by default.
125
- */
126
- export function tofuTrustPolicy(
127
- confirm: (ctx: SeedTrustContext) => Promise<boolean>
128
- ): SeedTrustPolicy {
129
- return {
130
- async evaluate(ctx) {
131
- if (ctx.knownOwnerKeys.has(ctx.signerKey)) {
132
- return { trusted: true };
133
- }
134
- const accepted = await confirm(ctx);
135
- return accepted
136
- ? { trusted: true, anchorAs: 'operator' }
137
- : { trusted: false, reason: 'TOFU confirmation declined for unknown signer key' };
138
- },
139
- };
140
- }
1
+ /**
2
+ * Trust-anchor policy for control-network seeds.
3
+ *
4
+ * A seed carries an ed25519 signature over its body and the `signerKey` that
5
+ * produced it. Verifying that signature only proves the seed is internally
6
+ * consistent — it does NOT prove the signer is an owner, because both the
7
+ * signer key and the seed's own `isOwner` peer flags are attacker-supplied.
8
+ * The trust decision must therefore rest on an anchor that does not come from
9
+ * the seed body:
10
+ *
11
+ * 1. Anchored — keys in the receiver's NODE-LOCAL `TrustedOwnerStore`: the
12
+ * non-replicated, per-party record of owner keys established out of band
13
+ * (founding the party, the `CadreInvite` that enrolled this node, an
14
+ * operator pin). Deliberately NOT the replicated `CadreControl.OwnerKey`
15
+ * table — any connecting node can genesis-insert its own key there and let
16
+ * it replicate, so that table can be made to say "yes" by a stranger.
17
+ * 2. Pinned out-of-band — keys handed to the node for this seed only (carried
18
+ * by a `CadreInvite.ownerKeys`, or pinned by an operator) without having
19
+ * been anchored yet.
20
+ * 3. TOFU (opt-in) — an explicit confirmation callback invoked on first sight
21
+ * of an unknown signer key. Interactive hosts only; off by default.
22
+ *
23
+ * Secure default (`anchoredTrustPolicy`): a node with an empty anchor and no
24
+ * pinned keys rejects the seed — a seed can no longer vouch for its own signer,
25
+ * and neither can a polluted replicated table.
26
+ *
27
+ * A key accepted via anchor 2 or 3 is reported back through
28
+ * {@link SeedTrustDecision.anchorAs} so `applySeed` can persist it into the
29
+ * node-local anchor, and later seeds from the same owner are accepted without
30
+ * re-supplying the invite/confirmation.
31
+ */
32
+ import type { TrustSource } from './trusted-owner-store.js';
33
+
34
+ export interface SeedTrustContext {
35
+ /** Party the seed claims to belong to. */
36
+ partyId: string;
37
+ /** ed25519 base64url signer key — already signature-verified by `applySeed`. */
38
+ signerKey: string;
39
+ /**
40
+ * The receiver's anchored owner keys, sourced from its node-local
41
+ * `TrustedOwnerStore` — NOT from the seed, and NOT from the replicated
42
+ * `OwnerKey` table. Empty for a node whose anchor was never seeded.
43
+ */
44
+ knownOwnerKeys: ReadonlySet<string>;
45
+ }
46
+
47
+ export interface SeedTrustDecision {
48
+ /** Whether the signer key is trusted. */
49
+ trusted: boolean;
50
+ /** Human-readable reason, surfaced as the seed-apply error when not trusted. */
51
+ reason?: string;
52
+ /**
53
+ * Set when the key was trusted via an anchor OUTSIDE the node-local store (a
54
+ * pinned key, a TOFU confirmation): the provenance to record it under, so
55
+ * `applySeed` persists it and the next seed from that owner is anchored
56
+ * without re-supplying the pin. Omitted when the key was already anchored
57
+ * (nothing to persist) or not trusted at all.
58
+ */
59
+ anchorAs?: Exclude<TrustSource, 'genesis'>;
60
+ }
61
+
62
+ /**
63
+ * Decides whether a signature-verified seed's signer key should be trusted.
64
+ * May be synchronous (anchored/pinned) or asynchronous (TOFU confirmation).
65
+ */
66
+ export interface SeedTrustPolicy {
67
+ evaluate(ctx: SeedTrustContext): Promise<SeedTrustDecision> | SeedTrustDecision;
68
+ }
69
+
70
+ /**
71
+ * Default policy: trust only keys already in the receiver's node-local
72
+ * trusted-owner anchor. A node whose anchor was never seeded (no genesis, no
73
+ * invite pin, no operator pin) rejects every seed.
74
+ */
75
+ export function anchoredTrustPolicy(): SeedTrustPolicy {
76
+ return {
77
+ evaluate({ signerKey, knownOwnerKeys }) {
78
+ if (knownOwnerKeys.has(signerKey)) {
79
+ return { trusted: true };
80
+ }
81
+ return {
82
+ trusted: false,
83
+ reason: 'Signer key is not an anchored owner (anchored trust policy)',
84
+ };
85
+ },
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Cold-start policy: trust anchored keys plus a set pinned out-of-band
91
+ * (typically `CadreInvite.ownerKeys` or operator config). Lets an unenrolled
92
+ * invitee accept its first seed without the seed vouching for itself.
93
+ *
94
+ * @param anchorAs - provenance under which a pin-only acceptance is persisted
95
+ * into the node-local anchor ('invite' by default — the invite-redemption
96
+ * case; pass 'operator' for an operator-supplied pin).
97
+ */
98
+ export function pinnedKeyTrustPolicy(
99
+ pinned: Iterable<string>,
100
+ anchorAs: Exclude<TrustSource, 'genesis'> = 'invite'
101
+ ): SeedTrustPolicy {
102
+ const pinnedSet = new Set(pinned);
103
+ return {
104
+ evaluate({ signerKey, knownOwnerKeys }) {
105
+ if (knownOwnerKeys.has(signerKey)) {
106
+ return { trusted: true };
107
+ }
108
+ if (pinnedSet.has(signerKey)) {
109
+ return { trusted: true, anchorAs };
110
+ }
111
+ return {
112
+ trusted: false,
113
+ reason: 'Signer key is neither an anchored nor a pinned owner (pinned-key trust policy)',
114
+ };
115
+ },
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Opt-in interactive policy: trust keys already in the node-local anchor, and
121
+ * on an unknown key invoke `confirm` (e.g. a trust-circle UI prompt). The key is
122
+ * trusted iff `confirm` resolves true, and a confirmed key is persisted into the
123
+ * anchor as an 'operator' pin (a human at the console is the same provenance as
124
+ * an explicit operator pin) so the prompt is not repeated. Not enabled by default.
125
+ */
126
+ export function tofuTrustPolicy(
127
+ confirm: (ctx: SeedTrustContext) => Promise<boolean>
128
+ ): SeedTrustPolicy {
129
+ return {
130
+ async evaluate(ctx) {
131
+ if (ctx.knownOwnerKeys.has(ctx.signerKey)) {
132
+ return { trusted: true };
133
+ }
134
+ const accepted = await confirm(ctx);
135
+ return accepted
136
+ ? { trusted: true, anchorAs: 'operator' }
137
+ : { trusted: false, reason: 'TOFU confirmation declined for unknown signer key' };
138
+ },
139
+ };
140
+ }