@serfab/cadre-core 0.0.2

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 (54) hide show
  1. package/README.md +252 -0
  2. package/dist/arachnode-stub.d.ts +60 -0
  3. package/dist/arachnode-stub.js +120 -0
  4. package/dist/arachnode-stub.js.map +1 -0
  5. package/dist/cadre-node.d.ts +252 -0
  6. package/dist/cadre-node.js +753 -0
  7. package/dist/cadre-node.js.map +1 -0
  8. package/dist/control-database.d.ts +62 -0
  9. package/dist/control-database.js +295 -0
  10. package/dist/control-database.js.map +1 -0
  11. package/dist/enrollment.d.ts +76 -0
  12. package/dist/enrollment.js +126 -0
  13. package/dist/enrollment.js.map +1 -0
  14. package/dist/hibernation-manager.d.ts +68 -0
  15. package/dist/hibernation-manager.js +212 -0
  16. package/dist/hibernation-manager.js.map +1 -0
  17. package/dist/index.d.ts +12 -0
  18. package/dist/index.js +24 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/seed-bootstrap.d.ts +164 -0
  21. package/dist/seed-bootstrap.js +554 -0
  22. package/dist/seed-bootstrap.js.map +1 -0
  23. package/dist/strand-database.d.ts +39 -0
  24. package/dist/strand-database.js +108 -0
  25. package/dist/strand-database.js.map +1 -0
  26. package/dist/strand-formation-manager.d.ts +87 -0
  27. package/dist/strand-formation-manager.js +168 -0
  28. package/dist/strand-formation-manager.js.map +1 -0
  29. package/dist/strand-instance-manager.d.ts +60 -0
  30. package/dist/strand-instance-manager.js +209 -0
  31. package/dist/strand-instance-manager.js.map +1 -0
  32. package/dist/strand-solicitation.d.ts +145 -0
  33. package/dist/strand-solicitation.js +180 -0
  34. package/dist/strand-solicitation.js.map +1 -0
  35. package/dist/strand-watcher.d.ts +70 -0
  36. package/dist/strand-watcher.js +136 -0
  37. package/dist/strand-watcher.js.map +1 -0
  38. package/dist/types.d.ts +484 -0
  39. package/dist/types.js +26 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +67 -0
  42. package/src/arachnode-stub.ts +160 -0
  43. package/src/cadre-node.ts +923 -0
  44. package/src/control-database.ts +376 -0
  45. package/src/enrollment.ts +199 -0
  46. package/src/hibernation-manager.ts +268 -0
  47. package/src/index.ts +71 -0
  48. package/src/seed-bootstrap.ts +753 -0
  49. package/src/strand-database.ts +168 -0
  50. package/src/strand-formation-manager.ts +277 -0
  51. package/src/strand-instance-manager.ts +281 -0
  52. package/src/strand-solicitation.ts +318 -0
  53. package/src/strand-watcher.ts +197 -0
  54. package/src/types.ts +553 -0
@@ -0,0 +1,168 @@
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';
5
+ import type { Libp2p } from '@libp2p/interface';
6
+ import type { IRepo } from '@optimystic/db-core';
7
+ import type { SAppConfig } from './types.js';
8
+
9
+ const log = debug('sereus:cadre:strand-db');
10
+
11
+ /**
12
+ * Minimal interface for the CollectionFactory returned by the optimystic plugin.
13
+ * We only need the methods we actually use.
14
+ */
15
+ interface CollectionFactory {
16
+ registerLibp2pNode(networkName: string, node: Libp2p, coordinatedRepo: IRepo): void;
17
+ shutdown(): Promise<void>;
18
+ }
19
+
20
+ /** Result of registering the optimystic plugin */
21
+ interface OptimysticPluginResult {
22
+ collectionFactory: CollectionFactory;
23
+ vtables: Array<{ name: string; module: unknown; auxData: unknown }>;
24
+ functions: Array<{ schema: unknown }>;
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ export interface StrandDatabaseConfig {
29
+ /** The strand ID */
30
+ strandId: string;
31
+ /** sApp configuration containing the schema */
32
+ sAppConfig: SAppConfig;
33
+ /** Libp2p node for the strand network */
34
+ libp2pNode: Libp2p;
35
+ /** Coordinated repo from the libp2p node */
36
+ coordinatedRepo: IRepo;
37
+ }
38
+
39
+ /**
40
+ * StrandDatabase manages the sApp schema for a strand using Quereus with Optimystic backend.
41
+ * Each strand instance has its own isolated database with the sApp's schema applied.
42
+ */
43
+ export class StrandDatabase {
44
+ private db: Database | null = null;
45
+ private collectionFactory: CollectionFactory | null = null;
46
+ private readonly config: StrandDatabaseConfig;
47
+ private initialized = false;
48
+
49
+ constructor(config: StrandDatabaseConfig) {
50
+ this.config = config;
51
+ }
52
+
53
+ /**
54
+ * Initialize the database - register plugins and execute sApp schema
55
+ */
56
+ async initialize(): Promise<void> {
57
+ if (this.initialized) {
58
+ log('StrandDatabase for strand %s already initialized', this.config.strandId);
59
+ return;
60
+ }
61
+
62
+ log('Initializing StrandDatabase for strand: %s (sApp: %s v%s)',
63
+ this.config.strandId,
64
+ this.config.sAppConfig.id,
65
+ this.config.sAppConfig.version
66
+ );
67
+
68
+ // Create database instance
69
+ this.db = new Database();
70
+
71
+ // Register crypto plugin (provides digest, sign, verify functions)
72
+ await registerPlugin(this.db, cryptoPlugin);
73
+ log('Registered crypto plugin');
74
+
75
+ // Register optimystic plugin with network transactor as default
76
+ const networkName = `strand-${this.config.strandId}`;
77
+ const pluginResult = optimysticPlugin(this.db, {
78
+ default_transactor: 'network',
79
+ default_key_network: 'libp2p',
80
+ default_network_name: networkName,
81
+ enable_cache: true,
82
+ }) as OptimysticPluginResult;
83
+
84
+ // Register vtables and functions manually since we need access to collectionFactory
85
+ for (const vtable of pluginResult.vtables as Array<{ name: string; module: unknown; auxData: unknown }>) {
86
+ this.db.registerModule(vtable.name, vtable.module as any, vtable.auxData);
87
+ }
88
+ for (const func of pluginResult.functions as Array<{ schema: unknown }>) {
89
+ this.db.registerFunction(func.schema as any);
90
+ }
91
+
92
+ this.collectionFactory = pluginResult.collectionFactory;
93
+
94
+ // Inject the libp2p node into the collection factory
95
+ this.collectionFactory.registerLibp2pNode(
96
+ networkName,
97
+ this.config.libp2pNode,
98
+ this.config.coordinatedRepo
99
+ );
100
+ log('Registered libp2p node with collection factory');
101
+
102
+ // Set optimystic as the default virtual table module so that
103
+ // `declare schema` tables (which omit USING) are backed by optimystic
104
+ // instead of the built-in memory module.
105
+ this.db.setDefaultVtabName('optimystic');
106
+ this.db.setDefaultVtabArgs({
107
+ networkName,
108
+ transactor: 'network',
109
+ keyNetwork: 'libp2p',
110
+ });
111
+ log('Set default vtab to optimystic (networkName=%s)', networkName);
112
+
113
+ // Execute the sApp schema DDL
114
+ await this.executeSchema();
115
+
116
+ this.initialized = true;
117
+ log('StrandDatabase for strand %s initialized successfully', this.config.strandId);
118
+ }
119
+
120
+ private async executeSchema(): Promise<void> {
121
+ const rawSchema = this.config.sAppConfig.schema;
122
+ log('Applying sApp schema for strand %s', this.config.strandId);
123
+
124
+ // Wrap the raw schema DDL in a declarative schema block and apply it.
125
+ // This ensures proper schema management with diff/apply semantics.
126
+ // The schema is applied to a named schema 'App' to keep it isolated.
127
+ const wrappedSchema = `
128
+ declare schema App {
129
+ ${rawSchema}
130
+ }
131
+ apply schema App;
132
+ `;
133
+
134
+ await this.db!.exec(wrappedSchema);
135
+ log('sApp schema applied to strand %s', this.config.strandId);
136
+ }
137
+
138
+ /**
139
+ * Get the underlying database for queries
140
+ */
141
+ getDatabase(): Database {
142
+ this.ensureInitialized();
143
+ return this.db!;
144
+ }
145
+
146
+ /**
147
+ * Close the database and cleanup resources
148
+ */
149
+ async close(): Promise<void> {
150
+ if (this.collectionFactory) {
151
+ await this.collectionFactory.shutdown();
152
+ this.collectionFactory = null;
153
+ }
154
+ if (this.db) {
155
+ this.db.close();
156
+ this.db = null;
157
+ }
158
+ this.initialized = false;
159
+ log('StrandDatabase for strand %s closed', this.config.strandId);
160
+ }
161
+
162
+ private ensureInitialized(): void {
163
+ if (!this.initialized || !this.db) {
164
+ throw new Error(`StrandDatabase for strand ${this.config.strandId} not initialized. Call initialize() first.`);
165
+ }
166
+ }
167
+ }
168
+
@@ -0,0 +1,277 @@
1
+ import debug from 'debug';
2
+ import type { Libp2p } from '@libp2p/interface';
3
+ import {
4
+ SessionManager,
5
+ createBootstrapManager,
6
+ DEFAULT_PROTOCOL_ID,
7
+ type SessionHooks,
8
+ type SessionConfig,
9
+ type BootstrapLink,
10
+ type BootstrapResult,
11
+ type BootstrapMode,
12
+ type DialogParty,
13
+ type ProvisionResult
14
+ } from '@serfab/strand-proto';
15
+ import type {
16
+ OpenInvitation,
17
+ FormStrandResult,
18
+ StrandFormationDisclosure
19
+ } from './types.js';
20
+ import type {
21
+ DisclosureValidator,
22
+ FormationUsageRecorder,
23
+ StrandProvisioner
24
+ } from './strand-solicitation.js';
25
+
26
+ const log = debug('sereus:cadre:formation-manager');
27
+
28
+ /**
29
+ * Configuration for StrandFormationManager
30
+ */
31
+ export interface StrandFormationManagerConfig {
32
+ /** Session timeout in milliseconds */
33
+ sessionTimeoutMs?: number;
34
+ /** Step timeout in milliseconds */
35
+ stepTimeoutMs?: number;
36
+ /** Maximum concurrent sessions */
37
+ maxConcurrentSessions?: number;
38
+ /** Enable debug logging */
39
+ enableDebugLogging?: boolean;
40
+ /** Protocol ID override */
41
+ protocolId?: string;
42
+ }
43
+
44
+ /**
45
+ * Options for creating a StrandFormationManager
46
+ */
47
+ export interface StrandFormationManagerOptions {
48
+ /** Validates disclosures from initiators */
49
+ disclosureValidator?: DisclosureValidator;
50
+ /** Records and validates token usage */
51
+ formationUsageRecorder?: FormationUsageRecorder;
52
+ /** Provisions strands after validation */
53
+ strandProvisioner?: StrandProvisioner;
54
+ /** This party's ID for identification */
55
+ partyId: string;
56
+ /** This party's cadre peer addresses */
57
+ cadrePeerAddrs?: string[];
58
+ /** Configuration options */
59
+ config?: StrandFormationManagerConfig;
60
+ }
61
+
62
+ /**
63
+ * StrandFormationManager bridges cadre-core's strand solicitation interfaces
64
+ * with strand-proto's SessionManager for actual protocol handling over libp2p.
65
+ *
66
+ * It implements SessionHooks by delegating to the existing interfaces:
67
+ * - DisclosureValidator -> validateIdentity
68
+ * - FormationUsageRecorder -> validateToken
69
+ * - StrandProvisioner -> provisionStrand
70
+ */
71
+ export class StrandFormationManager {
72
+ private readonly sessionManager: SessionManager;
73
+ private readonly disclosureValidator?: DisclosureValidator;
74
+ private readonly formationUsageRecorder?: FormationUsageRecorder;
75
+ private readonly strandProvisioner?: StrandProvisioner;
76
+ private readonly partyId: string;
77
+ private readonly cadrePeerAddrs: string[];
78
+ private registeredNodes: Set<Libp2p> = new Set();
79
+
80
+ constructor(options: StrandFormationManagerOptions) {
81
+ this.disclosureValidator = options.disclosureValidator;
82
+ this.formationUsageRecorder = options.formationUsageRecorder;
83
+ this.strandProvisioner = options.strandProvisioner;
84
+ this.partyId = options.partyId;
85
+ this.cadrePeerAddrs = options.cadrePeerAddrs ?? [];
86
+
87
+ // Create SessionHooks that delegate to our interfaces
88
+ const hooks = this.createSessionHooks();
89
+
90
+ // Create the underlying SessionManager
91
+ const config: Partial<SessionConfig> = {
92
+ sessionTimeoutMs: options.config?.sessionTimeoutMs ?? 30000,
93
+ stepTimeoutMs: options.config?.stepTimeoutMs ?? 5000,
94
+ maxConcurrentSessions: options.config?.maxConcurrentSessions ?? 100,
95
+ enableDebugLogging: options.config?.enableDebugLogging ?? false,
96
+ protocolId: options.config?.protocolId ?? DEFAULT_PROTOCOL_ID
97
+ };
98
+
99
+ this.sessionManager = createBootstrapManager(hooks, config);
100
+ log('StrandFormationManager created for party: %s', this.partyId);
101
+ }
102
+
103
+ /**
104
+ * Register this manager as a protocol handler on a libp2p node.
105
+ * Call this on the control network node to handle incoming formation requests.
106
+ */
107
+ registerResponder(node: Libp2p, protocolId?: string): void {
108
+ if (this.registeredNodes.has(node)) {
109
+ log('Node already registered');
110
+ return;
111
+ }
112
+ this.sessionManager.register(node, protocolId);
113
+ this.registeredNodes.add(node);
114
+ log('Registered as responder on node');
115
+ }
116
+
117
+ /**
118
+ * Unregister the protocol handler from a libp2p node.
119
+ */
120
+ unregisterResponder(node: Libp2p, protocolId?: string): void {
121
+ if (!this.registeredNodes.has(node)) {
122
+ return;
123
+ }
124
+ this.sessionManager.unregister(node, protocolId);
125
+ this.registeredNodes.delete(node);
126
+ log('Unregistered from node');
127
+ }
128
+
129
+ /**
130
+ * Form a strand with a responder via an open invitation.
131
+ *
132
+ * This is the initiator-side operation. It:
133
+ * 1. Converts the OpenInvitation to a BootstrapLink
134
+ * 2. Calls SessionManager.initiateBootstrap()
135
+ * 3. Returns the result as a FormStrandResult
136
+ */
137
+ async formStrand(
138
+ invitation: OpenInvitation,
139
+ disclosure: StrandFormationDisclosure,
140
+ node: Libp2p
141
+ ): Promise<FormStrandResult> {
142
+ log('Forming strand with invitation token: %s', invitation.token);
143
+
144
+ // Convert OpenInvitation to BootstrapLink
145
+ const link: BootstrapLink = {
146
+ responderPeerAddrs: invitation.bootstrap,
147
+ token: invitation.token,
148
+ tokenExpiryUtc: invitation.expiration.toISOString(),
149
+ mode: 'responderCreates' // Responder provisions the strand
150
+ };
151
+
152
+ // The disclosure is passed via the identityBundle in the contact message
153
+ // We store it temporarily so the hooks can access it
154
+ // Note: strand-proto's DialerSession constructs the contact message internally
155
+ // We need to extend this to pass the disclosure - for now we use partyId
156
+
157
+ const result = await this.sessionManager.initiateBootstrap(link, node as any);
158
+
159
+ log('Strand formed: %s', result.strand.strandId);
160
+
161
+ return {
162
+ memberKey: this.partyId, // The initiator's member key
163
+ invitePrivateKey: '', // Would come from key generation
164
+ strandId: result.strand.strandId
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Get the number of active sessions
170
+ */
171
+ getActiveSessionCounts(): { listeners: number; dialers: number } {
172
+ return this.sessionManager.getActiveSessionCounts();
173
+ }
174
+
175
+ /**
176
+ * Create SessionHooks that delegate to our interfaces
177
+ */
178
+ private createSessionHooks(): SessionHooks {
179
+ return {
180
+ validateToken: async (token: string, sessionId: string) => {
181
+ log('validateToken: %s (session: %s)', token, sessionId);
182
+
183
+ if (!this.formationUsageRecorder) {
184
+ // No recorder configured - accept all tokens
185
+ return { mode: 'responderCreates' as BootstrapMode, valid: true };
186
+ }
187
+
188
+ const tokenCheck = await this.formationUsageRecorder.isTokenValid(token);
189
+ if (!tokenCheck.valid) {
190
+ log('Token invalid: %s', token);
191
+ return { mode: 'responderCreates' as BootstrapMode, valid: false };
192
+ }
193
+
194
+ const isUsed = await this.formationUsageRecorder.isTokenUsed(token);
195
+ if (isUsed) {
196
+ log('Token already used: %s', token);
197
+ return { mode: 'responderCreates' as BootstrapMode, valid: false };
198
+ }
199
+
200
+ return { mode: 'responderCreates' as BootstrapMode, valid: true };
201
+ },
202
+
203
+ validateIdentity: async (identity: unknown, sessionId: string) => {
204
+ log('validateIdentity (session: %s)', sessionId);
205
+
206
+ if (!this.disclosureValidator) {
207
+ // No validator configured - accept all identities
208
+ return true;
209
+ }
210
+
211
+ // Extract disclosure from identity bundle
212
+ const disclosure = identity as StrandFormationDisclosure;
213
+ // We need the token here - it's passed via the contact message
214
+ // For now, use a placeholder - this will be refined
215
+ const token = (identity as any)?.token ?? '';
216
+
217
+ return this.disclosureValidator.validateDisclosure(token, disclosure);
218
+ },
219
+
220
+ provisionStrand: async (
221
+ creator: DialogParty,
222
+ creatorPartyId: string,
223
+ otherPartyId: string,
224
+ sessionId: string
225
+ ): Promise<ProvisionResult> => {
226
+ log('provisionStrand: creator=%s, other=%s (session: %s)',
227
+ creatorPartyId, otherPartyId, sessionId);
228
+
229
+ if (!this.strandProvisioner) {
230
+ // No provisioner - return a placeholder
231
+ const strandId = `strand-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
232
+ return {
233
+ strand: { strandId, createdBy: creator },
234
+ dbConnectionInfo: { endpoint: 'local', credentialsRef: '' }
235
+ };
236
+ }
237
+
238
+ // Determine initiator/responder keys based on creator
239
+ const initiatorKey = creator === 'initiator' ? creatorPartyId : otherPartyId;
240
+ const responderKey = creator === 'responder' ? creatorPartyId : otherPartyId;
241
+
242
+ const result = await this.strandProvisioner.provisionStrand(
243
+ '', // sAppId - would come from invitation
244
+ initiatorKey,
245
+ responderKey
246
+ );
247
+
248
+ return {
249
+ strand: { strandId: result.strandId, createdBy: creator },
250
+ dbConnectionInfo: { endpoint: 'local', credentialsRef: '' }
251
+ };
252
+ },
253
+
254
+ validateResponse: async (response: unknown, sessionId: string) => {
255
+ log('validateResponse (session: %s)', sessionId);
256
+ // Accept all responses for now
257
+ return true;
258
+ },
259
+
260
+ validateDatabaseResult: async (result: unknown, sessionId: string) => {
261
+ log('validateDatabaseResult (session: %s)', sessionId);
262
+ // Accept all database results for now
263
+ return true;
264
+ }
265
+ };
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Create a StrandFormationManager with the given options
271
+ */
272
+ export function createStrandFormationManager(
273
+ options: StrandFormationManagerOptions
274
+ ): StrandFormationManager {
275
+ return new StrandFormationManager(options);
276
+ }
277
+