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