@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.
- package/README.md +252 -0
- package/dist/arachnode-stub.d.ts +60 -0
- package/dist/arachnode-stub.js +120 -0
- package/dist/arachnode-stub.js.map +1 -0
- package/dist/cadre-node.d.ts +252 -0
- package/dist/cadre-node.js +753 -0
- package/dist/cadre-node.js.map +1 -0
- package/dist/control-database.d.ts +62 -0
- package/dist/control-database.js +295 -0
- package/dist/control-database.js.map +1 -0
- package/dist/enrollment.d.ts +76 -0
- package/dist/enrollment.js +126 -0
- package/dist/enrollment.js.map +1 -0
- package/dist/hibernation-manager.d.ts +68 -0
- package/dist/hibernation-manager.js +212 -0
- package/dist/hibernation-manager.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +164 -0
- package/dist/seed-bootstrap.js +554 -0
- package/dist/seed-bootstrap.js.map +1 -0
- package/dist/strand-database.d.ts +39 -0
- package/dist/strand-database.js +108 -0
- package/dist/strand-database.js.map +1 -0
- package/dist/strand-formation-manager.d.ts +87 -0
- package/dist/strand-formation-manager.js +168 -0
- package/dist/strand-formation-manager.js.map +1 -0
- package/dist/strand-instance-manager.d.ts +60 -0
- package/dist/strand-instance-manager.js +209 -0
- package/dist/strand-instance-manager.js.map +1 -0
- package/dist/strand-solicitation.d.ts +145 -0
- package/dist/strand-solicitation.js +180 -0
- package/dist/strand-solicitation.js.map +1 -0
- package/dist/strand-watcher.d.ts +70 -0
- package/dist/strand-watcher.js +136 -0
- package/dist/strand-watcher.js.map +1 -0
- package/dist/types.d.ts +484 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/package.json +67 -0
- package/src/arachnode-stub.ts +160 -0
- package/src/cadre-node.ts +923 -0
- package/src/control-database.ts +376 -0
- package/src/enrollment.ts +199 -0
- package/src/hibernation-manager.ts +268 -0
- package/src/index.ts +71 -0
- package/src/seed-bootstrap.ts +753 -0
- package/src/strand-database.ts +168 -0
- package/src/strand-formation-manager.ts +277 -0
- package/src/strand-instance-manager.ts +281 -0
- package/src/strand-solicitation.ts +318 -0
- package/src/strand-watcher.ts +197 -0
- package/src/types.ts +553 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
3
|
+
import { createLibp2pNode, type IRawStorage } from '@optimystic/db-p2p';
|
|
4
|
+
import type { IRepo } from '@optimystic/db-core';
|
|
5
|
+
import { StrandDatabase } from './strand-database.js';
|
|
6
|
+
import type {
|
|
7
|
+
StrandInstance,
|
|
8
|
+
StrandRow,
|
|
9
|
+
StorageConfig,
|
|
10
|
+
NetworkConfig,
|
|
11
|
+
LatencyHint,
|
|
12
|
+
NodeProfile,
|
|
13
|
+
SAppConfig,
|
|
14
|
+
SAppInfo,
|
|
15
|
+
RawStorageProvider
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extended Libp2p node with coordinatedRepo attached by createLibp2pNode.
|
|
20
|
+
* The db-p2p createLibp2pNode function attaches these properties after node creation.
|
|
21
|
+
*/
|
|
22
|
+
interface Libp2pNodeWithRepo extends Libp2p {
|
|
23
|
+
coordinatedRepo: IRepo;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const log = debug('sereus:cadre:strand-manager');
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for starting a strand instance
|
|
30
|
+
*/
|
|
31
|
+
export interface StartStrandConfig {
|
|
32
|
+
strandRow: StrandRow;
|
|
33
|
+
/** sApp configuration provided by the hosting application */
|
|
34
|
+
sAppConfig: SAppConfig;
|
|
35
|
+
storage?: StorageConfig;
|
|
36
|
+
network?: NetworkConfig;
|
|
37
|
+
profile: NodeProfile;
|
|
38
|
+
defaultLatencyHint: LatencyHint;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the isolated storage path for a specific strand.
|
|
43
|
+
*
|
|
44
|
+
* @deprecated This function uses Node.js path module which is not available in React Native.
|
|
45
|
+
* Use a storage provider factory function instead, which receives the strandId and can
|
|
46
|
+
* create strand-specific storage paths using platform-appropriate methods.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // Instead of using getStrandStoragePath, use a storage provider factory:
|
|
50
|
+
* const storage = {
|
|
51
|
+
* provider: (strandId: string) => new FileRawStorage(`./data/strands/${strandId}`)
|
|
52
|
+
* };
|
|
53
|
+
*/
|
|
54
|
+
export function getStrandStoragePath(basePath: string, strandId: string): string {
|
|
55
|
+
// Check if we're in a Node.js environment
|
|
56
|
+
if (typeof process === 'undefined' || !process.versions?.node) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'getStrandStoragePath is not available in React Native. ' +
|
|
59
|
+
'Use a storage provider factory function instead.'
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Dynamically require path only in Node.js
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
65
|
+
const path = require('path');
|
|
66
|
+
|
|
67
|
+
// Sanitize strandId for filesystem safety (UUIDs should be safe, but just in case)
|
|
68
|
+
const safeId = strandId.replace(/[^a-zA-Z0-9-]/g, '_');
|
|
69
|
+
return path.join(basePath, 'strands', safeId);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a storage provider for a specific strand.
|
|
74
|
+
* If the provider is a factory function, call it with the strandId.
|
|
75
|
+
*
|
|
76
|
+
* @param provider - Storage provider (instance or factory)
|
|
77
|
+
* @param strandId - The strand ID to create storage for
|
|
78
|
+
* @returns The resolved IRawStorage instance, or undefined if no provider
|
|
79
|
+
*/
|
|
80
|
+
function resolveStrandStorage(
|
|
81
|
+
provider: RawStorageProvider | undefined,
|
|
82
|
+
strandId: string
|
|
83
|
+
): IRawStorage | undefined {
|
|
84
|
+
if (!provider) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return typeof provider === 'function' ? provider(strandId) : provider;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Manages individual strand instances - creates and destroys isolated libp2p nodes
|
|
92
|
+
* for each strand the cadre participates in.
|
|
93
|
+
*/
|
|
94
|
+
export class StrandInstanceManager {
|
|
95
|
+
private instances: Map<string, StrandInstance> = new Map();
|
|
96
|
+
private stopping = false;
|
|
97
|
+
|
|
98
|
+
constructor() {
|
|
99
|
+
log('StrandInstanceManager created');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get all current strand instances
|
|
104
|
+
*/
|
|
105
|
+
getInstances(): Map<string, StrandInstance> {
|
|
106
|
+
return new Map(this.instances);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get a specific strand instance
|
|
111
|
+
*/
|
|
112
|
+
getInstance(strandId: string): StrandInstance | undefined {
|
|
113
|
+
return this.instances.get(strandId);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if a strand is currently running
|
|
118
|
+
*/
|
|
119
|
+
hasStrand(strandId: string): boolean {
|
|
120
|
+
return this.instances.has(strandId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Start a new strand instance
|
|
125
|
+
*/
|
|
126
|
+
async startStrand(config: StartStrandConfig): Promise<StrandInstance> {
|
|
127
|
+
const { strandRow, sAppConfig } = config;
|
|
128
|
+
const strandId = strandRow.Id;
|
|
129
|
+
|
|
130
|
+
if (this.stopping) {
|
|
131
|
+
throw new Error('StrandInstanceManager is stopping');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this.instances.has(strandId)) {
|
|
135
|
+
log('Strand %s already running', strandId);
|
|
136
|
+
return this.instances.get(strandId)!;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
log('Starting strand instance: %s (sApp: %s v%s)', strandId, sAppConfig.id, sAppConfig.version);
|
|
140
|
+
|
|
141
|
+
// Convert SAppConfig to SAppInfo for the instance
|
|
142
|
+
const sAppInfo: SAppInfo = {
|
|
143
|
+
id: sAppConfig.id,
|
|
144
|
+
version: sAppConfig.version,
|
|
145
|
+
schema: sAppConfig.schema,
|
|
146
|
+
signature: sAppConfig.signature
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Determine latency hint: sApp config > default
|
|
150
|
+
const latencyHint = sAppConfig.latencyHint ?? config.defaultLatencyHint;
|
|
151
|
+
|
|
152
|
+
const instance: StrandInstance = {
|
|
153
|
+
strandId,
|
|
154
|
+
status: 'starting',
|
|
155
|
+
sAppInfo,
|
|
156
|
+
memberPrivateKey: strandRow.MemberPrivateKey ?? undefined,
|
|
157
|
+
connectedPeers: 0,
|
|
158
|
+
lastActivity: new Date(),
|
|
159
|
+
latencyHint
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
this.instances.set(strandId, instance);
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
// Resolve storage for this strand
|
|
166
|
+
// If a factory function is provided, it will be called with the strandId
|
|
167
|
+
// to create strand-specific storage (e.g., strand-isolated directories)
|
|
168
|
+
const strandStorage = resolveStrandStorage(config.storage?.provider, strandId);
|
|
169
|
+
if (strandStorage) {
|
|
170
|
+
log('Strand %s using provided storage provider', strandId);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Create isolated libp2p node for this strand
|
|
174
|
+
// Note: protocolPrefix would be used by libp2p services, but createLibp2pNode
|
|
175
|
+
// may not support it yet - tracked for future implementation
|
|
176
|
+
const _protocolPrefix = `/sereus/strand/${strandId}`;
|
|
177
|
+
|
|
178
|
+
// Determine relay mode: if explicitly set in config, use that;
|
|
179
|
+
// otherwise default to true for storage profile nodes
|
|
180
|
+
const enableRelay = config.network?.enableRelay ?? (config.profile === 'storage');
|
|
181
|
+
|
|
182
|
+
const node = await createLibp2pNode({
|
|
183
|
+
port: 0, // Random port
|
|
184
|
+
bootstrapNodes: [], // Will be populated from strand cohort
|
|
185
|
+
networkName: `strand-${strandId}`,
|
|
186
|
+
storage: strandStorage,
|
|
187
|
+
fretProfile: config.profile === 'storage' ? 'core' : 'edge',
|
|
188
|
+
relay: enableRelay,
|
|
189
|
+
clusterSize: 3,
|
|
190
|
+
clusterPolicy: {
|
|
191
|
+
allowDownsize: true,
|
|
192
|
+
sizeTolerance: 0.5
|
|
193
|
+
},
|
|
194
|
+
arachnode: {
|
|
195
|
+
enableRingZulu: true
|
|
196
|
+
// Storage ring participation stub - will be added when arachnode is built
|
|
197
|
+
// storageRing: config.profile === 'storage' ? { ring: 0 } : undefined
|
|
198
|
+
},
|
|
199
|
+
...(config.network?.transports && { transports: config.network.transports }),
|
|
200
|
+
...(config.network?.listenAddrs && { listenAddrs: config.network.listenAddrs })
|
|
201
|
+
}) as Libp2pNodeWithRepo;
|
|
202
|
+
|
|
203
|
+
instance.libp2pNode = node;
|
|
204
|
+
|
|
205
|
+
// Create and initialize the StrandDatabase
|
|
206
|
+
const strandDb = new StrandDatabase({
|
|
207
|
+
strandId,
|
|
208
|
+
sAppConfig,
|
|
209
|
+
libp2pNode: node,
|
|
210
|
+
coordinatedRepo: node.coordinatedRepo
|
|
211
|
+
});
|
|
212
|
+
await strandDb.initialize();
|
|
213
|
+
instance.database = strandDb;
|
|
214
|
+
|
|
215
|
+
instance.status = 'active';
|
|
216
|
+
instance.lastActivity = new Date();
|
|
217
|
+
|
|
218
|
+
log('Strand %s started successfully with sApp %s', strandId, sAppConfig.id);
|
|
219
|
+
return instance;
|
|
220
|
+
|
|
221
|
+
} catch (error) {
|
|
222
|
+
instance.status = 'error';
|
|
223
|
+
instance.error = error instanceof Error ? error.message : String(error);
|
|
224
|
+
log('Failed to start strand %s: %s', strandId, instance.error);
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Stop a strand instance
|
|
231
|
+
*/
|
|
232
|
+
async stopStrand(strandId: string): Promise<void> {
|
|
233
|
+
const instance = this.instances.get(strandId);
|
|
234
|
+
if (!instance) {
|
|
235
|
+
log('Strand %s not found', strandId);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
log('Stopping strand instance: %s', strandId);
|
|
240
|
+
instance.status = 'stopping';
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// Close the database before stopping libp2p
|
|
244
|
+
if (instance.database) {
|
|
245
|
+
await instance.database.close();
|
|
246
|
+
instance.database = undefined;
|
|
247
|
+
}
|
|
248
|
+
if (instance.libp2pNode) {
|
|
249
|
+
await instance.libp2pNode.stop();
|
|
250
|
+
instance.libp2pNode = undefined;
|
|
251
|
+
}
|
|
252
|
+
instance.status = 'stopped';
|
|
253
|
+
this.instances.delete(strandId);
|
|
254
|
+
log('Strand %s stopped successfully', strandId);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
instance.status = 'error';
|
|
257
|
+
instance.error = error instanceof Error ? error.message : String(error);
|
|
258
|
+
log('Error stopping strand %s: %s', strandId, instance.error);
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Stop all strand instances
|
|
265
|
+
*/
|
|
266
|
+
async stopAll(): Promise<void> {
|
|
267
|
+
this.stopping = true;
|
|
268
|
+
log('Stopping all strand instances (%d)', this.instances.size);
|
|
269
|
+
|
|
270
|
+
const stopPromises = Array.from(this.instances.keys()).map(id =>
|
|
271
|
+
this.stopStrand(id).catch(err => {
|
|
272
|
+
log('Error stopping strand %s during shutdown: %s', id, err);
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
await Promise.all(stopPromises);
|
|
277
|
+
this.stopping = false;
|
|
278
|
+
log('All strand instances stopped');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import { toString as uint8ArrayToString } from 'uint8arrays';
|
|
3
|
+
import { generateKeyPair, privateKeyToProtobuf } from '@libp2p/crypto/keys';
|
|
4
|
+
import { peerIdFromPrivateKey } from '@libp2p/peer-id';
|
|
5
|
+
import type { Libp2p } from '@libp2p/interface';
|
|
6
|
+
import type {
|
|
7
|
+
OpenInvitation,
|
|
8
|
+
FormStrandResult,
|
|
9
|
+
ValidateFormationResult,
|
|
10
|
+
StrandFormationDisclosure
|
|
11
|
+
} from './types.js';
|
|
12
|
+
import {
|
|
13
|
+
StrandFormationManager,
|
|
14
|
+
type StrandFormationManagerConfig
|
|
15
|
+
} from './strand-formation-manager.js';
|
|
16
|
+
|
|
17
|
+
const log = debug('sereus:cadre:solicitation');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Interface for validating formation disclosures
|
|
21
|
+
*/
|
|
22
|
+
export interface DisclosureValidator {
|
|
23
|
+
/**
|
|
24
|
+
* Validate the disclosure provided by an initiator
|
|
25
|
+
* @param token The invitation token
|
|
26
|
+
* @param disclosure The disclosure object from the initiator
|
|
27
|
+
* @returns Whether the disclosure is acceptable
|
|
28
|
+
*/
|
|
29
|
+
validateDisclosure(token: string, disclosure: StrandFormationDisclosure): Promise<boolean>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Interface for recording formation usage
|
|
34
|
+
*/
|
|
35
|
+
export interface FormationUsageRecorder {
|
|
36
|
+
/**
|
|
37
|
+
* Record that a formation invite was used
|
|
38
|
+
*/
|
|
39
|
+
recordUsage(token: string, initiatorKey: string, strandId: string): Promise<void>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Check if a token has already been used (for single-use invites)
|
|
43
|
+
*/
|
|
44
|
+
isTokenUsed(token: string): Promise<boolean>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a token is valid and not expired
|
|
48
|
+
*/
|
|
49
|
+
isTokenValid(token: string): Promise<{ valid: boolean; invitation?: OpenInvitation }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Interface for strand provisioning during formation
|
|
54
|
+
*/
|
|
55
|
+
export interface StrandProvisioner {
|
|
56
|
+
/**
|
|
57
|
+
* Provision a new strand after formation is validated
|
|
58
|
+
*/
|
|
59
|
+
provisionStrand(
|
|
60
|
+
sAppId: string,
|
|
61
|
+
initiatorKey: string,
|
|
62
|
+
responderKey: string
|
|
63
|
+
): Promise<{ strandId: string }>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Interface for signing formation approvals
|
|
68
|
+
*/
|
|
69
|
+
export interface FormationSigner {
|
|
70
|
+
/**
|
|
71
|
+
* Sign a formation approval
|
|
72
|
+
*/
|
|
73
|
+
signFormation(token: string, disclosure: StrandFormationDisclosure): Promise<{
|
|
74
|
+
validationKey: string;
|
|
75
|
+
validationSignature: string;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface StrandSolicitationServiceOptions {
|
|
80
|
+
disclosureValidator?: DisclosureValidator;
|
|
81
|
+
formationUsageRecorder?: FormationUsageRecorder;
|
|
82
|
+
strandProvisioner?: StrandProvisioner;
|
|
83
|
+
formationSigner?: FormationSigner;
|
|
84
|
+
/** Party ID for this node (used in protocol messages) */
|
|
85
|
+
partyId?: string;
|
|
86
|
+
/** Cadre peer addresses for this node */
|
|
87
|
+
cadrePeerAddrs?: string[];
|
|
88
|
+
/** Configuration for the formation manager */
|
|
89
|
+
formationConfig?: StrandFormationManagerConfig;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Strand Solicitation API for forming strands via open invitations.
|
|
94
|
+
*
|
|
95
|
+
* This service handles the high-level API defined in api.md:
|
|
96
|
+
* - formStrand(invitation, disclosure, node) - called by initiator
|
|
97
|
+
* - validateStrandFormation(token, disclosure) - called by responder
|
|
98
|
+
*
|
|
99
|
+
* When a libp2p node is provided, the underlying protocol is handled by
|
|
100
|
+
* strand-proto's SessionManager via StrandFormationManager.
|
|
101
|
+
*/
|
|
102
|
+
export class StrandSolicitationService {
|
|
103
|
+
private readonly disclosureValidator?: DisclosureValidator;
|
|
104
|
+
private readonly formationUsageRecorder?: FormationUsageRecorder;
|
|
105
|
+
private readonly strandProvisioner?: StrandProvisioner;
|
|
106
|
+
private readonly formationSigner?: FormationSigner;
|
|
107
|
+
private readonly partyId: string;
|
|
108
|
+
private readonly cadrePeerAddrs: string[];
|
|
109
|
+
private formationManager?: StrandFormationManager;
|
|
110
|
+
private readonly formationConfig?: StrandFormationManagerConfig;
|
|
111
|
+
|
|
112
|
+
constructor(options?: StrandSolicitationServiceOptions) {
|
|
113
|
+
this.disclosureValidator = options?.disclosureValidator;
|
|
114
|
+
this.formationUsageRecorder = options?.formationUsageRecorder;
|
|
115
|
+
this.strandProvisioner = options?.strandProvisioner;
|
|
116
|
+
this.formationSigner = options?.formationSigner;
|
|
117
|
+
this.partyId = options?.partyId ?? `party-${Date.now()}`;
|
|
118
|
+
this.cadrePeerAddrs = options?.cadrePeerAddrs ?? [];
|
|
119
|
+
this.formationConfig = options?.formationConfig;
|
|
120
|
+
log('StrandSolicitationService created for party: %s', this.partyId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get or create the StrandFormationManager.
|
|
125
|
+
* Lazily initialized to allow configuration after construction.
|
|
126
|
+
*/
|
|
127
|
+
private getFormationManager(): StrandFormationManager {
|
|
128
|
+
if (!this.formationManager) {
|
|
129
|
+
this.formationManager = new StrandFormationManager({
|
|
130
|
+
disclosureValidator: this.disclosureValidator,
|
|
131
|
+
formationUsageRecorder: this.formationUsageRecorder,
|
|
132
|
+
strandProvisioner: this.strandProvisioner,
|
|
133
|
+
partyId: this.partyId,
|
|
134
|
+
cadrePeerAddrs: this.cadrePeerAddrs,
|
|
135
|
+
config: this.formationConfig
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return this.formationManager;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Register as a responder on a libp2p node.
|
|
143
|
+
* This enables the node to handle incoming strand formation requests.
|
|
144
|
+
*/
|
|
145
|
+
registerResponder(node: Libp2p): void {
|
|
146
|
+
this.getFormationManager().registerResponder(node);
|
|
147
|
+
log('Registered as responder');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Unregister as a responder from a libp2p node.
|
|
152
|
+
*/
|
|
153
|
+
unregisterResponder(node: Libp2p): void {
|
|
154
|
+
this.getFormationManager().unregisterResponder(node);
|
|
155
|
+
log('Unregistered as responder');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Form a strand with a responder via an open invitation.
|
|
160
|
+
*
|
|
161
|
+
* Called by the initiator (the party who received an out-of-band invitation).
|
|
162
|
+
* This generates a member key, contacts the responder's cadre, and negotiates
|
|
163
|
+
* strand formation.
|
|
164
|
+
*
|
|
165
|
+
* @param invitation The open invitation (or just token for legacy API)
|
|
166
|
+
* @param disclosure Identity/context information to share with the responder
|
|
167
|
+
* @param node Optional libp2p node for real protocol handling
|
|
168
|
+
* @returns The member key and strand info if successful
|
|
169
|
+
*/
|
|
170
|
+
async formStrand(
|
|
171
|
+
invitation: OpenInvitation | string,
|
|
172
|
+
disclosure: StrandFormationDisclosure,
|
|
173
|
+
node?: Libp2p
|
|
174
|
+
): Promise<FormStrandResult> {
|
|
175
|
+
// Handle legacy API where just token was passed
|
|
176
|
+
const token = typeof invitation === 'string' ? invitation : invitation.token;
|
|
177
|
+
log('Forming strand with token: %s', token);
|
|
178
|
+
|
|
179
|
+
// Generate a new keypair for this strand membership
|
|
180
|
+
const privateKey = await generateKeyPair('Ed25519');
|
|
181
|
+
const peerId = peerIdFromPrivateKey(privateKey);
|
|
182
|
+
const privateKeyBytes = privateKeyToProtobuf(privateKey);
|
|
183
|
+
|
|
184
|
+
const memberKey = peerId.toString();
|
|
185
|
+
const invitePrivateKey = uint8ArrayToString(privateKeyBytes, 'base64');
|
|
186
|
+
|
|
187
|
+
log('Generated member key: %s', memberKey);
|
|
188
|
+
|
|
189
|
+
// If we have a full invitation and a node, use the real protocol
|
|
190
|
+
if (typeof invitation !== 'string' && node) {
|
|
191
|
+
log('Using strand-proto for real protocol handling');
|
|
192
|
+
const result = await this.getFormationManager().formStrand(
|
|
193
|
+
invitation,
|
|
194
|
+
{ ...disclosure, partyId: memberKey },
|
|
195
|
+
node
|
|
196
|
+
);
|
|
197
|
+
return {
|
|
198
|
+
memberKey,
|
|
199
|
+
invitePrivateKey,
|
|
200
|
+
strandId: result.strandId
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Fallback: placeholder strandId (for testing without network)
|
|
205
|
+
log('No node provided, using placeholder strandId');
|
|
206
|
+
const strandId = `strand-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
memberKey,
|
|
210
|
+
invitePrivateKey,
|
|
211
|
+
strandId
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Validate a strand formation request.
|
|
217
|
+
*
|
|
218
|
+
* Called by the responder (the party who created the open invitation) when
|
|
219
|
+
* an initiator contacts them to form a strand. Validates the disclosure
|
|
220
|
+
* and returns a signed approval.
|
|
221
|
+
*
|
|
222
|
+
* @param token The invitation token being used
|
|
223
|
+
* @param disclosure The disclosure from the initiator
|
|
224
|
+
* @returns Validation key and signature if approved
|
|
225
|
+
* @throws If validation fails or token is invalid
|
|
226
|
+
*/
|
|
227
|
+
async validateStrandFormation(
|
|
228
|
+
token: string,
|
|
229
|
+
disclosure: StrandFormationDisclosure
|
|
230
|
+
): Promise<ValidateFormationResult> {
|
|
231
|
+
log('Validating strand formation for token: %s', token);
|
|
232
|
+
|
|
233
|
+
// Check token validity
|
|
234
|
+
if (this.formationUsageRecorder) {
|
|
235
|
+
const tokenCheck = await this.formationUsageRecorder.isTokenValid(token);
|
|
236
|
+
if (!tokenCheck.valid) {
|
|
237
|
+
log('Token invalid or expired: %s', token);
|
|
238
|
+
throw new Error('Invalid or expired token');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if already used (for single-use tokens)
|
|
242
|
+
const isUsed = await this.formationUsageRecorder.isTokenUsed(token);
|
|
243
|
+
if (isUsed) {
|
|
244
|
+
log('Token already used: %s', token);
|
|
245
|
+
throw new Error('Token has already been used');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Validate the disclosure
|
|
250
|
+
if (this.disclosureValidator) {
|
|
251
|
+
const isValid = await this.disclosureValidator.validateDisclosure(token, disclosure);
|
|
252
|
+
if (!isValid) {
|
|
253
|
+
log('Disclosure validation failed for token: %s', token);
|
|
254
|
+
throw new Error('Disclosure validation failed');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Sign the formation approval
|
|
259
|
+
if (!this.formationSigner) {
|
|
260
|
+
throw new Error('FormationSigner not configured');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const { validationKey, validationSignature } = await this.formationSigner.signFormation(
|
|
264
|
+
token,
|
|
265
|
+
disclosure
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
log('Formation validated, key: %s', validationKey);
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
validationKey,
|
|
272
|
+
validationSignature
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Create an open invitation for others to form strands with this party.
|
|
278
|
+
*
|
|
279
|
+
* @param sAppId The sApp to use for formed strands
|
|
280
|
+
* @param expirationMs How long the invitation is valid (ms from now)
|
|
281
|
+
* @param bootstrap Bootstrap addresses for contacting this party's cadre
|
|
282
|
+
* @returns The open invitation to share out-of-band
|
|
283
|
+
*/
|
|
284
|
+
async createOpenInvitation(
|
|
285
|
+
sAppId: string,
|
|
286
|
+
expirationMs: number,
|
|
287
|
+
bootstrap: string[]
|
|
288
|
+
): Promise<OpenInvitation> {
|
|
289
|
+
// Generate a unique token
|
|
290
|
+
const token = `invite-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
291
|
+
const expiration = new Date(Date.now() + expirationMs);
|
|
292
|
+
|
|
293
|
+
log('Created open invitation: %s (expires %s)', token, expiration.toISOString());
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
token,
|
|
297
|
+
sAppId,
|
|
298
|
+
expiration,
|
|
299
|
+
bootstrap
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Record that a formation was completed successfully.
|
|
305
|
+
* Called after strand provisioning to track usage.
|
|
306
|
+
*/
|
|
307
|
+
async recordFormationComplete(
|
|
308
|
+
token: string,
|
|
309
|
+
initiatorKey: string,
|
|
310
|
+
strandId: string
|
|
311
|
+
): Promise<void> {
|
|
312
|
+
if (this.formationUsageRecorder) {
|
|
313
|
+
await this.formationUsageRecorder.recordUsage(token, initiatorKey, strandId);
|
|
314
|
+
log('Recorded formation usage: token=%s strand=%s', token, strandId);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|