@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,376 @@
1
+ import debug from 'debug';
2
+ import { toString as uint8ArrayToString } from 'uint8arrays';
3
+ import { Database, registerPlugin } from '@quereus/quereus';
4
+ import cryptoPlugin from '@optimystic/quereus-plugin-crypto/plugin';
5
+ import optimysticPlugin from '@optimystic/quereus-plugin-optimystic/plugin';
6
+ import { digest, randomBytes } from '@optimystic/quereus-plugin-crypto';
7
+ import type { Libp2p } from '@libp2p/interface';
8
+ import type { IRepo } from '@optimystic/db-core';
9
+ import type { StrandRow } from './types.js';
10
+
11
+ const log = debug('sereus:cadre:control-db');
12
+
13
+ /**
14
+ * Embedded control schema for cross-platform compatibility.
15
+ * This schema is embedded directly in the code to work in React Native and other
16
+ * environments where file system access is not available.
17
+ */
18
+ const CONTROL_SCHEMA = `-- This manages a Sereus party's cadre, or set of nodes, and their participation in strands (networks)
19
+ declare schema CadreControl {
20
+ -- A key that can authorize various control changes
21
+ table AuthorityKey (
22
+ Key text primary key,
23
+ constraint Authorized check (
24
+ -- Bootstrap: first authority key needs no existing authorization
25
+ (select count(1) from AuthorityKey) <= 1
26
+
27
+ -- Old authority can authorize by signature and transaction stamp id (not repeatable)
28
+ or (old.Key is not null and old.Key = context.AuthorityKey and verify(digest(context.StampId, 'sha256', 'utf8'), context.Signature, old.Key, 'ed25519'))
29
+
30
+ -- or other authorities can authorize by signature and transaction stamp id (not repeatable)
31
+ or exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(context.StampId, 'sha256', 'utf8'), context.Signature, A.Key, 'ed25519'))
32
+ )
33
+ ) with context (AuthorityKey text null, Signature text null, StampId text);
34
+
35
+ -- A key that can validate a strand formation disclosure
36
+ table ValidationKey (
37
+ Key text primary key,
38
+ constraint Authorized check (
39
+ -- Authorities can authorize by signature and transaction stamp id (not repeatable)
40
+ exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(context.StampId, 'sha256', 'utf8'), context.Signature, A.Key, 'ed25519'))
41
+ )
42
+ ) with context (AuthorityKey text, StampId text, Signature text);
43
+
44
+ -- A network of members sharing an sApp database, each contributing peer nodes (their cadre) to the overall cohort
45
+ -- Cadre peers should participate in each of these strands
46
+ table Strand (
47
+ Id text primary key, -- UUID
48
+ MemberPrivateKey text null unique, -- Our private key as a member of this strand
49
+ Type text, -- Types: 'o' = Open, 'c' = Closed -- Open can still control writes in the sApp, but only Closed controls reads
50
+ constraint Authorized check (
51
+ -- Authorized by authority signature and transaction stamp id (not repeatable)
52
+ exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(context.StampId, 'sha256', 'utf8'), context.Signature, A.Key, 'ed25519'))
53
+
54
+ -- or authorized by a cadre peer who has received a valid formation invitation
55
+ or exists (select 1 from FormationUsage FU where FU.StrandId = new.Id)
56
+ ),
57
+ -- TODO: constraint to ensure member private key only if closed
58
+ ) with context (AuthorityKey text, StampId text, Signature text);
59
+
60
+ -- A peer (node) that is part of the cadre
61
+ table CadrePeer (
62
+ PeerId text primary key,
63
+ Multiaddr text,
64
+ constraint AuthorizedInsert check on insert, delete (
65
+ -- Authorized by an authority key
66
+ -- Use utf8 input encoding since peer IDs are base58btc strings, not base64url
67
+ exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(coalesce(new.PeerId, old.PeerId), 'sha256', 'utf8'), context.Signature, A.Key, 'ed25519'))
68
+ ),
69
+ constraint AuthorizedUpdate check on update (
70
+ -- Peer can change its own multiaddr (using utf8 encoding for both values)
71
+ verify(digest(new.PeerId, 'sha256', 'utf8') || digest(new.Multiaddr, 'sha256', 'utf8'), context.Signature, new.PeerId, 'ed25519')
72
+ -- or authorized by an authority key
73
+ or exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(new.PeerId, 'sha256', 'utf8'), context.Signature, A.Key, 'ed25519'))
74
+ )
75
+ ) with context (AuthorityKey text null, Signature text);
76
+
77
+ -- An open invitation to form a strand with this party
78
+ table FormationInvite (
79
+ Token text primary key, -- Just a random string
80
+ sAppId text, -- The app for the strand that will be formed
81
+ ExpiresAt datetime null,
82
+ TotalUses int null check (TotalUses >= 0),
83
+ ValidationUrl text null, -- Web hook - send disclosure, IP address...
84
+ constraint AuthorizedAddOrRemove check on insert, delete (
85
+ -- Authorized by an authority key to add or remove this invite
86
+ exists (select 1 from AuthorityKey A where A.Key = context.AuthorityKey and verify(digest(context.StampId), context.Signature, A.Key))
87
+ )
88
+ ) with context (AuthorityKey text, StampId text, Signature text);
89
+
90
+ table FormationUsage (
91
+ Token text,
92
+ UseNumber int,
93
+ Disclosure text,
94
+ StrandId text,
95
+ primary key (Token, UseNumber),
96
+ constraint InsertOnly check on update, delete (false),
97
+ constraint Monotonic check (
98
+ new.UseNumber = coalesce((select max(UseNumber) from FormationUsage U where U.Token = new.Token), 0) + 1
99
+ ),
100
+ constraint Authorized check on insert (
101
+ -- Satisfies an invitation
102
+ exists (
103
+ select 1 from FormationInvite FI
104
+ where FI.Token = new.Token
105
+ and (FI.TotalUses is null or FI.TotalUses >= new.UseNumber)
106
+ and (FI.ExpiresAt is null or FI.ExpiresAt > context.Now)
107
+ and (FI.ValidationUrl is null or verify(digest(new.Token, new.Disclosure), context.ValidationSignature, context.ValidationKey))
108
+ )
109
+ ),
110
+ constraint StrandExists check (exists (select 1 from Strand S where S.Id = new.StrandId)),
111
+ ) with context (PeerId text, PeerSignature text, Now datetime, ValidationKey text null, ValidationSignature text null);
112
+ }
113
+
114
+ apply schema CadreControl;`;
115
+
116
+ /**
117
+ * Generate a unique stamp ID for transaction authorization.
118
+ * Format: 32 bytes base64url encoded
119
+ * - First 16 bytes: SHA-256 hash of peer ID (for distributed uniqueness)
120
+ * - Last 16 bytes: Random bytes (for collision resistance)
121
+ */
122
+ function generateStampId(peerId: string): string {
123
+ // Hash the peer ID and get first 16 bytes (128 bits)
124
+ const peerIdHash = digest(peerId, 'sha256', 'utf8', 'bytes') as Uint8Array;
125
+ const peerIdHashPart = peerIdHash.slice(0, 16);
126
+
127
+ // Generate 16 random bytes
128
+ const randomPart = randomBytes(128, 'bytes') as Uint8Array;
129
+
130
+ // Combine peer ID hash and random bytes
131
+ const combined = new Uint8Array(32);
132
+ combined.set(peerIdHashPart, 0);
133
+ combined.set(randomPart, 16);
134
+
135
+ // Convert to base64url
136
+ return uint8ArrayToString(combined, 'base64url');
137
+ }
138
+
139
+ /**
140
+ * Minimal interface for the CollectionFactory returned by the optimystic plugin.
141
+ * We only need the methods we actually use.
142
+ */
143
+ interface CollectionFactory {
144
+ registerLibp2pNode(networkName: string, node: Libp2p, coordinatedRepo: IRepo): void;
145
+ shutdown(): Promise<void>;
146
+ }
147
+
148
+ /** Result of registering the optimystic plugin */
149
+ interface OptimysticPluginResult {
150
+ collectionFactory: CollectionFactory;
151
+ vtables: Array<{ name: string; module: unknown; auxData: unknown }>;
152
+ functions: Array<{ schema: unknown }>;
153
+ [key: string]: unknown;
154
+ }
155
+
156
+ export interface ControlDatabaseConfig {
157
+ /** Party ID for the control network */
158
+ partyId: string;
159
+ /**
160
+ * Optional path to the control schema file.
161
+ * If not provided, uses the embedded schema for cross-platform compatibility.
162
+ * Only use this if you need to override the default schema (e.g., for testing).
163
+ */
164
+ schemaPath?: string;
165
+ /** Libp2p node for the control network (injected) */
166
+ libp2pNode: Libp2p;
167
+ /** Coordinated repo from the libp2p node */
168
+ coordinatedRepo: IRepo;
169
+ }
170
+
171
+ /**
172
+ * ControlDatabase manages the CadreControl schema using Quereus with Optimystic backend.
173
+ * It provides typed query methods for accessing control network data.
174
+ */
175
+ export class ControlDatabase {
176
+ private db: Database | null = null;
177
+ private collectionFactory: CollectionFactory | null = null;
178
+ private readonly config: ControlDatabaseConfig;
179
+ private initialized = false;
180
+
181
+ constructor(config: ControlDatabaseConfig) {
182
+ this.config = config;
183
+ }
184
+
185
+ /**
186
+ * Initialize the database - load schema and register plugins
187
+ */
188
+ async initialize(): Promise<void> {
189
+ if (this.initialized) {
190
+ log('ControlDatabase already initialized');
191
+ return;
192
+ }
193
+
194
+ log('Initializing ControlDatabase for party: %s', this.config.partyId);
195
+
196
+ // Create database instance
197
+ this.db = new Database();
198
+
199
+ // Register crypto plugin (provides digest, sign, verify functions)
200
+ await registerPlugin(this.db, cryptoPlugin);
201
+ log('Registered crypto plugin');
202
+
203
+ // Register optimystic plugin with network transactor as default
204
+ const networkName = `control-${this.config.partyId}`;
205
+ const pluginResult = optimysticPlugin(this.db, {
206
+ default_transactor: 'network',
207
+ default_key_network: 'libp2p',
208
+ default_network_name: networkName,
209
+ enable_cache: true,
210
+ }) as OptimysticPluginResult;
211
+
212
+ // Register vtables and functions manually since we need access to collectionFactory
213
+ for (const vtable of pluginResult.vtables as Array<{ name: string; module: unknown; auxData: unknown }>) {
214
+ this.db.registerModule(vtable.name, vtable.module as any, vtable.auxData);
215
+ }
216
+ for (const func of pluginResult.functions as Array<{ schema: unknown }>) {
217
+ this.db.registerFunction(func.schema as any);
218
+ }
219
+
220
+ this.collectionFactory = pluginResult.collectionFactory;
221
+
222
+ // Inject the libp2p node into the collection factory
223
+ this.collectionFactory.registerLibp2pNode(
224
+ networkName,
225
+ this.config.libp2pNode,
226
+ this.config.coordinatedRepo
227
+ );
228
+ log('Registered libp2p node with collection factory');
229
+
230
+ // Load and execute the schema
231
+ await this.loadSchema();
232
+
233
+ this.initialized = true;
234
+ log('ControlDatabase initialized successfully');
235
+ }
236
+
237
+ private async loadSchema(): Promise<void> {
238
+ let schemaContent: string;
239
+
240
+ if (this.config.schemaPath) {
241
+ // Load from file if explicitly provided (for testing or custom schemas)
242
+ // This only works in Node.js environments
243
+ log('Loading schema from file: %s', this.config.schemaPath);
244
+
245
+ // Check if we're in a Node.js environment
246
+ if (typeof process !== 'undefined' && process.versions?.node) {
247
+ try {
248
+ // Use require to conditionally load fs only in Node.js
249
+ // This won't be bundled by React Native's Metro bundler
250
+ const fs = require('fs/promises');
251
+ schemaContent = await fs.readFile(this.config.schemaPath, 'utf-8');
252
+ } catch (error) {
253
+ throw new Error(
254
+ `Failed to load schema from ${this.config.schemaPath}: ${error instanceof Error ? error.message : String(error)}`
255
+ );
256
+ }
257
+ } else {
258
+ throw new Error(
259
+ 'Loading schema from file is not supported in React Native. ' +
260
+ 'Remove the schemaPath option to use the embedded schema instead.'
261
+ );
262
+ }
263
+ } else {
264
+ // Use embedded schema for cross-platform compatibility
265
+ log('Using embedded control schema');
266
+ schemaContent = CONTROL_SCHEMA;
267
+ }
268
+
269
+ await this.db!.exec(schemaContent);
270
+ log('Schema loaded and executed');
271
+ }
272
+
273
+ /**
274
+ * Query all strands from the control database
275
+ */
276
+ async queryStrands(): Promise<StrandRow[]> {
277
+ this.ensureInitialized();
278
+ const results: StrandRow[] = [];
279
+ for await (const row of this.db!.eval('select Id, MemberPrivateKey, Type from CadreControl.Strand')) {
280
+ results.push({
281
+ Id: row.Id as string,
282
+ MemberPrivateKey: row.MemberPrivateKey as string | null,
283
+ Type: row.Type as 'o' | 'c',
284
+ });
285
+ }
286
+ return results;
287
+ }
288
+
289
+ /**
290
+ * Get the underlying database for advanced queries
291
+ */
292
+ getDatabase(): Database {
293
+ this.ensureInitialized();
294
+ return this.db!;
295
+ }
296
+
297
+ /**
298
+ * Insert the initial authority key (bootstrap - no existing authorities required)
299
+ */
300
+ async insertAuthorityKey(key: string): Promise<void> {
301
+ this.ensureInitialized();
302
+ log('Inserting authority key: %s', key);
303
+
304
+ // For bootstrap, context values are not verified since no existing authorities
305
+ // Use fully qualified table name since schema is CadreControl
306
+ // StampId() provides the transaction's unique stamp from the optimystic transaction
307
+ await this.db!.exec(`
308
+ insert into CadreControl.AuthorityKey (Key)
309
+ with context AuthorityKey = null, Signature = null, StampId = StampId()
310
+ values ('${key}')
311
+ `);
312
+ log('Authority key inserted');
313
+ }
314
+
315
+ /**
316
+ * Insert a strand into the control database using authority signature.
317
+ * This starts a transaction, gets the StampId, signs it, and inserts the strand.
318
+ *
319
+ * @param strandId - Unique identifier for the strand
320
+ * @param type - Strand type: 'o' for open, 'c' for closed
321
+ * @param authorityKey - Public key of the authorizing authority
322
+ * @param signStampId - Function to sign the stamp ID with the authority's private key
323
+ * @param memberPrivateKey - Optional private key for membership in closed strands
324
+ */
325
+ async insertStrand(
326
+ strandId: string,
327
+ type: 'o' | 'c',
328
+ authorityKey: string,
329
+ signStampId: (stampId: string) => string,
330
+ memberPrivateKey?: string
331
+ ): Promise<void> {
332
+ this.ensureInitialized();
333
+ log('Inserting strand: %s (type: %s)', strandId, type);
334
+
335
+ const memberKeyValue = memberPrivateKey ? `'${memberPrivateKey}'` : 'null';
336
+
337
+ // Generate a unique stamp ID using the peer ID for distributed uniqueness
338
+ const peerId = this.config.libp2pNode.peerId.toString();
339
+ const stampId = generateStampId(peerId);
340
+
341
+ // Sign the stamp ID with the authority key
342
+ const signature = signStampId(stampId);
343
+
344
+ // Insert with the signed stamp ID
345
+ await this.db!.exec(`
346
+ insert into CadreControl.Strand (Id, Type, MemberPrivateKey)
347
+ with context AuthorityKey = '${authorityKey}', Signature = '${signature}', StampId = '${stampId}'
348
+ values ('${strandId}', '${type}', ${memberKeyValue})
349
+ `);
350
+
351
+ log('Strand inserted: %s', strandId);
352
+ }
353
+
354
+ /**
355
+ * Close the database and cleanup resources
356
+ */
357
+ async close(): Promise<void> {
358
+ if (this.collectionFactory) {
359
+ await this.collectionFactory.shutdown();
360
+ this.collectionFactory = null;
361
+ }
362
+ if (this.db) {
363
+ this.db.close();
364
+ this.db = null;
365
+ }
366
+ this.initialized = false;
367
+ log('ControlDatabase closed');
368
+ }
369
+
370
+ private ensureInitialized(): void {
371
+ if (!this.initialized || !this.db) {
372
+ throw new Error('ControlDatabase not initialized. Call initialize() first.');
373
+ }
374
+ }
375
+ }
376
+
@@ -0,0 +1,199 @@
1
+ import debug from 'debug';
2
+ import { generateKeyPair, privateKeyToProtobuf } from '@libp2p/crypto/keys';
3
+ import { peerIdFromPrivateKey } from '@libp2p/peer-id';
4
+ import type {
5
+ CreatePeerResult,
6
+ MemberRegistration,
7
+ MemberRegistrationResult
8
+ } from './types.js';
9
+
10
+ const log = debug('sereus:cadre:enrollment');
11
+
12
+ /**
13
+ * Interface for verifying member signatures for strand joining
14
+ */
15
+ export interface MemberVerifier {
16
+ /**
17
+ * Verify that the signature is valid for the given member registration
18
+ * @param registration The member registration data
19
+ * @param signature The signature to verify
20
+ * @returns true if signature is valid
21
+ */
22
+ verifyMember(registration: MemberRegistration, signature: string): Promise<boolean>;
23
+
24
+ /**
25
+ * Check if the member key is authorized to join the strand
26
+ * (e.g., was invited via FormationInvite)
27
+ */
28
+ isAuthorizedToJoin(strandId: string, memberKey: string): Promise<boolean>;
29
+ }
30
+
31
+ /**
32
+ * Interface for registering members in a strand
33
+ */
34
+ export interface MemberRegistry {
35
+ /**
36
+ * Register a member in the strand's member list
37
+ */
38
+ registerMember(strandId: string, memberKey: string, peerIds: string[]): Promise<void>;
39
+
40
+ /**
41
+ * Check if a member is already registered in the strand
42
+ */
43
+ isMemberRegistered(strandId: string, memberKey: string): Promise<boolean>;
44
+ }
45
+
46
+ /**
47
+ * Enrollment API for creating peer identities and managing strand membership
48
+ *
49
+ * Peer Creation:
50
+ * `createCadrePeer()` generates an Ed25519 keypair for a new node.
51
+ * For cadre peer authorization, use the Seed Bootstrap API instead.
52
+ *
53
+ * Member Registration:
54
+ * Accept invitations to join strands as a member.
55
+ */
56
+ export class EnrollmentService {
57
+ private readonly memberVerifier?: MemberVerifier;
58
+ private readonly memberRegistry?: MemberRegistry;
59
+
60
+ constructor(options?: {
61
+ memberVerifier?: MemberVerifier;
62
+ memberRegistry?: MemberRegistry;
63
+ }) {
64
+ this.memberVerifier = options?.memberVerifier;
65
+ this.memberRegistry = options?.memberRegistry;
66
+ log('EnrollmentService created');
67
+ }
68
+
69
+ /**
70
+ * Create a new cadre peer identity
71
+ *
72
+ * Generates a new Ed25519 keypair for a cadre node.
73
+ * The private key should be stored securely by the node.
74
+ * The PeerId can be used with the Seed Bootstrap API for authorization.
75
+ */
76
+ async createCadrePeer(): Promise<CreatePeerResult> {
77
+ log('Creating new cadre peer identity');
78
+
79
+ // Generate Ed25519 keypair (same as libp2p uses)
80
+ const privateKey = await generateKeyPair('Ed25519');
81
+ const peerId = peerIdFromPrivateKey(privateKey);
82
+
83
+ // Export private key as protobuf bytes for storage
84
+ const privateKeyBytes = privateKeyToProtobuf(privateKey);
85
+
86
+ log('Created peer with ID: %s', peerId.toString());
87
+
88
+ return {
89
+ peerId,
90
+ privateKey: privateKeyBytes
91
+ };
92
+ }
93
+
94
+ // ============================================================================
95
+ // Member Registration API
96
+ // ============================================================================
97
+
98
+ /**
99
+ * Register a member into a strand.
100
+ *
101
+ * Called by an invited party to accept an invitation and join as a member.
102
+ * The signature proves ownership of the member key.
103
+ *
104
+ * @param registration The member registration data (strandId, key, peerIds)
105
+ * @param signature Signature over the registration proving key ownership
106
+ * @returns Success/failure with optional reason
107
+ */
108
+ async registerMember(
109
+ registration: MemberRegistration,
110
+ signature: string
111
+ ): Promise<MemberRegistrationResult> {
112
+ const { strandId, key, peerIds } = registration;
113
+
114
+ log('Registering member %s for strand %s with %d peers', key, strandId, peerIds.length);
115
+
116
+ // Check required dependencies
117
+ if (!this.memberVerifier) {
118
+ log('MemberVerifier not configured');
119
+ return { success: false, reason: 'MemberVerifier not configured' };
120
+ }
121
+
122
+ if (!this.memberRegistry) {
123
+ log('MemberRegistry not configured');
124
+ return { success: false, reason: 'MemberRegistry not configured' };
125
+ }
126
+
127
+ // Verify the signature
128
+ const isValidSignature = await this.memberVerifier.verifyMember(registration, signature);
129
+ if (!isValidSignature) {
130
+ log('Invalid signature for member registration: %s', key);
131
+ return { success: false, reason: 'Invalid signature' };
132
+ }
133
+
134
+ // Check if member is authorized to join (e.g., has a valid invitation)
135
+ const isAuthorized = await this.memberVerifier.isAuthorizedToJoin(strandId, key);
136
+ if (!isAuthorized) {
137
+ log('Member %s not authorized to join strand %s', key, strandId);
138
+ return { success: false, reason: 'Not authorized to join strand' };
139
+ }
140
+
141
+ // Check if already registered
142
+ const alreadyRegistered = await this.memberRegistry.isMemberRegistered(strandId, key);
143
+ if (alreadyRegistered) {
144
+ log('Member %s already registered in strand %s', key, strandId);
145
+ return { success: false, reason: 'Member already registered' };
146
+ }
147
+
148
+ // Register the member
149
+ try {
150
+ await this.memberRegistry.registerMember(strandId, key, peerIds);
151
+ log('Member %s successfully registered in strand %s', key, strandId);
152
+ return { success: true };
153
+ } catch (error) {
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ log('Failed to register member %s: %s', key, message);
156
+ return { success: false, reason: `Registration failed: ${message}` };
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Validate a member registration without actually registering
162
+ * Useful for pre-flight checks
163
+ */
164
+ async validateMemberRegistration(
165
+ registration: MemberRegistration,
166
+ signature: string
167
+ ): Promise<{ valid: boolean; reason?: string }> {
168
+ const { strandId, key } = registration;
169
+
170
+ if (!this.memberVerifier) {
171
+ return { valid: false, reason: 'MemberVerifier not configured' };
172
+ }
173
+
174
+ if (!this.memberRegistry) {
175
+ return { valid: false, reason: 'MemberRegistry not configured' };
176
+ }
177
+
178
+ // Verify signature
179
+ const isValidSignature = await this.memberVerifier.verifyMember(registration, signature);
180
+ if (!isValidSignature) {
181
+ return { valid: false, reason: 'Invalid signature' };
182
+ }
183
+
184
+ // Check authorization
185
+ const isAuthorized = await this.memberVerifier.isAuthorizedToJoin(strandId, key);
186
+ if (!isAuthorized) {
187
+ return { valid: false, reason: 'Not authorized to join strand' };
188
+ }
189
+
190
+ // Check if already registered
191
+ const alreadyRegistered = await this.memberRegistry.isMemberRegistered(strandId, key);
192
+ if (alreadyRegistered) {
193
+ return { valid: false, reason: 'Member already registered' };
194
+ }
195
+
196
+ return { valid: true };
197
+ }
198
+ }
199
+