@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,77 +1,38 @@
1
- import type { StrandMode } from './types.js';
2
-
3
- /**
4
- * A single CadrePeer membership row, as returned by
5
- * `ControlDatabase.queryCadrePeers()`.
6
- */
7
- export interface CohortPeerRow {
8
- peerId: string;
9
- /** Comma-joined list of multiaddr strings; `''` when no addrs were recorded. */
10
- multiaddr: string | null;
11
- }
12
-
13
- /**
14
- * Cohort-derived discovery seed for a strand's libp2p node. `bootstrapNodes`
15
- * here are **strand-network** addresses resolved on demand from co-cadre
16
- * siblings (see `collectStrandAddrs` / `STRAND_ADDR_PROTOCOL`), NOT the control
17
- * addresses stored in `CadrePeer.Multiaddr`.
18
- */
19
- export interface CohortSeed {
20
- /** Dialable strand-network multiaddr strings for cohort peers other than self (deduplicated). */
21
- bootstrapNodes: string[];
22
- /** True when at least one CadrePeer row other than self exists (even if it has no resolvable strand addr). */
23
- hasOtherPeers: boolean;
24
- }
25
-
26
- /**
27
- * Membership-only view of a strand's cohort, derived from the control network's
28
- * CadrePeer rows. The bootstrap addresses themselves are resolved separately
29
- * (over the strand-addr RPC) from these peerIds, never from the rows' addr field.
30
- */
31
- export interface CohortMembers {
32
- /** PeerIds of cohort members other than self — the strand-addr RPC fan-out targets (deduplicated, in row order). */
33
- otherPeerIds: string[];
34
- /** True when at least one CadrePeer row other than self exists (even if it has no resolvable strand addr). */
35
- hasOtherPeers: boolean;
36
- }
37
-
38
- /**
39
- * Derive the membership view of a strand's cohort from the control network's
40
- * CadrePeer rows: the peerIds of members other than `selfPeerId` (the strand-addr
41
- * RPC fan-out targets) and whether any other member exists at all.
42
- *
43
- * Deliberately does NOT read `CadrePeer.Multiaddr` — that field carries each
44
- * node's *control*-network address, which must not seed the *strand* mesh
45
- * (dialing it reaches the remote's control instance, not its strand instance).
46
- * The strand-network bootstrap addresses are resolved on demand over the control
47
- * mesh from these peerIds. `hasOtherPeers` reflects membership presence, NOT
48
- * dialability, so a cohort whose strand addrs are not yet resolvable still
49
- * selects `networked` mode.
50
- */
51
- export function deriveCohortMembers(peers: CohortPeerRow[], selfPeerId?: string): CohortMembers {
52
- const otherPeerIds: string[] = [];
53
- const seen = new Set<string>();
54
-
55
- for (const peer of peers) {
56
- if (selfPeerId !== undefined && peer.peerId === selfPeerId) {
57
- continue;
58
- }
59
- if (seen.has(peer.peerId)) {
60
- continue;
61
- }
62
- seen.add(peer.peerId);
63
- otherPeerIds.push(peer.peerId);
64
- }
65
-
66
- return { otherPeerIds, hasOtherPeers: otherPeerIds.length > 0 };
67
- }
68
-
69
- /**
70
- * Choose the strand mode. An explicit mode (from `addStrand`) always wins. When
71
- * omitted (the `handleStrandAdded` discovery path, or an `addStrand` caller that
72
- * leaves it unset), infer: `bootstrap` for a solo/first node (no other peers),
73
- * `networked` once the cohort has other members.
74
- */
75
- export function selectStrandMode(explicitMode: StrandMode | undefined, hasOtherPeers: boolean): StrandMode {
76
- return explicitMode ?? (hasOtherPeers ? 'networked' : 'bootstrap');
77
- }
1
+ /**
2
+ * A single CadrePeer membership row, as returned by
3
+ * `ControlDatabase.queryCadrePeers()`.
4
+ */
5
+ export interface CohortPeerRow {
6
+ peerId: string;
7
+ /** Comma-joined list of multiaddr strings; `''` when no addrs were recorded. */
8
+ multiaddr: string | null;
9
+ }
10
+
11
+ /**
12
+ * Derive the membership view of a strand's cohort from the control network's
13
+ * CadrePeer rows: the peerIds of members other than `selfPeerId` — the
14
+ * strand-addr RPC fan-out targets, deduplicated, in row order.
15
+ *
16
+ * Deliberately does NOT read `CadrePeer.Multiaddr` — that field carries each
17
+ * node's *control*-network address, which must not seed the *strand* mesh
18
+ * (dialing it reaches the remote's control instance, not its strand instance).
19
+ * The strand-network bootstrap addresses are resolved on demand over the control
20
+ * mesh from these peerIds.
21
+ */
22
+ export function deriveCohortMembers(peers: CohortPeerRow[], selfPeerId?: string): string[] {
23
+ const otherPeerIds: string[] = [];
24
+ const seen = new Set<string>();
25
+
26
+ for (const peer of peers) {
27
+ if (selfPeerId !== undefined && peer.peerId === selfPeerId) {
28
+ continue;
29
+ }
30
+ if (seen.has(peer.peerId)) {
31
+ continue;
32
+ }
33
+ seen.add(peer.peerId);
34
+ otherPeerIds.push(peer.peerId);
35
+ }
36
+
37
+ return otherPeerIds;
38
+ }
@@ -1,204 +1,202 @@
1
- import debug from 'debug';
2
- import { Database } from '@quereus/quereus';
3
- import { connectToStrand } from '@serfab/quereus-plugin-sereus';
4
- import type { SereusPluginResult } from '@serfab/quereus-plugin-sereus';
5
- import type { Libp2p } from '@libp2p/interface';
6
- import type { IRepo } from '@optimystic/db-core';
7
- import type { IRawStorage } from '@optimystic/db-p2p';
8
- import type { SAppConfig, StrandMode } from './types.js';
9
- import { bootstrapFounderMembership } from './strand-membership-writer.js';
10
- import { strandMemberKeyPair } from './strand-member-key.js';
11
-
12
- const log = debug('sereus:cadre:strand-db');
13
- const timing = debug('sereus:cadre:timing');
14
-
15
- export interface StrandDatabaseConfig {
16
- /** The strand ID */
17
- strandId: string;
18
- /** sApp configuration containing the schema */
19
- sAppConfig: SAppConfig;
20
- /** Libp2p node for the strand network */
21
- libp2pNode: Libp2p;
22
- /** Coordinated repo from the libp2p node */
23
- coordinatedRepo: IRepo;
24
- /**
25
- * Lifecycle mode. `'bootstrap'` selects a purely local transactor so the strand
26
- * can initialize (e.g. apply schema DDL) without network round trips on a solo
27
- * node. `'networked'` (the default) uses the network transactor.
28
- */
29
- mode?: StrandMode;
30
- /**
31
- * Raw storage backing the strand. When mode is `'bootstrap'` this is also used
32
- * by the optimystic plugin's local transactor so DML lands on the host's
33
- * persistent storage instead of in-memory. Must be the same instance the
34
- * libp2p node was created with — sharing the instance avoids cache divergence
35
- * across the two consumers.
36
- */
37
- rawStorage?: IRawStorage;
38
- /**
39
- * Strand type (`'o'` open / `'c'` closed) from the control-network strand row.
40
- * Drives the founder bootstrap: open strands get a `Header` only; closed strands
41
- * also get the founding `Member`+`Manager`.
42
- */
43
- strandType: 'o' | 'c';
44
- /**
45
- * The closed-strand `MemberPrivateKey` (base64 protobuf) from the strand row.
46
- * Required when `founder` is true and `strandType` is `'c'` — it derives the
47
- * founding `Member.Key`/`Manager.MemberKey`. Absent for open strands.
48
- */
49
- memberPrivateKey?: string;
50
- /**
51
- * Whether this node founds the strand. When true, {@link initialize} runs the
52
- * one-time founder membership bootstrap after the schema is applied. Joiners
53
- * leave this false and write nothing (rows arrive via sync). Defaults to false.
54
- */
55
- founder?: boolean;
56
- }
57
-
58
- /**
59
- * StrandDatabase manages the sApp schema for a strand using Quereus with the
60
- * Optimystic backend. Each strand instance has its own isolated database with
61
- * the sApp's schema applied.
62
- *
63
- * This class owns the `Database` lifecycle (creation, `getDatabase()`, `close()`)
64
- * but delegates the actual SQL-surface composition — plugin registration, node
65
- * wiring, catalog hydration, schema apply — to `connectToStrand` from
66
- * `@serfab/quereus-plugin-sereus`, the single shared composition. The libp2p
67
- * node is injected here, so `connectToStrand` never *creates* the node;
68
- * `StrandInstanceManager` owns the node lifecycle. (The strand connection's
69
- * `shutdown` still stops the injected node via the collection factory, so the
70
- * manager's own `node.stop()` is an idempotent second stop — see `close()`.)
71
- */
72
- export class StrandDatabase {
73
- private db: Database | null = null;
74
- private shutdownStrand: SereusPluginResult['shutdown'] | null = null;
75
- private readonly config: StrandDatabaseConfig;
76
- private initialized = false;
77
-
78
- constructor(config: StrandDatabaseConfig) {
79
- this.config = config;
80
- }
81
-
82
- /**
83
- * Initialize the database — create the `Database` and delegate the strand
84
- * SQL-surface composition (plugins, node wiring, hydrate, schema apply) to
85
- * `connectToStrand` with the injected libp2p node.
86
- */
87
- async initialize(): Promise<void> {
88
- if (this.initialized) {
89
- log('StrandDatabase for strand %s already initialized', this.config.strandId);
90
- return;
91
- }
92
-
93
- const sid = this.config.strandId;
94
- const mode: StrandMode = this.config.mode ?? 'networked';
95
- log('Initializing StrandDatabase for strand: %s (sApp: %s v%s, mode=%s)',
96
- sid, this.config.sAppConfig.id, this.config.sAppConfig.version, mode);
97
-
98
- this.db = new Database();
99
-
100
- // Delegate to the shared composition. The node is injected, so:
101
- // - `connectToStrand` never creates a node (its `createdNode` stays null);
102
- // `StrandInstanceManager` owns the node lifecycle. Its `shutdown` does
103
- // still stop the injected node via the collection factory, so the
104
- // manager's later `node.stop()` is an idempotent second stop;
105
- // - `storage` (the strand's raw storage, same instance the node uses) is
106
- // handed to the plugin's local transactor in bootstrap mode so DML lands
107
- // on the host backend rather than in-memory.
108
- const t0 = performance.now();
109
- const result = await connectToStrand(this.db, {
110
- strandId: sid,
111
- schema: this.config.sAppConfig.schema,
112
- mode,
113
- storage: this.config.rawStorage,
114
- libp2pNode: this.config.libp2pNode,
115
- coordinatedRepo: this.config.coordinatedRepo,
116
- enableCache: true,
117
- });
118
- this.shutdownStrand = result.shutdown;
119
- timing('[strandDb:%s] connectToStrand: %dms (hydrated tables=%d, indexes=%d)',
120
- sid, Math.round(performance.now() - t0),
121
- result.hydrated?.tables ?? 0, result.hydrated?.indexes ?? 0);
122
-
123
- // Founder-only: write the one-time membership bootstrap now that the schema is
124
- // applied (connectToStrand has returned). A throw here propagates out of
125
- // initialize() so buildStrandRuntime's rollback tears the half-built strand
126
- // down rather than leaking a node. Joiners skip this — their rows arrive via sync.
127
- if (this.config.founder === true) {
128
- await this.bootstrapFounder();
129
- }
130
-
131
- this.initialized = true;
132
- log('StrandDatabase for strand %s initialized successfully', sid);
133
- }
134
-
135
- /**
136
- * Run the founder membership bootstrap against the freshly-composed strand DB.
137
- *
138
- * Derives the founding keypair from the closed-strand `memberPrivateKey` (the
139
- * `Member.Key`/`Manager.MemberKey` are its public key); a closed strand with no
140
- * `memberPrivateKey` throws, because it could never seat a founding manager.
141
- * Open strands derive no keypair (Header only). Idempotent — see
142
- * {@link bootstrapFounderMembership}.
143
- */
144
- private async bootstrapFounder(): Promise<void> {
145
- const { strandId, strandType, memberPrivateKey, sAppConfig } = this.config;
146
- const founderKeyPair = strandType === 'c'
147
- ? this.deriveFounderKeyPair(strandId, memberPrivateKey)
148
- : undefined;
149
- await bootstrapFounderMembership(this.db!, {
150
- strandId,
151
- type: strandType,
152
- sApp: sAppConfig,
153
- founderKeyPair,
154
- });
155
- }
156
-
157
- /**
158
- * Derive the founding keypair for a closed strand from its `memberPrivateKey`,
159
- * failing loudly when the key is absent (a closed strand with no founding
160
- * Manager can never admit anyone).
161
- */
162
- private deriveFounderKeyPair(strandId: string, memberPrivateKey: string | undefined) {
163
- if (!memberPrivateKey) {
164
- throw new Error(
165
- `Cannot found closed strand ${strandId}: the strand row has no MemberPrivateKey. ` +
166
- 'A closed strand needs a founding Member/Manager derived from that key.',
167
- );
168
- }
169
- return strandMemberKeyPair(memberPrivateKey);
170
- }
171
-
172
- /**
173
- * Get the underlying database for queries
174
- */
175
- getDatabase(): Database {
176
- this.ensureInitialized();
177
- return this.db!;
178
- }
179
-
180
- /**
181
- * Close the database and cleanup resources. Runs the strand-connection
182
- * shutdown (collection-factory teardown, which also stops the injected node),
183
- * then closes the `Database`. `StrandInstanceManager.releaseRuntime` issues a
184
- * further idempotent `node.stop()` after this returns.
185
- */
186
- async close(): Promise<void> {
187
- if (this.shutdownStrand) {
188
- await this.shutdownStrand();
189
- this.shutdownStrand = null;
190
- }
191
- if (this.db) {
192
- void this.db.close();
193
- this.db = null;
194
- }
195
- this.initialized = false;
196
- log('StrandDatabase for strand %s closed', this.config.strandId);
197
- }
198
-
199
- private ensureInitialized(): void {
200
- if (!this.initialized || !this.db) {
201
- throw new Error(`StrandDatabase for strand ${this.config.strandId} not initialized. Call initialize() first.`);
202
- }
203
- }
204
- }
1
+ import debug from 'debug';
2
+ import { Database } from '@quereus/quereus';
3
+ import { connectToStrand } from '@serfab/quereus-plugin-sereus';
4
+ import type { SereusPluginResult } from '@serfab/quereus-plugin-sereus';
5
+ import type { Libp2p } from '@libp2p/interface';
6
+ import type { IRepo } from '@optimystic/db-core';
7
+ import type { SAppConfig } from './types.js';
8
+ import { bootstrapFounderMembership } from './strand-membership-writer.js';
9
+ import { strandMemberKeyPair } from './strand-member-key.js';
10
+
11
+ const log = debug('sereus:cadre:strand-db');
12
+ const timing = debug('sereus:cadre:timing');
13
+
14
+ export interface StrandDatabaseConfig {
15
+ /** The strand ID */
16
+ strandId: string;
17
+ /** sApp configuration containing the schema */
18
+ sAppConfig: SAppConfig;
19
+ /** Libp2p node for the strand network */
20
+ libp2pNode: Libp2p;
21
+ /** Coordinated repo from the libp2p node */
22
+ coordinatedRepo: IRepo;
23
+ /**
24
+ * Strand type (`'o'` open / `'c'` closed) from the control-network strand row.
25
+ * Drives the founder bootstrap: open strands get a `Header` only; closed strands
26
+ * also get the founding `Member`+`Manager`.
27
+ */
28
+ strandType: 'o' | 'c';
29
+ /**
30
+ * The closed-strand `MemberPrivateKey` (base64 protobuf) from the strand row.
31
+ * Required when `founder` is true and `strandType` is `'c'` — it derives the
32
+ * founding `Member.Key`/`Manager.MemberKey`. Absent for open strands.
33
+ */
34
+ memberPrivateKey?: string;
35
+ /**
36
+ * Whether this node founds the strand. When true, {@link initialize} runs the
37
+ * one-time founder membership bootstrap after the schema is applied. Joiners
38
+ * leave this false and write nothing (rows arrive via sync). Defaults to false.
39
+ */
40
+ founder?: boolean;
41
+ }
42
+
43
+ /**
44
+ * StrandDatabase manages the sApp schema for a strand using Quereus with the
45
+ * Optimystic backend. Each strand instance has its own isolated database with
46
+ * the sApp's schema applied.
47
+ *
48
+ * This class owns the `Database` lifecycle (creation, `getDatabase()`, `close()`)
49
+ * but delegates the actual SQL-surface composition — plugin registration, node
50
+ * wiring, catalog hydration, schema apply — to `connectToStrand` from
51
+ * `@serfab/quereus-plugin-sereus`, the single shared composition. The libp2p
52
+ * node is injected here, so `connectToStrand` never *creates* the node;
53
+ * `StrandInstanceManager` owns the node lifecycle. (The strand connection's
54
+ * `shutdown` still stops the injected node via the collection factory, so the
55
+ * manager's own `node.stop()` is an idempotent second stop — see `close()`.)
56
+ */
57
+ export class StrandDatabase {
58
+ private db: Database | null = null;
59
+ private shutdownStrand: SereusPluginResult['shutdown'] | null = null;
60
+ private resolvedTransactor: SereusPluginResult['transactor'] | null = null;
61
+ private readonly config: StrandDatabaseConfig;
62
+ private initialized = false;
63
+
64
+ constructor(config: StrandDatabaseConfig) {
65
+ this.config = config;
66
+ }
67
+
68
+ /**
69
+ * Initialize the database — create the `Database` and delegate the strand
70
+ * SQL-surface composition (plugins, node wiring, hydrate, schema apply) to
71
+ * `connectToStrand` with the injected libp2p node.
72
+ */
73
+ async initialize(): Promise<void> {
74
+ if (this.initialized) {
75
+ log('StrandDatabase for strand %s already initialized', this.config.strandId);
76
+ return;
77
+ }
78
+
79
+ const sid = this.config.strandId;
80
+ log('Initializing StrandDatabase for strand: %s (sApp: %s v%s)',
81
+ sid, this.config.sAppConfig.id, this.config.sAppConfig.version);
82
+
83
+ this.db = new Database();
84
+
85
+ // Delegate to the shared composition. The node is injected, so:
86
+ // - `connectToStrand` never creates a node (its `createdNode` stays null);
87
+ // `StrandInstanceManager` owns the node lifecycle. Its `shutdown` does
88
+ // still stop the injected node via the collection factory, so the
89
+ // manager's later `node.stop()` is an idempotent second stop;
90
+ // - no `storage` is passed: the plugin consumes it only to build a local
91
+ // transactor's raw-storage factory or to create a node it was not given,
92
+ // and cadre-core always injects the node and runs the network transactor.
93
+ // (The plugin keeps its `storage` option for the browser entry point.)
94
+ const t0 = performance.now();
95
+ const result = await connectToStrand(this.db, {
96
+ strandId: sid,
97
+ schema: this.config.sAppConfig.schema,
98
+ libp2pNode: this.config.libp2pNode,
99
+ coordinatedRepo: this.config.coordinatedRepo,
100
+ enableCache: true,
101
+ });
102
+ this.shutdownStrand = result.shutdown;
103
+ this.resolvedTransactor = result.transactor;
104
+ timing('[strandDb:%s] connectToStrand: %dms (hydrated tables=%d, indexes=%d)',
105
+ sid, Math.round(performance.now() - t0),
106
+ result.hydrated?.tables ?? 0, result.hydrated?.indexes ?? 0);
107
+
108
+ // Founder-only: write the one-time membership bootstrap now that the schema is
109
+ // applied (connectToStrand has returned). A throw here propagates out of
110
+ // initialize() so buildStrandRuntime's rollback tears the half-built strand
111
+ // down rather than leaking a node. Joiners skip this — their rows arrive via sync.
112
+ if (this.config.founder === true) {
113
+ await this.bootstrapFounder();
114
+ }
115
+
116
+ this.initialized = true;
117
+ log('StrandDatabase for strand %s initialized successfully', sid);
118
+ }
119
+
120
+ /**
121
+ * Run the founder membership bootstrap against the freshly-composed strand DB.
122
+ *
123
+ * Derives the founding keypair from the closed-strand `memberPrivateKey` (the
124
+ * `Member.Key`/`Manager.MemberKey` are its public key); a closed strand with no
125
+ * `memberPrivateKey` throws, because it could never seat a founding manager.
126
+ * Open strands derive no keypair (Header only). Idempotent — see
127
+ * {@link bootstrapFounderMembership}.
128
+ */
129
+ private async bootstrapFounder(): Promise<void> {
130
+ const { strandId, strandType, memberPrivateKey, sAppConfig } = this.config;
131
+ const founderKeyPair = strandType === 'c'
132
+ ? this.deriveFounderKeyPair(strandId, memberPrivateKey)
133
+ : undefined;
134
+ await bootstrapFounderMembership(this.db!, {
135
+ strandId,
136
+ type: strandType,
137
+ sApp: sAppConfig,
138
+ founderKeyPair,
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Derive the founding keypair for a closed strand from its `memberPrivateKey`,
144
+ * failing loudly when the key is absent (a closed strand with no founding
145
+ * Manager can never admit anyone).
146
+ */
147
+ private deriveFounderKeyPair(strandId: string, memberPrivateKey: string | undefined) {
148
+ if (!memberPrivateKey) {
149
+ throw new Error(
150
+ `Cannot found closed strand ${strandId}: the strand row has no MemberPrivateKey. ` +
151
+ 'A closed strand needs a founding Member/Manager derived from that key.',
152
+ );
153
+ }
154
+ return strandMemberKeyPair(memberPrivateKey);
155
+ }
156
+
157
+ /**
158
+ * Get the underlying database for queries
159
+ */
160
+ getDatabase(): Database {
161
+ this.ensureInitialized();
162
+ return this.db!;
163
+ }
164
+
165
+ /**
166
+ * The Optimystic transactor this strand's connection resolved to — always
167
+ * `'network'` here, since cadre-core passes no `transactor` option and takes
168
+ * the plugin's default. Exposed so a spec measuring or asserting the network
169
+ * path (`strand-solo-write-budget.spec.ts`) can pin the engine it ran on
170
+ * rather than assume it.
171
+ */
172
+ getTransactor(): SereusPluginResult['transactor'] {
173
+ this.ensureInitialized();
174
+ return this.resolvedTransactor!;
175
+ }
176
+
177
+ /**
178
+ * Close the database and cleanup resources. Runs the strand-connection
179
+ * shutdown (collection-factory teardown, which also stops the injected node),
180
+ * then closes the `Database`. `StrandInstanceManager.releaseRuntime` issues a
181
+ * further idempotent `node.stop()` after this returns.
182
+ */
183
+ async close(): Promise<void> {
184
+ if (this.shutdownStrand) {
185
+ await this.shutdownStrand();
186
+ this.shutdownStrand = null;
187
+ }
188
+ if (this.db) {
189
+ void this.db.close();
190
+ this.db = null;
191
+ }
192
+ this.resolvedTransactor = null;
193
+ this.initialized = false;
194
+ log('StrandDatabase for strand %s closed', this.config.strandId);
195
+ }
196
+
197
+ private ensureInitialized(): void {
198
+ if (!this.initialized || !this.db) {
199
+ throw new Error(`StrandDatabase for strand ${this.config.strandId} not initialized. Call initialize() first.`);
200
+ }
201
+ }
202
+ }