@serfab/cadre-core 1.15.0 → 1.17.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 +1 -1
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +55 -138
- package/dist/cadre-invite-protocol.js +94 -449
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +530 -247
- package/dist/cadre-node.js +1046 -525
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +22 -23
- package/dist/control-cohort.js +24 -24
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +416 -136
- package/dist/control-database.js +757 -378
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +5 -4
- package/dist/control-protocol-guard.js +6 -3
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +80 -0
- package/dist/control-schema-version.js +203 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +254 -70
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +5 -5
- package/dist/control-write-retry.js +10 -8
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +26 -7
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +3 -3
- package/dist/joined-strand-store.js +3 -3
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +250 -0
- package/dist/member-redemption.js +485 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +247 -0
- package/dist/owner-grant-protocol.js +330 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +131 -109
- package/dist/seed-bootstrap.js +260 -248
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +1 -0
- package/dist/strand-database.js +3 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +4 -3
- package/dist/strand-first-sync-gate.js +4 -3
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +1 -1
- package/dist/strand-membership-writer.d.ts +13 -2
- package/dist/strand-membership-writer.js +52 -16
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +31 -30
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/types.d.ts +286 -103
- package/dist/types.js.map +1 -1
- package/package.json +16 -7
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +131 -531
- package/src/cadre-join-request.ts +188 -167
- package/src/cadre-node.ts +1166 -541
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +127 -128
- package/src/control-database.ts +940 -456
- package/src/control-protocol-guard.ts +7 -4
- package/src/control-schema-version.ts +251 -0
- package/src/control-schema.ts +265 -70
- package/src/control-write-retry.ts +569 -567
- package/src/delegate-admission.ts +37 -0
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/index.ts +102 -5
- package/src/joined-strand-store.ts +4 -4
- package/src/key-store.ts +10 -4
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +642 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +532 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-authorization.ts +558 -488
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +301 -312
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +3 -0
- package/src/strand-first-sync-gate.ts +4 -3
- package/src/strand-instance-manager.ts +1 -1
- package/src/strand-membership-writer.ts +61 -16
- package/src/strand-move.ts +45 -36
- package/src/strand-peer-redial.ts +2 -3
- package/src/types.ts +304 -103
package/src/seed-bootstrap.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import debug from 'debug';
|
|
2
2
|
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
3
|
-
import { digest,
|
|
3
|
+
import { digest, verify } from '@optimystic/quereus-plugin-crypto';
|
|
4
4
|
import type { Libp2p, Connection, PeerId, Stream } from '@libp2p/interface';
|
|
5
5
|
import { multiaddr, type Multiaddr } from '@multiformats/multiaddr';
|
|
6
6
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
@@ -26,14 +26,16 @@ import type {
|
|
|
26
26
|
AddDroneOptions,
|
|
27
27
|
DroneInitResult,
|
|
28
28
|
CadreInviteRow,
|
|
29
|
+
OwnerGrantRow,
|
|
29
30
|
NodeProfile,
|
|
30
31
|
PeerAddressRecord,
|
|
31
32
|
DeviceTokenRecord,
|
|
32
33
|
RevocationRow,
|
|
33
|
-
RevocationLedgerOpenResult
|
|
34
|
+
RevocationLedgerOpenResult,
|
|
35
|
+
MachineRemovalResult
|
|
34
36
|
} from './types.js';
|
|
35
|
-
import type { ControlDatabase
|
|
36
|
-
import { generateStampId } from './control-database.js';
|
|
37
|
+
import type { ControlDatabase } from './control-database.js';
|
|
38
|
+
import { generateStampId, ownerKeyLabel } from './control-database.js';
|
|
37
39
|
import { canonicalJson } from './canonical-json.js';
|
|
38
40
|
import { deviceTokenAddDigest } from './peer-authorization.js';
|
|
39
41
|
import { ed25519PublicKeyB64FromPeerId } from './ed25519-key.js';
|
|
@@ -43,6 +45,8 @@ import {
|
|
|
43
45
|
anchoredTrustPolicy,
|
|
44
46
|
} from './seed-trust-policy.js';
|
|
45
47
|
import type { TrustedOwnerStore } from './trusted-owner-store.js';
|
|
48
|
+
import { withinOwnerApproval, type OwnerSign, type OwnerSigner } from './owner-signer.js';
|
|
49
|
+
import { namedOwnerKey } from './owner-keys.js';
|
|
46
50
|
|
|
47
51
|
const log = debug('sereus:cadre:seed-bootstrap');
|
|
48
52
|
|
|
@@ -113,8 +117,16 @@ export function seedDigest(seed: Pick<ControlNetworkSeed, 'partyId' | 'peers'>):
|
|
|
113
117
|
return digest([canonicalSeedPayload(seed)], 'sha256', 'base64url') as string;
|
|
114
118
|
}
|
|
115
119
|
|
|
120
|
+
/** Add `relayAddr` to the entry for `selfPeerId` in `peers`, when that entry exists and lacks it. */
|
|
121
|
+
function addSelfRelayAddr(peers: SeedPeer[], selfPeerId: string, relayAddr: string): void {
|
|
122
|
+
const self = peers.find((peer) => peer.peerId === selfPeerId);
|
|
123
|
+
if (self && !self.multiaddrs.includes(relayAddr)) {
|
|
124
|
+
self.multiaddrs.push(relayAddr);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
/**
|
|
117
|
-
* An {@link ApplySeedResult} for a seed refused before the
|
|
129
|
+
* An {@link ApplySeedResult} for a seed refused before the dial loop ran,
|
|
118
130
|
* so the dial counters read zero rather than being absent.
|
|
119
131
|
*/
|
|
120
132
|
function seedRejected(error: string, code?: SeedRefusalCode): ApplySeedResult {
|
|
@@ -123,8 +135,8 @@ function seedRejected(error: string, code?: SeedRefusalCode): ApplySeedResult {
|
|
|
123
135
|
peersAdded: 0,
|
|
124
136
|
error,
|
|
125
137
|
...(code !== undefined ? { code } : {}),
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
preferredDialsAttempted: 0,
|
|
139
|
+
preferredDialsFailed: 0,
|
|
128
140
|
};
|
|
129
141
|
}
|
|
130
142
|
|
|
@@ -138,8 +150,8 @@ interface SeedApplyOptions {
|
|
|
138
150
|
remotePeerId?: string;
|
|
139
151
|
}
|
|
140
152
|
|
|
141
|
-
/** What dialing a seed's
|
|
142
|
-
type
|
|
153
|
+
/** What dialing a seed's preferred peers produced — the dial half of an {@link ApplySeedResult}. */
|
|
154
|
+
type PreferredDialCounts = Pick<ApplySeedResult, 'preferredDialsAttempted' | 'preferredDialsFailed'>;
|
|
143
155
|
|
|
144
156
|
/**
|
|
145
157
|
* Parse each address, dropping (and logging) any that is malformed, so one bad
|
|
@@ -209,10 +221,12 @@ function deliveryDialAddrs(peerId: string, addrs: readonly string[]): Multiaddr[
|
|
|
209
221
|
export interface SeedBootstrapConfig {
|
|
210
222
|
/** Party ID for this cadre */
|
|
211
223
|
partyId: string;
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
/**
|
|
225
|
+
* The owner signer every seed and owner-signed write of this service goes through
|
|
226
|
+
* (`owner-signer.ts`). Unset for a listener-only service, which receives and applies seeds
|
|
227
|
+
* but refuses every owner write.
|
|
228
|
+
*/
|
|
229
|
+
ownerSigner?: OwnerSigner;
|
|
216
230
|
/**
|
|
217
231
|
* Trust anchor for incoming seeds. Decides whether a signature-verified
|
|
218
232
|
* `signerKey` should be trusted, against the receiver's anchored owner
|
|
@@ -269,7 +283,7 @@ export interface SeedBootstrapConfig {
|
|
|
269
283
|
* sized for the forms that dial inside it. Delivery does not set `runOnLimitedConnection`,
|
|
270
284
|
* so it does not use a limited relayed connection today; the relayed-dial count is the upper
|
|
271
285
|
* bound on the dial it can use, the same choice `CadreNode.controlDialBudget` makes for every
|
|
272
|
-
* address. It holds only link work because the receiver acks before
|
|
286
|
+
* address. It holds only link work because the receiver acks before it dials the seed's peers.
|
|
273
287
|
*
|
|
274
288
|
* NOTE: no transfer allowance — a seed is a peer list of a few KB, and `MAX_SEED_SIZE` (1 MiB)
|
|
275
289
|
* is a defensive cap. If seeds ever grow toward that cap, add an allowance the way
|
|
@@ -278,7 +292,7 @@ export interface SeedBootstrapConfig {
|
|
|
278
292
|
seedDeliverTimeoutMs?: number;
|
|
279
293
|
/**
|
|
280
294
|
* Time limits for each peer this service dials from a list of addresses —
|
|
281
|
-
* {@link SeedBootstrapService.applySeed}'s
|
|
295
|
+
* {@link SeedBootstrapService.applySeed}'s dials of the seed's preferred peers and
|
|
282
296
|
* {@link SeedBootstrapService.deliverSeed}'s dial of a peer id with addresses — per
|
|
283
297
|
* address and per peer (see `peer-dial.ts`). Defaults to {@link DEFAULT_PEER_DIAL_BUDGET}; a `CadreNode`
|
|
284
298
|
* passes its `network.controlCohort` limits.
|
|
@@ -298,7 +312,7 @@ export interface SeedEventCallbacks {
|
|
|
298
312
|
* `seed` is the applied seed itself: the inbound protocol handler applies it
|
|
299
313
|
* INSIDE the service, so this callback is the only seam through which a
|
|
300
314
|
* `CadreNode` sees a wire-delivered seed's contents — which it needs to
|
|
301
|
-
* retain the
|
|
315
|
+
* retain the preferred peers as cold-start bootstrap dial targets.
|
|
302
316
|
*/
|
|
303
317
|
onSeedApplied?: (partyId: string, peersAdded: number, seed: ControlNetworkSeed) => void;
|
|
304
318
|
/** Called when seed application fails */
|
|
@@ -316,7 +330,6 @@ export class SeedBootstrapService {
|
|
|
316
330
|
private readonly config: SeedBootstrapConfig;
|
|
317
331
|
private libp2pNode: Libp2p | null = null;
|
|
318
332
|
private controlDatabase: ControlDatabase | null = null;
|
|
319
|
-
private readonly ownerPublicKey: string | null;
|
|
320
333
|
private readonly trustPolicy: SeedTrustPolicy;
|
|
321
334
|
private readonly seedReadTimeoutMs: number;
|
|
322
335
|
private readonly maxConcurrentSeeds: number;
|
|
@@ -333,19 +346,6 @@ export class SeedBootstrapService {
|
|
|
333
346
|
this.maxConcurrentSeeds = config.maxConcurrentSeeds ?? DEFAULT_MAX_CONCURRENT_SEEDS;
|
|
334
347
|
this.seedDeliverTimeoutMs = config.seedDeliverTimeoutMs ?? relayedRequestBudgetMs(config.linkRoundTripMs);
|
|
335
348
|
this.dialBudget = config.dialBudget ?? DEFAULT_PEER_DIAL_BUDGET;
|
|
336
|
-
|
|
337
|
-
// Derive public key from private key if not provided
|
|
338
|
-
if (config.ownerPrivateKey && !config.ownerPublicKey) {
|
|
339
|
-
this.ownerPublicKey = getPublicKey(
|
|
340
|
-
config.ownerPrivateKey,
|
|
341
|
-
'ed25519',
|
|
342
|
-
'base64url',
|
|
343
|
-
'base64url'
|
|
344
|
-
) as string;
|
|
345
|
-
} else {
|
|
346
|
-
this.ownerPublicKey = config.ownerPublicKey ?? null;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
349
|
log('SeedBootstrapService created for party: %s', config.partyId);
|
|
350
350
|
}
|
|
351
351
|
|
|
@@ -358,15 +358,19 @@ export class SeedBootstrapService {
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
/**
|
|
361
|
-
* Whether this service holds an owner
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
* drain to skip owner work on a non-owner node.
|
|
361
|
+
* Whether this service holds an owner signer, i.e. can produce the owner signatures that
|
|
362
|
+
* gate `CadrePeer` / `DeviceToken` inserts, deletes, and re-authorizations. A
|
|
363
|
+
* seed-listener-only service (`enableSeedListener`, no signer) returns false: it can
|
|
364
|
+
* receive/apply seeds but cannot author or re-issue owner writes. Used by the
|
|
365
|
+
* write-while-alone re-replication drain to skip owner work on a non-owner node.
|
|
367
366
|
*/
|
|
368
367
|
canAuthorize(): boolean {
|
|
369
|
-
return
|
|
368
|
+
return this.config.ownerSigner !== undefined;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The owner signer this service was built with, or null for a listener-only service. */
|
|
372
|
+
get ownerSigner(): OwnerSigner | null {
|
|
373
|
+
return this.config.ownerSigner ?? null;
|
|
370
374
|
}
|
|
371
375
|
|
|
372
376
|
/**
|
|
@@ -374,7 +378,7 @@ export class SeedBootstrapService {
|
|
|
374
378
|
*
|
|
375
379
|
* `registerHandler` (default true) gates registration of the shared inbound
|
|
376
380
|
* `/sereus/seed/1.0.0` handler on `libp2pNode`. Persistent services
|
|
377
|
-
* (`
|
|
381
|
+
* (`CadreNode.useOwnerSigner`, `enableSeedListener`) own that handler and leave
|
|
378
382
|
* it on. The throwaway temp service CadreNode builds in `applySeed` passes
|
|
379
383
|
* `false`: it only needs the stored `libp2pNode` / `controlDatabase` for
|
|
380
384
|
* dialing and known-key lookup, and must NOT bind a discarded closure to the
|
|
@@ -412,8 +416,11 @@ export class SeedBootstrapService {
|
|
|
412
416
|
* `UpdatedAt` but no self-signature (`Sig` null) — the owner cannot produce
|
|
413
417
|
* the peer's self-signature, so the peer must self-publish (see
|
|
414
418
|
* {@link CadreNode.registerSelf}) before the row resolves.
|
|
419
|
+
*
|
|
420
|
+
* @param sign - The signature function of an owner approval the caller already holds, so a
|
|
421
|
+
* multi-step action asks once; omitted, this write asks for its own approval.
|
|
415
422
|
*/
|
|
416
|
-
async authorizePeer(options: AuthorizePeerOptions): Promise<void> {
|
|
423
|
+
async authorizePeer(options: AuthorizePeerOptions, sign?: OwnerSign): Promise<void> {
|
|
417
424
|
const { peerId, multiaddrs } = options;
|
|
418
425
|
log('Authorizing peer: %s', peerId);
|
|
419
426
|
// CadrePeer.Multiaddr stores a comma-joined list; use '' when no addrs provided.
|
|
@@ -425,7 +432,7 @@ export class SeedBootstrapService {
|
|
|
425
432
|
updatedAt: Date.now(),
|
|
426
433
|
sig: null,
|
|
427
434
|
profile: null,
|
|
428
|
-
});
|
|
435
|
+
}, `Add the machine ${peerId} to the cadre`, sign);
|
|
429
436
|
log('Peer %s authorized successfully', peerId);
|
|
430
437
|
}
|
|
431
438
|
|
|
@@ -442,7 +449,7 @@ export class SeedBootstrapService {
|
|
|
442
449
|
* in which case the row in the database is NOT this record and carries whatever
|
|
443
450
|
* `Sig` that writer had, so the caller must self-update to publish its signature.
|
|
444
451
|
*/
|
|
445
|
-
async insertSelfPeerRecord(record: PeerAddressRecord): Promise<boolean> {
|
|
452
|
+
async insertSelfPeerRecord(record: PeerAddressRecord, sign?: OwnerSign): Promise<boolean> {
|
|
446
453
|
return await this.insertCadrePeerRow({
|
|
447
454
|
peerId: record.peerId,
|
|
448
455
|
publicKey: record.publicKey,
|
|
@@ -450,19 +457,18 @@ export class SeedBootstrapService {
|
|
|
450
457
|
updatedAt: record.updatedAt,
|
|
451
458
|
sig: record.sig,
|
|
452
459
|
profile: record.profile,
|
|
453
|
-
});
|
|
460
|
+
}, 'Publish this machine\'s own address record', sign);
|
|
454
461
|
}
|
|
455
462
|
|
|
456
463
|
/**
|
|
457
|
-
* Shared owner-signed `CadrePeer` INSERT — the row fields and the owner-
|
|
464
|
+
* Shared owner-signed `CadrePeer` INSERT — the row fields and the owner-signer
|
|
458
465
|
* precondition; the stamp mint, voucher signature, write lock, existence check and
|
|
459
466
|
* membership notify are all {@link ControlDatabase.insertCadrePeer}'s (see there for
|
|
460
467
|
* the anti-replay and insert-race rationale). No `CadrePeer` write may bypass that
|
|
461
468
|
* method: the write is what admits the peer's traffic.
|
|
462
469
|
*
|
|
463
|
-
* The DB check precedes the owner-
|
|
464
|
-
* always surfaced them
|
|
465
|
-
* DB check).
|
|
470
|
+
* The DB check precedes the owner-signer check here, matching the order this method has
|
|
471
|
+
* always surfaced them.
|
|
466
472
|
*
|
|
467
473
|
* @returns `true` when this call performed the INSERT, `false` when the row was
|
|
468
474
|
* already seated by a concurrent writer. The loser needs to know: an authorize seats
|
|
@@ -476,14 +482,11 @@ export class SeedBootstrapService {
|
|
|
476
482
|
updatedAt: number;
|
|
477
483
|
sig: string | null;
|
|
478
484
|
profile: NodeProfile | null;
|
|
479
|
-
}): Promise<boolean> {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return await this.controlDatabase.insertCadrePeer(
|
|
485
|
-
row, ownerKey, message => this.signMessageBytes(message),
|
|
486
|
-
);
|
|
485
|
+
}, action: string, sign: OwnerSign | undefined): Promise<boolean> {
|
|
486
|
+
const controlDatabase = this.requireControlDatabase();
|
|
487
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
488
|
+
return await this.withOwnerSign(action, sign, (approved) =>
|
|
489
|
+
controlDatabase.insertCadrePeer(row, ownerKey, approved));
|
|
487
490
|
}
|
|
488
491
|
|
|
489
492
|
/**
|
|
@@ -503,17 +506,17 @@ export class SeedBootstrapService {
|
|
|
503
506
|
* fresh signature — unaffected by the cleared row's retired stamp
|
|
504
507
|
* (`DeviceToken.NotRevoked`).
|
|
505
508
|
*/
|
|
506
|
-
async insertSelfDeviceToken(record: DeviceTokenRecord): Promise<void> {
|
|
509
|
+
async insertSelfDeviceToken(record: DeviceTokenRecord, sign?: OwnerSign): Promise<void> {
|
|
510
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
511
|
+
const controlDatabase = this.requireControlDatabase();
|
|
507
512
|
const stampId = generateStampId(record.peerId);
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
await this.controlDatabase.execWrite(`
|
|
513
|
+
const digestBytes = uint8ArrayFromString(deviceTokenAddDigest(this.config.partyId, { ...record, stampId }), 'base64url');
|
|
514
|
+
const signature = await this.withOwnerSign('Publish this machine\'s push token', sign, (approved) => approved(digestBytes));
|
|
515
|
+
await controlDatabase.execWrite(`
|
|
513
516
|
insert into CadreControl.DeviceToken (PeerId, Platform, Token, UpdatedAt, Sig, StampId)
|
|
514
517
|
with context OwnerKey = ?, Signature = ?
|
|
515
518
|
values (?, ?, ?, ?, ?, ?)
|
|
516
|
-
`, [
|
|
519
|
+
`, [ownerKey, signature, record.peerId, record.platform, record.token, record.updatedAt, record.sig, stampId], 'device-token-insert');
|
|
517
520
|
log('Device token inserted (owner-signed): %s', record.peerId);
|
|
518
521
|
}
|
|
519
522
|
|
|
@@ -535,79 +538,48 @@ export class SeedBootstrapService {
|
|
|
535
538
|
* Both digests, the stamp read, and the transaction come from
|
|
536
539
|
* {@link ControlDatabase.deleteDeviceToken} — one shared implementation across
|
|
537
540
|
* `CadrePeer` / `DeviceToken` / `Strand` / `ValidationKey`. What stays here is the
|
|
538
|
-
* owner-
|
|
541
|
+
* owner-signer precondition; a no-op on an already-absent row is the shared body's
|
|
539
542
|
* behavior, and unlike {@link removePeer} nothing here rides on it.
|
|
540
543
|
*/
|
|
541
|
-
async deleteDeviceToken(peerId: string): Promise<void> {
|
|
542
|
-
// Fail fast on a
|
|
543
|
-
// owner-
|
|
544
|
-
const ownerKey = this.
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
await this.controlDatabase.deleteDeviceToken(
|
|
549
|
-
peerId, ownerKey, message => this.signMessageBytes(message),
|
|
550
|
-
);
|
|
544
|
+
async deleteDeviceToken(peerId: string, sign?: OwnerSign): Promise<void> {
|
|
545
|
+
// Fail fast on a signerless service BEFORE the DB read, so a non-owner gets the
|
|
546
|
+
// owner-signer error rather than a silent no-op on an absent row.
|
|
547
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
548
|
+
const controlDatabase = this.requireControlDatabase();
|
|
549
|
+
await this.withOwnerSign(`Clear the push token of ${peerId}`, sign, (approved) =>
|
|
550
|
+
controlDatabase.deleteDeviceToken(peerId, ownerKey, approved));
|
|
551
551
|
}
|
|
552
552
|
|
|
553
553
|
/**
|
|
554
|
-
* Return the configured owner
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
554
|
+
* Return the configured owner signer, or throw if none is set. The single precondition
|
|
555
|
+
* gate for every owner-signed write. {@link removePeer} / {@link reauthorizePeer} read the
|
|
556
|
+
* row's `StampId` from the DB BEFORE they sign, so they call this up front — otherwise a
|
|
557
|
+
* signerless service would either surface the wrong "Control database not initialized"
|
|
558
|
+
* error or, worse, silently no-op when the target row is absent (the early
|
|
559
|
+
* `stampId === null` return) instead of rejecting.
|
|
560
560
|
*/
|
|
561
|
-
private
|
|
562
|
-
if (!this.config.
|
|
563
|
-
throw new Error('Owner
|
|
561
|
+
private requireOwnerSigner(): OwnerSigner {
|
|
562
|
+
if (!this.config.ownerSigner) {
|
|
563
|
+
throw new Error('Owner signer required to authorize peers');
|
|
564
564
|
}
|
|
565
|
-
return this.config.
|
|
565
|
+
return this.config.ownerSigner;
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
-
/**
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
* constructor derives the public key whenever `ownerPrivateKey` is set — so gating on
|
|
573
|
-
* {@link requireOwnerPrivateKey} first makes the pair inseparable and the second throw
|
|
574
|
-
* unreachable. Callers that must reject a keyless service BEFORE any DB read use this as
|
|
575
|
-
* their first line and get the owner-key precondition for free.
|
|
576
|
-
*/
|
|
577
|
-
private requireOwnerPublicKey(): string {
|
|
578
|
-
this.requireOwnerPrivateKey();
|
|
579
|
-
if (!this.ownerPublicKey) {
|
|
580
|
-
throw new Error('Owner public key required to authorize peers');
|
|
568
|
+
/** The control database {@link initialize} attached, or a throw. */
|
|
569
|
+
private requireControlDatabase(): ControlDatabase {
|
|
570
|
+
if (!this.controlDatabase) {
|
|
571
|
+
throw new Error('Control database not initialized');
|
|
581
572
|
}
|
|
582
|
-
return this.
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/**
|
|
586
|
-
* Sign a base64url digest with the owner key (ed25519). The single place the
|
|
587
|
-
* owner private key is applied; callers pass the canonical domain-tagged digest for
|
|
588
|
-
* the specific action ({@link deviceTokenAddDigest}), or the raw-bytes form via
|
|
589
|
-
* {@link signMessageBytes}. Throws if no owner key is set.
|
|
590
|
-
*/
|
|
591
|
-
private signDigest(digestB64url: string): string {
|
|
592
|
-
return sign(
|
|
593
|
-
digestB64url,
|
|
594
|
-
this.requireOwnerPrivateKey(),
|
|
595
|
-
'ed25519',
|
|
596
|
-
'base64url',
|
|
597
|
-
'base64url',
|
|
598
|
-
'base64url'
|
|
599
|
-
) as string;
|
|
573
|
+
return this.controlDatabase;
|
|
600
574
|
}
|
|
601
575
|
|
|
602
576
|
/**
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* satisfies the same schema CHECK as one from the callers that sign the bytes directly — see
|
|
607
|
-
* `control-revocation-replay.spec.ts`'s "raw-bytes and digest-string signers agree".
|
|
577
|
+
* Run `body` with an owner signature function: the caller's `sign` when it passes one (a
|
|
578
|
+
* `CadreNode` action already inside its approval), otherwise one approval of this service's
|
|
579
|
+
* signer for `action`, the plain-words description a prompting signer shows.
|
|
608
580
|
*/
|
|
609
|
-
private
|
|
610
|
-
return this.
|
|
581
|
+
private async withOwnerSign<T>(action: string, sign: OwnerSign | undefined, body: (sign: OwnerSign) => Promise<T>): Promise<T> {
|
|
582
|
+
return await withinOwnerApproval(this.requireOwnerSigner(), { partyId: this.config.partyId, action }, sign, body);
|
|
611
583
|
}
|
|
612
584
|
|
|
613
585
|
/**
|
|
@@ -632,16 +604,14 @@ export class SeedBootstrapService {
|
|
|
632
604
|
* Both digests, the stamp read, the transaction, and the post-commit membership notify
|
|
633
605
|
* come from {@link ControlDatabase.deleteCadrePeer} — one shared implementation across
|
|
634
606
|
* `CadrePeer` / `DeviceToken` / `Strand` / `ValidationKey`. What stays here is the
|
|
635
|
-
* owner-
|
|
607
|
+
* owner-signer precondition and the absent-row gate the notify depends on (below).
|
|
636
608
|
*/
|
|
637
|
-
async removePeer(peerId: string): Promise<void> {
|
|
638
|
-
// Fail fast on a
|
|
639
|
-
// remove digest, and this precedence (owner
|
|
609
|
+
async removePeer(peerId: string, sign?: OwnerSign): Promise<void> {
|
|
610
|
+
// Fail fast on a signerless service BEFORE any DB read: a non-owner cannot sign the
|
|
611
|
+
// remove digest, and this precedence (owner signer, then control DB) is what the
|
|
640
612
|
// unit contract asserts.
|
|
641
|
-
const ownerKey = this.
|
|
642
|
-
|
|
643
|
-
throw new Error('Control database not initialized');
|
|
644
|
-
}
|
|
613
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
614
|
+
const controlDatabase = this.requireControlDatabase();
|
|
645
615
|
// This absent-row gate must stay OUTSIDE the delete, even though the delete repeats it
|
|
646
616
|
// internally: deleteCadrePeer's membership notify fires whenever its body resolves,
|
|
647
617
|
// with no idea whether the body wrote anything, so delegating an absent peer would
|
|
@@ -649,7 +619,7 @@ export class SeedBootstrapService {
|
|
|
649
619
|
// NOTE: deleteCadrePeer re-reads the StampId, so a peer removed by another writer
|
|
650
620
|
// between the two reads no-ops silently yet still notifies. Narrow concurrent-removal
|
|
651
621
|
// window only — the common "already absent" case is caught here.
|
|
652
|
-
const stampId = await
|
|
622
|
+
const stampId = await controlDatabase.queryCadrePeerStampId(peerId);
|
|
653
623
|
if (stampId === null) {
|
|
654
624
|
log('removePeer: no CadrePeer row for %s (already absent)', peerId);
|
|
655
625
|
return;
|
|
@@ -657,83 +627,73 @@ export class SeedBootstrapService {
|
|
|
657
627
|
|
|
658
628
|
log('Removing peer: %s', peerId);
|
|
659
629
|
|
|
660
|
-
await this.
|
|
661
|
-
peerId, ownerKey,
|
|
662
|
-
);
|
|
630
|
+
await this.withOwnerSign(`Remove the machine ${peerId} from the cadre`, sign, (approved) =>
|
|
631
|
+
controlDatabase.deleteCadrePeer(peerId, ownerKey, approved));
|
|
663
632
|
|
|
664
633
|
log('Peer %s removed successfully (stamp retired)', peerId);
|
|
665
634
|
}
|
|
666
635
|
|
|
667
636
|
/**
|
|
668
|
-
* Seat `ownerPublicKey` as a further owner, signed by this service's owner
|
|
637
|
+
* Seat `ownerPublicKey` as a further owner, signed by this service's owner signer. The
|
|
669
638
|
* liveness check, the stamp, the signature and the membership notify are
|
|
670
|
-
* {@link ControlDatabase.insertOwnerKeyVouched}'s; what stays here is the owner-
|
|
639
|
+
* {@link ControlDatabase.insertOwnerKeyVouched}'s; what stays here is the owner-signer
|
|
671
640
|
* precondition, checked before any read (see {@link removePeer}).
|
|
672
641
|
*
|
|
642
|
+
* @param label - the user's name for the key, bound into the signature
|
|
643
|
+
* ({@link ControlDatabase.insertOwnerKey} has the rule).
|
|
673
644
|
* @returns `true` when the row was seated, `false` when the key is already a live owner.
|
|
674
645
|
*/
|
|
675
|
-
async addOwner(ownerPublicKey: string): Promise<boolean> {
|
|
676
|
-
const ownerKey = this.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return await this.
|
|
681
|
-
ownerPublicKey, ownerKey,
|
|
682
|
-
);
|
|
646
|
+
async addOwner(ownerPublicKey: string, label?: string, sign?: OwnerSign): Promise<boolean> {
|
|
647
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
648
|
+
const controlDatabase = this.requireControlDatabase();
|
|
649
|
+
// Checked before the approval opens, so a label the row would refuse never prompts the user.
|
|
650
|
+
const storedLabel = ownerKeyLabel(label);
|
|
651
|
+
return await this.withOwnerSign(`Add owner key ${namedOwnerKey(ownerPublicKey, storedLabel)}`, sign, (approved) =>
|
|
652
|
+
controlDatabase.insertOwnerKeyVouched(ownerPublicKey, ownerKey, approved, storedLabel));
|
|
683
653
|
}
|
|
684
654
|
|
|
685
655
|
/**
|
|
686
|
-
* Remove `ownerPublicKey` from the owners, signed by this service's owner
|
|
656
|
+
* Remove `ownerPublicKey` from the owners, signed by this service's owner signer: the delete
|
|
687
657
|
* and its `Revocation` tombstone in one transaction. The by-name refusals (self-removal,
|
|
688
658
|
* the last owner), the digests and the membership notify are
|
|
689
|
-
* {@link ControlDatabase.deleteOwnerKey}'s; what stays here is the owner-
|
|
659
|
+
* {@link ControlDatabase.deleteOwnerKey}'s; what stays here is the owner-signer precondition,
|
|
690
660
|
* checked before any read (see {@link removePeer}).
|
|
691
661
|
*
|
|
692
662
|
* @returns `true` when the row was removed, `false` when the key is not a live owner here.
|
|
693
663
|
*/
|
|
694
|
-
async removeOwner(ownerPublicKey: string): Promise<boolean> {
|
|
695
|
-
const ownerKey = this.
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
return await this.controlDatabase.deleteOwnerKey(
|
|
700
|
-
ownerPublicKey, ownerKey, message => this.signMessageBytes(message),
|
|
701
|
-
);
|
|
664
|
+
async removeOwner(ownerPublicKey: string, sign?: OwnerSign): Promise<boolean> {
|
|
665
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
666
|
+
const controlDatabase = this.requireControlDatabase();
|
|
667
|
+
return await this.withOwnerSign(`Remove owner key ${ownerPublicKey}`, sign, (approved) =>
|
|
668
|
+
controlDatabase.deleteOwnerKey(ownerPublicKey, ownerKey, approved));
|
|
702
669
|
}
|
|
703
670
|
|
|
704
671
|
/**
|
|
705
|
-
* Re-vouch another owner's row under this service's owner
|
|
672
|
+
* Re-vouch another owner's row under this service's owner signer, so it stands on a remaining
|
|
706
673
|
* owner's signature before the owner that added it is removed
|
|
707
|
-
* ({@link ControlDatabase.revouchOwnerKey}; for `CadreNode.
|
|
708
|
-
* here is the owner-
|
|
674
|
+
* ({@link ControlDatabase.revouchOwnerKey}; for `CadreNode.removeOwner` only). What stays
|
|
675
|
+
* here is the owner-signer precondition, checked before any read (see {@link removePeer}).
|
|
709
676
|
*
|
|
710
677
|
* @returns `true` when the row was re-vouched, `false` when the key is not a live owner here.
|
|
711
678
|
*/
|
|
712
|
-
async revouchOwner(ownerPublicKey: string): Promise<boolean> {
|
|
713
|
-
const ownerKey = this.
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
return await this.controlDatabase.revouchOwnerKey(
|
|
718
|
-
ownerPublicKey, ownerKey, message => this.signMessageBytes(message),
|
|
719
|
-
);
|
|
679
|
+
async revouchOwner(ownerPublicKey: string, sign?: OwnerSign): Promise<boolean> {
|
|
680
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
681
|
+
const controlDatabase = this.requireControlDatabase();
|
|
682
|
+
return await this.withOwnerSign(`Re-vouch owner key ${ownerPublicKey}`, sign, (approved) =>
|
|
683
|
+
controlDatabase.revouchOwnerKey(ownerPublicKey, ownerKey, approved));
|
|
720
684
|
}
|
|
721
685
|
|
|
722
686
|
/**
|
|
723
|
-
* Remove a machine completely, signed by this service's owner
|
|
724
|
-
* `
|
|
725
|
-
*
|
|
726
|
-
* ({@link
|
|
727
|
-
* checked before any read (see {@link removePeer}).
|
|
687
|
+
* Remove a machine completely, signed by this service's owner signer: its `CadrePeer` row, its
|
|
688
|
+
* `DeviceToken` row, and the withdrawal of every invitation in `target.inviteKeys`, all in one
|
|
689
|
+
* transaction ({@link ControlDatabase.removeMachine}). What stays here is the owner-signer
|
|
690
|
+
* precondition, checked before any read (see {@link removePeer}).
|
|
728
691
|
*/
|
|
729
|
-
async removeMachine(target: Parameters<ControlDatabase['removeMachine']>[0]): Promise<
|
|
730
|
-
const ownerKey = this.
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
return await this.controlDatabase.removeMachine(
|
|
735
|
-
target, ownerKey, message => this.signMessageBytes(message),
|
|
736
|
-
);
|
|
692
|
+
async removeMachine(target: Parameters<ControlDatabase['removeMachine']>[0], sign?: OwnerSign): Promise<MachineRemovalResult> {
|
|
693
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
694
|
+
const controlDatabase = this.requireControlDatabase();
|
|
695
|
+
return await this.withOwnerSign(`Remove the machine ${target.peerId} from the cadre`, sign, (approved) =>
|
|
696
|
+
controlDatabase.removeMachine(target, ownerKey, approved));
|
|
737
697
|
}
|
|
738
698
|
|
|
739
699
|
/**
|
|
@@ -745,25 +705,22 @@ export class SeedBootstrapService {
|
|
|
745
705
|
*
|
|
746
706
|
* The stamp read, the voucher signature, the write lock and the membership notify are
|
|
747
707
|
* {@link ControlDatabase.reauthorizeCadrePeer}'s — including the caveat that it rebinds
|
|
748
|
-
* `VouchOwner` to this node's owner key. What stays here is the owner-
|
|
708
|
+
* `VouchOwner` to this node's owner key. What stays here is the owner-signer precondition.
|
|
749
709
|
*
|
|
750
710
|
* A no-op (no throw, no notify) when the row does not exist.
|
|
751
711
|
*
|
|
752
712
|
* @param peerId - the membership row to re-touch.
|
|
753
713
|
* @param updatedAt - the strictly-increasing freshness stamp to write.
|
|
754
|
-
* @throws if no owner
|
|
714
|
+
* @throws if no owner signer is configured (a non-owner cannot
|
|
755
715
|
* re-sign another peer's row) or the control database is not initialized.
|
|
756
716
|
*/
|
|
757
|
-
async reauthorizePeer(peerId: string, updatedAt: number): Promise<void> {
|
|
758
|
-
// Fail fast on a
|
|
717
|
+
async reauthorizePeer(peerId: string, updatedAt: number, sign?: OwnerSign): Promise<void> {
|
|
718
|
+
// Fail fast on a signerless service before any DB read (see removePeer): a non-owner
|
|
759
719
|
// cannot re-sign the voucher, and must not silently no-op on an absent row.
|
|
760
|
-
const ownerKey = this.
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
const retouched = await this.controlDatabase.reauthorizeCadrePeer(
|
|
765
|
-
peerId, updatedAt, ownerKey, message => this.signMessageBytes(message),
|
|
766
|
-
);
|
|
720
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
721
|
+
const controlDatabase = this.requireControlDatabase();
|
|
722
|
+
const retouched = await this.withOwnerSign(`Re-issue the membership of ${peerId}`, sign, (approved) =>
|
|
723
|
+
controlDatabase.reauthorizeCadrePeer(peerId, updatedAt, ownerKey, approved));
|
|
767
724
|
if (!retouched) {
|
|
768
725
|
log('reauthorizePeer: no CadrePeer row for %s (nothing to re-touch)', peerId);
|
|
769
726
|
return;
|
|
@@ -781,22 +738,19 @@ export class SeedBootstrapService {
|
|
|
781
738
|
*
|
|
782
739
|
* The signatures, the single transaction, and the strictly-monotonic
|
|
783
740
|
* `reissuedAt` contract are {@link ControlDatabase.reissueRevocations}'s. What
|
|
784
|
-
* stays here is the owner-
|
|
741
|
+
* stays here is the owner-signer precondition.
|
|
785
742
|
*
|
|
786
743
|
* @returns how many tombstones were re-issued (`rows.length` on success).
|
|
787
|
-
* @throws if no owner
|
|
744
|
+
* @throws if no owner signer is configured or the control database is not
|
|
788
745
|
* initialized.
|
|
789
746
|
*/
|
|
790
|
-
async reissueRevocations(rows: readonly RevocationRow[], reissuedAt: number): Promise<number> {
|
|
791
|
-
// Fail fast on a
|
|
747
|
+
async reissueRevocations(rows: readonly RevocationRow[], reissuedAt: number, sign?: OwnerSign): Promise<number> {
|
|
748
|
+
// Fail fast on a signerless service before any DB work (see removePeer): a
|
|
792
749
|
// non-owner cannot sign the reissue digests.
|
|
793
|
-
const ownerKey = this.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
return this.controlDatabase.reissueRevocations(
|
|
798
|
-
rows, reissuedAt, ownerKey, message => this.signMessageBytes(message),
|
|
799
|
-
);
|
|
750
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
751
|
+
const controlDatabase = this.requireControlDatabase();
|
|
752
|
+
return await this.withOwnerSign(`Re-send ${rows.length} removal record(s) to the cadre`, sign, (approved) =>
|
|
753
|
+
controlDatabase.reissueRevocations(rows, reissuedAt, ownerKey, approved));
|
|
800
754
|
}
|
|
801
755
|
|
|
802
756
|
/**
|
|
@@ -804,100 +758,140 @@ export class SeedBootstrapService {
|
|
|
804
758
|
* never-written block that the storage layer re-consults on every read.
|
|
805
759
|
*
|
|
806
760
|
* The signature, the insert-if-absent guard and the `'already-open'` mapping are
|
|
807
|
-
* {@link ControlDatabase.openRevocationLedger}'s. What stays here is the owner-
|
|
761
|
+
* {@link ControlDatabase.openRevocationLedger}'s. What stays here is the owner-signer
|
|
808
762
|
* precondition.
|
|
809
763
|
*
|
|
810
|
-
* @throws if no owner
|
|
764
|
+
* @throws if no owner signer is configured or the control database is not
|
|
811
765
|
* initialized.
|
|
812
766
|
*/
|
|
813
|
-
async openRevocationLedger(): Promise<RevocationLedgerOpenResult> {
|
|
814
|
-
// Fail fast on a
|
|
767
|
+
async openRevocationLedger(sign?: OwnerSign): Promise<RevocationLedgerOpenResult> {
|
|
768
|
+
// Fail fast on a signerless service before any DB work (see removePeer): a
|
|
815
769
|
// non-owner cannot sign the marker's append digest.
|
|
816
|
-
const ownerKey = this.
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
return this.controlDatabase.openRevocationLedger(
|
|
821
|
-
ownerKey, message => this.signMessageBytes(message),
|
|
822
|
-
);
|
|
770
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
771
|
+
const controlDatabase = this.requireControlDatabase();
|
|
772
|
+
return await this.withOwnerSign('Open the cadre\'s removal ledger', sign, (approved) =>
|
|
773
|
+
controlDatabase.openRevocationLedger(ownerKey, approved));
|
|
823
774
|
}
|
|
824
775
|
|
|
825
776
|
/**
|
|
826
777
|
* Owner-signed INSERT of a `CadreInvite` row — an invitation any member machine can redeem
|
|
827
778
|
* on this owner's behalf ({@link ControlDatabase.insertCadreInvite}). The keypair mint, the
|
|
828
779
|
* expiry and use-count defaults, and the bundle the holder receives are
|
|
829
|
-
* `CadreNode.createCadreInvitation`'s; what stays here is the owner-
|
|
830
|
-
* the
|
|
780
|
+
* `CadreNode.createCadreInvitation`'s; what stays here is the owner-signer precondition and
|
|
781
|
+
* the approval, as {@link insertSelfDeviceToken} keeps them for device tokens.
|
|
831
782
|
*
|
|
832
783
|
* @returns the row as stored, which the invitation bundle carries verbatim.
|
|
833
|
-
* @throws if no owner
|
|
784
|
+
* @throws if no owner signer is configured or the control database is not initialized.
|
|
834
785
|
*/
|
|
835
|
-
async insertCadreInvite(invite: Parameters<ControlDatabase['insertCadreInvite']>[0]): Promise<CadreInviteRow> {
|
|
836
|
-
// Fail fast on a
|
|
837
|
-
const ownerKey = this.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
return await this.controlDatabase.insertCadreInvite(invite, ownerKey, message => this.signMessageBytes(message));
|
|
786
|
+
async insertCadreInvite(invite: Parameters<ControlDatabase['insertCadreInvite']>[0], sign?: OwnerSign): Promise<CadreInviteRow> {
|
|
787
|
+
// Fail fast on a signerless service before any DB work (see removePeer).
|
|
788
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
789
|
+
const controlDatabase = this.requireControlDatabase();
|
|
790
|
+
return await this.withOwnerSign('Create a machine invitation to the cadre', sign, (approved) =>
|
|
791
|
+
controlDatabase.insertCadreInvite(invite, ownerKey, approved));
|
|
842
792
|
}
|
|
843
793
|
|
|
844
794
|
/**
|
|
845
795
|
* Owner-signed WITHDRAWAL of a `CadreInvite`: the `Revocation` tombstone over its stamp, with
|
|
846
796
|
* the row kept ({@link ControlDatabase.withdrawCadreInvite}). What stays here is the
|
|
847
|
-
* owner-
|
|
797
|
+
* owner-signer precondition.
|
|
848
798
|
*
|
|
849
799
|
* @returns `true` when this call filed the tombstone, `false` when the row is absent here or
|
|
850
800
|
* already withdrawn.
|
|
851
|
-
* @throws if no owner
|
|
801
|
+
* @throws if no owner signer is configured or the control database is not initialized.
|
|
852
802
|
*/
|
|
853
|
-
async withdrawCadreInvite(key: string): Promise<boolean> {
|
|
854
|
-
const ownerKey = this.
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
803
|
+
async withdrawCadreInvite(key: string, sign?: OwnerSign): Promise<boolean> {
|
|
804
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
805
|
+
const controlDatabase = this.requireControlDatabase();
|
|
806
|
+
return await this.withOwnerSign(`Withdraw the cadre invitation ${key}`, sign, (approved) =>
|
|
807
|
+
controlDatabase.withdrawCadreInvite(key, ownerKey, approved));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Owner-signed INSERT of an `OwnerGrant` row — a grant of ownership any member machine can
|
|
812
|
+
* redeem on this owner's behalf ({@link ControlDatabase.insertOwnerGrant}). The keypair mint,
|
|
813
|
+
* the lifetime default and the bundle the holder receives are `CadreNode.createOwnerGrant`'s;
|
|
814
|
+
* what stays here is the owner-signer precondition and the approval, as for
|
|
815
|
+
* {@link insertCadreInvite}. The label is checked before the approval opens, so one the row
|
|
816
|
+
* would refuse never prompts the user.
|
|
817
|
+
*
|
|
818
|
+
* @returns the row as stored, which the grant bundle carries verbatim.
|
|
819
|
+
* @throws if no owner signer is configured, the control database is not initialized, or the label is too long.
|
|
820
|
+
*/
|
|
821
|
+
async insertOwnerGrant(grant: Parameters<ControlDatabase['insertOwnerGrant']>[0], sign?: OwnerSign): Promise<OwnerGrantRow> {
|
|
822
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
823
|
+
const controlDatabase = this.requireControlDatabase();
|
|
824
|
+
const label = ownerKeyLabel(grant.label);
|
|
825
|
+
const named = label === null ? 'an owner grant' : `the owner grant "${label}"`;
|
|
826
|
+
return await this.withOwnerSign(`Create ${named}: whoever holds it can become an owner of the cadre`, sign, (approved) =>
|
|
827
|
+
controlDatabase.insertOwnerGrant({ ...grant, label }, ownerKey, approved));
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Owner-signed WITHDRAWAL of an `OwnerGrant`: the `Revocation` tombstone over its stamp, with
|
|
832
|
+
* the row kept ({@link ControlDatabase.withdrawOwnerGrant}). What stays here is the
|
|
833
|
+
* owner-signer precondition.
|
|
834
|
+
*
|
|
835
|
+
* @returns `true` when this call filed the tombstone, `false` when the row is absent here or
|
|
836
|
+
* already withdrawn.
|
|
837
|
+
* @throws if no owner signer is configured or the control database is not initialized.
|
|
838
|
+
*/
|
|
839
|
+
async withdrawOwnerGrant(key: string, sign?: OwnerSign): Promise<boolean> {
|
|
840
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
841
|
+
const controlDatabase = this.requireControlDatabase();
|
|
842
|
+
return await this.withOwnerSign(`Withdraw the owner grant ${key}`, sign, (approved) =>
|
|
843
|
+
controlDatabase.withdrawOwnerGrant(key, ownerKey, approved));
|
|
859
844
|
}
|
|
860
845
|
|
|
861
846
|
/**
|
|
862
847
|
* Create a seed from the current control network state.
|
|
863
|
-
* The seed contains peer information and is signed by an owner
|
|
848
|
+
* The seed contains peer information and is signed by an owner; its `signerKey` is the
|
|
849
|
+
* owner signer's key.
|
|
850
|
+
*/
|
|
851
|
+
async createSeed(sign?: OwnerSign): Promise<ControlNetworkSeed> {
|
|
852
|
+
return await this.mintSeed(sign, null);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* {@link createSeed}, with `selfRelayAddr` added to this node's own peer entry before the
|
|
857
|
+
* seed is signed: the signature covers every address ({@link seedDigest}), so nothing may
|
|
858
|
+
* be added after it.
|
|
864
859
|
*/
|
|
865
|
-
async
|
|
866
|
-
|
|
860
|
+
private async mintSeed(sign: OwnerSign | undefined, selfRelayAddr: string | null): Promise<ControlNetworkSeed> {
|
|
861
|
+
const signer = this.config.ownerSigner;
|
|
862
|
+
if (!signer) {
|
|
867
863
|
throw new Error('Owner key required to create seeds');
|
|
868
864
|
}
|
|
869
|
-
|
|
865
|
+
|
|
870
866
|
if (!this.controlDatabase || !this.libp2pNode) {
|
|
871
867
|
throw new Error('Service not initialized');
|
|
872
868
|
}
|
|
873
|
-
|
|
869
|
+
|
|
874
870
|
log('Creating seed for party: %s', this.config.partyId);
|
|
875
|
-
|
|
871
|
+
|
|
876
872
|
// Query all peers from the control database
|
|
877
|
-
const
|
|
878
|
-
|
|
873
|
+
const selfPeerId = this.libp2pNode.peerId.toString();
|
|
874
|
+
const peers = await this.queryPeers(selfPeerId);
|
|
875
|
+
if (selfRelayAddr !== null) {
|
|
876
|
+
addSelfRelayAddr(peers, selfPeerId, selfRelayAddr);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
879
|
// Create the seed data (without signature)
|
|
880
880
|
const seedData = {
|
|
881
881
|
partyId: this.config.partyId,
|
|
882
882
|
peers,
|
|
883
883
|
};
|
|
884
|
-
|
|
884
|
+
|
|
885
885
|
// Sign the seed over its canonical byte representation
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
'ed25519',
|
|
890
|
-
'base64url',
|
|
891
|
-
'base64url',
|
|
892
|
-
'base64url'
|
|
893
|
-
) as string;
|
|
894
|
-
|
|
886
|
+
const digestBytes = uint8ArrayFromString(seedDigest(seedData), 'base64url');
|
|
887
|
+
const signature = await this.withOwnerSign('Mint a seed for a machine joining the cadre', sign, (approved) => approved(digestBytes));
|
|
888
|
+
|
|
895
889
|
const seed: ControlNetworkSeed = {
|
|
896
890
|
...seedData,
|
|
897
891
|
signature,
|
|
898
|
-
signerKey:
|
|
892
|
+
signerKey: signer.publicKey,
|
|
899
893
|
};
|
|
900
|
-
|
|
894
|
+
|
|
901
895
|
log('Created seed with %d peers', peers.length);
|
|
902
896
|
return seed;
|
|
903
897
|
}
|
|
@@ -909,9 +903,8 @@ export class SeedBootstrapService {
|
|
|
909
903
|
* `signerKey` that does NOT come from the seed body: the receiver's
|
|
910
904
|
* node-local {@link SeedBootstrapConfig.trustedOwners} anchor, optionally
|
|
911
905
|
* augmented by pinned keys or TOFU via the configured/overriding
|
|
912
|
-
* `SeedTrustPolicy`. A forged self-
|
|
913
|
-
*
|
|
914
|
-
* by a key a stranger genesis-inserted into the replicated `OwnerKey` table.
|
|
906
|
+
* `SeedTrustPolicy`. A forged self-signed seed does not pass, and neither does
|
|
907
|
+
* one signed by a key a stranger genesis-inserted into the replicated `OwnerKey` table.
|
|
915
908
|
*
|
|
916
909
|
* A key accepted via a pin/TOFU is persisted into the anchor (the policy says
|
|
917
910
|
* so via `SeedTrustDecision.anchorAs`), so the next seed from that owner is
|
|
@@ -930,13 +923,13 @@ export class SeedBootstrapService {
|
|
|
930
923
|
if (!merged.success) {
|
|
931
924
|
return merged;
|
|
932
925
|
}
|
|
933
|
-
return { ...merged, ...await this.
|
|
926
|
+
return { ...merged, ...await this.dialPreferredPeers(seed) };
|
|
934
927
|
}
|
|
935
928
|
|
|
936
929
|
/**
|
|
937
930
|
* The first half of {@link applySeed}: check the signature and the signer's trust, then merge
|
|
938
931
|
* the seed's peer addresses into the peer store. Every rejection happens here, so this result is
|
|
939
|
-
* what the inbound handler acks with; the
|
|
932
|
+
* what the inbound handler acks with; the dials that follow cannot change it.
|
|
940
933
|
*/
|
|
941
934
|
private async verifyAndMergeSeed(
|
|
942
935
|
seed: ControlNetworkSeed,
|
|
@@ -976,7 +969,7 @@ export class SeedBootstrapService {
|
|
|
976
969
|
partyId: seed.partyId,
|
|
977
970
|
signerKey: seed.signerKey,
|
|
978
971
|
knownOwnerKeys,
|
|
979
|
-
// Optional-chained like `
|
|
972
|
+
// Optional-chained like `dialPreferredPeers`: partial libp2p handles (unit-test doubles)
|
|
980
973
|
// omit `peerId`. A real node always has one, and a proof bound to a real peer id can
|
|
981
974
|
// only fail against '' — the safe direction.
|
|
982
975
|
localPeerId: this.libp2pNode.peerId?.toString() ?? '',
|
|
@@ -991,55 +984,56 @@ export class SeedBootstrapService {
|
|
|
991
984
|
|
|
992
985
|
const peersAdded = await mergeSeedPeers(this.libp2pNode, seed.peers);
|
|
993
986
|
log('Merged seed: %d peers added', peersAdded);
|
|
994
|
-
return { success: true, peersAdded,
|
|
987
|
+
return { success: true, peersAdded, preferredDialsAttempted: 0, preferredDialsFailed: 0 };
|
|
995
988
|
}
|
|
996
989
|
|
|
997
990
|
/**
|
|
998
|
-
* The second half of {@link applySeed}: dial the seed's
|
|
991
|
+
* The second half of {@link applySeed}: dial the seed's preferred peers (`SeedPeer.preferred`:
|
|
992
|
+
* the machine that minted it and every always-on machine) to establish connections.
|
|
999
993
|
*
|
|
1000
|
-
* Best-effort and COUNTED:
|
|
994
|
+
* Best-effort and COUNTED: a peer that is momentarily down leaves this node seeded but
|
|
1001
995
|
* unconnected, which the caller can only see if the outcome is reported (see
|
|
1002
|
-
* `ApplySeedResult.
|
|
996
|
+
* `ApplySeedResult.preferredDialsFailed`). Recovery is not this loop's job —
|
|
1003
997
|
* `CadreNode.dialColdStartBootstrap` retries these same addresses on every control-cohort
|
|
1004
998
|
* reconcile pass until the control database has siblings.
|
|
1005
999
|
*
|
|
1006
1000
|
* The inbound handler runs this AFTER it has acked and closed the stream
|
|
1007
|
-
* ({@link handleSeedStream}), because an unreachable
|
|
1001
|
+
* ({@link handleSeedStream}), because an unreachable peer can take `dialBudget.totalMs`.
|
|
1008
1002
|
*/
|
|
1009
|
-
private async
|
|
1003
|
+
private async dialPreferredPeers(seed: ControlNetworkSeed): Promise<PreferredDialCounts> {
|
|
1010
1004
|
const node = this.libp2pNode;
|
|
1011
1005
|
if (!node) {
|
|
1012
|
-
log('Seed service shut down before its
|
|
1013
|
-
return {
|
|
1006
|
+
log('Seed service shut down before its dials; none attempted');
|
|
1007
|
+
return { preferredDialsAttempted: 0, preferredDialsFailed: 0 };
|
|
1014
1008
|
}
|
|
1015
|
-
// `createSeed` projects every non-revoked CadrePeer row, so
|
|
1016
|
-
// a seed minted after it joined finds ITSELF in the
|
|
1017
|
-
//
|
|
1009
|
+
// `createSeed` projects every non-revoked CadrePeer row, so a preferred machine applying
|
|
1010
|
+
// a seed minted after it joined finds ITSELF in the list. Dialing self always throws,
|
|
1011
|
+
// which would report a healthy node as "seeded but stranded".
|
|
1018
1012
|
// Optional-chained: partial libp2p handles (unit-test doubles) omit `peerId`,
|
|
1019
1013
|
// and an undefined self simply matches nothing.
|
|
1020
|
-
// Every one of
|
|
1021
|
-
// (`dialPeerAddrs`), so
|
|
1014
|
+
// Every one of a peer's addresses is a candidate, each on its own time limit
|
|
1015
|
+
// (`dialPeerAddrs`), so a peer whose first address never answers neither
|
|
1022
1016
|
// stalls seed application nor goes undialed at its other addresses. The seed's
|
|
1023
|
-
// addresses carry no peer id; the dial binds each to the
|
|
1017
|
+
// addresses carry no peer id; the dial binds each to the peer's.
|
|
1024
1018
|
const selfPeerId = node.peerId?.toString();
|
|
1025
|
-
let
|
|
1026
|
-
let
|
|
1027
|
-
for (const peer of seed.peers.filter(p => p.
|
|
1019
|
+
let preferredDialsAttempted = 0;
|
|
1020
|
+
let preferredDialsFailed = 0;
|
|
1021
|
+
for (const peer of seed.peers.filter(p => p.preferred)) {
|
|
1028
1022
|
if (peer.multiaddrs.length === 0 || peer.peerId === selfPeerId) {
|
|
1029
1023
|
continue;
|
|
1030
1024
|
}
|
|
1031
|
-
|
|
1025
|
+
preferredDialsAttempted++;
|
|
1032
1026
|
try {
|
|
1033
1027
|
const addrs = parseDialAddrs(peer.multiaddrs);
|
|
1034
1028
|
|
|
1035
|
-
log('Dialing
|
|
1036
|
-
await dialPeerAddrs(node, peer.peerId, addrs, this.dialBudget, `
|
|
1029
|
+
log('Dialing preferred seed peer: %s (%d addr(s))', peer.peerId, addrs.length);
|
|
1030
|
+
await dialPeerAddrs(node, peer.peerId, addrs, this.dialBudget, `Seed dial of ${peer.peerId}`);
|
|
1037
1031
|
} catch (error) {
|
|
1038
|
-
// Counted even when the
|
|
1039
|
-
// not connected to it, and `
|
|
1040
|
-
|
|
1032
|
+
// Counted even when the peer reaches us only through our own relay: this node is still
|
|
1033
|
+
// not connected to it, and `preferredDialsFailed` is how the caller learns that.
|
|
1034
|
+
preferredDialsFailed++;
|
|
1041
1035
|
if (error instanceof SelfRelayOnlyError) {
|
|
1042
|
-
log('
|
|
1036
|
+
log('Seed peer %s is reachable only by relaying through this node; waiting for it to reconnect', peer.peerId);
|
|
1043
1037
|
} else {
|
|
1044
1038
|
log('Failed to dial peer %s: %o', peer.peerId, error);
|
|
1045
1039
|
}
|
|
@@ -1047,8 +1041,8 @@ export class SeedBootstrapService {
|
|
|
1047
1041
|
}
|
|
1048
1042
|
}
|
|
1049
1043
|
|
|
1050
|
-
log('Dialed seed
|
|
1051
|
-
return {
|
|
1044
|
+
log('Dialed preferred seed peers: %d/%d dial(s) failed', preferredDialsFailed, preferredDialsAttempted);
|
|
1045
|
+
return { preferredDialsAttempted, preferredDialsFailed };
|
|
1052
1046
|
}
|
|
1053
1047
|
|
|
1054
1048
|
/**
|
|
@@ -1165,7 +1159,7 @@ export class SeedBootstrapService {
|
|
|
1165
1159
|
}
|
|
1166
1160
|
const peerId = peerIdFromString(target.peerId);
|
|
1167
1161
|
const addrs = deliveryDialAddrs(target.peerId, target.multiaddrs);
|
|
1168
|
-
await mergeSeedPeers(node, [{ peerId: target.peerId, multiaddrs: target.multiaddrs,
|
|
1162
|
+
await mergeSeedPeers(node, [{ peerId: target.peerId, multiaddrs: target.multiaddrs, preferred: false }]);
|
|
1169
1163
|
if (addrs.length === 0) {
|
|
1170
1164
|
return (signal) => node.dialProtocol(peerId, SEED_PROTOCOL, { signal });
|
|
1171
1165
|
}
|
|
@@ -1295,16 +1289,16 @@ export class SeedBootstrapService {
|
|
|
1295
1289
|
}
|
|
1296
1290
|
|
|
1297
1291
|
/**
|
|
1298
|
-
* The peers a seed carries: the control database's own projection
|
|
1292
|
+
* The peers a seed `minterPeerId` mints carries: the control database's own projection
|
|
1299
1293
|
* ({@link ControlDatabase.querySeedPeers}, shared with the cadre invitation redemption
|
|
1300
|
-
* reply), which reads through the revocation filter and
|
|
1301
|
-
*
|
|
1294
|
+
* reply), which reads through the revocation filter and marks the minter and every
|
|
1295
|
+
* always-on machine preferred — a dial hint, not a trust decision.
|
|
1302
1296
|
*/
|
|
1303
|
-
private async queryPeers(): Promise<SeedPeer[]> {
|
|
1297
|
+
private async queryPeers(minterPeerId: string): Promise<SeedPeer[]> {
|
|
1304
1298
|
if (!this.controlDatabase) {
|
|
1305
1299
|
return [];
|
|
1306
1300
|
}
|
|
1307
|
-
return await this.controlDatabase.querySeedPeers();
|
|
1301
|
+
return await this.controlDatabase.querySeedPeers(minterPeerId);
|
|
1308
1302
|
}
|
|
1309
1303
|
|
|
1310
1304
|
/**
|
|
@@ -1323,14 +1317,14 @@ export class SeedBootstrapService {
|
|
|
1323
1317
|
|
|
1324
1318
|
/**
|
|
1325
1319
|
* Read one inbound seed frame, verify and merge it, write the ack and close the
|
|
1326
|
-
* stream, then dial the seed's
|
|
1320
|
+
* stream, then dial the seed's preferred peers.
|
|
1327
1321
|
*
|
|
1328
|
-
* The ack and the close come BEFORE
|
|
1322
|
+
* The ack and the close come BEFORE those dials: an unreachable peer can hold
|
|
1329
1323
|
* a dial for `dialBudget.totalMs` (86 s at the default declared link), which is not
|
|
1330
1324
|
* link time and does not belong inside the sender's delivery deadline. The close is
|
|
1331
1325
|
* what releases the sender, which reads the ack to end-of-stream. The stream stays
|
|
1332
1326
|
* counted in {@link activeStreams} through the dials, so {@link maxConcurrentSeeds}
|
|
1333
|
-
* still bounds concurrent
|
|
1327
|
+
* still bounds concurrent dial phases.
|
|
1334
1328
|
*
|
|
1335
1329
|
* Hardened against a buggy/compromised own-cadre node: a concurrency cap (over
|
|
1336
1330
|
* {@link maxConcurrentSeeds}, reply without applying), a read timeout (a peer
|
|
@@ -1367,7 +1361,7 @@ export class SeedBootstrapService {
|
|
|
1367
1361
|
);
|
|
1368
1362
|
|
|
1369
1363
|
if (merged.success) {
|
|
1370
|
-
await this.
|
|
1364
|
+
await this.dialPreferredPeers(seed);
|
|
1371
1365
|
this.eventCallbacks.onSeedApplied?.(seed.partyId, merged.peersAdded, seed);
|
|
1372
1366
|
} else {
|
|
1373
1367
|
this.eventCallbacks.onSeedError?.(seed.partyId, merged.error ?? 'Unknown error');
|
|
@@ -1440,19 +1434,22 @@ export class SeedBootstrapService {
|
|
|
1440
1434
|
* does not listen. Call `CadreNode.reconcileControlCohort()` after delivering
|
|
1441
1435
|
* the seed to dial at once.
|
|
1442
1436
|
*
|
|
1437
|
+
* The vouch and the seed are one owner approval.
|
|
1438
|
+
*
|
|
1443
1439
|
* @param options - Drone peer info from provider API
|
|
1444
1440
|
* @returns Seed and encoded seed for drone initialization
|
|
1445
1441
|
*/
|
|
1446
|
-
async addDrone(options: AddDroneOptions): Promise<DroneInitResult> {
|
|
1442
|
+
async addDrone(options: AddDroneOptions, sign?: OwnerSign): Promise<DroneInitResult> {
|
|
1447
1443
|
const { dronePeerId, droneMultiaddrs } = options;
|
|
1448
1444
|
|
|
1449
1445
|
log('Adding drone: %s', dronePeerId);
|
|
1450
1446
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1447
|
+
const seed = await this.withOwnerSign(`Add the machine ${dronePeerId} to the cadre`, sign, async (approved) => {
|
|
1448
|
+
// 1. Authorize the new drone peer
|
|
1449
|
+
await this.authorizePeer({ peerId: dronePeerId, multiaddrs: droneMultiaddrs }, approved);
|
|
1450
|
+
// 2. Create seed with current state
|
|
1451
|
+
return await this.createSeed(approved);
|
|
1452
|
+
});
|
|
1456
1453
|
|
|
1457
1454
|
// 3. Encode for transport
|
|
1458
1455
|
const encodedSeed = this.encodeSeed(seed);
|
|
@@ -1469,29 +1466,21 @@ export class SeedBootstrapService {
|
|
|
1469
1466
|
* 1. Authorizes the new phone peer
|
|
1470
1467
|
* 2. Creates a seed with relay addresses for dialing
|
|
1471
1468
|
*
|
|
1469
|
+
* The vouch and the seed are one owner approval.
|
|
1470
|
+
*
|
|
1472
1471
|
* @param phonePeerId - Peer ID of the new phone
|
|
1473
1472
|
* @returns Seed with relay addresses for out-of-band delivery
|
|
1474
1473
|
*/
|
|
1475
|
-
async addPhoneWithRelay(phonePeerId: string): Promise<DroneInitResult> {
|
|
1474
|
+
async addPhoneWithRelay(phonePeerId: string, sign?: OwnerSign): Promise<DroneInitResult> {
|
|
1476
1475
|
log('Adding phone with relay: %s', phonePeerId);
|
|
1477
1476
|
|
|
1478
|
-
// 1. Authorize the new phone peer (no multiaddrs - NAT'd)
|
|
1479
|
-
await this.authorizePeer({ peerId: phonePeerId });
|
|
1480
|
-
|
|
1481
|
-
// 2. Get relay address for this node
|
|
1482
1477
|
const relayAddr = await this.getRelayAddress();
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const ourPeerId = this.libp2pNode.peerId.toString();
|
|
1490
|
-
const ourPeer = seed.peers.find(p => p.peerId === ourPeerId);
|
|
1491
|
-
if (ourPeer && !ourPeer.multiaddrs.includes(relayAddr)) {
|
|
1492
|
-
ourPeer.multiaddrs.push(relayAddr);
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1478
|
+
const seed = await this.withOwnerSign(`Add the phone ${phonePeerId} to the cadre`, sign, async (approved) => {
|
|
1479
|
+
// 1. Authorize the new phone peer (no multiaddrs - NAT'd)
|
|
1480
|
+
await this.authorizePeer({ peerId: phonePeerId }, approved);
|
|
1481
|
+
// 2. Create seed - with our relay address in our own entry, when we have one
|
|
1482
|
+
return await this.mintSeed(approved, relayAddr);
|
|
1483
|
+
});
|
|
1495
1484
|
|
|
1496
1485
|
const encodedSeed = this.encodeSeed(seed);
|
|
1497
1486
|
|