@serfab/cadre-core 0.8.1 → 0.10.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 (197) hide show
  1. package/README.md +6 -4
  2. package/dist/bootstrap-peer-store-file.d.ts +13 -0
  3. package/dist/bootstrap-peer-store-file.js +42 -0
  4. package/dist/bootstrap-peer-store-file.js.map +1 -0
  5. package/dist/bootstrap-peer-store.d.ts +73 -0
  6. package/dist/bootstrap-peer-store.js +128 -0
  7. package/dist/bootstrap-peer-store.js.map +1 -0
  8. package/dist/cadre-node.d.ts +1006 -101
  9. package/dist/cadre-node.js +1977 -283
  10. package/dist/cadre-node.js.map +1 -1
  11. package/dist/control-authorization.d.ts +93 -0
  12. package/dist/control-authorization.js +57 -0
  13. package/dist/control-authorization.js.map +1 -0
  14. package/dist/control-cohort.d.ts +15 -15
  15. package/dist/control-cohort.js +14 -14
  16. package/dist/control-cohort.js.map +1 -1
  17. package/dist/control-database.d.ts +1033 -76
  18. package/dist/control-database.js +1520 -153
  19. package/dist/control-database.js.map +1 -1
  20. package/dist/control-formation-recorder.d.ts +91 -12
  21. package/dist/control-formation-recorder.js +131 -16
  22. package/dist/control-formation-recorder.js.map +1 -1
  23. package/dist/control-schema.d.ts +1 -1
  24. package/dist/control-schema.js +774 -186
  25. package/dist/control-schema.js.map +1 -1
  26. package/dist/control-stream.d.ts +38 -4
  27. package/dist/control-stream.js +70 -4
  28. package/dist/control-stream.js.map +1 -1
  29. package/dist/control-write-retry.d.ts +151 -0
  30. package/dist/control-write-retry.js +411 -0
  31. package/dist/control-write-retry.js.map +1 -0
  32. package/dist/delegate-admission.d.ts +134 -0
  33. package/dist/delegate-admission.js +244 -0
  34. package/dist/delegate-admission.js.map +1 -0
  35. package/dist/device-token.d.ts +12 -12
  36. package/dist/device-token.js +15 -14
  37. package/dist/device-token.js.map +1 -1
  38. package/dist/ed25519-key.d.ts +68 -0
  39. package/dist/ed25519-key.js +106 -0
  40. package/dist/ed25519-key.js.map +1 -0
  41. package/dist/file-durable-slot.d.ts +20 -0
  42. package/dist/file-durable-slot.js +53 -0
  43. package/dist/file-durable-slot.js.map +1 -0
  44. package/dist/formation-approval.d.ts +157 -0
  45. package/dist/formation-approval.js +375 -0
  46. package/dist/formation-approval.js.map +1 -0
  47. package/dist/fs-atomic.d.ts +38 -0
  48. package/dist/fs-atomic.js +97 -0
  49. package/dist/fs-atomic.js.map +1 -0
  50. package/dist/identity-key.d.ts +49 -0
  51. package/dist/identity-key.js +81 -0
  52. package/dist/identity-key.js.map +1 -0
  53. package/dist/index.d.ts +16 -6
  54. package/dist/index.js +44 -10
  55. package/dist/index.js.map +1 -1
  56. package/dist/key-store-file.d.ts +2 -24
  57. package/dist/key-store-file.js +10 -77
  58. package/dist/key-store-file.js.map +1 -1
  59. package/dist/key-store.d.ts +1 -1
  60. package/dist/key-store.js +1 -1
  61. package/dist/membership-connection-gater.d.ts +146 -0
  62. package/dist/membership-connection-gater.js +180 -0
  63. package/dist/membership-connection-gater.js.map +1 -0
  64. package/dist/node-local-snapshot.d.ts +109 -0
  65. package/dist/node-local-snapshot.js +182 -0
  66. package/dist/node-local-snapshot.js.map +1 -0
  67. package/dist/peer-authorization.d.ts +150 -11
  68. package/dist/peer-authorization.js +170 -13
  69. package/dist/peer-authorization.js.map +1 -1
  70. package/dist/peer-record.d.ts +24 -11
  71. package/dist/peer-record.js +29 -13
  72. package/dist/peer-record.js.map +1 -1
  73. package/dist/push-node.d.ts +38 -0
  74. package/dist/push-node.js +48 -0
  75. package/dist/push-node.js.map +1 -0
  76. package/dist/push-notifier-apns.d.ts +10 -2
  77. package/dist/push-notifier-apns.js +14 -10
  78. package/dist/push-notifier-apns.js.map +1 -1
  79. package/dist/push-notifier-fcm.d.ts +7 -1
  80. package/dist/push-notifier-fcm.js +9 -10
  81. package/dist/push-notifier-fcm.js.map +1 -1
  82. package/dist/push-notifier.d.ts +16 -35
  83. package/dist/push-notifier.js +16 -44
  84. package/dist/push-notifier.js.map +1 -1
  85. package/dist/relay-addrs.d.ts +45 -0
  86. package/dist/relay-addrs.js +103 -0
  87. package/dist/relay-addrs.js.map +1 -0
  88. package/dist/relay-reservation.d.ts +266 -0
  89. package/dist/relay-reservation.js +613 -0
  90. package/dist/relay-reservation.js.map +1 -0
  91. package/dist/seed-bootstrap.d.ts +263 -79
  92. package/dist/seed-bootstrap.js +413 -211
  93. package/dist/seed-bootstrap.js.map +1 -1
  94. package/dist/seed-trust-policy.d.ts +50 -24
  95. package/dist/seed-trust-policy.js +28 -19
  96. package/dist/seed-trust-policy.js.map +1 -1
  97. package/dist/strand-addr-protocol.d.ts +42 -13
  98. package/dist/strand-addr-protocol.js +58 -52
  99. package/dist/strand-addr-protocol.js.map +1 -1
  100. package/dist/strand-backfill.d.ts +109 -0
  101. package/dist/strand-backfill.js +279 -0
  102. package/dist/strand-backfill.js.map +1 -0
  103. package/dist/strand-database.d.ts +5 -5
  104. package/dist/strand-database.js +4 -4
  105. package/dist/strand-database.js.map +1 -1
  106. package/dist/strand-formation-manager.d.ts +38 -9
  107. package/dist/strand-formation-manager.js +120 -17
  108. package/dist/strand-formation-manager.js.map +1 -1
  109. package/dist/strand-formation-protocol.d.ts +104 -5
  110. package/dist/strand-formation-protocol.js +233 -5
  111. package/dist/strand-formation-protocol.js.map +1 -1
  112. package/dist/strand-instance-manager.d.ts +40 -2
  113. package/dist/strand-instance-manager.js +93 -15
  114. package/dist/strand-instance-manager.js.map +1 -1
  115. package/dist/strand-member-key.d.ts +6 -6
  116. package/dist/strand-member-key.js +6 -6
  117. package/dist/strand-member-key.js.map +1 -1
  118. package/dist/strand-member-registry.d.ts +12 -12
  119. package/dist/strand-member-registry.js +22 -14
  120. package/dist/strand-member-registry.js.map +1 -1
  121. package/dist/strand-membership-writer.d.ts +532 -138
  122. package/dist/strand-membership-writer.js +800 -179
  123. package/dist/strand-membership-writer.js.map +1 -1
  124. package/dist/strand-solicitation.d.ts +99 -33
  125. package/dist/strand-solicitation.js +100 -52
  126. package/dist/strand-solicitation.js.map +1 -1
  127. package/dist/strand-transport-key.d.ts +40 -0
  128. package/dist/strand-transport-key.js +56 -0
  129. package/dist/strand-transport-key.js.map +1 -0
  130. package/dist/strand-wake-protocol.d.ts +9 -7
  131. package/dist/strand-wake-protocol.js +22 -46
  132. package/dist/strand-wake-protocol.js.map +1 -1
  133. package/dist/strand-watcher.d.ts +17 -2
  134. package/dist/strand-watcher.js +47 -3
  135. package/dist/strand-watcher.js.map +1 -1
  136. package/dist/trusted-owner-store-file.d.ts +12 -0
  137. package/dist/trusted-owner-store-file.js +39 -0
  138. package/dist/trusted-owner-store-file.js.map +1 -0
  139. package/dist/trusted-owner-store.d.ts +64 -0
  140. package/dist/trusted-owner-store.js +126 -0
  141. package/dist/trusted-owner-store.js.map +1 -0
  142. package/dist/types.d.ts +325 -50
  143. package/dist/types.js +13 -0
  144. package/dist/types.js.map +1 -1
  145. package/package.json +23 -8
  146. package/src/bootstrap-peer-store-file.ts +44 -0
  147. package/src/bootstrap-peer-store.ts +172 -0
  148. package/src/cadre-node.ts +2129 -295
  149. package/src/control-authorization.ts +114 -0
  150. package/src/control-cohort.ts +17 -17
  151. package/src/control-database.ts +1766 -191
  152. package/src/control-formation-recorder.ts +174 -21
  153. package/src/control-schema.ts +786 -198
  154. package/src/control-stream.ts +81 -4
  155. package/src/control-write-retry.ts +473 -0
  156. package/src/delegate-admission.ts +302 -0
  157. package/src/device-token.ts +19 -14
  158. package/src/ed25519-key.ts +134 -0
  159. package/src/file-durable-slot.ts +61 -0
  160. package/src/formation-approval.ts +604 -0
  161. package/src/fs-atomic.ts +107 -0
  162. package/src/identity-key.ts +109 -0
  163. package/src/index.ts +157 -24
  164. package/src/key-store-file.ts +10 -81
  165. package/src/key-store.ts +1 -1
  166. package/src/membership-connection-gater.ts +204 -0
  167. package/src/node-local-snapshot.ts +260 -0
  168. package/src/peer-authorization.ts +219 -13
  169. package/src/peer-record.ts +31 -13
  170. package/src/push-node.ts +64 -0
  171. package/src/push-notifier-apns.ts +16 -12
  172. package/src/push-notifier-fcm.ts +9 -10
  173. package/src/push-notifier.ts +16 -57
  174. package/src/relay-addrs.ts +111 -0
  175. package/src/relay-reservation.ts +823 -0
  176. package/src/seed-bootstrap.ts +490 -226
  177. package/src/seed-trust-policy.ts +65 -32
  178. package/src/strand-addr-protocol.ts +84 -53
  179. package/src/strand-backfill.ts +366 -0
  180. package/src/strand-database.ts +6 -6
  181. package/src/strand-formation-manager.ts +145 -19
  182. package/src/strand-formation-protocol.ts +310 -9
  183. package/src/strand-instance-manager.ts +113 -16
  184. package/src/strand-member-key.ts +7 -7
  185. package/src/strand-member-registry.ts +28 -23
  186. package/src/strand-membership-writer.ts +1430 -640
  187. package/src/strand-solicitation.ts +155 -90
  188. package/src/strand-transport-key.ts +58 -0
  189. package/src/strand-wake-protocol.ts +29 -46
  190. package/src/strand-watcher.ts +66 -3
  191. package/src/trusted-owner-store-file.ts +41 -0
  192. package/src/trusted-owner-store.ts +171 -0
  193. package/src/types.ts +342 -51
  194. package/dist/authority-key.d.ts +0 -41
  195. package/dist/authority-key.js +0 -52
  196. package/dist/authority-key.js.map +0 -1
  197. package/src/authority-key.ts +0 -68
@@ -2,6 +2,7 @@ import debug from 'debug';
2
2
  import type { PrivateKey } from '@libp2p/interface';
3
3
  import { createLibp2pNode, type IRawStorage } from '@optimystic/db-p2p';
4
4
  import { StrandDatabase } from './strand-database.js';
5
+ import { StrandBackfill, type StrandBackfillConfig } from './strand-backfill.js';
5
6
  import { assertSchemaSignature } from './schema-verification.js';
6
7
  import type {
7
8
  StrandInstance,
@@ -16,6 +17,8 @@ import type {
16
17
  StrandMode,
17
18
  Libp2pNodeWithRepo
18
19
  } from './types.js';
20
+ import { resolveStrandClusterSize, STRAND_CLUSTER_POLICY } from './types.js';
21
+ import { resolveListenAddrs } from './relay-addrs.js';
19
22
 
20
23
  const log = debug('sereus:cadre:strand-manager');
21
24
  const timing = debug('sereus:cadre:timing');
@@ -48,11 +51,30 @@ export interface StartStrandConfig {
48
51
  requireSignedSchemas?: boolean;
49
52
  /**
50
53
  * Whether this node founds the strand (vs. joins it). Forwarded to the
51
- * StrandDatabase so the founder bootstrap (Header, founding Member/Authority)
54
+ * StrandDatabase so the founder bootstrap (Header, founding Member/Manager)
52
55
  * runs once at bring-up. Joiners leave this unset and write nothing. See
53
56
  * {@link StrandConfig.founder}.
54
57
  */
55
58
  founder?: boolean;
59
+ /**
60
+ * Number of nodes Optimystic is told this strand's replication cluster should
61
+ * have. Same rule as {@link CadreNodeConfig.strandClusterSize}, which CadreNode
62
+ * forwards here: every node on the strand should use the same value, and it is
63
+ * frozen when the strand's libp2p node is created. Defaults to
64
+ * `DEFAULT_STRAND_CLUSTER_SIZE` (4); values below `MIN_CLUSTER_SIZE` (2) are
65
+ * rejected by `resolveStrandClusterSize`. Strand-only — the
66
+ * control network's breadth is the fixed `CONTROL_REPLICATION_BREADTH`.
67
+ */
68
+ clusterSize?: number;
69
+ /**
70
+ * Tuning for the strand peer-join block catch-up ({@link StrandBackfill}),
71
+ * forwarded from {@link CadreNodeConfig.strandBackfill}. When the strand's
72
+ * libp2p node connects to a peer this runtime has not yet caught up, every
73
+ * block in the strand's own raw store is pushed to it. Runs only on
74
+ * `networked` strands with per-strand storage; `{ enabled: false }` restores
75
+ * the pre-existing no-backfill behaviour.
76
+ */
77
+ backfill?: StrandBackfillConfig;
56
78
  }
57
79
 
58
80
  /**
@@ -132,6 +154,15 @@ export class StrandInstanceManager {
132
154
  * without the caller re-threading storage/network/profile/key/sApp config.
133
155
  */
134
156
  private launchConfigs: Map<string, StartStrandConfig> = new Map();
157
+ /**
158
+ * The per-strand peer-join block catch-up, keyed by strand id. Private (not on
159
+ * the public {@link StrandInstance}) because it is runtime plumbing with the
160
+ * same lifetime as the strand's libp2p node: created in `buildStrandRuntime`,
161
+ * stopped and dropped in `releaseRuntime` — so quiesce → resume rebuilds it
162
+ * with a fresh caught-up-peer memo, which is intended (a resumed node may have
163
+ * missed writes).
164
+ */
165
+ private backfills: Map<string, StrandBackfill> = new Map();
135
166
  private stopping = false;
136
167
 
137
168
  constructor() {
@@ -160,7 +191,16 @@ export class StrandInstanceManager {
160
191
  }
161
192
 
162
193
  /**
163
- * Start a new strand instance
194
+ * Start a new strand instance.
195
+ *
196
+ * A failed launch leaves NOTHING tracked: the instance and its retained launch
197
+ * config are both dropped before the error is rethrown, so the strand id is
198
+ * free for a genuine retry. This matches the pre-registration failure path
199
+ * (a rejected schema signature, which throws before anything is recorded) —
200
+ * both failure modes of this call leave the same residue: none. Callers learn
201
+ * of the failure from the rejected promise (and, on the control-discovered
202
+ * path, from CadreNode's `strand:error` event), not from an error record left
203
+ * behind in `instances`.
164
204
  */
165
205
  async startStrand(config: StartStrandConfig): Promise<StrandInstance> {
166
206
  const { strandRow, sAppConfig } = config;
@@ -201,7 +241,10 @@ export class StrandInstanceManager {
201
241
  memberPrivateKey: strandRow.MemberPrivateKey ?? undefined,
202
242
  connectedPeers: 0,
203
243
  lastActivity: new Date(),
204
- latencyHint
244
+ latencyHint,
245
+ // Seed only — buildStrandRuntime overwrites this with the same resolution,
246
+ // so an instance that errors mid-build still reads sensibly.
247
+ mode: config.mode ?? 'networked'
205
248
  };
206
249
 
207
250
  this.instances.set(strandId, instance);
@@ -213,9 +256,15 @@ export class StrandInstanceManager {
213
256
  log('Strand %s started successfully with sApp %s', strandId, sAppConfig.id);
214
257
  return instance;
215
258
  } catch (error) {
259
+ // Status/error first — the (now discarded) record is still what `log` reports on.
216
260
  instance.status = 'error';
217
261
  instance.error = error instanceof Error ? error.message : String(error);
218
262
  log('Failed to start strand %s: %s', strandId, instance.error);
263
+ // Drop the dead record so this strand id can be launched again. Keep the
264
+ // `launchConfigs` has an entry iff `instances` does invariant — resumeStrand
265
+ // reads both, and a config without an instance would strand the config.
266
+ this.instances.delete(strandId);
267
+ this.launchConfigs.delete(strandId);
219
268
  throw error;
220
269
  }
221
270
  }
@@ -231,6 +280,7 @@ export class StrandInstanceManager {
231
280
  private async buildStrandRuntime(instance: StrandInstance, config: StartStrandConfig): Promise<void> {
232
281
  const strandId = instance.strandId;
233
282
  const { sAppConfig } = config;
283
+ const mode = config.mode ?? 'networked';
234
284
 
235
285
  // Resolve storage for this strand. If a factory function is provided, it is
236
286
  // called with the strandId to create strand-specific storage (e.g.,
@@ -240,43 +290,60 @@ export class StrandInstanceManager {
240
290
  log('Strand %s using provided storage provider', strandId);
241
291
  }
242
292
 
243
- // Note: protocolPrefix would be used by libp2p services, but createLibp2pNode
244
- // may not support it yet - tracked for future implementation.
245
- const _protocolPrefix = `/sereus/strand/${strandId}`;
293
+ // db-p2p namespaces every one of the node's protocol ids by network name
294
+ // (`/optimystic/<networkName>/...`), so anything dialing this node's own
295
+ // services must derive its prefix from the SAME string the node was built
296
+ // with — hence one binding for both, not two literals that can drift.
297
+ const networkName = `strand-${strandId}`;
298
+ const protocolPrefix = `/optimystic/${networkName}`;
246
299
 
247
300
  // Determine relay mode: if explicitly set in config, use that;
248
301
  // otherwise default to true for storage profile nodes.
249
302
  const enableRelay = config.network?.enableRelay ?? (config.profile === 'storage');
303
+ const listenAddrs = resolveListenAddrs(config.network);
250
304
 
251
305
  try {
252
306
  let t0 = performance.now();
253
307
  const node = await createLibp2pNode({
254
308
  port: 0, // Random port
255
309
  bootstrapNodes: config.bootstrapNodes ?? [],
256
- networkName: `strand-${strandId}`,
310
+ networkName,
257
311
  storage: strandStorage,
258
312
  fretProfile: config.profile === 'storage' ? 'core' : 'edge',
259
313
  relay: enableRelay,
260
- clusterSize: 3,
261
- clusterPolicy: {
262
- allowDownsize: true,
263
- sizeTolerance: 0.5
264
- },
314
+ clusterSize: resolveStrandClusterSize(config.clusterSize),
315
+ // Deliberately NOT CONTROL_CLUSTER_POLICY: a strand is application data with its own
316
+ // breadth reasoning, and the shape match with the control policy is a coincidence.
317
+ clusterPolicy: STRAND_CLUSTER_POLICY,
265
318
  arachnode: {
266
319
  enableRingZulu: config.profile === 'storage'
267
320
  },
268
321
  ...(config.privateKey && { privateKey: config.privateKey }),
269
322
  ...(config.network?.transports && { transports: config.network.transports }),
270
- ...(config.network?.listenAddrs && { listenAddrs: config.network.listenAddrs }),
323
+ // NOTE: strand nodes receive the same RESOLVED listen addrs as the
324
+ // control node. A fixed-port listen addr (e.g. cadre-cli's example
325
+ // `/ip4/0.0.0.0/tcp/4001`) would have control + strand nodes racing to
326
+ // bind one port — EADDRINUSE. Unverified; if a deployment configures a
327
+ // fixed port and strands fail to start, rewrite the port per node here.
328
+ // An inherited `/p2p-circuit` addr, by contrast, is deliberate — it is
329
+ // what gives a NAT'd strand node a reachable relay slot — and works
330
+ // because the launch path announces this strand's derived peerId to
331
+ // the relay first (delegate admission; see cadre-node.ts). Those circuit
332
+ // entries come either from a hand-written `network.listenAddrs` or from
333
+ // `network.relayAddrs`, which `resolveListenAddrs` folds into the same list.
334
+ ...(listenAddrs && { listenAddrs }),
271
335
  ...(config.network?.connectionGater && { connectionGater: config.network.connectionGater })
272
336
  }) as Libp2pNodeWithRepo;
273
337
  timing('[buildStrandRuntime:%s] createLibp2pNode: %dms', strandId, Math.round(performance.now() - t0));
274
338
 
275
339
  instance.libp2pNode = node;
340
+ // Assigned on every runtime (re)build — resume re-enters here with
341
+ // resumeConfig, so a bootstrap → networked shift refreshes the field.
342
+ instance.mode = mode;
276
343
 
277
344
  // Create and initialize the StrandDatabase. In bootstrap mode the same
278
345
  // strandStorage instance also backs the optimystic local transactor so
279
- // DML lands on the host's persistent storage (e.g. MMKV on RN). Sharing
346
+ // DML lands on the host's persistent storage (e.g. LevelDB on RN). Sharing
280
347
  // the instance — not creating a second one over the same id+prefix —
281
348
  // avoids cache divergence between the libp2p and database paths.
282
349
  //
@@ -288,11 +355,11 @@ export class StrandInstanceManager {
288
355
  sAppConfig,
289
356
  libp2pNode: node,
290
357
  coordinatedRepo: node.coordinatedRepo,
291
- mode: config.mode ?? 'networked',
358
+ mode,
292
359
  rawStorage: strandStorage,
293
360
  // Founder bootstrap inputs: the strand's type drives which membership rows
294
361
  // are written, and the closed-strand MemberPrivateKey derives the founding
295
- // Member/Authority key. Both come off the control-network strand row.
362
+ // Member/Manager key. Both come off the control-network strand row.
296
363
  strandType: config.strandRow.Type,
297
364
  memberPrivateKey: config.strandRow.MemberPrivateKey ?? undefined,
298
365
  founder: config.founder
@@ -301,6 +368,28 @@ export class StrandInstanceManager {
301
368
  await strandDb.initialize();
302
369
  timing('[buildStrandRuntime:%s] strandDatabase.initialize: %dms', strandId, Math.round(performance.now() - t0));
303
370
 
371
+ // Peer-join block catch-up: push this strand's own blocks to each newly
372
+ // connected peer, so a machine that joined after blocks were committed
373
+ // still ends up physically holding them (a bootstrap strand has no mesh
374
+ // to catch up, and without per-strand storage there is nothing to copy).
375
+ if (mode === 'networked' && strandStorage && config.backfill?.enabled !== false) {
376
+ if (node.keyNetwork) {
377
+ const backfill = new StrandBackfill({
378
+ strandId,
379
+ libp2p: node,
380
+ peerNetwork: node.keyNetwork,
381
+ storage: strandStorage,
382
+ // The same prefix the receiver registered its block-transfer handler
383
+ // under — derived from networkName above, never re-spelled here.
384
+ protocolPrefix
385
+ }, config.backfill);
386
+ backfill.start();
387
+ this.backfills.set(strandId, backfill);
388
+ } else {
389
+ log('Strand %s: libp2p node exposes no keyNetwork; peer-join block catch-up is inert', strandId);
390
+ }
391
+ }
392
+
304
393
  instance.status = 'active';
305
394
  instance.lastActivity = new Date();
306
395
  } catch (error) {
@@ -323,6 +412,14 @@ export class StrandInstanceManager {
323
412
  * Shared by `quiesceStrand`, `stopStrand`, and that rollback path.
324
413
  */
325
414
  private async releaseRuntime(instance: StrandInstance): Promise<void> {
415
+ // Backfill first — before the database closes and the libp2p node stops — so
416
+ // no NEW catch-up push is issued against a torn-down transport. A push already
417
+ // in flight is not awaited; it fails into the module's own per-chunk catch.
418
+ const backfill = this.backfills.get(instance.strandId);
419
+ if (backfill) {
420
+ backfill.stop();
421
+ this.backfills.delete(instance.strandId);
422
+ }
326
423
  if (instance.database) {
327
424
  await instance.database.close();
328
425
  instance.database = undefined;
@@ -1,6 +1,6 @@
1
1
  import { generateKeyPair, privateKeyToProtobuf, privateKeyFromProtobuf } from '@libp2p/crypto/keys';
2
2
  import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
3
- import { authorityKeyFromLibp2p, type AuthorityKeyPair } from './authority-key.js';
3
+ import { ed25519KeyPairFromLibp2p, type Ed25519KeyPair } from './ed25519-key.js';
4
4
 
5
5
  /**
6
6
  * Mint a fresh ed25519 strand member private key, encoded as base64 protobuf.
@@ -22,18 +22,18 @@ export async function generateStrandMemberKey(): Promise<string> {
22
22
  * base64url keypair the strand RBAC constraints consume.
23
23
  *
24
24
  * The `Strand` control-layer `MemberPrivateKey` is the closed-strand read-gating
25
- * secret; the founding `Member.Key`/`Authority.MemberKey` (the strand RBAC layer)
25
+ * secret; the founding `Member.Key`/`Manager.MemberKey` (the strand RBAC layer)
26
26
  * are *derived from it* — they are the `publicKeyB64` of the keypair this returns.
27
27
  * Decode the protobuf to a libp2p private key, then reuse
28
- * {@link authorityKeyFromLibp2p} so the same seed→public derivation used for node
29
- * authority keys yields a stable `{ privateKeyB64, publicKeyB64 }` whose public
28
+ * {@link ed25519KeyPairFromLibp2p} so the same seed→public derivation used for node
29
+ * owner keys yields a stable `{ privateKeyB64, publicKeyB64 }` whose public
30
30
  * key a later strand signature verifies against.
31
31
  *
32
32
  * @param memberPrivateKey - The strand's `MemberPrivateKey` (base64 protobuf).
33
- * @returns The base64url seed/public-key pair for founding membership/authority.
33
+ * @returns The base64url seed/public-key pair for founding membership/manager.
34
34
  * @throws If the decoded key is not Ed25519 or the raw bytes are malformed.
35
35
  */
36
- export function strandMemberKeyPair(memberPrivateKey: string): AuthorityKeyPair {
36
+ export function strandMemberKeyPair(memberPrivateKey: string): Ed25519KeyPair {
37
37
  const privateKey = privateKeyFromProtobuf(uint8ArrayFromString(memberPrivateKey, 'base64'));
38
- return authorityKeyFromLibp2p(privateKey);
38
+ return ed25519KeyPairFromLibp2p(privateKey);
39
39
  }
@@ -2,13 +2,13 @@ import debug from 'debug';
2
2
  import type { Database, SqlValue } from '@quereus/quereus';
3
3
  import type { MemberRegistration } from './types.js';
4
4
  import type { MemberRegistry, MemberVerifier } from './enrollment.js';
5
- import type { AuthorityKeyPair } from './authority-key.js';
5
+ import type { Ed25519KeyPair } from './ed25519-key.js';
6
6
  import {
7
7
  verifyStrandPayload,
8
8
  consumeInvite,
9
- addMemberByAuthority,
9
+ addMemberByManager,
10
+ listOutstandingInvites,
10
11
  } from './strand-membership-writer.js';
11
- import { canonicalDatetime } from './canonical-datetime.js';
12
12
 
13
13
  const log = debug('sereus:cadre:strand-member-registry');
14
14
 
@@ -37,10 +37,10 @@ export type StrandAdmission =
37
37
  readonly invitePrivateKey: string;
38
38
  }
39
39
  | {
40
- /** Admit directly by authority signature (the authority-side join). */
41
- readonly mode: 'authority';
42
- /** The admitting authority's strand keypair. */
43
- readonly authorityKeyPair: AuthorityKeyPair;
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
44
  };
45
45
 
46
46
  /**
@@ -80,11 +80,11 @@ export class StrandMemberVerifier implements MemberVerifier {
80
80
  * single-use, and cryptographic gates are all enforced by the deferred
81
81
  * `Strand.*` constraints when the member is actually written.
82
82
  *
83
- * Expired invites are filtered out so this pre-flight matches the on-engine
84
- * `ConsumedInvite.NotExpired` gate — `Now` is canonicalised the same way the
85
- * writer canonicalises it, so `Expiration > ?` compares like-for-like canonical
86
- * strings. A member that already holds a `ConsumedInvite` short-circuits above,
87
- * regardless of any invite's expiry.
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
88
  */
89
89
  async isAuthorizedToJoin(_strandId: string, memberKey: string): Promise<boolean> {
90
90
  const consumed = await scalarCount(
@@ -93,15 +93,20 @@ export class StrandMemberVerifier implements MemberVerifier {
93
93
  [memberKey],
94
94
  );
95
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.
96
102
  return true;
97
103
  }
98
- const nowCanonical = await canonicalDatetime(this.db, Date.now());
99
- const invites = await scalarCount(
100
- this.db,
101
- 'select count(1) as c from Strand.Invite where Expiration is null or Expiration > ?',
102
- [nowCanonical],
103
- );
104
- return invites > 0;
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;
105
110
  }
106
111
  }
107
112
 
@@ -115,8 +120,8 @@ export class StrandMemberVerifier implements MemberVerifier {
115
120
  * `Member.Authorized` is taken:
116
121
  * - `invite` → {@link consumeInvite} (atomic `Member` + `ConsumedInvite`), the
117
122
  * invitee-side flow that redeems a single-use invite.
118
- * - `authority` → {@link addMemberByAuthority}, the authority-side flow that
119
- * seats a member already trusted by an authority.
123
+ * - `manager` → {@link addMemberByManager}, the manager-side flow that
124
+ * seats a member already trusted by a manager.
120
125
  *
121
126
  * Scoped to one strand's db; the `strandId` argument is accepted for interface
122
127
  * compatibility but writes target this db's `Strand.*` tables.
@@ -152,8 +157,8 @@ export class StrandMemberRegistry implements MemberRegistry {
152
157
  return;
153
158
  }
154
159
 
155
- await addMemberByAuthority(this.db, {
156
- authorityKeyPair: this.admission.authorityKeyPair,
160
+ await addMemberByManager(this.db, {
161
+ managerKeyPair: this.admission.managerKeyPair,
157
162
  memberKey,
158
163
  });
159
164
  }