@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/dist/seed-bootstrap.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
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 { multiaddr } from '@multiformats/multiaddr';
|
|
5
5
|
import { peerIdFromString } from '@libp2p/peer-id';
|
|
6
6
|
import { withDeadline, exchangeFrame, readStreamToEnd, replyAndClose } from './control-stream.js';
|
|
7
7
|
import { dialPeerAddrs, SelfRelayOnlyError, PeerUnreachableError, DEFAULT_PEER_DIAL_BUDGET, withTrailingPeerId, } from './peer-dial.js';
|
|
8
8
|
import { relayedRequestBudgetMs } from './link-budget.js';
|
|
9
|
-
import { generateStampId } from './control-database.js';
|
|
9
|
+
import { generateStampId, ownerKeyLabel } from './control-database.js';
|
|
10
10
|
import { canonicalJson } from './canonical-json.js';
|
|
11
11
|
import { deviceTokenAddDigest } from './peer-authorization.js';
|
|
12
12
|
import { ed25519PublicKeyB64FromPeerId } from './ed25519-key.js';
|
|
13
13
|
import { anchoredTrustPolicy, } from './seed-trust-policy.js';
|
|
14
|
+
import { withinOwnerApproval } from './owner-signer.js';
|
|
15
|
+
import { namedOwnerKey } from './owner-keys.js';
|
|
14
16
|
const log = debug('sereus:cadre:seed-bootstrap');
|
|
15
17
|
/** Protocol ID for seed delivery */
|
|
16
18
|
export const SEED_PROTOCOL = '/sereus/seed/1.0.0';
|
|
@@ -71,7 +73,27 @@ export function seedDigest(seed) {
|
|
|
71
73
|
return digest([canonicalSeedPayload(seed)], 'sha256', 'base64url');
|
|
72
74
|
}
|
|
73
75
|
/**
|
|
74
|
-
*
|
|
76
|
+
* The peers `seed` nominates for dialing: its preferred peers (`SeedPeer.preferred`: the machine
|
|
77
|
+
* that minted it and every always-on machine) that have an address and are not `selfPeerId`.
|
|
78
|
+
*
|
|
79
|
+
* Self is dropped because `createSeed` projects every non-revoked CadrePeer row, so a preferred
|
|
80
|
+
* machine applying a seed minted after it joined finds itself in the list, and a dial of self
|
|
81
|
+
* always throws. An undefined `selfPeerId` matches nothing.
|
|
82
|
+
*/
|
|
83
|
+
export function seedDialTargets(seed, selfPeerId) {
|
|
84
|
+
return seed.peers
|
|
85
|
+
.filter((peer) => peer.preferred && peer.multiaddrs.length > 0 && peer.peerId !== selfPeerId)
|
|
86
|
+
.map(({ peerId, multiaddrs }) => ({ peerId, multiaddrs }));
|
|
87
|
+
}
|
|
88
|
+
/** Add `relayAddr` to the entry for `selfPeerId` in `peers`, when that entry exists and lacks it. */
|
|
89
|
+
function addSelfRelayAddr(peers, selfPeerId, relayAddr) {
|
|
90
|
+
const self = peers.find((peer) => peer.peerId === selfPeerId);
|
|
91
|
+
if (self && !self.multiaddrs.includes(relayAddr)) {
|
|
92
|
+
self.multiaddrs.push(relayAddr);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* An {@link ApplySeedResult} for a seed refused before the dial loop ran,
|
|
75
97
|
* so the dial counters read zero rather than being absent.
|
|
76
98
|
*/
|
|
77
99
|
function seedRejected(error, code) {
|
|
@@ -80,8 +102,8 @@ function seedRejected(error, code) {
|
|
|
80
102
|
peersAdded: 0,
|
|
81
103
|
error,
|
|
82
104
|
...(code !== undefined ? { code } : {}),
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
preferredDialsAttempted: 0,
|
|
106
|
+
preferredDialsFailed: 0,
|
|
85
107
|
};
|
|
86
108
|
}
|
|
87
109
|
/**
|
|
@@ -162,13 +184,6 @@ export class SeedBootstrapService {
|
|
|
162
184
|
this.maxConcurrentSeeds = config.maxConcurrentSeeds ?? DEFAULT_MAX_CONCURRENT_SEEDS;
|
|
163
185
|
this.seedDeliverTimeoutMs = config.seedDeliverTimeoutMs ?? relayedRequestBudgetMs(config.linkRoundTripMs);
|
|
164
186
|
this.dialBudget = config.dialBudget ?? DEFAULT_PEER_DIAL_BUDGET;
|
|
165
|
-
// Derive public key from private key if not provided
|
|
166
|
-
if (config.ownerPrivateKey && !config.ownerPublicKey) {
|
|
167
|
-
this.ownerPublicKey = getPublicKey(config.ownerPrivateKey, 'ed25519', 'base64url', 'base64url');
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
this.ownerPublicKey = config.ownerPublicKey ?? null;
|
|
171
|
-
}
|
|
172
187
|
log('SeedBootstrapService created for party: %s', config.partyId);
|
|
173
188
|
}
|
|
174
189
|
/**
|
|
@@ -179,22 +194,25 @@ export class SeedBootstrapService {
|
|
|
179
194
|
this.eventCallbacks = callbacks;
|
|
180
195
|
}
|
|
181
196
|
/**
|
|
182
|
-
* Whether this service holds an owner
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* drain to skip owner work on a non-owner node.
|
|
197
|
+
* Whether this service holds an owner signer, i.e. can produce the owner signatures that
|
|
198
|
+
* gate `CadrePeer` / `DeviceToken` inserts, deletes, and re-authorizations. A
|
|
199
|
+
* seed-listener-only service (`enableSeedListener`, no signer) returns false: it can
|
|
200
|
+
* receive/apply seeds but cannot author or re-issue owner writes. Used by the
|
|
201
|
+
* write-while-alone re-replication drain to skip owner work on a non-owner node.
|
|
188
202
|
*/
|
|
189
203
|
canAuthorize() {
|
|
190
|
-
return
|
|
204
|
+
return this.config.ownerSigner !== undefined;
|
|
205
|
+
}
|
|
206
|
+
/** The owner signer this service was built with, or null for a listener-only service. */
|
|
207
|
+
get ownerSigner() {
|
|
208
|
+
return this.config.ownerSigner ?? null;
|
|
191
209
|
}
|
|
192
210
|
/**
|
|
193
211
|
* Initialize the service with libp2p node and control database.
|
|
194
212
|
*
|
|
195
213
|
* `registerHandler` (default true) gates registration of the shared inbound
|
|
196
214
|
* `/sereus/seed/1.0.0` handler on `libp2pNode`. Persistent services
|
|
197
|
-
* (`
|
|
215
|
+
* (`CadreNode.useOwnerSigner`, `enableSeedListener`) own that handler and leave
|
|
198
216
|
* it on. The throwaway temp service CadreNode builds in `applySeed` passes
|
|
199
217
|
* `false`: it only needs the stored `libp2pNode` / `controlDatabase` for
|
|
200
218
|
* dialing and known-key lookup, and must NOT bind a discarded closure to the
|
|
@@ -226,8 +244,11 @@ export class SeedBootstrapService {
|
|
|
226
244
|
* `UpdatedAt` but no self-signature (`Sig` null) — the owner cannot produce
|
|
227
245
|
* the peer's self-signature, so the peer must self-publish (see
|
|
228
246
|
* {@link CadreNode.registerSelf}) before the row resolves.
|
|
247
|
+
*
|
|
248
|
+
* @param sign - The signature function of an owner approval the caller already holds, so a
|
|
249
|
+
* multi-step action asks once; omitted, this write asks for its own approval.
|
|
229
250
|
*/
|
|
230
|
-
async authorizePeer(options) {
|
|
251
|
+
async authorizePeer(options, sign) {
|
|
231
252
|
const { peerId, multiaddrs } = options;
|
|
232
253
|
log('Authorizing peer: %s', peerId);
|
|
233
254
|
// CadrePeer.Multiaddr stores a comma-joined list; use '' when no addrs provided.
|
|
@@ -239,7 +260,7 @@ export class SeedBootstrapService {
|
|
|
239
260
|
updatedAt: Date.now(),
|
|
240
261
|
sig: null,
|
|
241
262
|
profile: null,
|
|
242
|
-
});
|
|
263
|
+
}, `Add the machine ${peerId} to the cadre`, sign);
|
|
243
264
|
log('Peer %s authorized successfully', peerId);
|
|
244
265
|
}
|
|
245
266
|
/**
|
|
@@ -255,7 +276,7 @@ export class SeedBootstrapService {
|
|
|
255
276
|
* in which case the row in the database is NOT this record and carries whatever
|
|
256
277
|
* `Sig` that writer had, so the caller must self-update to publish its signature.
|
|
257
278
|
*/
|
|
258
|
-
async insertSelfPeerRecord(record) {
|
|
279
|
+
async insertSelfPeerRecord(record, sign) {
|
|
259
280
|
return await this.insertCadrePeerRow({
|
|
260
281
|
peerId: record.peerId,
|
|
261
282
|
publicKey: record.publicKey,
|
|
@@ -263,30 +284,27 @@ export class SeedBootstrapService {
|
|
|
263
284
|
updatedAt: record.updatedAt,
|
|
264
285
|
sig: record.sig,
|
|
265
286
|
profile: record.profile,
|
|
266
|
-
});
|
|
287
|
+
}, 'Publish this machine\'s own address record', sign);
|
|
267
288
|
}
|
|
268
289
|
/**
|
|
269
|
-
* Shared owner-signed `CadrePeer` INSERT — the row fields and the owner-
|
|
290
|
+
* Shared owner-signed `CadrePeer` INSERT — the row fields and the owner-signer
|
|
270
291
|
* precondition; the stamp mint, voucher signature, write lock, existence check and
|
|
271
292
|
* membership notify are all {@link ControlDatabase.insertCadrePeer}'s (see there for
|
|
272
293
|
* the anti-replay and insert-race rationale). No `CadrePeer` write may bypass that
|
|
273
294
|
* method: the write is what admits the peer's traffic.
|
|
274
295
|
*
|
|
275
|
-
* The DB check precedes the owner-
|
|
276
|
-
* always surfaced them
|
|
277
|
-
* DB check).
|
|
296
|
+
* The DB check precedes the owner-signer check here, matching the order this method has
|
|
297
|
+
* always surfaced them.
|
|
278
298
|
*
|
|
279
299
|
* @returns `true` when this call performed the INSERT, `false` when the row was
|
|
280
300
|
* already seated by a concurrent writer. The loser needs to know: an authorize seats
|
|
281
301
|
* a row with a null `Sig`, so a self-publish that lost the race must fall through to
|
|
282
302
|
* a self-update or its record never lands.
|
|
283
303
|
*/
|
|
284
|
-
async insertCadrePeerRow(row) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const ownerKey = this.requireOwnerPublicKey();
|
|
289
|
-
return await this.controlDatabase.insertCadrePeer(row, ownerKey, message => this.signMessageBytes(message));
|
|
304
|
+
async insertCadrePeerRow(row, action, sign) {
|
|
305
|
+
const controlDatabase = this.requireControlDatabase();
|
|
306
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
307
|
+
return await this.withOwnerSign(action, sign, (approved) => controlDatabase.insertCadrePeer(row, ownerKey, approved));
|
|
290
308
|
}
|
|
291
309
|
/**
|
|
292
310
|
* Owner-signed INSERT of a peer's OWN self-signed `DeviceToken` row.
|
|
@@ -305,17 +323,17 @@ export class SeedBootstrapService {
|
|
|
305
323
|
* fresh signature — unaffected by the cleared row's retired stamp
|
|
306
324
|
* (`DeviceToken.NotRevoked`).
|
|
307
325
|
*/
|
|
308
|
-
async insertSelfDeviceToken(record) {
|
|
326
|
+
async insertSelfDeviceToken(record, sign) {
|
|
327
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
328
|
+
const controlDatabase = this.requireControlDatabase();
|
|
309
329
|
const stampId = generateStampId(record.peerId);
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
await this.controlDatabase.execWrite(`
|
|
330
|
+
const digestBytes = uint8ArrayFromString(deviceTokenAddDigest(this.config.partyId, { ...record, stampId }), 'base64url');
|
|
331
|
+
const signature = await this.withOwnerSign('Publish this machine\'s push token', sign, (approved) => approved(digestBytes));
|
|
332
|
+
await controlDatabase.execWrite(`
|
|
315
333
|
insert into CadreControl.DeviceToken (PeerId, Platform, Token, UpdatedAt, Sig, StampId)
|
|
316
334
|
with context OwnerKey = ?, Signature = ?
|
|
317
335
|
values (?, ?, ?, ?, ?, ?)
|
|
318
|
-
`, [
|
|
336
|
+
`, [ownerKey, signature, record.peerId, record.platform, record.token, record.updatedAt, record.sig, stampId], 'device-token-insert');
|
|
319
337
|
log('Device token inserted (owner-signed): %s', record.peerId);
|
|
320
338
|
}
|
|
321
339
|
/**
|
|
@@ -336,66 +354,44 @@ export class SeedBootstrapService {
|
|
|
336
354
|
* Both digests, the stamp read, and the transaction come from
|
|
337
355
|
* {@link ControlDatabase.deleteDeviceToken} — one shared implementation across
|
|
338
356
|
* `CadrePeer` / `DeviceToken` / `Strand` / `ValidationKey`. What stays here is the
|
|
339
|
-
* owner-
|
|
357
|
+
* owner-signer precondition; a no-op on an already-absent row is the shared body's
|
|
340
358
|
* behavior, and unlike {@link removePeer} nothing here rides on it.
|
|
341
359
|
*/
|
|
342
|
-
async deleteDeviceToken(peerId) {
|
|
343
|
-
// Fail fast on a
|
|
344
|
-
// owner-
|
|
345
|
-
const ownerKey = this.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
await this.controlDatabase.deleteDeviceToken(peerId, ownerKey, message => this.signMessageBytes(message));
|
|
360
|
+
async deleteDeviceToken(peerId, sign) {
|
|
361
|
+
// Fail fast on a signerless service BEFORE the DB read, so a non-owner gets the
|
|
362
|
+
// owner-signer error rather than a silent no-op on an absent row.
|
|
363
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
364
|
+
const controlDatabase = this.requireControlDatabase();
|
|
365
|
+
await this.withOwnerSign(`Clear the push token of ${peerId}`, sign, (approved) => controlDatabase.deleteDeviceToken(peerId, ownerKey, approved));
|
|
350
366
|
}
|
|
351
367
|
/**
|
|
352
|
-
* Return the configured owner
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
368
|
+
* Return the configured owner signer, or throw if none is set. The single precondition
|
|
369
|
+
* gate for every owner-signed write. {@link removePeer} / {@link reauthorizePeer} read the
|
|
370
|
+
* row from the DB BEFORE they sign, so they call this up front — otherwise a signerless
|
|
371
|
+
* service would either surface the wrong "Control database not initialized" error or,
|
|
372
|
+
* worse, silently no-op when the target row is absent (their early absent return) instead
|
|
373
|
+
* of rejecting.
|
|
358
374
|
*/
|
|
359
|
-
|
|
360
|
-
if (!this.config.
|
|
361
|
-
throw new Error('Owner
|
|
375
|
+
requireOwnerSigner() {
|
|
376
|
+
if (!this.config.ownerSigner) {
|
|
377
|
+
throw new Error('Owner signer required to authorize peers');
|
|
362
378
|
}
|
|
363
|
-
return this.config.
|
|
379
|
+
return this.config.ownerSigner;
|
|
364
380
|
}
|
|
365
|
-
/**
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
* constructor derives the public key whenever `ownerPrivateKey` is set — so gating on
|
|
370
|
-
* {@link requireOwnerPrivateKey} first makes the pair inseparable and the second throw
|
|
371
|
-
* unreachable. Callers that must reject a keyless service BEFORE any DB read use this as
|
|
372
|
-
* their first line and get the owner-key precondition for free.
|
|
373
|
-
*/
|
|
374
|
-
requireOwnerPublicKey() {
|
|
375
|
-
this.requireOwnerPrivateKey();
|
|
376
|
-
if (!this.ownerPublicKey) {
|
|
377
|
-
throw new Error('Owner public key required to authorize peers');
|
|
381
|
+
/** The control database {@link initialize} attached, or a throw. */
|
|
382
|
+
requireControlDatabase() {
|
|
383
|
+
if (!this.controlDatabase) {
|
|
384
|
+
throw new Error('Control database not initialized');
|
|
378
385
|
}
|
|
379
|
-
return this.
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Sign a base64url digest with the owner key (ed25519). The single place the
|
|
383
|
-
* owner private key is applied; callers pass the canonical domain-tagged digest for
|
|
384
|
-
* the specific action ({@link deviceTokenAddDigest}), or the raw-bytes form via
|
|
385
|
-
* {@link signMessageBytes}. Throws if no owner key is set.
|
|
386
|
-
*/
|
|
387
|
-
signDigest(digestB64url) {
|
|
388
|
-
return sign(digestB64url, this.requireOwnerPrivateKey(), 'ed25519', 'base64url', 'base64url', 'base64url');
|
|
386
|
+
return this.controlDatabase;
|
|
389
387
|
}
|
|
390
388
|
/**
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
* satisfies the same schema CHECK as one from the callers that sign the bytes directly — see
|
|
395
|
-
* `control-revocation-replay.spec.ts`'s "raw-bytes and digest-string signers agree".
|
|
389
|
+
* Run `body` with an owner signature function: the caller's `sign` when it passes one (a
|
|
390
|
+
* `CadreNode` action already inside its approval), otherwise one approval of this service's
|
|
391
|
+
* signer for `action`, the plain-words description a prompting signer shows.
|
|
396
392
|
*/
|
|
397
|
-
|
|
398
|
-
return this.
|
|
393
|
+
async withOwnerSign(action, sign, body) {
|
|
394
|
+
return await withinOwnerApproval(this.requireOwnerSigner(), { partyId: this.config.partyId, action }, sign, body);
|
|
399
395
|
}
|
|
400
396
|
/**
|
|
401
397
|
* Remove a peer from the cadre by owner signature.
|
|
@@ -419,16 +415,14 @@ export class SeedBootstrapService {
|
|
|
419
415
|
* Both digests, the stamp read, the transaction, and the post-commit membership notify
|
|
420
416
|
* come from {@link ControlDatabase.deleteCadrePeer} — one shared implementation across
|
|
421
417
|
* `CadrePeer` / `DeviceToken` / `Strand` / `ValidationKey`. What stays here is the
|
|
422
|
-
* owner-
|
|
418
|
+
* owner-signer precondition and the absent-row gate the notify depends on (below).
|
|
423
419
|
*/
|
|
424
|
-
async removePeer(peerId) {
|
|
425
|
-
// Fail fast on a
|
|
426
|
-
// remove digest, and this precedence (owner
|
|
420
|
+
async removePeer(peerId, sign) {
|
|
421
|
+
// Fail fast on a signerless service BEFORE any DB read: a non-owner cannot sign the
|
|
422
|
+
// remove digest, and this precedence (owner signer, then control DB) is what the
|
|
427
423
|
// unit contract asserts.
|
|
428
|
-
const ownerKey = this.
|
|
429
|
-
|
|
430
|
-
throw new Error('Control database not initialized');
|
|
431
|
-
}
|
|
424
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
425
|
+
const controlDatabase = this.requireControlDatabase();
|
|
432
426
|
// This absent-row gate must stay OUTSIDE the delete, even though the delete repeats it
|
|
433
427
|
// internally: deleteCadrePeer's membership notify fires whenever its body resolves,
|
|
434
428
|
// with no idea whether the body wrote anything, so delegating an absent peer would
|
|
@@ -436,106 +430,111 @@ export class SeedBootstrapService {
|
|
|
436
430
|
// NOTE: deleteCadrePeer re-reads the StampId, so a peer removed by another writer
|
|
437
431
|
// between the two reads no-ops silently yet still notifies. Narrow concurrent-removal
|
|
438
432
|
// window only — the common "already absent" case is caught here.
|
|
439
|
-
const stampId = await
|
|
433
|
+
const stampId = await controlDatabase.queryCadrePeerStampId(peerId);
|
|
440
434
|
if (stampId === null) {
|
|
441
435
|
log('removePeer: no CadrePeer row for %s (already absent)', peerId);
|
|
442
436
|
return;
|
|
443
437
|
}
|
|
444
438
|
log('Removing peer: %s', peerId);
|
|
445
|
-
await this.controlDatabase.deleteCadrePeer(peerId, ownerKey,
|
|
439
|
+
await this.withOwnerSign(`Remove the machine ${peerId} from the cadre`, sign, (approved) => controlDatabase.deleteCadrePeer(peerId, ownerKey, approved));
|
|
446
440
|
log('Peer %s removed successfully (stamp retired)', peerId);
|
|
447
441
|
}
|
|
448
442
|
/**
|
|
449
|
-
* Seat `ownerPublicKey` as a further owner, signed by this service's owner
|
|
443
|
+
* Seat `ownerPublicKey` as a further owner, signed by this service's owner signer. The
|
|
450
444
|
* liveness check, the stamp, the signature and the membership notify are
|
|
451
|
-
* {@link ControlDatabase.insertOwnerKeyVouched}'s; what stays here is the owner-
|
|
445
|
+
* {@link ControlDatabase.insertOwnerKeyVouched}'s; what stays here is the owner-signer
|
|
452
446
|
* precondition, checked before any read (see {@link removePeer}).
|
|
453
447
|
*
|
|
448
|
+
* @param label - the user's name for the key, bound into the signature
|
|
449
|
+
* ({@link ControlDatabase.insertOwnerKey} has the rule).
|
|
454
450
|
* @returns `true` when the row was seated, `false` when the key is already a live owner.
|
|
455
451
|
*/
|
|
456
|
-
async addOwner(ownerPublicKey) {
|
|
457
|
-
const ownerKey = this.
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
return await this.controlDatabase.insertOwnerKeyVouched(ownerPublicKey, ownerKey,
|
|
452
|
+
async addOwner(ownerPublicKey, label, sign) {
|
|
453
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
454
|
+
const controlDatabase = this.requireControlDatabase();
|
|
455
|
+
// Checked before the approval opens, so a label the row would refuse never prompts the user.
|
|
456
|
+
const storedLabel = ownerKeyLabel(label);
|
|
457
|
+
return await this.withOwnerSign(`Add owner key ${namedOwnerKey(ownerPublicKey, storedLabel)}`, sign, (approved) => controlDatabase.insertOwnerKeyVouched(ownerPublicKey, ownerKey, approved, storedLabel));
|
|
462
458
|
}
|
|
463
459
|
/**
|
|
464
|
-
* Remove `ownerPublicKey` from the owners, signed by this service's owner
|
|
460
|
+
* Remove `ownerPublicKey` from the owners, signed by this service's owner signer: the delete
|
|
465
461
|
* and its `Revocation` tombstone in one transaction. The by-name refusals (self-removal,
|
|
466
462
|
* the last owner), the digests and the membership notify are
|
|
467
|
-
* {@link ControlDatabase.deleteOwnerKey}'s; what stays here is the owner-
|
|
463
|
+
* {@link ControlDatabase.deleteOwnerKey}'s; what stays here is the owner-signer precondition,
|
|
468
464
|
* checked before any read (see {@link removePeer}).
|
|
469
465
|
*
|
|
470
466
|
* @returns `true` when the row was removed, `false` when the key is not a live owner here.
|
|
471
467
|
*/
|
|
472
|
-
async removeOwner(ownerPublicKey) {
|
|
473
|
-
const ownerKey = this.
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
return await this.controlDatabase.deleteOwnerKey(ownerPublicKey, ownerKey, message => this.signMessageBytes(message));
|
|
468
|
+
async removeOwner(ownerPublicKey, sign) {
|
|
469
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
470
|
+
const controlDatabase = this.requireControlDatabase();
|
|
471
|
+
return await this.withOwnerSign(`Remove owner key ${ownerPublicKey}`, sign, (approved) => controlDatabase.deleteOwnerKey(ownerPublicKey, ownerKey, approved));
|
|
478
472
|
}
|
|
479
473
|
/**
|
|
480
|
-
* Re-vouch another owner's row under this service's owner
|
|
474
|
+
* Re-vouch another owner's row under this service's owner signer, so it stands on a remaining
|
|
481
475
|
* owner's signature before the owner that added it is removed
|
|
482
|
-
* ({@link ControlDatabase.revouchOwnerKey}; for `CadreNode.
|
|
483
|
-
* here is the owner-
|
|
476
|
+
* ({@link ControlDatabase.revouchOwnerKey}; for `CadreNode.removeOwner` only). What stays
|
|
477
|
+
* here is the owner-signer precondition, checked before any read (see {@link removePeer}).
|
|
484
478
|
*
|
|
485
479
|
* @returns `true` when the row was re-vouched, `false` when the key is not a live owner here.
|
|
486
480
|
*/
|
|
487
|
-
async revouchOwner(ownerPublicKey) {
|
|
488
|
-
const ownerKey = this.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
}
|
|
492
|
-
return await this.controlDatabase.revouchOwnerKey(ownerPublicKey, ownerKey, message => this.signMessageBytes(message));
|
|
481
|
+
async revouchOwner(ownerPublicKey, sign) {
|
|
482
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
483
|
+
const controlDatabase = this.requireControlDatabase();
|
|
484
|
+
return await this.withOwnerSign(`Re-vouch owner key ${ownerPublicKey}`, sign, (approved) => controlDatabase.revouchOwnerKey(ownerPublicKey, ownerKey, approved));
|
|
493
485
|
}
|
|
494
486
|
/**
|
|
495
|
-
* Remove a machine completely, signed by this service's owner
|
|
496
|
-
* `
|
|
497
|
-
*
|
|
498
|
-
* ({@link
|
|
499
|
-
* checked before any read (see {@link removePeer}).
|
|
487
|
+
* Remove a machine completely, signed by this service's owner signer: its `CadrePeer` row, its
|
|
488
|
+
* `DeviceToken` row, and the withdrawal of every invitation in `target.inviteKeys`, all in one
|
|
489
|
+
* transaction ({@link ControlDatabase.removeMachine}). What stays here is the owner-signer
|
|
490
|
+
* precondition, checked before any read (see {@link removePeer}).
|
|
500
491
|
*/
|
|
501
|
-
async removeMachine(target) {
|
|
502
|
-
const ownerKey = this.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}
|
|
506
|
-
return await this.controlDatabase.removeMachine(target, ownerKey, message => this.signMessageBytes(message));
|
|
492
|
+
async removeMachine(target, sign) {
|
|
493
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
494
|
+
const controlDatabase = this.requireControlDatabase();
|
|
495
|
+
return await this.withOwnerSign(`Remove the machine ${target.peerId} from the cadre`, sign, (approved) => controlDatabase.removeMachine(target, ownerKey, approved));
|
|
507
496
|
}
|
|
508
497
|
/**
|
|
509
|
-
* Owner "re-touch" of an existing `CadrePeer` membership row
|
|
510
|
-
*
|
|
511
|
-
* the write-while-alone re-replication
|
|
512
|
-
* a membership row that committed local-only
|
|
513
|
-
* pushed to the cohort once it grows
|
|
498
|
+
* Owner "re-touch" of an existing `CadrePeer` membership row, in one of two modes
|
|
499
|
+
* ({@link CadrePeerRetouchMode}): `'rebroadcast'` bumps `UpdatedAt` and re-vouches the row so
|
|
500
|
+
* it is re-emitted as a fresh, broadcasting transaction — the write-while-alone re-replication
|
|
501
|
+
* primitive (`control-write-ensure-replicated`): a membership row that committed local-only
|
|
502
|
+
* (its block's cluster ≤1 at insert) is pushed to the cohort once it grows — and writes only
|
|
503
|
+
* while the machine has not self-published; `'revouch'` rebinds the voucher to this node's
|
|
504
|
+
* owner key, leaving `UpdatedAt` alone, whether or not the machine has self-published
|
|
505
|
+
* (`CadreNode.removeOwner`).
|
|
514
506
|
*
|
|
515
|
-
* The
|
|
507
|
+
* The row read, the mode's decision, the stamp bump, the voucher signature, the write lock,
|
|
508
|
+
* the lost-update retry and the membership notify are
|
|
516
509
|
* {@link ControlDatabase.reauthorizeCadrePeer}'s — including the caveat that it rebinds
|
|
517
|
-
* `VouchOwner` to this node's owner key. What stays here is the owner-
|
|
518
|
-
*
|
|
519
|
-
* A no-op (no throw, no notify) when the row does not exist.
|
|
510
|
+
* `VouchOwner` to this node's owner key. What stays here is the owner-signer precondition.
|
|
520
511
|
*
|
|
521
512
|
* @param peerId - the membership row to re-touch.
|
|
522
|
-
* @param
|
|
523
|
-
* @
|
|
513
|
+
* @param mode - re-broadcast or re-vouch.
|
|
514
|
+
* @returns what was found and done ({@link CadrePeerRetouchOutcome}); an absent row, or a
|
|
515
|
+
* self-published one in `'rebroadcast'` mode, writes nothing and does not throw.
|
|
516
|
+
* @throws if no owner signer is configured (a non-owner cannot
|
|
524
517
|
* re-sign another peer's row) or the control database is not initialized.
|
|
525
518
|
*/
|
|
526
|
-
async reauthorizePeer(peerId,
|
|
527
|
-
// Fail fast on a
|
|
519
|
+
async reauthorizePeer(peerId, mode, sign) {
|
|
520
|
+
// Fail fast on a signerless service before any DB read (see removePeer): a non-owner
|
|
528
521
|
// cannot re-sign the voucher, and must not silently no-op on an absent row.
|
|
529
|
-
const ownerKey = this.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
522
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
523
|
+
const controlDatabase = this.requireControlDatabase();
|
|
524
|
+
const action = mode === 'revouch' ? `Re-vouch the machine ${peerId}` : `Re-issue the membership of ${peerId}`;
|
|
525
|
+
const outcome = await this.withOwnerSign(action, sign, (approved) => controlDatabase.reauthorizeCadrePeer(peerId, mode, ownerKey, approved));
|
|
526
|
+
switch (outcome) {
|
|
527
|
+
case 'absent':
|
|
528
|
+
log('reauthorizePeer(%s): no CadrePeer row for %s (nothing to re-touch)', mode, peerId);
|
|
529
|
+
break;
|
|
530
|
+
case 'self-published':
|
|
531
|
+
log('reauthorizePeer(%s): %s has self-published; its row is its own to refresh', mode, peerId);
|
|
532
|
+
break;
|
|
533
|
+
case 'retouched':
|
|
534
|
+
log('Peer %s re-authorized (%s)', peerId, mode);
|
|
535
|
+
break;
|
|
537
536
|
}
|
|
538
|
-
|
|
537
|
+
return outcome;
|
|
539
538
|
}
|
|
540
539
|
/**
|
|
541
540
|
* Owner re-issue of a batch of `Revocation` tombstones: bump each row's
|
|
@@ -547,81 +546,97 @@ export class SeedBootstrapService {
|
|
|
547
546
|
*
|
|
548
547
|
* The signatures, the single transaction, and the strictly-monotonic
|
|
549
548
|
* `reissuedAt` contract are {@link ControlDatabase.reissueRevocations}'s. What
|
|
550
|
-
* stays here is the owner-
|
|
549
|
+
* stays here is the owner-signer precondition.
|
|
551
550
|
*
|
|
552
551
|
* @returns how many tombstones were re-issued (`rows.length` on success).
|
|
553
|
-
* @throws if no owner
|
|
552
|
+
* @throws if no owner signer is configured or the control database is not
|
|
554
553
|
* initialized.
|
|
555
554
|
*/
|
|
556
|
-
async reissueRevocations(rows, reissuedAt) {
|
|
557
|
-
// Fail fast on a
|
|
555
|
+
async reissueRevocations(rows, reissuedAt, sign) {
|
|
556
|
+
// Fail fast on a signerless service before any DB work (see removePeer): a
|
|
558
557
|
// non-owner cannot sign the reissue digests.
|
|
559
|
-
const ownerKey = this.
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
return this.controlDatabase.reissueRevocations(rows, reissuedAt, ownerKey, message => this.signMessageBytes(message));
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Owner filing of the singleton `Revocation` ledger marker, so the table is never a
|
|
567
|
-
* never-written block that the storage layer re-consults on every read.
|
|
568
|
-
*
|
|
569
|
-
* The signature, the insert-if-absent guard and the `'already-open'` mapping are
|
|
570
|
-
* {@link ControlDatabase.openRevocationLedger}'s. What stays here is the owner-key
|
|
571
|
-
* precondition.
|
|
572
|
-
*
|
|
573
|
-
* @throws if no owner private key is configured or the control database is not
|
|
574
|
-
* initialized.
|
|
575
|
-
*/
|
|
576
|
-
async openRevocationLedger() {
|
|
577
|
-
// Fail fast on a keyless service before any DB work (see removePeer): a
|
|
578
|
-
// non-owner cannot sign the marker's append digest.
|
|
579
|
-
const ownerKey = this.requireOwnerPublicKey();
|
|
580
|
-
if (!this.controlDatabase) {
|
|
581
|
-
throw new Error('Control database not initialized');
|
|
582
|
-
}
|
|
583
|
-
return this.controlDatabase.openRevocationLedger(ownerKey, message => this.signMessageBytes(message));
|
|
558
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
559
|
+
const controlDatabase = this.requireControlDatabase();
|
|
560
|
+
return await this.withOwnerSign(`Re-send ${rows.length} removal record(s) to the cadre`, sign, (approved) => controlDatabase.reissueRevocations(rows, reissuedAt, ownerKey, approved));
|
|
584
561
|
}
|
|
585
562
|
/**
|
|
586
563
|
* Owner-signed INSERT of a `CadreInvite` row — an invitation any member machine can redeem
|
|
587
564
|
* on this owner's behalf ({@link ControlDatabase.insertCadreInvite}). The keypair mint, the
|
|
588
565
|
* expiry and use-count defaults, and the bundle the holder receives are
|
|
589
|
-
* `CadreNode.createCadreInvitation`'s; what stays here is the owner-
|
|
590
|
-
* the
|
|
566
|
+
* `CadreNode.createCadreInvitation`'s; what stays here is the owner-signer precondition and
|
|
567
|
+
* the approval, as {@link insertSelfDeviceToken} keeps them for device tokens.
|
|
591
568
|
*
|
|
592
569
|
* @returns the row as stored, which the invitation bundle carries verbatim.
|
|
593
|
-
* @throws if no owner
|
|
570
|
+
* @throws if no owner signer is configured or the control database is not initialized.
|
|
594
571
|
*/
|
|
595
|
-
async insertCadreInvite(invite) {
|
|
596
|
-
// Fail fast on a
|
|
597
|
-
const ownerKey = this.
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
return await this.controlDatabase.insertCadreInvite(invite, ownerKey, message => this.signMessageBytes(message));
|
|
572
|
+
async insertCadreInvite(invite, sign) {
|
|
573
|
+
// Fail fast on a signerless service before any DB work (see removePeer).
|
|
574
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
575
|
+
const controlDatabase = this.requireControlDatabase();
|
|
576
|
+
return await this.withOwnerSign('Create a machine invitation to the cadre', sign, (approved) => controlDatabase.insertCadreInvite(invite, ownerKey, approved));
|
|
602
577
|
}
|
|
603
578
|
/**
|
|
604
579
|
* Owner-signed WITHDRAWAL of a `CadreInvite`: the `Revocation` tombstone over its stamp, with
|
|
605
580
|
* the row kept ({@link ControlDatabase.withdrawCadreInvite}). What stays here is the
|
|
606
|
-
* owner-
|
|
581
|
+
* owner-signer precondition.
|
|
607
582
|
*
|
|
608
583
|
* @returns `true` when this call filed the tombstone, `false` when the row is absent here or
|
|
609
584
|
* already withdrawn.
|
|
610
|
-
* @throws if no owner
|
|
585
|
+
* @throws if no owner signer is configured or the control database is not initialized.
|
|
611
586
|
*/
|
|
612
|
-
async withdrawCadreInvite(key) {
|
|
613
|
-
const ownerKey = this.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
587
|
+
async withdrawCadreInvite(key, sign) {
|
|
588
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
589
|
+
const controlDatabase = this.requireControlDatabase();
|
|
590
|
+
return await this.withOwnerSign(`Withdraw the cadre invitation ${key}`, sign, (approved) => controlDatabase.withdrawCadreInvite(key, ownerKey, approved));
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Owner-signed INSERT of an `OwnerGrant` row — a grant of ownership any member machine can
|
|
594
|
+
* redeem on this owner's behalf ({@link ControlDatabase.insertOwnerGrant}). The keypair mint,
|
|
595
|
+
* the lifetime default and the bundle the holder receives are `CadreNode.createOwnerGrant`'s;
|
|
596
|
+
* what stays here is the owner-signer precondition and the approval, as for
|
|
597
|
+
* {@link insertCadreInvite}. The label is checked before the approval opens, so one the row
|
|
598
|
+
* would refuse never prompts the user.
|
|
599
|
+
*
|
|
600
|
+
* @returns the row as stored, which the grant bundle carries verbatim.
|
|
601
|
+
* @throws if no owner signer is configured, the control database is not initialized, or the label is too long.
|
|
602
|
+
*/
|
|
603
|
+
async insertOwnerGrant(grant, sign) {
|
|
604
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
605
|
+
const controlDatabase = this.requireControlDatabase();
|
|
606
|
+
const label = ownerKeyLabel(grant.label);
|
|
607
|
+
const named = label === null ? 'an owner grant' : `the owner grant "${label}"`;
|
|
608
|
+
return await this.withOwnerSign(`Create ${named}: whoever holds it can become an owner of the cadre`, sign, (approved) => controlDatabase.insertOwnerGrant({ ...grant, label }, ownerKey, approved));
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Owner-signed WITHDRAWAL of an `OwnerGrant`: the `Revocation` tombstone over its stamp, with
|
|
612
|
+
* the row kept ({@link ControlDatabase.withdrawOwnerGrant}). What stays here is the
|
|
613
|
+
* owner-signer precondition.
|
|
614
|
+
*
|
|
615
|
+
* @returns `true` when this call filed the tombstone, `false` when the row is absent here or
|
|
616
|
+
* already withdrawn.
|
|
617
|
+
* @throws if no owner signer is configured or the control database is not initialized.
|
|
618
|
+
*/
|
|
619
|
+
async withdrawOwnerGrant(key, sign) {
|
|
620
|
+
const ownerKey = this.requireOwnerSigner().publicKey;
|
|
621
|
+
const controlDatabase = this.requireControlDatabase();
|
|
622
|
+
return await this.withOwnerSign(`Withdraw the owner grant ${key}`, sign, (approved) => controlDatabase.withdrawOwnerGrant(key, ownerKey, approved));
|
|
618
623
|
}
|
|
619
624
|
/**
|
|
620
625
|
* Create a seed from the current control network state.
|
|
621
|
-
* The seed contains peer information and is signed by an owner
|
|
626
|
+
* The seed contains peer information and is signed by an owner; its `signerKey` is the
|
|
627
|
+
* owner signer's key.
|
|
622
628
|
*/
|
|
623
|
-
async createSeed() {
|
|
624
|
-
|
|
629
|
+
async createSeed(sign) {
|
|
630
|
+
return await this.mintSeed(sign, null);
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* {@link createSeed}, with `selfRelayAddr` added to this node's own peer entry before the
|
|
634
|
+
* seed is signed: the signature covers every address ({@link seedDigest}), so nothing may
|
|
635
|
+
* be added after it.
|
|
636
|
+
*/
|
|
637
|
+
async mintSeed(sign, selfRelayAddr) {
|
|
638
|
+
const signer = this.config.ownerSigner;
|
|
639
|
+
if (!signer) {
|
|
625
640
|
throw new Error('Owner key required to create seeds');
|
|
626
641
|
}
|
|
627
642
|
if (!this.controlDatabase || !this.libp2pNode) {
|
|
@@ -629,18 +644,23 @@ export class SeedBootstrapService {
|
|
|
629
644
|
}
|
|
630
645
|
log('Creating seed for party: %s', this.config.partyId);
|
|
631
646
|
// Query all peers from the control database
|
|
632
|
-
const
|
|
647
|
+
const selfPeerId = this.libp2pNode.peerId.toString();
|
|
648
|
+
const peers = await this.queryPeers(selfPeerId);
|
|
649
|
+
if (selfRelayAddr !== null) {
|
|
650
|
+
addSelfRelayAddr(peers, selfPeerId, selfRelayAddr);
|
|
651
|
+
}
|
|
633
652
|
// Create the seed data (without signature)
|
|
634
653
|
const seedData = {
|
|
635
654
|
partyId: this.config.partyId,
|
|
636
655
|
peers,
|
|
637
656
|
};
|
|
638
657
|
// Sign the seed over its canonical byte representation
|
|
639
|
-
const
|
|
658
|
+
const digestBytes = uint8ArrayFromString(seedDigest(seedData), 'base64url');
|
|
659
|
+
const signature = await this.withOwnerSign('Mint a seed for a machine joining the cadre', sign, (approved) => approved(digestBytes));
|
|
640
660
|
const seed = {
|
|
641
661
|
...seedData,
|
|
642
662
|
signature,
|
|
643
|
-
signerKey:
|
|
663
|
+
signerKey: signer.publicKey,
|
|
644
664
|
};
|
|
645
665
|
log('Created seed with %d peers', peers.length);
|
|
646
666
|
return seed;
|
|
@@ -652,9 +672,8 @@ export class SeedBootstrapService {
|
|
|
652
672
|
* `signerKey` that does NOT come from the seed body: the receiver's
|
|
653
673
|
* node-local {@link SeedBootstrapConfig.trustedOwners} anchor, optionally
|
|
654
674
|
* augmented by pinned keys or TOFU via the configured/overriding
|
|
655
|
-
* `SeedTrustPolicy`. A forged self-
|
|
656
|
-
*
|
|
657
|
-
* by a key a stranger genesis-inserted into the replicated `OwnerKey` table.
|
|
675
|
+
* `SeedTrustPolicy`. A forged self-signed seed does not pass, and neither does
|
|
676
|
+
* one signed by a key a stranger genesis-inserted into the replicated `OwnerKey` table.
|
|
658
677
|
*
|
|
659
678
|
* A key accepted via a pin/TOFU is persisted into the anchor (the policy says
|
|
660
679
|
* so via `SeedTrustDecision.anchorAs`), so the next seed from that owner is
|
|
@@ -670,12 +689,12 @@ export class SeedBootstrapService {
|
|
|
670
689
|
if (!merged.success) {
|
|
671
690
|
return merged;
|
|
672
691
|
}
|
|
673
|
-
return { ...merged, ...await this.
|
|
692
|
+
return { ...merged, ...await this.dialPreferredPeers(seed) };
|
|
674
693
|
}
|
|
675
694
|
/**
|
|
676
695
|
* The first half of {@link applySeed}: check the signature and the signer's trust, then merge
|
|
677
696
|
* the seed's peer addresses into the peer store. Every rejection happens here, so this result is
|
|
678
|
-
* what the inbound handler acks with; the
|
|
697
|
+
* what the inbound handler acks with; the dials that follow cannot change it.
|
|
679
698
|
*/
|
|
680
699
|
async verifyAndMergeSeed(seed, options) {
|
|
681
700
|
if (!this.libp2pNode) {
|
|
@@ -709,7 +728,7 @@ export class SeedBootstrapService {
|
|
|
709
728
|
partyId: seed.partyId,
|
|
710
729
|
signerKey: seed.signerKey,
|
|
711
730
|
knownOwnerKeys,
|
|
712
|
-
// Optional-chained like `
|
|
731
|
+
// Optional-chained like `dialPreferredPeers`: partial libp2p handles (unit-test doubles)
|
|
713
732
|
// omit `peerId`. A real node always has one, and a proof bound to a real peer id can
|
|
714
733
|
// only fail against '' — the safe direction.
|
|
715
734
|
localPeerId: this.libp2pNode.peerId?.toString() ?? '',
|
|
@@ -723,54 +742,51 @@ export class SeedBootstrapService {
|
|
|
723
742
|
await this.anchorAcceptedSigner(seed.signerKey, decision);
|
|
724
743
|
const peersAdded = await mergeSeedPeers(this.libp2pNode, seed.peers);
|
|
725
744
|
log('Merged seed: %d peers added', peersAdded);
|
|
726
|
-
return { success: true, peersAdded,
|
|
745
|
+
return { success: true, peersAdded, preferredDialsAttempted: 0, preferredDialsFailed: 0 };
|
|
727
746
|
}
|
|
728
747
|
/**
|
|
729
|
-
* The second half of {@link applySeed}: dial the seed's
|
|
748
|
+
* The second half of {@link applySeed}: dial the seed's preferred peers (`SeedPeer.preferred`:
|
|
749
|
+
* the machine that minted it and every always-on machine) to establish connections.
|
|
730
750
|
*
|
|
731
|
-
* Best-effort and COUNTED:
|
|
751
|
+
* Best-effort and COUNTED: a peer that is momentarily down leaves this node seeded but
|
|
732
752
|
* unconnected, which the caller can only see if the outcome is reported (see
|
|
733
|
-
* `ApplySeedResult.
|
|
753
|
+
* `ApplySeedResult.preferredDialsFailed`). Recovery is not this loop's job —
|
|
734
754
|
* `CadreNode.dialColdStartBootstrap` retries these same addresses on every control-cohort
|
|
735
755
|
* reconcile pass until the control database has siblings.
|
|
736
756
|
*
|
|
737
|
-
* The inbound handler runs this AFTER it has acked and
|
|
738
|
-
* ({@link handleSeedStream}), because an unreachable
|
|
757
|
+
* The inbound handler runs this AFTER it has acked, closed the stream and reported the seed
|
|
758
|
+
* applied ({@link handleSeedStream}), because an unreachable peer can take `dialBudget.totalMs`.
|
|
759
|
+
* The node may stop while these dials are in flight (cadre-cli restarts a claimed node on
|
|
760
|
+
* `seed:applied`): the node's stop aborts the dial in flight and every later one is refused
|
|
761
|
+
* at once (`dialAddr`), and each is counted as failed here.
|
|
739
762
|
*/
|
|
740
|
-
async
|
|
763
|
+
async dialPreferredPeers(seed) {
|
|
741
764
|
const node = this.libp2pNode;
|
|
742
765
|
if (!node) {
|
|
743
|
-
log('Seed service shut down before its
|
|
744
|
-
return {
|
|
766
|
+
log('Seed service shut down before its dials; none attempted');
|
|
767
|
+
return { preferredDialsAttempted: 0, preferredDialsFailed: 0 };
|
|
745
768
|
}
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
// Every one of an owner's addresses is a candidate, each on its own time limit
|
|
752
|
-
// (`dialPeerAddrs`), so an owner whose first address never answers neither
|
|
769
|
+
// Self is not dialed: dialing self always throws, which would report a healthy node as
|
|
770
|
+
// "seeded but stranded". Optional-chained: partial libp2p handles (unit-test doubles)
|
|
771
|
+
// omit `peerId`.
|
|
772
|
+
// Every one of a peer's addresses is a candidate, each on its own time limit
|
|
773
|
+
// (`dialPeerAddrs`), so a peer whose first address never answers neither
|
|
753
774
|
// stalls seed application nor goes undialed at its other addresses. The seed's
|
|
754
|
-
// addresses carry no peer id; the dial binds each to the
|
|
755
|
-
const
|
|
756
|
-
let
|
|
757
|
-
|
|
758
|
-
for (const peer of seed.peers.filter(p => p.isOwner)) {
|
|
759
|
-
if (peer.multiaddrs.length === 0 || peer.peerId === selfPeerId) {
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
ownerDialsAttempted++;
|
|
775
|
+
// addresses carry no peer id; the dial binds each to the peer's.
|
|
776
|
+
const targets = seedDialTargets(seed, node.peerId?.toString());
|
|
777
|
+
let preferredDialsFailed = 0;
|
|
778
|
+
for (const peer of targets) {
|
|
763
779
|
try {
|
|
764
780
|
const addrs = parseDialAddrs(peer.multiaddrs);
|
|
765
|
-
log('Dialing
|
|
766
|
-
await dialPeerAddrs(node, peer.peerId, addrs, this.dialBudget, `
|
|
781
|
+
log('Dialing preferred seed peer: %s (%d addr(s))', peer.peerId, addrs.length);
|
|
782
|
+
await dialPeerAddrs(node, peer.peerId, addrs, this.dialBudget, `Seed dial of ${peer.peerId}`);
|
|
767
783
|
}
|
|
768
784
|
catch (error) {
|
|
769
|
-
// Counted even when the
|
|
770
|
-
// not connected to it, and `
|
|
771
|
-
|
|
785
|
+
// Counted even when the peer reaches us only through our own relay: this node is still
|
|
786
|
+
// not connected to it, and `preferredDialsFailed` is how the caller learns that.
|
|
787
|
+
preferredDialsFailed++;
|
|
772
788
|
if (error instanceof SelfRelayOnlyError) {
|
|
773
|
-
log('
|
|
789
|
+
log('Seed peer %s is reachable only by relaying through this node; waiting for it to reconnect', peer.peerId);
|
|
774
790
|
}
|
|
775
791
|
else {
|
|
776
792
|
log('Failed to dial peer %s: %o', peer.peerId, error);
|
|
@@ -778,8 +794,8 @@ export class SeedBootstrapService {
|
|
|
778
794
|
// Continue - not all peers need to be reachable
|
|
779
795
|
}
|
|
780
796
|
}
|
|
781
|
-
log('Dialed seed
|
|
782
|
-
return {
|
|
797
|
+
log('Dialed preferred seed peers: %d/%d dial(s) failed', preferredDialsFailed, targets.length);
|
|
798
|
+
return { preferredDialsAttempted: targets.length, preferredDialsFailed };
|
|
783
799
|
}
|
|
784
800
|
/**
|
|
785
801
|
* Persist a signer that a pin/TOFU accepted into the node-local anchor, so a
|
|
@@ -883,7 +899,7 @@ export class SeedBootstrapService {
|
|
|
883
899
|
}
|
|
884
900
|
const peerId = peerIdFromString(target.peerId);
|
|
885
901
|
const addrs = deliveryDialAddrs(target.peerId, target.multiaddrs);
|
|
886
|
-
await mergeSeedPeers(node, [{ peerId: target.peerId, multiaddrs: target.multiaddrs,
|
|
902
|
+
await mergeSeedPeers(node, [{ peerId: target.peerId, multiaddrs: target.multiaddrs, preferred: false }]);
|
|
887
903
|
if (addrs.length === 0) {
|
|
888
904
|
return (signal) => node.dialProtocol(peerId, SEED_PROTOCOL, { signal });
|
|
889
905
|
}
|
|
@@ -984,16 +1000,16 @@ export class SeedBootstrapService {
|
|
|
984
1000
|
}
|
|
985
1001
|
}
|
|
986
1002
|
/**
|
|
987
|
-
* The peers a seed carries: the control database's own projection
|
|
1003
|
+
* The peers a seed `minterPeerId` mints carries: the control database's own projection
|
|
988
1004
|
* ({@link ControlDatabase.querySeedPeers}, shared with the cadre invitation redemption
|
|
989
|
-
* reply), which reads through the revocation filter and
|
|
990
|
-
*
|
|
1005
|
+
* reply), which reads through the revocation filter and marks the minter and every
|
|
1006
|
+
* always-on machine preferred — a dial hint, not a trust decision.
|
|
991
1007
|
*/
|
|
992
|
-
async queryPeers() {
|
|
1008
|
+
async queryPeers(minterPeerId) {
|
|
993
1009
|
if (!this.controlDatabase) {
|
|
994
1010
|
return [];
|
|
995
1011
|
}
|
|
996
|
-
return await this.controlDatabase.querySeedPeers();
|
|
1012
|
+
return await this.controlDatabase.querySeedPeers(minterPeerId);
|
|
997
1013
|
}
|
|
998
1014
|
/**
|
|
999
1015
|
* Register the seed protocol handler. The inbound closure just delegates to
|
|
@@ -1009,14 +1025,16 @@ export class SeedBootstrapService {
|
|
|
1009
1025
|
}
|
|
1010
1026
|
/**
|
|
1011
1027
|
* Read one inbound seed frame, verify and merge it, write the ack and close the
|
|
1012
|
-
* stream, then dial the seed's
|
|
1028
|
+
* stream, report the seed applied, then dial the seed's preferred peers.
|
|
1013
1029
|
*
|
|
1014
|
-
* The ack
|
|
1015
|
-
* a dial for `dialBudget.totalMs` (86 s at the default declared link), which is
|
|
1016
|
-
* link time and does not belong inside the sender's delivery deadline
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
*
|
|
1030
|
+
* The ack, the close and `onSeedApplied` come BEFORE those dials: an unreachable peer
|
|
1031
|
+
* can hold a dial for `dialBudget.totalMs` (86 s at the default declared link), which is
|
|
1032
|
+
* not link time and does not belong inside the sender's delivery deadline, nor ahead of
|
|
1033
|
+
* a claimed node's restart (which stops this node, so its dials would be wasted). The
|
|
1034
|
+
* close is what releases the sender, which reads the ack to end-of-stream, and it is
|
|
1035
|
+
* awaited before `onSeedApplied`, so the ack is flushed before anything the callback
|
|
1036
|
+
* starts. The stream stays counted in {@link activeStreams} through the dials, so
|
|
1037
|
+
* {@link maxConcurrentSeeds} still bounds concurrent dial phases.
|
|
1020
1038
|
*
|
|
1021
1039
|
* Hardened against a buggy/compromised own-cadre node: a concurrency cap (over
|
|
1022
1040
|
* {@link maxConcurrentSeeds}, reply without applying), a read timeout (a peer
|
|
@@ -1045,8 +1063,8 @@ export class SeedBootstrapService {
|
|
|
1045
1063
|
acked = true;
|
|
1046
1064
|
await replyAndClose(stream, { accepted: merged.success, reason: merged.error, code: merged.code }, 'Seed');
|
|
1047
1065
|
if (merged.success) {
|
|
1048
|
-
await this.dialSeedOwners(seed);
|
|
1049
1066
|
this.eventCallbacks.onSeedApplied?.(seed.partyId, merged.peersAdded, seed);
|
|
1067
|
+
await this.dialPreferredPeers(seed);
|
|
1050
1068
|
}
|
|
1051
1069
|
else {
|
|
1052
1070
|
this.eventCallbacks.onSeedError?.(seed.partyId, merged.error ?? 'Unknown error');
|
|
@@ -1117,16 +1135,20 @@ export class SeedBootstrapService {
|
|
|
1117
1135
|
* does not listen. Call `CadreNode.reconcileControlCohort()` after delivering
|
|
1118
1136
|
* the seed to dial at once.
|
|
1119
1137
|
*
|
|
1138
|
+
* The vouch and the seed are one owner approval.
|
|
1139
|
+
*
|
|
1120
1140
|
* @param options - Drone peer info from provider API
|
|
1121
1141
|
* @returns Seed and encoded seed for drone initialization
|
|
1122
1142
|
*/
|
|
1123
|
-
async addDrone(options) {
|
|
1143
|
+
async addDrone(options, sign) {
|
|
1124
1144
|
const { dronePeerId, droneMultiaddrs } = options;
|
|
1125
1145
|
log('Adding drone: %s', dronePeerId);
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1146
|
+
const seed = await this.withOwnerSign(`Add the machine ${dronePeerId} to the cadre`, sign, async (approved) => {
|
|
1147
|
+
// 1. Authorize the new drone peer
|
|
1148
|
+
await this.authorizePeer({ peerId: dronePeerId, multiaddrs: droneMultiaddrs }, approved);
|
|
1149
|
+
// 2. Create seed with current state
|
|
1150
|
+
return await this.createSeed(approved);
|
|
1151
|
+
});
|
|
1130
1152
|
// 3. Encode for transport
|
|
1131
1153
|
const encodedSeed = this.encodeSeed(seed);
|
|
1132
1154
|
log('Drone %s added, seed created with %d peers', dronePeerId, seed.peers.length);
|
|
@@ -1139,25 +1161,20 @@ export class SeedBootstrapService {
|
|
|
1139
1161
|
* 1. Authorizes the new phone peer
|
|
1140
1162
|
* 2. Creates a seed with relay addresses for dialing
|
|
1141
1163
|
*
|
|
1164
|
+
* The vouch and the seed are one owner approval.
|
|
1165
|
+
*
|
|
1142
1166
|
* @param phonePeerId - Peer ID of the new phone
|
|
1143
1167
|
* @returns Seed with relay addresses for out-of-band delivery
|
|
1144
1168
|
*/
|
|
1145
|
-
async addPhoneWithRelay(phonePeerId) {
|
|
1169
|
+
async addPhoneWithRelay(phonePeerId, sign) {
|
|
1146
1170
|
log('Adding phone with relay: %s', phonePeerId);
|
|
1147
|
-
// 1. Authorize the new phone peer (no multiaddrs - NAT'd)
|
|
1148
|
-
await this.authorizePeer({ peerId: phonePeerId });
|
|
1149
|
-
// 2. Get relay address for this node
|
|
1150
1171
|
const relayAddr = await this.getRelayAddress();
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
if (ourPeer && !ourPeer.multiaddrs.includes(relayAddr)) {
|
|
1158
|
-
ourPeer.multiaddrs.push(relayAddr);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1172
|
+
const seed = await this.withOwnerSign(`Add the phone ${phonePeerId} to the cadre`, sign, async (approved) => {
|
|
1173
|
+
// 1. Authorize the new phone peer (no multiaddrs - NAT'd)
|
|
1174
|
+
await this.authorizePeer({ peerId: phonePeerId }, approved);
|
|
1175
|
+
// 2. Create seed - with our relay address in our own entry, when we have one
|
|
1176
|
+
return await this.mintSeed(approved, relayAddr);
|
|
1177
|
+
});
|
|
1161
1178
|
const encodedSeed = this.encodeSeed(seed);
|
|
1162
1179
|
log('Phone %s added with relay, seed created', phonePeerId);
|
|
1163
1180
|
return { seed, encodedSeed };
|