@serfab/cadre-core 0.6.0 → 0.8.1

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 (166) hide show
  1. package/README.md +9 -8
  2. package/dist/authority-key.d.ts +41 -0
  3. package/dist/authority-key.js +52 -0
  4. package/dist/authority-key.js.map +1 -0
  5. package/dist/cadre-node.d.ts +777 -11
  6. package/dist/cadre-node.js +1912 -82
  7. package/dist/cadre-node.js.map +1 -1
  8. package/dist/canonical-datetime.d.ts +33 -0
  9. package/dist/canonical-datetime.js +38 -0
  10. package/dist/canonical-datetime.js.map +1 -0
  11. package/dist/canonical-json.d.ts +12 -0
  12. package/dist/canonical-json.js +33 -0
  13. package/dist/canonical-json.js.map +1 -0
  14. package/dist/control-cohort.d.ts +63 -0
  15. package/dist/control-cohort.js +80 -0
  16. package/dist/control-cohort.js.map +1 -0
  17. package/dist/control-database.d.ts +259 -5
  18. package/dist/control-database.js +541 -128
  19. package/dist/control-database.js.map +1 -1
  20. package/dist/control-formation-recorder.d.ts +89 -0
  21. package/dist/control-formation-recorder.js +139 -0
  22. package/dist/control-formation-recorder.js.map +1 -0
  23. package/dist/control-schema.d.ts +12 -0
  24. package/dist/control-schema.js +199 -0
  25. package/dist/control-schema.js.map +1 -0
  26. package/dist/control-stream.d.ts +54 -0
  27. package/dist/control-stream.js +104 -0
  28. package/dist/control-stream.js.map +1 -0
  29. package/dist/device-token.d.ts +50 -0
  30. package/dist/device-token.js +66 -0
  31. package/dist/device-token.js.map +1 -0
  32. package/dist/diagnostics/connection-path.d.ts +118 -0
  33. package/dist/diagnostics/connection-path.js +204 -0
  34. package/dist/diagnostics/connection-path.js.map +1 -0
  35. package/dist/diagnostics/webrtc-turn-tracker.d.ts +50 -0
  36. package/dist/diagnostics/webrtc-turn-tracker.js +196 -0
  37. package/dist/diagnostics/webrtc-turn-tracker.js.map +1 -0
  38. package/dist/hibernation-manager.d.ts +79 -0
  39. package/dist/hibernation-manager.js +173 -23
  40. package/dist/hibernation-manager.js.map +1 -1
  41. package/dist/index.d.ts +25 -4
  42. package/dist/index.js +59 -4
  43. package/dist/index.js.map +1 -1
  44. package/dist/key-store-file.d.ts +52 -0
  45. package/dist/key-store-file.js +194 -0
  46. package/dist/key-store-file.js.map +1 -0
  47. package/dist/key-store.d.ts +75 -0
  48. package/dist/key-store.js +66 -0
  49. package/dist/key-store.js.map +1 -0
  50. package/dist/peer-authorization.d.ts +26 -0
  51. package/dist/peer-authorization.js +40 -0
  52. package/dist/peer-authorization.js.map +1 -0
  53. package/dist/peer-record.d.ts +89 -0
  54. package/dist/peer-record.js +117 -0
  55. package/dist/peer-record.js.map +1 -0
  56. package/dist/push-credentials.d.ts +34 -0
  57. package/dist/push-credentials.js +81 -0
  58. package/dist/push-credentials.js.map +1 -0
  59. package/dist/push-fanout.d.ts +137 -0
  60. package/dist/push-fanout.js +197 -0
  61. package/dist/push-fanout.js.map +1 -0
  62. package/dist/push-notifier-apns.d.ts +53 -0
  63. package/dist/push-notifier-apns.js +195 -0
  64. package/dist/push-notifier-apns.js.map +1 -0
  65. package/dist/push-notifier-fcm.d.ts +38 -0
  66. package/dist/push-notifier-fcm.js +183 -0
  67. package/dist/push-notifier-fcm.js.map +1 -0
  68. package/dist/push-notifier-shared.d.ts +18 -0
  69. package/dist/push-notifier-shared.js +25 -0
  70. package/dist/push-notifier-shared.js.map +1 -0
  71. package/dist/push-notifier.d.ts +71 -0
  72. package/dist/push-notifier.js +50 -0
  73. package/dist/push-notifier.js.map +1 -0
  74. package/dist/schema-verification.d.ts +11 -2
  75. package/dist/schema-verification.js +15 -4
  76. package/dist/schema-verification.js.map +1 -1
  77. package/dist/seed-bootstrap.d.ts +210 -7
  78. package/dist/seed-bootstrap.js +429 -131
  79. package/dist/seed-bootstrap.js.map +1 -1
  80. package/dist/seed-trust-policy.d.ts +63 -0
  81. package/dist/seed-trust-policy.js +56 -0
  82. package/dist/seed-trust-policy.js.map +1 -0
  83. package/dist/strand-addr-protocol.d.ts +132 -0
  84. package/dist/strand-addr-protocol.js +313 -0
  85. package/dist/strand-addr-protocol.js.map +1 -0
  86. package/dist/strand-cohort.d.ts +54 -0
  87. package/dist/strand-cohort.js +38 -0
  88. package/dist/strand-cohort.js.map +1 -0
  89. package/dist/strand-database.d.ts +54 -6
  90. package/dist/strand-database.js +88 -89
  91. package/dist/strand-database.js.map +1 -1
  92. package/dist/strand-formation-manager.d.ts +79 -19
  93. package/dist/strand-formation-manager.js +196 -104
  94. package/dist/strand-formation-manager.js.map +1 -1
  95. package/dist/strand-formation-protocol.d.ts +162 -0
  96. package/dist/strand-formation-protocol.js +279 -0
  97. package/dist/strand-formation-protocol.js.map +1 -0
  98. package/dist/strand-instance-manager.d.ts +73 -4
  99. package/dist/strand-instance-manager.js +167 -41
  100. package/dist/strand-instance-manager.js.map +1 -1
  101. package/dist/strand-member-key.d.ts +29 -0
  102. package/dist/strand-member-key.js +38 -0
  103. package/dist/strand-member-key.js.map +1 -0
  104. package/dist/strand-member-registry.d.ts +98 -0
  105. package/dist/strand-member-registry.js +129 -0
  106. package/dist/strand-member-registry.js.map +1 -0
  107. package/dist/strand-membership-writer.d.ts +361 -0
  108. package/dist/strand-membership-writer.js +449 -0
  109. package/dist/strand-membership-writer.js.map +1 -0
  110. package/dist/strand-solicitation.d.ts +77 -2
  111. package/dist/strand-solicitation.js +25 -4
  112. package/dist/strand-solicitation.js.map +1 -1
  113. package/dist/strand-wake-payload.d.ts +30 -0
  114. package/dist/strand-wake-payload.js +20 -0
  115. package/dist/strand-wake-payload.js.map +1 -0
  116. package/dist/strand-wake-protocol.d.ts +114 -0
  117. package/dist/strand-wake-protocol.js +257 -0
  118. package/dist/strand-wake-protocol.js.map +1 -0
  119. package/dist/strand-watcher.d.ts +6 -2
  120. package/dist/strand-watcher.js +57 -17
  121. package/dist/strand-watcher.js.map +1 -1
  122. package/dist/types.d.ts +413 -23
  123. package/dist/types.js +22 -5
  124. package/dist/types.js.map +1 -1
  125. package/package.json +14 -8
  126. package/src/authority-key.ts +68 -0
  127. package/src/cadre-node.ts +2937 -929
  128. package/src/canonical-datetime.ts +39 -0
  129. package/src/canonical-json.ts +30 -0
  130. package/src/control-cohort.ts +103 -0
  131. package/src/control-database.ts +931 -383
  132. package/src/control-formation-recorder.ts +151 -0
  133. package/src/control-schema.ts +198 -0
  134. package/src/control-stream.ts +128 -0
  135. package/src/device-token.ts +89 -0
  136. package/src/diagnostics/connection-path.ts +277 -0
  137. package/src/diagnostics/webrtc-turn-tracker.ts +222 -0
  138. package/src/hibernation-manager.ts +197 -28
  139. package/src/index.ts +273 -79
  140. package/src/key-store-file.ts +203 -0
  141. package/src/key-store.ts +99 -0
  142. package/src/peer-authorization.ts +53 -0
  143. package/src/peer-record.ts +146 -0
  144. package/src/push-credentials.ts +84 -0
  145. package/src/push-fanout.ts +272 -0
  146. package/src/push-notifier-apns.ts +243 -0
  147. package/src/push-notifier-fcm.ts +221 -0
  148. package/src/push-notifier-shared.ts +28 -0
  149. package/src/push-notifier.ts +93 -0
  150. package/src/schema-verification.ts +18 -4
  151. package/src/seed-bootstrap.ts +504 -171
  152. package/src/seed-trust-policy.ts +107 -0
  153. package/src/strand-addr-protocol.ts +410 -0
  154. package/src/strand-cohort.ts +77 -0
  155. package/src/strand-database.ts +113 -126
  156. package/src/strand-formation-manager.ts +233 -142
  157. package/src/strand-formation-protocol.ts +391 -0
  158. package/src/strand-instance-manager.ts +207 -56
  159. package/src/strand-member-key.ts +39 -0
  160. package/src/strand-member-registry.ts +177 -0
  161. package/src/strand-membership-writer.ts +640 -0
  162. package/src/strand-solicitation.ts +93 -4
  163. package/src/strand-wake-payload.ts +32 -0
  164. package/src/strand-wake-protocol.ts +331 -0
  165. package/src/strand-watcher.ts +61 -17
  166. package/src/types.ts +1012 -575
@@ -1,32 +1,17 @@
1
1
  import debug from 'debug';
2
- import { Database, registerPlugin } from '@quereus/quereus';
3
- import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
4
- import optimysticPlugin from '@optimystic/quereus-plugin-optimystic/plugin';
2
+ import { Database } from '@quereus/quereus';
3
+ import { connectToStrand } from '@serfab/quereus-plugin-sereus';
4
+ import type { SereusPluginResult } from '@serfab/quereus-plugin-sereus';
5
5
  import type { Libp2p } from '@libp2p/interface';
6
6
  import type { IRepo } from '@optimystic/db-core';
7
7
  import type { IRawStorage } from '@optimystic/db-p2p';
8
8
  import type { SAppConfig, StrandMode } from './types.js';
9
+ import { bootstrapFounderMembership } from './strand-membership-writer.js';
10
+ import { strandMemberKeyPair } from './strand-member-key.js';
9
11
 
10
12
  const log = debug('sereus:cadre:strand-db');
11
13
  const timing = debug('sereus:cadre:timing');
12
14
 
13
- /**
14
- * Minimal interface for the CollectionFactory returned by the optimystic plugin.
15
- * We only need the methods we actually use.
16
- */
17
- interface CollectionFactory {
18
- registerLibp2pNode(networkName: string, node: Libp2p, coordinatedRepo: IRepo): void;
19
- shutdown(): Promise<void>;
20
- }
21
-
22
- /** Result of registering the optimystic plugin */
23
- interface OptimysticPluginResult {
24
- collectionFactory: CollectionFactory;
25
- vtables: Array<{ name: string; module: unknown; auxData: unknown }>;
26
- functions: Array<{ schema: unknown }>;
27
- [key: string]: unknown;
28
- }
29
-
30
15
  export interface StrandDatabaseConfig {
31
16
  /** The strand ID */
32
17
  strandId: string;
@@ -50,15 +35,43 @@ export interface StrandDatabaseConfig {
50
35
  * across the two consumers.
51
36
  */
52
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`+`Authority`.
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`/`Authority.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;
53
56
  }
54
57
 
55
58
  /**
56
- * StrandDatabase manages the sApp schema for a strand using Quereus with Optimystic backend.
57
- * Each strand instance has its own isolated database with the sApp's schema applied.
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()`.)
58
71
  */
59
72
  export class StrandDatabase {
60
73
  private db: Database | null = null;
61
- private collectionFactory: CollectionFactory | null = null;
74
+ private shutdownStrand: SereusPluginResult['shutdown'] | null = null;
62
75
  private readonly config: StrandDatabaseConfig;
63
76
  private initialized = false;
64
77
 
@@ -67,7 +80,9 @@ export class StrandDatabase {
67
80
  }
68
81
 
69
82
  /**
70
- * Initialize the database - register plugins and execute sApp schema
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.
71
86
  */
72
87
  async initialize(): Promise<void> {
73
88
  if (this.initialized) {
@@ -75,113 +90,83 @@ export class StrandDatabase {
75
90
  return;
76
91
  }
77
92
 
78
- log('Initializing StrandDatabase for strand: %s (sApp: %s v%s)',
79
- this.config.strandId,
80
- this.config.sAppConfig.id,
81
- this.config.sAppConfig.version
82
- );
83
-
84
- // Create database instance
85
- this.db = new Database();
86
93
  const sid = this.config.strandId;
87
-
88
- // Register crypto plugin (provides digest, sign, verify functions)
89
- let t0 = performance.now();
90
- await registerPlugin(this.db, cryptoPlugin);
91
- timing('[strandDb:%s] cryptoPlugin: %dms', sid, Math.round(performance.now() - t0));
92
- log('Registered crypto plugin');
93
-
94
- // Register optimystic plugin. In `bootstrap` mode we route through the
95
- // local transactor so a solo node can initialize (schema apply etc.)
96
- // without network round trips. In `networked` mode the network transactor
97
- // is used. Mode is fixed for the lifetime of this Database instance; the
98
- // caller must restart the strand to transition modes.
99
- t0 = performance.now();
100
- const networkName = `strand-${sid}`;
101
94
  const mode: StrandMode = this.config.mode ?? 'networked';
102
- const defaultTransactor = mode === 'bootstrap' ? 'local' : 'network';
103
- // In bootstrap mode the local transactor IS the data path. Hand the plugin
104
- // the strand's raw storage (same instance the libp2p node uses) so writes
105
- // persist to the host backend instead of in-memory storage.
106
- const rawStorage = this.config.rawStorage;
107
- const pluginConfig: Record<string, unknown> = {
108
- default_transactor: defaultTransactor,
109
- default_key_network: 'libp2p',
110
- default_network_name: networkName,
111
- enable_cache: true,
112
- };
113
- if (mode === 'bootstrap' && rawStorage) {
114
- pluginConfig.rawStorageFactory = () => rawStorage;
115
- }
116
- // The plugin's published signature is `Record<string, SqlValue>` but it
117
- // also reads `rawStorageFactory` (a function reference) from the same map.
118
- // Cast through unknown rather than widen the public type.
119
- const pluginResult = optimysticPlugin(
120
- this.db,
121
- pluginConfig as unknown as Parameters<typeof optimysticPlugin>[1]
122
- ) as OptimysticPluginResult;
123
- log('Optimystic plugin registered for strand %s (mode=%s, transactor=%s, persistentStorage=%s)',
124
- sid, mode, defaultTransactor, mode === 'bootstrap' && !!rawStorage);
95
+ log('Initializing StrandDatabase for strand: %s (sApp: %s v%s, mode=%s)',
96
+ sid, this.config.sAppConfig.id, this.config.sAppConfig.version, mode);
125
97
 
126
- // Register vtables and functions manually since we need access to collectionFactory
127
- for (const vtable of pluginResult.vtables as Array<{ name: string; module: unknown; auxData: unknown }>) {
128
- this.db.registerModule(vtable.name, vtable.module as any, vtable.auxData);
129
- }
130
- for (const func of pluginResult.functions as Array<{ schema: unknown }>) {
131
- this.db.registerFunction(func.schema as any);
132
- }
133
- timing('[strandDb:%s] optimysticPlugin: %dms', sid, Math.round(performance.now() - t0));
134
-
135
- this.collectionFactory = pluginResult.collectionFactory;
136
-
137
- // Inject the libp2p node into the collection factory
138
- t0 = performance.now();
139
- this.collectionFactory.registerLibp2pNode(
140
- networkName,
141
- this.config.libp2pNode,
142
- this.config.coordinatedRepo
143
- );
144
- timing('[strandDb:%s] registerLibp2pNode: %dms', sid, Math.round(performance.now() - t0));
145
- log('Registered libp2p node with collection factory');
98
+ this.db = new Database();
146
99
 
147
- // Set optimystic as the default virtual table module so that
148
- // `declare schema` tables (which omit USING) are backed by optimystic
149
- // instead of the built-in memory module.
150
- t0 = performance.now();
151
- this.db.setDefaultVtabName('optimystic');
152
- this.db.setDefaultVtabArgs({
153
- networkName,
154
- transactor: defaultTransactor,
155
- keyNetwork: 'libp2p',
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,
156
117
  });
157
- timing('[strandDb:%s] setDefaultVtab: %dms', sid, Math.round(performance.now() - t0));
158
- log('Set default vtab to optimystic (networkName=%s, transactor=%s)', networkName, defaultTransactor);
159
-
160
- // Execute the sApp schema DDL
161
- t0 = performance.now();
162
- await this.executeSchema();
163
- timing('[strandDb:%s] executeSchema: %dms', sid, Math.round(performance.now() - t0));
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
+ }
164
130
 
165
131
  this.initialized = true;
166
132
  log('StrandDatabase for strand %s initialized successfully', sid);
167
133
  }
168
134
 
169
- private async executeSchema(): Promise<void> {
170
- const rawSchema = this.config.sAppConfig.schema;
171
- log('Applying sApp schema for strand %s', this.config.strandId);
172
-
173
- // Wrap the raw schema DDL in a declarative schema block and apply it.
174
- // This ensures proper schema management with diff/apply semantics.
175
- // The schema is applied to a named schema 'App' to keep it isolated.
176
- const wrappedSchema = `
177
- declare schema App {
178
- ${rawSchema}
179
- }
180
- apply schema App;
181
- `;
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`/`Authority.MemberKey` are its public key); a closed strand with no
140
+ * `memberPrivateKey` throws, because it could never seat a founding authority.
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
+ }
182
156
 
183
- await this.db!.exec(wrappedSchema);
184
- log('sApp schema applied to strand %s', this.config.strandId);
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
+ * Authority 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/Authority derived from that key.',
167
+ );
168
+ }
169
+ return strandMemberKeyPair(memberPrivateKey);
185
170
  }
186
171
 
187
172
  /**
@@ -193,15 +178,18 @@ export class StrandDatabase {
193
178
  }
194
179
 
195
180
  /**
196
- * Close the database and cleanup resources
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.
197
185
  */
198
186
  async close(): Promise<void> {
199
- if (this.collectionFactory) {
200
- await this.collectionFactory.shutdown();
201
- this.collectionFactory = null;
187
+ if (this.shutdownStrand) {
188
+ await this.shutdownStrand();
189
+ this.shutdownStrand = null;
202
190
  }
203
191
  if (this.db) {
204
- this.db.close();
192
+ void this.db.close();
205
193
  this.db = null;
206
194
  }
207
195
  this.initialized = false;
@@ -214,4 +202,3 @@ export class StrandDatabase {
214
202
  }
215
203
  }
216
204
  }
217
-