@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,6 +1,7 @@
1
1
  import debug from 'debug';
2
2
  import type { PrivateKey } from '@libp2p/interface';
3
3
  import { createLibp2pNode, type IRawStorage } from '@optimystic/db-p2p';
4
+ import { wrapStorageWithCache, disposeStorageCache } from '@serfab/quereus-plugin-sereus';
4
5
  import { StrandDatabase } from './strand-database.js';
5
6
  import { StrandBackfill, type StrandBackfillConfig } from './strand-backfill.js';
6
7
  import { assertSchemaSignature } from './schema-verification.js';
@@ -14,7 +15,6 @@ import type {
14
15
  SAppConfig,
15
16
  SAppInfo,
16
17
  RawStorageProvider,
17
- StrandMode,
18
18
  Libp2pNodeWithRepo
19
19
  } from './types.js';
20
20
  import { resolveStrandClusterSize, STRAND_CLUSTER_POLICY } from './types.js';
@@ -37,12 +37,6 @@ export interface StartStrandConfig {
37
37
  privateKey?: PrivateKey;
38
38
  /** Cohort-derived discovery seed (multiaddr strings). Defaults to [] when omitted. */
39
39
  bootstrapNodes?: string[];
40
- /**
41
- * Lifecycle mode for this strand; selects the default transactor used by the
42
- * StrandDatabase. When omitted, the StrandDatabase falls back to `'networked'`;
43
- * callers (CadreNode) infer it from cohort membership before reaching here.
44
- */
45
- mode?: StrandMode;
46
40
  /**
47
41
  * Require a valid author signature on the sApp schema before bring-up.
48
42
  * Defaults to true (fail closed) when omitted; set false only for dev/test
@@ -70,23 +64,21 @@ export interface StartStrandConfig {
70
64
  * Tuning for the strand peer-join block catch-up ({@link StrandBackfill}),
71
65
  * forwarded from {@link CadreNodeConfig.strandBackfill}. When the strand's
72
66
  * 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.
67
+ * block in the strand's own raw store is pushed to it. Runs only on strands
68
+ * with per-strand storage; `{ enabled: false }` restores the pre-existing
69
+ * no-backfill behaviour.
76
70
  */
77
71
  backfill?: StrandBackfillConfig;
78
72
  }
79
73
 
80
74
  /**
81
- * Volatile inputs re-resolved when resuming a quiesced strand. These may have
82
- * changed since the strand first launched — the cohort discovery seed grows as
83
- * peers are learned, and cohort membership can push a strand `bootstrap → networked`.
75
+ * Volatile inputs re-resolved when resuming a quiesced strand. The cohort
76
+ * discovery seed is the only one — it grows as peers are learned since the
77
+ * strand first launched.
84
78
  */
85
79
  export interface ResumeStrandOverrides {
86
80
  /** Freshly-resolved cohort discovery seed (multiaddr strings). */
87
81
  bootstrapNodes?: string[];
88
- /** Freshly-resolved lifecycle mode. */
89
- mode?: StrandMode;
90
82
  }
91
83
 
92
84
  /**
@@ -128,6 +120,9 @@ export function getStrandStoragePath(basePath: string, strandId: string): string
128
120
  * Resolve a storage provider for a specific strand.
129
121
  * If the provider is a factory function, call it with the strandId.
130
122
  *
123
+ * Called only from {@link StrandInstanceManager.startStrand}, which owns the result
124
+ * for the instance's lifetime — see `strandStorages`.
125
+ *
131
126
  * @param provider - Storage provider (instance or factory)
132
127
  * @param strandId - The strand ID to create storage for
133
128
  * @returns The resolved IRawStorage instance, or undefined if no provider
@@ -139,7 +134,11 @@ function resolveStrandStorage(
139
134
  if (!provider) {
140
135
  return undefined;
141
136
  }
142
- return typeof provider === 'function' ? provider(strandId) : provider;
137
+ const storage = typeof provider === 'function' ? provider(strandId) : provider;
138
+ // Wrapped in the write-through raw-storage cache (quereus-plugin-sereus's cached-storage.ts).
139
+ // Called ONCE per strand launch — `startStrand` keeps the result for the instance's
140
+ // lifetime — so the wrap survives every runtime rebuild the instance goes through.
141
+ return wrapStorageWithCache(storage, strandId);
143
142
  }
144
143
 
145
144
  /**
@@ -163,6 +162,26 @@ export class StrandInstanceManager {
163
162
  * missed writes).
164
163
  */
165
164
  private backfills: Map<string, StrandBackfill> = new Map();
165
+ /**
166
+ * The resolved (cache-wrapped) raw storage per strand id — the instance's OWN
167
+ * store, resolved once in `startStrand` and held until `stopStrand` disposes it.
168
+ * Private for the same reason `backfills` is: runtime plumbing, not part of the
169
+ * public {@link StrandInstance}.
170
+ *
171
+ * Deliberately NOT touched by `releaseRuntime`: that is what lets a quiesce →
172
+ * resume cycle rebuild the libp2p node over the SAME store, keeping its
173
+ * write-through cache warm (and, on an in-memory backend, keeping the strand's
174
+ * blocks at all). An entry exists iff `instances` does AND the launch config
175
+ * supplied a storage provider.
176
+ *
177
+ * NOTE: a hibernating strand therefore keeps its store — and its share of the
178
+ * process-wide cache pool — resident for as long as the instance is tracked. That
179
+ * is the point (a warm wake), and the pool evicts under pressure, so the cost is
180
+ * one map entry per hibernating strand today. If a device ever hibernates strands
181
+ * by the hundred, revisit: dropping the store at quiesce and paying for a cold
182
+ * wake becomes the better trade.
183
+ */
184
+ private strandStorages: Map<string, IRawStorage> = new Map();
166
185
  private stopping = false;
167
186
 
168
187
  constructor() {
@@ -201,6 +220,10 @@ export class StrandInstanceManager {
201
220
  * of the failure from the rejected promise (and, on the control-discovered
202
221
  * path, from CadreNode's `strand:error` event), not from an error record left
203
222
  * behind in `instances`.
223
+ *
224
+ * The strand's raw storage is resolved HERE, once, and held for the instance's
225
+ * lifetime (see `strandStorages`) — `buildStrandRuntime` only reads it, so a
226
+ * hibernation wake never re-enters the embedder's provider.
204
227
  */
205
228
  async startStrand(config: StartStrandConfig): Promise<StrandInstance> {
206
229
  const { strandRow, sAppConfig } = config;
@@ -231,6 +254,15 @@ export class StrandInstanceManager {
231
254
  signature: sAppConfig.signature
232
255
  };
233
256
 
257
+ // Resolve this strand's storage ONCE, before anything is recorded. If a factory
258
+ // function is provided, it is called with the strandId to create strand-specific
259
+ // storage (e.g. strand-isolated directories). A provider that throws therefore
260
+ // fails the launch alongside the schema-signature check, leaving nothing tracked.
261
+ const strandStorage = resolveStrandStorage(config.storage?.provider, strandId);
262
+ if (strandStorage) {
263
+ log('Strand %s using provided storage provider', strandId);
264
+ }
265
+
234
266
  // Determine latency hint: sApp config > default
235
267
  const latencyHint = sAppConfig.latencyHint ?? config.defaultLatencyHint;
236
268
 
@@ -241,14 +273,14 @@ export class StrandInstanceManager {
241
273
  memberPrivateKey: strandRow.MemberPrivateKey ?? undefined,
242
274
  connectedPeers: 0,
243
275
  lastActivity: new Date(),
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'
276
+ latencyHint
248
277
  };
249
278
 
250
279
  this.instances.set(strandId, instance);
251
280
  this.launchConfigs.set(strandId, config);
281
+ if (strandStorage) {
282
+ this.strandStorages.set(strandId, strandStorage);
283
+ }
252
284
 
253
285
  try {
254
286
  await this.buildStrandRuntime(instance, config);
@@ -265,6 +297,9 @@ export class StrandInstanceManager {
265
297
  // reads both, and a config without an instance would strand the config.
266
298
  this.instances.delete(strandId);
267
299
  this.launchConfigs.delete(strandId);
300
+ // The store this launch resolved goes with it: nothing owns it any more, and a
301
+ // retained cache wrapper would be handed back (already retired) on a retry.
302
+ await this.disposeStrandStorage(strandId);
268
303
  throw error;
269
304
  }
270
305
  }
@@ -273,22 +308,18 @@ export class StrandInstanceManager {
273
308
  * Build (or rebuild) the libp2p node + StrandDatabase for an instance and
274
309
  * attach them, transitioning it to `active`. Shared by `startStrand` (fresh
275
310
  * launch) and `resumeStrand` (rehydrating a quiesced instance). Reads all
276
- * volatile inputs (bootstrapNodes, mode, storage, network, profile,
277
- * privateKey, sApp config) from `config`, so the caller controls the
278
- * cohort-derived values.
311
+ * volatile inputs (bootstrapNodes, network, profile, privateKey, sApp config)
312
+ * from `config`, so the caller controls the cohort-derived values. Storage is the
313
+ * one input it does NOT re-read from `config`: that belongs to the instance and
314
+ * comes from `strandStorages`.
279
315
  */
280
316
  private async buildStrandRuntime(instance: StrandInstance, config: StartStrandConfig): Promise<void> {
281
317
  const strandId = instance.strandId;
282
318
  const { sAppConfig } = config;
283
- const mode = config.mode ?? 'networked';
284
319
 
285
- // Resolve storage for this strand. If a factory function is provided, it is
286
- // called with the strandId to create strand-specific storage (e.g.,
287
- // strand-isolated directories).
288
- const strandStorage = resolveStrandStorage(config.storage?.provider, strandId);
289
- if (strandStorage) {
290
- log('Strand %s using provided storage provider', strandId);
291
- }
320
+ // The store the instance OWNS (resolved once in `startStrand`), not a fresh
321
+ // resolution: a rebuild must reach the same backend through the same warm cache.
322
+ const strandStorage = this.strandStorages.get(strandId);
292
323
 
293
324
  // db-p2p namespaces every one of the node's protocol ids by network name
294
325
  // (`/optimystic/<networkName>/...`), so anything dialing this node's own
@@ -337,15 +368,8 @@ export class StrandInstanceManager {
337
368
  timing('[buildStrandRuntime:%s] createLibp2pNode: %dms', strandId, Math.round(performance.now() - t0));
338
369
 
339
370
  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;
343
-
344
- // Create and initialize the StrandDatabase. In bootstrap mode the same
345
- // strandStorage instance also backs the optimystic local transactor so
346
- // DML lands on the host's persistent storage (e.g. LevelDB on RN). Sharing
347
- // the instance — not creating a second one over the same id+prefix —
348
- // avoids cache divergence between the libp2p and database paths.
371
+
372
+ // Create and initialize the StrandDatabase.
349
373
  //
350
374
  // Attach before initialize so a failed init is cleaned up by
351
375
  // releaseRuntime below (close() is safe on a partially-initialized db).
@@ -355,8 +379,6 @@ export class StrandInstanceManager {
355
379
  sAppConfig,
356
380
  libp2pNode: node,
357
381
  coordinatedRepo: node.coordinatedRepo,
358
- mode,
359
- rawStorage: strandStorage,
360
382
  // Founder bootstrap inputs: the strand's type drives which membership rows
361
383
  // are written, and the closed-strand MemberPrivateKey derives the founding
362
384
  // Member/Manager key. Both come off the control-network strand row.
@@ -370,9 +392,18 @@ export class StrandInstanceManager {
370
392
 
371
393
  // Peer-join block catch-up: push this strand's own blocks to each newly
372
394
  // 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) {
395
+ // still ends up physically holding them (without per-strand storage there
396
+ // is nothing to copy). Armed for EVERY stored strand: `StrandBackfill`
397
+ // only does work when the strand's libp2p node reports a peer connection,
398
+ // so on a device that is genuinely alone it is inert, and arming it at
399
+ // launch is what closes the "founded alone, never replicates" hole — a
400
+ // peer that joins later gets the founder's blocks without any relaunch.
401
+ //
402
+ // NOTE: cost is one StrandBackfill object + one `connection:open` listener
403
+ // per running strand — linear in strand count, negligible at the handful a
404
+ // device or host runs today. If a node ever hosts strands by the hundred,
405
+ // move to one shared listener that dispatches by strand id.
406
+ if (strandStorage && config.backfill?.enabled !== false) {
376
407
  if (node.keyNetwork) {
377
408
  const backfill = new StrandBackfill({
378
409
  strandId,
@@ -410,6 +441,9 @@ export class StrandInstanceManager {
410
441
  * and zeroing connectedPeers. Tolerant of partially-built state — either handle
411
442
  * may be absent — so it doubles as rollback for a failed `buildStrandRuntime`.
412
443
  * Shared by `quiesceStrand`, `stopStrand`, and that rollback path.
444
+ *
445
+ * Leaves `strandStorages` untouched by design — the store outlives the runtime it
446
+ * was built into, which is what makes a resume warm. Only `stopStrand` disposes it.
413
447
  */
414
448
  private async releaseRuntime(instance: StrandInstance): Promise<void> {
415
449
  // Backfill first — before the database closes and the libp2p node stops — so
@@ -431,6 +465,29 @@ export class StrandInstanceManager {
431
465
  instance.connectedPeers = 0;
432
466
  }
433
467
 
468
+ /**
469
+ * Drop the strand's owned store and release its cache registration with the shared
470
+ * pool. Called only where the strand's whole lifetime ends (`stopStrand`, and the
471
+ * failed-launch rollback) — never on a quiesce.
472
+ *
473
+ * A dispose failure is logged, not thrown: the store is already unreferenced here,
474
+ * and failing the stop over a cache-bookkeeping error would leave the caller unable
475
+ * to tear the strand down. `disposeStorageCache` no-ops for an unwrapped store
476
+ * (e.g. `MemoryRawStorage`), so no instanceof test is needed.
477
+ */
478
+ private async disposeStrandStorage(strandId: string): Promise<void> {
479
+ const storage = this.strandStorages.get(strandId);
480
+ if (!storage) {
481
+ return;
482
+ }
483
+ this.strandStorages.delete(strandId);
484
+ try {
485
+ await disposeStorageCache(storage);
486
+ } catch (error) {
487
+ log('Failed to dispose storage cache for strand %s: %o', strandId, error);
488
+ }
489
+ }
490
+
434
491
  /**
435
492
  * Quiesce a strand: release its strand-network resources (stop the libp2p node,
436
493
  * close the StrandDatabase) while RETAINING the instance record — identity,
@@ -459,9 +516,9 @@ export class StrandInstanceManager {
459
516
  * Resume a previously-quiesced strand: rebuild its libp2p node + StrandDatabase
460
517
  * from the retained launch config and re-attach them, transitioning it back to
461
518
  * `active`. `overrides` re-applies volatile inputs that may have changed since
462
- * launch (cohort `bootstrapNodes`, lifecycle `mode`) and updates the retained
463
- * config so a later resume reuses the latest values. Returns the live instance
464
- * unchanged if it is already running.
519
+ * launch (the cohort `bootstrapNodes` seed) and updates the retained config so
520
+ * a later resume reuses the latest values. Returns the live instance unchanged
521
+ * if it is already running.
465
522
  */
466
523
  async resumeStrand(strandId: string, overrides?: ResumeStrandOverrides): Promise<StrandInstance> {
467
524
  if (this.stopping) {
@@ -486,8 +543,7 @@ export class StrandInstanceManager {
486
543
  // Re-apply volatile inputs and persist them so a subsequent resume reuses them.
487
544
  const resumeConfig: StartStrandConfig = {
488
545
  ...launchConfig,
489
- bootstrapNodes: overrides?.bootstrapNodes ?? launchConfig.bootstrapNodes,
490
- mode: overrides?.mode ?? launchConfig.mode
546
+ bootstrapNodes: overrides?.bootstrapNodes ?? launchConfig.bootstrapNodes
491
547
  };
492
548
  this.launchConfigs.set(strandId, resumeConfig);
493
549
 
@@ -523,6 +579,8 @@ export class StrandInstanceManager {
523
579
  instance.status = 'stopped';
524
580
  this.instances.delete(strandId);
525
581
  this.launchConfigs.delete(strandId);
582
+ // Storage is released only here — NOT in releaseRuntime, which a quiesce shares.
583
+ await this.disposeStrandStorage(strandId);
526
584
  log('Strand %s stopped successfully', strandId);
527
585
  } catch (error) {
528
586
  instance.status = 'error';
@@ -1,39 +1,39 @@
1
- import { generateKeyPair, privateKeyToProtobuf, privateKeyFromProtobuf } from '@libp2p/crypto/keys';
2
- import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
3
- import { ed25519KeyPairFromLibp2p, type Ed25519KeyPair } from './ed25519-key.js';
4
-
5
- /**
6
- * Mint a fresh ed25519 strand member private key, encoded as base64 protobuf.
7
- *
8
- * Closed strands (`Type:'c'`) gate membership on a `MemberPrivateKey` column
9
- * (see the `Strand` control-schema table). This produces one in the SAME
10
- * encoding {@link StrandSolicitationService.formStrand} uses for its
11
- * `invitePrivateKey`, so a host-minted key and a formation-issued key are
12
- * interchangeable when attaching a closed strand.
13
- */
14
- export async function generateStrandMemberKey(): Promise<string> {
15
- const privateKey = await generateKeyPair('Ed25519');
16
- return uint8ArrayToString(privateKeyToProtobuf(privateKey), 'base64');
17
- }
18
-
19
- /**
20
- * Bridge a strand's `MemberPrivateKey` (base64 protobuf libp2p ed25519 key, as
21
- * minted by {@link generateStrandMemberKey} or issued by formation) into the
22
- * base64url keypair the strand RBAC constraints consume.
23
- *
24
- * The `Strand` control-layer `MemberPrivateKey` is the closed-strand read-gating
25
- * secret; the founding `Member.Key`/`Manager.MemberKey` (the strand RBAC layer)
26
- * are *derived from it* — they are the `publicKeyB64` of the keypair this returns.
27
- * Decode the protobuf to a libp2p private key, then reuse
28
- * {@link ed25519KeyPairFromLibp2p} so the same seed→public derivation used for node
29
- * owner keys yields a stable `{ privateKeyB64, publicKeyB64 }` whose public
30
- * key a later strand signature verifies against.
31
- *
32
- * @param memberPrivateKey - The strand's `MemberPrivateKey` (base64 protobuf).
33
- * @returns The base64url seed/public-key pair for founding membership/manager.
34
- * @throws If the decoded key is not Ed25519 or the raw bytes are malformed.
35
- */
36
- export function strandMemberKeyPair(memberPrivateKey: string): Ed25519KeyPair {
37
- const privateKey = privateKeyFromProtobuf(uint8ArrayFromString(memberPrivateKey, 'base64'));
38
- return ed25519KeyPairFromLibp2p(privateKey);
39
- }
1
+ import { generateKeyPair, privateKeyToProtobuf, privateKeyFromProtobuf } from '@libp2p/crypto/keys';
2
+ import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
3
+ import { ed25519KeyPairFromLibp2p, type Ed25519KeyPair } from './ed25519-key.js';
4
+
5
+ /**
6
+ * Mint a fresh ed25519 strand member private key, encoded as base64 protobuf.
7
+ *
8
+ * Closed strands (`Type:'c'`) gate membership on a `MemberPrivateKey` column
9
+ * (see the `Strand` control-schema table). This produces one in the SAME
10
+ * encoding {@link StrandSolicitationService.formStrand} uses for its
11
+ * `invitePrivateKey`, so a host-minted key and a formation-issued key are
12
+ * interchangeable when attaching a closed strand.
13
+ */
14
+ export async function generateStrandMemberKey(): Promise<string> {
15
+ const privateKey = await generateKeyPair('Ed25519');
16
+ return uint8ArrayToString(privateKeyToProtobuf(privateKey), 'base64');
17
+ }
18
+
19
+ /**
20
+ * Bridge a strand's `MemberPrivateKey` (base64 protobuf libp2p ed25519 key, as
21
+ * minted by {@link generateStrandMemberKey} or issued by formation) into the
22
+ * base64url keypair the strand RBAC constraints consume.
23
+ *
24
+ * The `Strand` control-layer `MemberPrivateKey` is the closed-strand read-gating
25
+ * secret; the founding `Member.Key`/`Manager.MemberKey` (the strand RBAC layer)
26
+ * are *derived from it* — they are the `publicKeyB64` of the keypair this returns.
27
+ * Decode the protobuf to a libp2p private key, then reuse
28
+ * {@link ed25519KeyPairFromLibp2p} so the same seed→public derivation used for node
29
+ * owner keys yields a stable `{ privateKeyB64, publicKeyB64 }` whose public
30
+ * key a later strand signature verifies against.
31
+ *
32
+ * @param memberPrivateKey - The strand's `MemberPrivateKey` (base64 protobuf).
33
+ * @returns The base64url seed/public-key pair for founding membership/manager.
34
+ * @throws If the decoded key is not Ed25519 or the raw bytes are malformed.
35
+ */
36
+ export function strandMemberKeyPair(memberPrivateKey: string): Ed25519KeyPair {
37
+ const privateKey = privateKeyFromProtobuf(uint8ArrayFromString(memberPrivateKey, 'base64'));
38
+ return ed25519KeyPairFromLibp2p(privateKey);
39
+ }