@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
@@ -1,182 +1,182 @@
1
- import debug from 'debug';
2
- import type { Database, SqlValue } from '@quereus/quereus';
3
- import type { MemberRegistration } from './types.js';
4
- import type { MemberRegistry, MemberVerifier } from './enrollment.js';
5
- import type { Ed25519KeyPair } from './ed25519-key.js';
6
- import {
7
- verifyStrandPayload,
8
- consumeInvite,
9
- addMemberByManager,
10
- listOutstandingInvites,
11
- } from './strand-membership-writer.js';
12
-
13
- const log = debug('sereus:cadre:strand-member-registry');
14
-
15
- /**
16
- * The canonical payload a joining member signs to prove ownership of its strand
17
- * member key, verified by {@link StrandMemberVerifier.verifyMember}.
18
- *
19
- * Binds the member key to the strand id so a registration signature cannot be
20
- * replayed against a different strand. Peer-id binding is intentionally NOT part
21
- * of this payload — `MemberPeer` rows carry their OWN per-peer self-proofs
22
- * (`MemberKey || '|' || PeerId`, signed by the member) and are written by the
23
- * dedicated `registerMemberPeer` writer; see {@link StrandMemberRegistry}.
24
- */
25
- export function memberRegistrationPayload(registration: MemberRegistration): string {
26
- return `${registration.strandId}|${registration.key}`;
27
- }
28
-
29
- /** How a {@link StrandMemberRegistry} admits a member into the strand. */
30
- export type StrandAdmission =
31
- | {
32
- /** Redeem a specific single-use invite (the invitee-side join). */
33
- readonly mode: 'invite';
34
- /** The `Invite.Key` (invite public key, base64url) to redeem. */
35
- readonly inviteKey: string;
36
- /** The matching invite private seed (base64url), received out-of-band. */
37
- readonly invitePrivateKey: string;
38
- }
39
- | {
40
- /** Admit directly by manager signature (the manager-side join). */
41
- readonly mode: 'manager';
42
- /** The admitting manager's strand keypair. */
43
- readonly managerKeyPair: Ed25519KeyPair;
44
- };
45
-
46
- /**
47
- * Strand-`Database`-backed {@link MemberVerifier} — the reconciliation that lets
48
- * {@link EnrollmentService} make real authorization decisions against a strand's
49
- * `Invite`/`ConsumedInvite`/`Member` tables instead of a placeholder.
50
- *
51
- * Each instance is scoped to ONE strand's database (the `Strand.*` tables live
52
- * inside the connected strand DB), so the `strandId` arguments on the interface
53
- * are accepted for signature compatibility but the queries run against this db.
54
- */
55
- export class StrandMemberVerifier implements MemberVerifier {
56
- constructor(private readonly db: Database) {}
57
-
58
- /**
59
- * Verify the member's self-proof over {@link memberRegistrationPayload} against
60
- * the registration's own `key` (the member's ed25519 public key). This is the
61
- * off-engine counterpart to the on-engine signature checks — a pre-flight gate
62
- * before the constraint-enforced write.
63
- */
64
- async verifyMember(registration: MemberRegistration, signature: string): Promise<boolean> {
65
- const ok = verifyStrandPayload(memberRegistrationPayload(registration), signature, registration.key);
66
- if (!ok) {
67
- log('verifyMember: bad self-proof for member %s on strand %s', registration.key, registration.strandId);
68
- }
69
- return ok;
70
- }
71
-
72
- /**
73
- * A member is authorized to join when either it has already consumed an invite
74
- * (a `ConsumedInvite` row bears its key) OR the strand has at least one
75
- * outstanding `Invite` it could redeem.
76
- *
77
- * Strand invites are anonymous — `Invite.Key` is an invite public key, not a
78
- * member key, so an un-consumed invite cannot be bound to a specific member
79
- * here. This check is therefore a "door is open" pre-flight; the binding,
80
- * single-use, and cryptographic gates are all enforced by the deferred
81
- * `Strand.*` constraints when the member is actually written.
82
- *
83
- * "Outstanding" is {@link listOutstandingInvites}: an `Invite` that is neither
84
- * already consumed, nor cancelled by a manager, nor expired. All three exclusions
85
- * mirror gates the actual write would hit (`ConsumedInvite`'s primary key,
86
- * `NotCancelled`, `NotExpired`), so this pre-flight does not report a door the
87
- * constraints will slam. A raw `Invite` count would over-report on all three.
88
- */
89
- async isAuthorizedToJoin(_strandId: string, memberKey: string): Promise<boolean> {
90
- const consumed = await scalarCount(
91
- this.db,
92
- 'select count(1) as c from Strand.ConsumedInvite where MemberKey = ?',
93
- [memberKey],
94
- );
95
- if (consumed > 0) {
96
- // NOTE: a REVOKED member still bears its stale ConsumedInvite row (the table is
97
- // insert-only, and revocation does not delete it), so this short-circuit says
98
- // "authorized" for a party that can no longer actually join. Harmless because the
99
- // on-engine constraints are the real gate — Member.Authorized's invite branch
100
- // requires a FRESH consumption row, which a stale one is not — and this pre-flight
101
- // only decides whether attempting the write is worthwhile.
102
- return true;
103
- }
104
- // NOTE: materialises the whole outstanding set to answer a non-empty question (the
105
- // former code was a single `select count(1)`). Deliberate — sharing one definition of
106
- // "outstanding" with the manager-facing enumeration beats a second, drifting copy of
107
- // the three exclusions. If this pre-flight ever runs hot on a strand with many
108
- // invitations, add a short-circuiting variant rather than re-inlining the rules.
109
- return (await listOutstandingInvites(this.db)).length > 0;
110
- }
111
- }
112
-
113
- /**
114
- * Strand-`Database`-backed {@link MemberRegistry} — writes real `Strand.Member`
115
- * (and, in invite mode, `Strand.ConsumedInvite`) rows through the signed writer
116
- * primitives, so {@link EnrollmentService.registerMember} performs the actual
117
- * per-strand join handshake rather than returning "MemberRegistry not configured".
118
- *
119
- * The {@link StrandAdmission} chosen at construction selects which branch of
120
- * `Member.Authorized` is taken:
121
- * - `invite` → {@link consumeInvite} (atomic `Member` + `ConsumedInvite`), the
122
- * invitee-side flow that redeems a single-use invite.
123
- * - `manager` → {@link addMemberByManager}, the manager-side flow that
124
- * seats a member already trusted by a manager.
125
- *
126
- * Scoped to one strand's db; the `strandId` argument is accepted for interface
127
- * compatibility but writes target this db's `Strand.*` tables.
128
- *
129
- * NOTE: `peerIds` are NOT written as `MemberPeer` rows here. The signed peer path
130
- * now exists as the standalone `registerMemberPeer` writer (each peer self-proof is
131
- * signed by the member's own key), but wiring the enrollment `peerIds` through it is
132
- * a deliberate follow-on — the member self-proof available here does not carry the
133
- * per-peer signatures `MemberPeer.Authorized` requires. A non-empty `peerIds` is
134
- * logged and otherwise ignored so the member still lands; peer rows are reconciled
135
- * separately via `registerMemberPeer`.
136
- */
137
- export class StrandMemberRegistry implements MemberRegistry {
138
- constructor(
139
- private readonly db: Database,
140
- private readonly admission: StrandAdmission,
141
- ) {}
142
-
143
- async registerMember(strandId: string, memberKey: string, peerIds: string[]): Promise<void> {
144
- if (peerIds.length > 0) {
145
- log(
146
- 'registerMember: %d peerId(s) for member %s on strand %s deferred to the registerMemberPeer path (needs per-peer self-proofs); registering member only',
147
- peerIds.length, memberKey, strandId,
148
- );
149
- }
150
-
151
- if (this.admission.mode === 'invite') {
152
- await consumeInvite(this.db, {
153
- inviteKey: this.admission.inviteKey,
154
- invitePrivateKey: this.admission.invitePrivateKey,
155
- memberKey,
156
- });
157
- return;
158
- }
159
-
160
- await addMemberByManager(this.db, {
161
- managerKeyPair: this.admission.managerKeyPair,
162
- memberKey,
163
- });
164
- }
165
-
166
- async isMemberRegistered(_strandId: string, memberKey: string): Promise<boolean> {
167
- const count = await scalarCount(
168
- this.db,
169
- 'select count(1) as c from Strand.Member where Key = ?',
170
- [memberKey],
171
- );
172
- return count > 0;
173
- }
174
- }
175
-
176
- /** Run a `select count(1) as c ...` and return the scalar (0 if no row). */
177
- async function scalarCount(db: Database, sql: string, params: SqlValue[]): Promise<number> {
178
- for await (const row of db.eval(sql, params)) {
179
- return (row.c as number) ?? 0;
180
- }
181
- return 0;
182
- }
1
+ import debug from 'debug';
2
+ import type { Database, SqlValue } from '@quereus/quereus';
3
+ import type { MemberRegistration } from './types.js';
4
+ import type { MemberRegistry, MemberVerifier } from './enrollment.js';
5
+ import type { Ed25519KeyPair } from './ed25519-key.js';
6
+ import {
7
+ verifyStrandPayload,
8
+ consumeInvite,
9
+ addMemberByManager,
10
+ listOutstandingInvites,
11
+ } from './strand-membership-writer.js';
12
+
13
+ const log = debug('sereus:cadre:strand-member-registry');
14
+
15
+ /**
16
+ * The canonical payload a joining member signs to prove ownership of its strand
17
+ * member key, verified by {@link StrandMemberVerifier.verifyMember}.
18
+ *
19
+ * Binds the member key to the strand id so a registration signature cannot be
20
+ * replayed against a different strand. Peer-id binding is intentionally NOT part
21
+ * of this payload — `MemberPeer` rows carry their OWN per-peer self-proofs
22
+ * (`MemberKey || '|' || PeerId`, signed by the member) and are written by the
23
+ * dedicated `registerMemberPeer` writer; see {@link StrandMemberRegistry}.
24
+ */
25
+ export function memberRegistrationPayload(registration: MemberRegistration): string {
26
+ return `${registration.strandId}|${registration.key}`;
27
+ }
28
+
29
+ /** How a {@link StrandMemberRegistry} admits a member into the strand. */
30
+ export type StrandAdmission =
31
+ | {
32
+ /** Redeem a specific single-use invite (the invitee-side join). */
33
+ readonly mode: 'invite';
34
+ /** The `Invite.Key` (invite public key, base64url) to redeem. */
35
+ readonly inviteKey: string;
36
+ /** The matching invite private seed (base64url), received out-of-band. */
37
+ readonly invitePrivateKey: string;
38
+ }
39
+ | {
40
+ /** Admit directly by manager signature (the manager-side join). */
41
+ readonly mode: 'manager';
42
+ /** The admitting manager's strand keypair. */
43
+ readonly managerKeyPair: Ed25519KeyPair;
44
+ };
45
+
46
+ /**
47
+ * Strand-`Database`-backed {@link MemberVerifier} — the reconciliation that lets
48
+ * {@link EnrollmentService} make real authorization decisions against a strand's
49
+ * `Invite`/`ConsumedInvite`/`Member` tables instead of a placeholder.
50
+ *
51
+ * Each instance is scoped to ONE strand's database (the `Strand.*` tables live
52
+ * inside the connected strand DB), so the `strandId` arguments on the interface
53
+ * are accepted for signature compatibility but the queries run against this db.
54
+ */
55
+ export class StrandMemberVerifier implements MemberVerifier {
56
+ constructor(private readonly db: Database) {}
57
+
58
+ /**
59
+ * Verify the member's self-proof over {@link memberRegistrationPayload} against
60
+ * the registration's own `key` (the member's ed25519 public key). This is the
61
+ * off-engine counterpart to the on-engine signature checks — a pre-flight gate
62
+ * before the constraint-enforced write.
63
+ */
64
+ async verifyMember(registration: MemberRegistration, signature: string): Promise<boolean> {
65
+ const ok = verifyStrandPayload(memberRegistrationPayload(registration), signature, registration.key);
66
+ if (!ok) {
67
+ log('verifyMember: bad self-proof for member %s on strand %s', registration.key, registration.strandId);
68
+ }
69
+ return ok;
70
+ }
71
+
72
+ /**
73
+ * A member is authorized to join when either it has already consumed an invite
74
+ * (a `ConsumedInvite` row bears its key) OR the strand has at least one
75
+ * outstanding `Invite` it could redeem.
76
+ *
77
+ * Strand invites are anonymous — `Invite.Key` is an invite public key, not a
78
+ * member key, so an un-consumed invite cannot be bound to a specific member
79
+ * here. This check is therefore a "door is open" pre-flight; the binding,
80
+ * single-use, and cryptographic gates are all enforced by the deferred
81
+ * `Strand.*` constraints when the member is actually written.
82
+ *
83
+ * "Outstanding" is {@link listOutstandingInvites}: an `Invite` that is neither
84
+ * already consumed, nor cancelled by a manager, nor expired. All three exclusions
85
+ * mirror gates the actual write would hit (`ConsumedInvite`'s primary key,
86
+ * `NotCancelled`, `NotExpired`), so this pre-flight does not report a door the
87
+ * constraints will slam. A raw `Invite` count would over-report on all three.
88
+ */
89
+ async isAuthorizedToJoin(_strandId: string, memberKey: string): Promise<boolean> {
90
+ const consumed = await scalarCount(
91
+ this.db,
92
+ 'select count(1) as c from Strand.ConsumedInvite where MemberKey = ?',
93
+ [memberKey],
94
+ );
95
+ if (consumed > 0) {
96
+ // NOTE: a REVOKED member still bears its stale ConsumedInvite row (the table is
97
+ // insert-only, and revocation does not delete it), so this short-circuit says
98
+ // "authorized" for a party that can no longer actually join. Harmless because the
99
+ // on-engine constraints are the real gate — Member.Authorized's invite branch
100
+ // requires a FRESH consumption row, which a stale one is not — and this pre-flight
101
+ // only decides whether attempting the write is worthwhile.
102
+ return true;
103
+ }
104
+ // NOTE: materialises the whole outstanding set to answer a non-empty question (the
105
+ // former code was a single `select count(1)`). Deliberate — sharing one definition of
106
+ // "outstanding" with the manager-facing enumeration beats a second, drifting copy of
107
+ // the three exclusions. If this pre-flight ever runs hot on a strand with many
108
+ // invitations, add a short-circuiting variant rather than re-inlining the rules.
109
+ return (await listOutstandingInvites(this.db)).length > 0;
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Strand-`Database`-backed {@link MemberRegistry} — writes real `Strand.Member`
115
+ * (and, in invite mode, `Strand.ConsumedInvite`) rows through the signed writer
116
+ * primitives, so {@link EnrollmentService.registerMember} performs the actual
117
+ * per-strand join handshake rather than returning "MemberRegistry not configured".
118
+ *
119
+ * The {@link StrandAdmission} chosen at construction selects which branch of
120
+ * `Member.Authorized` is taken:
121
+ * - `invite` → {@link consumeInvite} (atomic `Member` + `ConsumedInvite`), the
122
+ * invitee-side flow that redeems a single-use invite.
123
+ * - `manager` → {@link addMemberByManager}, the manager-side flow that
124
+ * seats a member already trusted by a manager.
125
+ *
126
+ * Scoped to one strand's db; the `strandId` argument is accepted for interface
127
+ * compatibility but writes target this db's `Strand.*` tables.
128
+ *
129
+ * NOTE: `peerIds` are NOT written as `MemberPeer` rows here. The signed peer path
130
+ * now exists as the standalone `registerMemberPeer` writer (each peer self-proof is
131
+ * signed by the member's own key), but wiring the enrollment `peerIds` through it is
132
+ * a deliberate follow-on — the member self-proof available here does not carry the
133
+ * per-peer signatures `MemberPeer.Authorized` requires. A non-empty `peerIds` is
134
+ * logged and otherwise ignored so the member still lands; peer rows are reconciled
135
+ * separately via `registerMemberPeer`.
136
+ */
137
+ export class StrandMemberRegistry implements MemberRegistry {
138
+ constructor(
139
+ private readonly db: Database,
140
+ private readonly admission: StrandAdmission,
141
+ ) {}
142
+
143
+ async registerMember(strandId: string, memberKey: string, peerIds: string[]): Promise<void> {
144
+ if (peerIds.length > 0) {
145
+ log(
146
+ 'registerMember: %d peerId(s) for member %s on strand %s deferred to the registerMemberPeer path (needs per-peer self-proofs); registering member only',
147
+ peerIds.length, memberKey, strandId,
148
+ );
149
+ }
150
+
151
+ if (this.admission.mode === 'invite') {
152
+ await consumeInvite(this.db, {
153
+ inviteKey: this.admission.inviteKey,
154
+ invitePrivateKey: this.admission.invitePrivateKey,
155
+ memberKey,
156
+ });
157
+ return;
158
+ }
159
+
160
+ await addMemberByManager(this.db, {
161
+ managerKeyPair: this.admission.managerKeyPair,
162
+ memberKey,
163
+ });
164
+ }
165
+
166
+ async isMemberRegistered(_strandId: string, memberKey: string): Promise<boolean> {
167
+ const count = await scalarCount(
168
+ this.db,
169
+ 'select count(1) as c from Strand.Member where Key = ?',
170
+ [memberKey],
171
+ );
172
+ return count > 0;
173
+ }
174
+ }
175
+
176
+ /** Run a `select count(1) as c ...` and return the scalar (0 if no row). */
177
+ async function scalarCount(db: Database, sql: string, params: SqlValue[]): Promise<number> {
178
+ for await (const row of db.eval(sql, params)) {
179
+ return (row.c as number) ?? 0;
180
+ }
181
+ return 0;
182
+ }