@serfab/cadre-core 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/bootstrap-peer-store-file.d.ts +13 -0
- package/dist/bootstrap-peer-store-file.js +42 -0
- package/dist/bootstrap-peer-store-file.js.map +1 -0
- package/dist/bootstrap-peer-store.d.ts +73 -0
- package/dist/bootstrap-peer-store.js +128 -0
- package/dist/bootstrap-peer-store.js.map +1 -0
- package/dist/cadre-node.d.ts +1006 -101
- package/dist/cadre-node.js +1977 -283
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +93 -0
- package/dist/control-authorization.js +57 -0
- package/dist/control-authorization.js.map +1 -0
- package/dist/control-cohort.d.ts +15 -15
- package/dist/control-cohort.js +14 -14
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +1033 -76
- package/dist/control-database.js +1520 -153
- package/dist/control-database.js.map +1 -1
- package/dist/control-formation-recorder.d.ts +91 -12
- package/dist/control-formation-recorder.js +131 -16
- package/dist/control-formation-recorder.js.map +1 -1
- package/dist/control-schema.d.ts +1 -1
- package/dist/control-schema.js +774 -186
- package/dist/control-schema.js.map +1 -1
- package/dist/control-stream.d.ts +38 -4
- package/dist/control-stream.js +70 -4
- package/dist/control-stream.js.map +1 -1
- package/dist/control-write-retry.d.ts +151 -0
- package/dist/control-write-retry.js +411 -0
- package/dist/control-write-retry.js.map +1 -0
- package/dist/delegate-admission.d.ts +134 -0
- package/dist/delegate-admission.js +244 -0
- package/dist/delegate-admission.js.map +1 -0
- package/dist/device-token.d.ts +12 -12
- package/dist/device-token.js +15 -14
- package/dist/device-token.js.map +1 -1
- package/dist/ed25519-key.d.ts +68 -0
- package/dist/ed25519-key.js +106 -0
- package/dist/ed25519-key.js.map +1 -0
- package/dist/file-durable-slot.d.ts +20 -0
- package/dist/file-durable-slot.js +53 -0
- package/dist/file-durable-slot.js.map +1 -0
- package/dist/formation-approval.d.ts +157 -0
- package/dist/formation-approval.js +375 -0
- package/dist/formation-approval.js.map +1 -0
- package/dist/fs-atomic.d.ts +38 -0
- package/dist/fs-atomic.js +97 -0
- package/dist/fs-atomic.js.map +1 -0
- package/dist/identity-key.d.ts +49 -0
- package/dist/identity-key.js +81 -0
- package/dist/identity-key.js.map +1 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.js +44 -10
- package/dist/index.js.map +1 -1
- package/dist/key-store-file.d.ts +2 -24
- package/dist/key-store-file.js +10 -77
- package/dist/key-store-file.js.map +1 -1
- package/dist/key-store.d.ts +1 -1
- package/dist/key-store.js +1 -1
- package/dist/membership-connection-gater.d.ts +146 -0
- package/dist/membership-connection-gater.js +180 -0
- package/dist/membership-connection-gater.js.map +1 -0
- package/dist/node-local-snapshot.d.ts +109 -0
- package/dist/node-local-snapshot.js +182 -0
- package/dist/node-local-snapshot.js.map +1 -0
- package/dist/peer-authorization.d.ts +150 -11
- package/dist/peer-authorization.js +170 -13
- package/dist/peer-authorization.js.map +1 -1
- package/dist/peer-record.d.ts +24 -11
- package/dist/peer-record.js +29 -13
- package/dist/peer-record.js.map +1 -1
- package/dist/push-node.d.ts +38 -0
- package/dist/push-node.js +48 -0
- package/dist/push-node.js.map +1 -0
- package/dist/push-notifier-apns.d.ts +10 -2
- package/dist/push-notifier-apns.js +14 -10
- package/dist/push-notifier-apns.js.map +1 -1
- package/dist/push-notifier-fcm.d.ts +7 -1
- package/dist/push-notifier-fcm.js +9 -10
- package/dist/push-notifier-fcm.js.map +1 -1
- package/dist/push-notifier.d.ts +16 -35
- package/dist/push-notifier.js +16 -44
- package/dist/push-notifier.js.map +1 -1
- package/dist/relay-addrs.d.ts +45 -0
- package/dist/relay-addrs.js +103 -0
- package/dist/relay-addrs.js.map +1 -0
- package/dist/relay-reservation.d.ts +266 -0
- package/dist/relay-reservation.js +613 -0
- package/dist/relay-reservation.js.map +1 -0
- package/dist/seed-bootstrap.d.ts +263 -79
- package/dist/seed-bootstrap.js +413 -211
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.d.ts +50 -24
- package/dist/seed-trust-policy.js +28 -19
- package/dist/seed-trust-policy.js.map +1 -1
- package/dist/strand-addr-protocol.d.ts +42 -13
- package/dist/strand-addr-protocol.js +58 -52
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-backfill.d.ts +109 -0
- package/dist/strand-backfill.js +279 -0
- package/dist/strand-backfill.js.map +1 -0
- package/dist/strand-database.d.ts +5 -5
- package/dist/strand-database.js +4 -4
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-formation-manager.d.ts +38 -9
- package/dist/strand-formation-manager.js +120 -17
- package/dist/strand-formation-manager.js.map +1 -1
- package/dist/strand-formation-protocol.d.ts +104 -5
- package/dist/strand-formation-protocol.js +233 -5
- package/dist/strand-formation-protocol.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +40 -2
- package/dist/strand-instance-manager.js +93 -15
- package/dist/strand-instance-manager.js.map +1 -1
- package/dist/strand-member-key.d.ts +6 -6
- package/dist/strand-member-key.js +6 -6
- package/dist/strand-member-key.js.map +1 -1
- package/dist/strand-member-registry.d.ts +12 -12
- package/dist/strand-member-registry.js +22 -14
- package/dist/strand-member-registry.js.map +1 -1
- package/dist/strand-membership-writer.d.ts +532 -138
- package/dist/strand-membership-writer.js +800 -179
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-solicitation.d.ts +99 -33
- package/dist/strand-solicitation.js +100 -52
- package/dist/strand-solicitation.js.map +1 -1
- package/dist/strand-transport-key.d.ts +40 -0
- package/dist/strand-transport-key.js +56 -0
- package/dist/strand-transport-key.js.map +1 -0
- package/dist/strand-wake-protocol.d.ts +9 -7
- package/dist/strand-wake-protocol.js +22 -46
- package/dist/strand-wake-protocol.js.map +1 -1
- package/dist/strand-watcher.d.ts +17 -2
- package/dist/strand-watcher.js +47 -3
- package/dist/strand-watcher.js.map +1 -1
- package/dist/trusted-owner-store-file.d.ts +12 -0
- package/dist/trusted-owner-store-file.js +39 -0
- package/dist/trusted-owner-store-file.js.map +1 -0
- package/dist/trusted-owner-store.d.ts +64 -0
- package/dist/trusted-owner-store.js +126 -0
- package/dist/trusted-owner-store.js.map +1 -0
- package/dist/types.d.ts +325 -50
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +23 -8
- package/src/bootstrap-peer-store-file.ts +44 -0
- package/src/bootstrap-peer-store.ts +172 -0
- package/src/cadre-node.ts +2129 -295
- package/src/control-authorization.ts +114 -0
- package/src/control-cohort.ts +17 -17
- package/src/control-database.ts +1766 -191
- package/src/control-formation-recorder.ts +174 -21
- package/src/control-schema.ts +786 -198
- package/src/control-stream.ts +81 -4
- package/src/control-write-retry.ts +473 -0
- package/src/delegate-admission.ts +302 -0
- package/src/device-token.ts +19 -14
- package/src/ed25519-key.ts +134 -0
- package/src/file-durable-slot.ts +61 -0
- package/src/formation-approval.ts +604 -0
- package/src/fs-atomic.ts +107 -0
- package/src/identity-key.ts +109 -0
- package/src/index.ts +157 -24
- package/src/key-store-file.ts +10 -81
- package/src/key-store.ts +1 -1
- package/src/membership-connection-gater.ts +204 -0
- package/src/node-local-snapshot.ts +260 -0
- package/src/peer-authorization.ts +219 -13
- package/src/peer-record.ts +31 -13
- package/src/push-node.ts +64 -0
- package/src/push-notifier-apns.ts +16 -12
- package/src/push-notifier-fcm.ts +9 -10
- package/src/push-notifier.ts +16 -57
- package/src/relay-addrs.ts +111 -0
- package/src/relay-reservation.ts +823 -0
- package/src/seed-bootstrap.ts +490 -226
- package/src/seed-trust-policy.ts +65 -32
- package/src/strand-addr-protocol.ts +84 -53
- package/src/strand-backfill.ts +366 -0
- package/src/strand-database.ts +6 -6
- package/src/strand-formation-manager.ts +145 -19
- package/src/strand-formation-protocol.ts +310 -9
- package/src/strand-instance-manager.ts +113 -16
- package/src/strand-member-key.ts +7 -7
- package/src/strand-member-registry.ts +28 -23
- package/src/strand-membership-writer.ts +1430 -640
- package/src/strand-solicitation.ts +155 -90
- package/src/strand-transport-key.ts +58 -0
- package/src/strand-wake-protocol.ts +29 -46
- package/src/strand-watcher.ts +66 -3
- package/src/trusted-owner-store-file.ts +41 -0
- package/src/trusted-owner-store.ts +171 -0
- package/src/types.ts +342 -51
- package/dist/authority-key.d.ts +0 -41
- package/dist/authority-key.js +0 -52
- package/dist/authority-key.js.map +0 -1
- package/src/authority-key.ts +0 -68
|
@@ -2,6 +2,7 @@ import debug from 'debug';
|
|
|
2
2
|
import type { PrivateKey } from '@libp2p/interface';
|
|
3
3
|
import { createLibp2pNode, type IRawStorage } from '@optimystic/db-p2p';
|
|
4
4
|
import { StrandDatabase } from './strand-database.js';
|
|
5
|
+
import { StrandBackfill, type StrandBackfillConfig } from './strand-backfill.js';
|
|
5
6
|
import { assertSchemaSignature } from './schema-verification.js';
|
|
6
7
|
import type {
|
|
7
8
|
StrandInstance,
|
|
@@ -16,6 +17,8 @@ import type {
|
|
|
16
17
|
StrandMode,
|
|
17
18
|
Libp2pNodeWithRepo
|
|
18
19
|
} from './types.js';
|
|
20
|
+
import { resolveStrandClusterSize, STRAND_CLUSTER_POLICY } from './types.js';
|
|
21
|
+
import { resolveListenAddrs } from './relay-addrs.js';
|
|
19
22
|
|
|
20
23
|
const log = debug('sereus:cadre:strand-manager');
|
|
21
24
|
const timing = debug('sereus:cadre:timing');
|
|
@@ -48,11 +51,30 @@ export interface StartStrandConfig {
|
|
|
48
51
|
requireSignedSchemas?: boolean;
|
|
49
52
|
/**
|
|
50
53
|
* Whether this node founds the strand (vs. joins it). Forwarded to the
|
|
51
|
-
* StrandDatabase so the founder bootstrap (Header, founding Member/
|
|
54
|
+
* StrandDatabase so the founder bootstrap (Header, founding Member/Manager)
|
|
52
55
|
* runs once at bring-up. Joiners leave this unset and write nothing. See
|
|
53
56
|
* {@link StrandConfig.founder}.
|
|
54
57
|
*/
|
|
55
58
|
founder?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Number of nodes Optimystic is told this strand's replication cluster should
|
|
61
|
+
* have. Same rule as {@link CadreNodeConfig.strandClusterSize}, which CadreNode
|
|
62
|
+
* forwards here: every node on the strand should use the same value, and it is
|
|
63
|
+
* frozen when the strand's libp2p node is created. Defaults to
|
|
64
|
+
* `DEFAULT_STRAND_CLUSTER_SIZE` (4); values below `MIN_CLUSTER_SIZE` (2) are
|
|
65
|
+
* rejected by `resolveStrandClusterSize`. Strand-only — the
|
|
66
|
+
* control network's breadth is the fixed `CONTROL_REPLICATION_BREADTH`.
|
|
67
|
+
*/
|
|
68
|
+
clusterSize?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Tuning for the strand peer-join block catch-up ({@link StrandBackfill}),
|
|
71
|
+
* forwarded from {@link CadreNodeConfig.strandBackfill}. When the strand's
|
|
72
|
+
* libp2p node connects to a peer this runtime has not yet caught up, every
|
|
73
|
+
* block in the strand's own raw store is pushed to it. Runs only on
|
|
74
|
+
* `networked` strands with per-strand storage; `{ enabled: false }` restores
|
|
75
|
+
* the pre-existing no-backfill behaviour.
|
|
76
|
+
*/
|
|
77
|
+
backfill?: StrandBackfillConfig;
|
|
56
78
|
}
|
|
57
79
|
|
|
58
80
|
/**
|
|
@@ -132,6 +154,15 @@ export class StrandInstanceManager {
|
|
|
132
154
|
* without the caller re-threading storage/network/profile/key/sApp config.
|
|
133
155
|
*/
|
|
134
156
|
private launchConfigs: Map<string, StartStrandConfig> = new Map();
|
|
157
|
+
/**
|
|
158
|
+
* The per-strand peer-join block catch-up, keyed by strand id. Private (not on
|
|
159
|
+
* the public {@link StrandInstance}) because it is runtime plumbing with the
|
|
160
|
+
* same lifetime as the strand's libp2p node: created in `buildStrandRuntime`,
|
|
161
|
+
* stopped and dropped in `releaseRuntime` — so quiesce → resume rebuilds it
|
|
162
|
+
* with a fresh caught-up-peer memo, which is intended (a resumed node may have
|
|
163
|
+
* missed writes).
|
|
164
|
+
*/
|
|
165
|
+
private backfills: Map<string, StrandBackfill> = new Map();
|
|
135
166
|
private stopping = false;
|
|
136
167
|
|
|
137
168
|
constructor() {
|
|
@@ -160,7 +191,16 @@ export class StrandInstanceManager {
|
|
|
160
191
|
}
|
|
161
192
|
|
|
162
193
|
/**
|
|
163
|
-
* Start a new strand instance
|
|
194
|
+
* Start a new strand instance.
|
|
195
|
+
*
|
|
196
|
+
* A failed launch leaves NOTHING tracked: the instance and its retained launch
|
|
197
|
+
* config are both dropped before the error is rethrown, so the strand id is
|
|
198
|
+
* free for a genuine retry. This matches the pre-registration failure path
|
|
199
|
+
* (a rejected schema signature, which throws before anything is recorded) —
|
|
200
|
+
* both failure modes of this call leave the same residue: none. Callers learn
|
|
201
|
+
* of the failure from the rejected promise (and, on the control-discovered
|
|
202
|
+
* path, from CadreNode's `strand:error` event), not from an error record left
|
|
203
|
+
* behind in `instances`.
|
|
164
204
|
*/
|
|
165
205
|
async startStrand(config: StartStrandConfig): Promise<StrandInstance> {
|
|
166
206
|
const { strandRow, sAppConfig } = config;
|
|
@@ -201,7 +241,10 @@ export class StrandInstanceManager {
|
|
|
201
241
|
memberPrivateKey: strandRow.MemberPrivateKey ?? undefined,
|
|
202
242
|
connectedPeers: 0,
|
|
203
243
|
lastActivity: new Date(),
|
|
204
|
-
latencyHint
|
|
244
|
+
latencyHint,
|
|
245
|
+
// Seed only — buildStrandRuntime overwrites this with the same resolution,
|
|
246
|
+
// so an instance that errors mid-build still reads sensibly.
|
|
247
|
+
mode: config.mode ?? 'networked'
|
|
205
248
|
};
|
|
206
249
|
|
|
207
250
|
this.instances.set(strandId, instance);
|
|
@@ -213,9 +256,15 @@ export class StrandInstanceManager {
|
|
|
213
256
|
log('Strand %s started successfully with sApp %s', strandId, sAppConfig.id);
|
|
214
257
|
return instance;
|
|
215
258
|
} catch (error) {
|
|
259
|
+
// Status/error first — the (now discarded) record is still what `log` reports on.
|
|
216
260
|
instance.status = 'error';
|
|
217
261
|
instance.error = error instanceof Error ? error.message : String(error);
|
|
218
262
|
log('Failed to start strand %s: %s', strandId, instance.error);
|
|
263
|
+
// Drop the dead record so this strand id can be launched again. Keep the
|
|
264
|
+
// `launchConfigs` has an entry iff `instances` does invariant — resumeStrand
|
|
265
|
+
// reads both, and a config without an instance would strand the config.
|
|
266
|
+
this.instances.delete(strandId);
|
|
267
|
+
this.launchConfigs.delete(strandId);
|
|
219
268
|
throw error;
|
|
220
269
|
}
|
|
221
270
|
}
|
|
@@ -231,6 +280,7 @@ export class StrandInstanceManager {
|
|
|
231
280
|
private async buildStrandRuntime(instance: StrandInstance, config: StartStrandConfig): Promise<void> {
|
|
232
281
|
const strandId = instance.strandId;
|
|
233
282
|
const { sAppConfig } = config;
|
|
283
|
+
const mode = config.mode ?? 'networked';
|
|
234
284
|
|
|
235
285
|
// Resolve storage for this strand. If a factory function is provided, it is
|
|
236
286
|
// called with the strandId to create strand-specific storage (e.g.,
|
|
@@ -240,43 +290,60 @@ export class StrandInstanceManager {
|
|
|
240
290
|
log('Strand %s using provided storage provider', strandId);
|
|
241
291
|
}
|
|
242
292
|
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
293
|
+
// db-p2p namespaces every one of the node's protocol ids by network name
|
|
294
|
+
// (`/optimystic/<networkName>/...`), so anything dialing this node's own
|
|
295
|
+
// services must derive its prefix from the SAME string the node was built
|
|
296
|
+
// with — hence one binding for both, not two literals that can drift.
|
|
297
|
+
const networkName = `strand-${strandId}`;
|
|
298
|
+
const protocolPrefix = `/optimystic/${networkName}`;
|
|
246
299
|
|
|
247
300
|
// Determine relay mode: if explicitly set in config, use that;
|
|
248
301
|
// otherwise default to true for storage profile nodes.
|
|
249
302
|
const enableRelay = config.network?.enableRelay ?? (config.profile === 'storage');
|
|
303
|
+
const listenAddrs = resolveListenAddrs(config.network);
|
|
250
304
|
|
|
251
305
|
try {
|
|
252
306
|
let t0 = performance.now();
|
|
253
307
|
const node = await createLibp2pNode({
|
|
254
308
|
port: 0, // Random port
|
|
255
309
|
bootstrapNodes: config.bootstrapNodes ?? [],
|
|
256
|
-
networkName
|
|
310
|
+
networkName,
|
|
257
311
|
storage: strandStorage,
|
|
258
312
|
fretProfile: config.profile === 'storage' ? 'core' : 'edge',
|
|
259
313
|
relay: enableRelay,
|
|
260
|
-
clusterSize:
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
},
|
|
314
|
+
clusterSize: resolveStrandClusterSize(config.clusterSize),
|
|
315
|
+
// Deliberately NOT CONTROL_CLUSTER_POLICY: a strand is application data with its own
|
|
316
|
+
// breadth reasoning, and the shape match with the control policy is a coincidence.
|
|
317
|
+
clusterPolicy: STRAND_CLUSTER_POLICY,
|
|
265
318
|
arachnode: {
|
|
266
319
|
enableRingZulu: config.profile === 'storage'
|
|
267
320
|
},
|
|
268
321
|
...(config.privateKey && { privateKey: config.privateKey }),
|
|
269
322
|
...(config.network?.transports && { transports: config.network.transports }),
|
|
270
|
-
|
|
323
|
+
// NOTE: strand nodes receive the same RESOLVED listen addrs as the
|
|
324
|
+
// control node. A fixed-port listen addr (e.g. cadre-cli's example
|
|
325
|
+
// `/ip4/0.0.0.0/tcp/4001`) would have control + strand nodes racing to
|
|
326
|
+
// bind one port — EADDRINUSE. Unverified; if a deployment configures a
|
|
327
|
+
// fixed port and strands fail to start, rewrite the port per node here.
|
|
328
|
+
// An inherited `/p2p-circuit` addr, by contrast, is deliberate — it is
|
|
329
|
+
// what gives a NAT'd strand node a reachable relay slot — and works
|
|
330
|
+
// because the launch path announces this strand's derived peerId to
|
|
331
|
+
// the relay first (delegate admission; see cadre-node.ts). Those circuit
|
|
332
|
+
// entries come either from a hand-written `network.listenAddrs` or from
|
|
333
|
+
// `network.relayAddrs`, which `resolveListenAddrs` folds into the same list.
|
|
334
|
+
...(listenAddrs && { listenAddrs }),
|
|
271
335
|
...(config.network?.connectionGater && { connectionGater: config.network.connectionGater })
|
|
272
336
|
}) as Libp2pNodeWithRepo;
|
|
273
337
|
timing('[buildStrandRuntime:%s] createLibp2pNode: %dms', strandId, Math.round(performance.now() - t0));
|
|
274
338
|
|
|
275
339
|
instance.libp2pNode = node;
|
|
340
|
+
// Assigned on every runtime (re)build — resume re-enters here with
|
|
341
|
+
// resumeConfig, so a bootstrap → networked shift refreshes the field.
|
|
342
|
+
instance.mode = mode;
|
|
276
343
|
|
|
277
344
|
// Create and initialize the StrandDatabase. In bootstrap mode the same
|
|
278
345
|
// strandStorage instance also backs the optimystic local transactor so
|
|
279
|
-
// DML lands on the host's persistent storage (e.g.
|
|
346
|
+
// DML lands on the host's persistent storage (e.g. LevelDB on RN). Sharing
|
|
280
347
|
// the instance — not creating a second one over the same id+prefix —
|
|
281
348
|
// avoids cache divergence between the libp2p and database paths.
|
|
282
349
|
//
|
|
@@ -288,11 +355,11 @@ export class StrandInstanceManager {
|
|
|
288
355
|
sAppConfig,
|
|
289
356
|
libp2pNode: node,
|
|
290
357
|
coordinatedRepo: node.coordinatedRepo,
|
|
291
|
-
mode
|
|
358
|
+
mode,
|
|
292
359
|
rawStorage: strandStorage,
|
|
293
360
|
// Founder bootstrap inputs: the strand's type drives which membership rows
|
|
294
361
|
// are written, and the closed-strand MemberPrivateKey derives the founding
|
|
295
|
-
// Member/
|
|
362
|
+
// Member/Manager key. Both come off the control-network strand row.
|
|
296
363
|
strandType: config.strandRow.Type,
|
|
297
364
|
memberPrivateKey: config.strandRow.MemberPrivateKey ?? undefined,
|
|
298
365
|
founder: config.founder
|
|
@@ -301,6 +368,28 @@ export class StrandInstanceManager {
|
|
|
301
368
|
await strandDb.initialize();
|
|
302
369
|
timing('[buildStrandRuntime:%s] strandDatabase.initialize: %dms', strandId, Math.round(performance.now() - t0));
|
|
303
370
|
|
|
371
|
+
// Peer-join block catch-up: push this strand's own blocks to each newly
|
|
372
|
+
// connected peer, so a machine that joined after blocks were committed
|
|
373
|
+
// still ends up physically holding them (a bootstrap strand has no mesh
|
|
374
|
+
// to catch up, and without per-strand storage there is nothing to copy).
|
|
375
|
+
if (mode === 'networked' && strandStorage && config.backfill?.enabled !== false) {
|
|
376
|
+
if (node.keyNetwork) {
|
|
377
|
+
const backfill = new StrandBackfill({
|
|
378
|
+
strandId,
|
|
379
|
+
libp2p: node,
|
|
380
|
+
peerNetwork: node.keyNetwork,
|
|
381
|
+
storage: strandStorage,
|
|
382
|
+
// The same prefix the receiver registered its block-transfer handler
|
|
383
|
+
// under — derived from networkName above, never re-spelled here.
|
|
384
|
+
protocolPrefix
|
|
385
|
+
}, config.backfill);
|
|
386
|
+
backfill.start();
|
|
387
|
+
this.backfills.set(strandId, backfill);
|
|
388
|
+
} else {
|
|
389
|
+
log('Strand %s: libp2p node exposes no keyNetwork; peer-join block catch-up is inert', strandId);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
304
393
|
instance.status = 'active';
|
|
305
394
|
instance.lastActivity = new Date();
|
|
306
395
|
} catch (error) {
|
|
@@ -323,6 +412,14 @@ export class StrandInstanceManager {
|
|
|
323
412
|
* Shared by `quiesceStrand`, `stopStrand`, and that rollback path.
|
|
324
413
|
*/
|
|
325
414
|
private async releaseRuntime(instance: StrandInstance): Promise<void> {
|
|
415
|
+
// Backfill first — before the database closes and the libp2p node stops — so
|
|
416
|
+
// no NEW catch-up push is issued against a torn-down transport. A push already
|
|
417
|
+
// in flight is not awaited; it fails into the module's own per-chunk catch.
|
|
418
|
+
const backfill = this.backfills.get(instance.strandId);
|
|
419
|
+
if (backfill) {
|
|
420
|
+
backfill.stop();
|
|
421
|
+
this.backfills.delete(instance.strandId);
|
|
422
|
+
}
|
|
326
423
|
if (instance.database) {
|
|
327
424
|
await instance.database.close();
|
|
328
425
|
instance.database = undefined;
|
package/src/strand-member-key.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateKeyPair, privateKeyToProtobuf, privateKeyFromProtobuf } from '@libp2p/crypto/keys';
|
|
2
2
|
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
3
|
-
import {
|
|
3
|
+
import { ed25519KeyPairFromLibp2p, type Ed25519KeyPair } from './ed25519-key.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Mint a fresh ed25519 strand member private key, encoded as base64 protobuf.
|
|
@@ -22,18 +22,18 @@ export async function generateStrandMemberKey(): Promise<string> {
|
|
|
22
22
|
* base64url keypair the strand RBAC constraints consume.
|
|
23
23
|
*
|
|
24
24
|
* The `Strand` control-layer `MemberPrivateKey` is the closed-strand read-gating
|
|
25
|
-
* secret; the founding `Member.Key`/`
|
|
25
|
+
* secret; the founding `Member.Key`/`Manager.MemberKey` (the strand RBAC layer)
|
|
26
26
|
* are *derived from it* — they are the `publicKeyB64` of the keypair this returns.
|
|
27
27
|
* Decode the protobuf to a libp2p private key, then reuse
|
|
28
|
-
* {@link
|
|
29
|
-
*
|
|
28
|
+
* {@link ed25519KeyPairFromLibp2p} so the same seed→public derivation used for node
|
|
29
|
+
* owner keys yields a stable `{ privateKeyB64, publicKeyB64 }` whose public
|
|
30
30
|
* key a later strand signature verifies against.
|
|
31
31
|
*
|
|
32
32
|
* @param memberPrivateKey - The strand's `MemberPrivateKey` (base64 protobuf).
|
|
33
|
-
* @returns The base64url seed/public-key pair for founding membership/
|
|
33
|
+
* @returns The base64url seed/public-key pair for founding membership/manager.
|
|
34
34
|
* @throws If the decoded key is not Ed25519 or the raw bytes are malformed.
|
|
35
35
|
*/
|
|
36
|
-
export function strandMemberKeyPair(memberPrivateKey: string):
|
|
36
|
+
export function strandMemberKeyPair(memberPrivateKey: string): Ed25519KeyPair {
|
|
37
37
|
const privateKey = privateKeyFromProtobuf(uint8ArrayFromString(memberPrivateKey, 'base64'));
|
|
38
|
-
return
|
|
38
|
+
return ed25519KeyPairFromLibp2p(privateKey);
|
|
39
39
|
}
|
|
@@ -2,13 +2,13 @@ import debug from 'debug';
|
|
|
2
2
|
import type { Database, SqlValue } from '@quereus/quereus';
|
|
3
3
|
import type { MemberRegistration } from './types.js';
|
|
4
4
|
import type { MemberRegistry, MemberVerifier } from './enrollment.js';
|
|
5
|
-
import type {
|
|
5
|
+
import type { Ed25519KeyPair } from './ed25519-key.js';
|
|
6
6
|
import {
|
|
7
7
|
verifyStrandPayload,
|
|
8
8
|
consumeInvite,
|
|
9
|
-
|
|
9
|
+
addMemberByManager,
|
|
10
|
+
listOutstandingInvites,
|
|
10
11
|
} from './strand-membership-writer.js';
|
|
11
|
-
import { canonicalDatetime } from './canonical-datetime.js';
|
|
12
12
|
|
|
13
13
|
const log = debug('sereus:cadre:strand-member-registry');
|
|
14
14
|
|
|
@@ -37,10 +37,10 @@ export type StrandAdmission =
|
|
|
37
37
|
readonly invitePrivateKey: string;
|
|
38
38
|
}
|
|
39
39
|
| {
|
|
40
|
-
/** Admit directly by
|
|
41
|
-
readonly mode: '
|
|
42
|
-
/** The admitting
|
|
43
|
-
readonly
|
|
40
|
+
/** Admit directly by manager signature (the manager-side join). */
|
|
41
|
+
readonly mode: 'manager';
|
|
42
|
+
/** The admitting manager's strand keypair. */
|
|
43
|
+
readonly managerKeyPair: Ed25519KeyPair;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -80,11 +80,11 @@ export class StrandMemberVerifier implements MemberVerifier {
|
|
|
80
80
|
* single-use, and cryptographic gates are all enforced by the deferred
|
|
81
81
|
* `Strand.*` constraints when the member is actually written.
|
|
82
82
|
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
83
|
+
* "Outstanding" is {@link listOutstandingInvites}: an `Invite` that is neither
|
|
84
|
+
* already consumed, nor cancelled by a manager, nor expired. All three exclusions
|
|
85
|
+
* mirror gates the actual write would hit (`ConsumedInvite`'s primary key,
|
|
86
|
+
* `NotCancelled`, `NotExpired`), so this pre-flight does not report a door the
|
|
87
|
+
* constraints will slam. A raw `Invite` count would over-report on all three.
|
|
88
88
|
*/
|
|
89
89
|
async isAuthorizedToJoin(_strandId: string, memberKey: string): Promise<boolean> {
|
|
90
90
|
const consumed = await scalarCount(
|
|
@@ -93,15 +93,20 @@ export class StrandMemberVerifier implements MemberVerifier {
|
|
|
93
93
|
[memberKey],
|
|
94
94
|
);
|
|
95
95
|
if (consumed > 0) {
|
|
96
|
+
// NOTE: a REVOKED member still bears its stale ConsumedInvite row (the table is
|
|
97
|
+
// insert-only, and revocation does not delete it), so this short-circuit says
|
|
98
|
+
// "authorized" for a party that can no longer actually join. Harmless because the
|
|
99
|
+
// on-engine constraints are the real gate — Member.Authorized's invite branch
|
|
100
|
+
// requires a FRESH consumption row, which a stale one is not — and this pre-flight
|
|
101
|
+
// only decides whether attempting the write is worthwhile.
|
|
96
102
|
return true;
|
|
97
103
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
);
|
|
104
|
-
return invites > 0;
|
|
104
|
+
// NOTE: materialises the whole outstanding set to answer a non-empty question (the
|
|
105
|
+
// former code was a single `select count(1)`). Deliberate — sharing one definition of
|
|
106
|
+
// "outstanding" with the manager-facing enumeration beats a second, drifting copy of
|
|
107
|
+
// the three exclusions. If this pre-flight ever runs hot on a strand with many
|
|
108
|
+
// invitations, add a short-circuiting variant rather than re-inlining the rules.
|
|
109
|
+
return (await listOutstandingInvites(this.db)).length > 0;
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
|
|
@@ -115,8 +120,8 @@ export class StrandMemberVerifier implements MemberVerifier {
|
|
|
115
120
|
* `Member.Authorized` is taken:
|
|
116
121
|
* - `invite` → {@link consumeInvite} (atomic `Member` + `ConsumedInvite`), the
|
|
117
122
|
* invitee-side flow that redeems a single-use invite.
|
|
118
|
-
* - `
|
|
119
|
-
* seats a member already trusted by
|
|
123
|
+
* - `manager` → {@link addMemberByManager}, the manager-side flow that
|
|
124
|
+
* seats a member already trusted by a manager.
|
|
120
125
|
*
|
|
121
126
|
* Scoped to one strand's db; the `strandId` argument is accepted for interface
|
|
122
127
|
* compatibility but writes target this db's `Strand.*` tables.
|
|
@@ -152,8 +157,8 @@ export class StrandMemberRegistry implements MemberRegistry {
|
|
|
152
157
|
return;
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
await
|
|
156
|
-
|
|
160
|
+
await addMemberByManager(this.db, {
|
|
161
|
+
managerKeyPair: this.admission.managerKeyPair,
|
|
157
162
|
memberKey,
|
|
158
163
|
});
|
|
159
164
|
}
|