@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The strands this party joined from ANOTHER party, held in two records.
|
|
3
3
|
*
|
|
4
4
|
* A party's own strands come back after a restart through its control database: the
|
|
5
|
-
* `Strand` table holds their rows, the strand watcher
|
|
5
|
+
* `Strand` table holds their rows, the strand watcher reads it, and each row no sApp
|
|
6
6
|
* config claims is offered as `strand:discovered`. A strand joined through formation with
|
|
7
7
|
* another party (`CadreNode.formStrand`) has its `Strand` row in the OTHER party's control
|
|
8
8
|
* database, so it needs a record of its own (gotchoices/sereus#18):
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* published". A machine that is not an owner never publishes; its joins stay here.
|
|
18
18
|
*
|
|
19
19
|
* {@link JoinedStrandSession} moves joins between the two and is what the strand watcher
|
|
20
|
-
*
|
|
20
|
+
* reads beside the control rows.
|
|
21
21
|
*
|
|
22
22
|
* Two store implementations:
|
|
23
23
|
* - {@link MemoryJoinedStrandStore} — joins die with the process. The default only for a
|
|
@@ -69,11 +69,15 @@ export interface JoinedStrandStore {
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* The {@link StrandRow} a record stands for — the shape `addStrand` and `strand:discovered`
|
|
72
|
-
* carry.
|
|
73
|
-
* reads null as "not mine" and joins.
|
|
72
|
+
* carry. The founder columns are null because a joiner never founds; `CadreNode.launchStrand`
|
|
73
|
+
* reads a null `FounderPeerId` as "not mine" and joins.
|
|
74
74
|
*/
|
|
75
75
|
export function joinedStrandRow(record: JoinedStrandRecord): StrandRow {
|
|
76
|
-
return { Id: record.Id, Type: record.Type, MemberPrivateKey: record.MemberPrivateKey, FounderOwnerKey: null };
|
|
76
|
+
return { Id: record.Id, Type: record.Type, MemberPrivateKey: record.MemberPrivateKey, FounderOwnerKey: null, FounderPeerId: null };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isSameJoin(record: JoinedStrandRecord, row: StrandRow): boolean {
|
|
80
|
+
return record.Type === row.Type && record.MemberPrivateKey === row.MemberPrivateKey;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
83
|
/**
|
|
@@ -124,9 +128,9 @@ const SLOT_PREFIX = 'cadre/joined-strand/';
|
|
|
124
128
|
* arbitrary text, and base64url never contains the `/` that ends the segment.
|
|
125
129
|
*
|
|
126
130
|
* Loads every slot once, at the first {@link list}, then answers from memory and writes
|
|
127
|
-
* through, so
|
|
128
|
-
*
|
|
129
|
-
*
|
|
131
|
+
* through, so a strand watcher run does not read the enclave each time. A load that
|
|
132
|
+
* fails — `KeyStoreAccessError` from a refused unlock prompt, say — is rethrown rather
|
|
133
|
+
* than read as "no joins" and retried at the next call, exactly as
|
|
130
134
|
* `loadOrCreateIdentityKey` treats the identity slot. A slot that does not parse as a
|
|
131
135
|
* record for its own id is dropped with a log and its siblings kept: refusing to start
|
|
132
136
|
* repairs nothing, and the other joins are still good.
|
|
@@ -176,9 +180,10 @@ export class KeyStoreJoinedStrandStore implements JoinedStrandStore {
|
|
|
176
180
|
return result;
|
|
177
181
|
}
|
|
178
182
|
|
|
179
|
-
// NOTE: a failed load is retried at the next list(), which is every strand-watcher
|
|
180
|
-
//
|
|
181
|
-
//
|
|
183
|
+
// NOTE: a failed load is retried at the next list(), which is every strand-watcher run (a
|
|
184
|
+
// change to `Strand` or `JoinedStrand`, a local join, a retry, the backstop); over a backend
|
|
185
|
+
// that gates reads behind an unlock prompt the user keeps declining, that is a prompt per
|
|
186
|
+
// run. Every KeyStore shipped today is ungated for this reason
|
|
182
187
|
// (see `SecureStoreKeyStoreOptions.requireAuthentication`); if one is ever gated, back
|
|
183
188
|
// off the retry here.
|
|
184
189
|
private async loaded(): Promise<Map<string, JoinedStrandRecord>> {
|
|
@@ -234,7 +239,7 @@ function parseRecord(strandId: string, bytes: Uint8Array): JoinedStrandRecord |
|
|
|
234
239
|
* key; this module sees only the seam, so it stays free of the runtime.
|
|
235
240
|
*/
|
|
236
241
|
export interface PartyJoinedStrandLedger {
|
|
237
|
-
/** Every party-wide join, as offerable rows (
|
|
242
|
+
/** Every party-wide join, as offerable rows (null founder columns). */
|
|
238
243
|
list(): Promise<StrandRow[]>;
|
|
239
244
|
/** Whether this party's control database names `strandId`, as its own strand or as a party-wide join. */
|
|
240
245
|
names(strandId: string): Promise<boolean>;
|
|
@@ -251,7 +256,7 @@ export interface PartyJoinedStrandLedger {
|
|
|
251
256
|
* the moves between them. Rebuilt at every start, which is what ends the per-session state
|
|
252
257
|
* below.
|
|
253
258
|
*
|
|
254
|
-
* The watcher reads a row missing from a
|
|
259
|
+
* The watcher reads a row missing from a run's read as a removal and detaches the strand, so this
|
|
255
260
|
* holds the state a plain union would get wrong:
|
|
256
261
|
* - the last list each record answered, reused when a later read fails, so a failed read
|
|
257
262
|
* is not "every join was forgotten";
|
|
@@ -262,6 +267,8 @@ export interface PartyJoinedStrandLedger {
|
|
|
262
267
|
export class JoinedStrandSession {
|
|
263
268
|
private lastUnpublished: JoinedStrandRecord[] = [];
|
|
264
269
|
private lastPartyWide: StrandRow[] = [];
|
|
270
|
+
/** Whether the last party-wide read in {@link withControlRows} succeeded; see {@link lastReadPartial}. */
|
|
271
|
+
private lastPartyWideFresh = true;
|
|
265
272
|
private readonly keptForSession = new Map<string, StrandRow>();
|
|
266
273
|
/** Strands whose party-wide row {@link syncWithParty} removes, queued by a self-revocation. */
|
|
267
274
|
private readonly pendingRemovals = new Set<string>();
|
|
@@ -272,13 +279,23 @@ export class JoinedStrandSession {
|
|
|
272
279
|
*/
|
|
273
280
|
private exclusive: Promise<unknown> = Promise.resolve();
|
|
274
281
|
|
|
275
|
-
|
|
282
|
+
/**
|
|
283
|
+
* @param onLocalChange - Called after a move changes what {@link withControlRows} merges in
|
|
284
|
+
* from outside the control database (this machine's unpublished joins, the joins kept for
|
|
285
|
+
* this session), which no control-table change event reports. `CadreNode` wakes its strand
|
|
286
|
+
* watcher with it.
|
|
287
|
+
*/
|
|
288
|
+
constructor(
|
|
289
|
+
private readonly store: JoinedStrandStore,
|
|
290
|
+
private readonly ledger: PartyJoinedStrandLedger,
|
|
291
|
+
private readonly onLocalChange: () => void = () => {}
|
|
292
|
+
) {}
|
|
276
293
|
|
|
277
294
|
/**
|
|
278
295
|
* `control` plus every join, one row per id, in this precedence: the control rows (the
|
|
279
296
|
* party's own strands), then the party-wide joins, then this machine's unpublished joins
|
|
280
297
|
* and the joins kept for this session. A local record named by either of the first two in
|
|
281
|
-
* a read that succeeded this
|
|
298
|
+
* a read that succeeded this run is stale — published, or the strand is the party's own
|
|
282
299
|
* now — so it is forgotten here; this also cleans up after a crash between a publish and
|
|
283
300
|
* its local forget. One named only by the last-good party-wide list is kept: that list may
|
|
284
301
|
* predate a leave the record re-joined after.
|
|
@@ -289,7 +306,7 @@ export class JoinedStrandSession {
|
|
|
289
306
|
const offered = new Map<string, StrandRow>(control.map((row) => [row.Id, row]));
|
|
290
307
|
const partyWide = await this.partyWide();
|
|
291
308
|
// NOTE: a party-wide join whose id the party's own `Strand` table also holds (the party
|
|
292
|
-
// founded AND joined it — contrived) is shadowed here, not deleted: a watcher
|
|
309
|
+
// founded AND joined it — contrived) is shadowed here, not deleted: a watcher run must
|
|
293
310
|
// not make owner-signed writes. If the party later unpublishes its own row the join row
|
|
294
311
|
// resurfaces; if that is ever seen, remove the join row inside `unpublishStrand`.
|
|
295
312
|
for (const row of partyWide.rows) {
|
|
@@ -317,10 +334,18 @@ export class JoinedStrandSession {
|
|
|
317
334
|
return [...offered.values()];
|
|
318
335
|
}
|
|
319
336
|
|
|
337
|
+
/**
|
|
338
|
+
* True when the last {@link withControlRows} offered the party-wide joins from the last
|
|
339
|
+
* list that answered, because its read of `JoinedStrand` failed.
|
|
340
|
+
*/
|
|
341
|
+
lastReadPartial(): boolean {
|
|
342
|
+
return !this.lastPartyWideFresh;
|
|
343
|
+
}
|
|
344
|
+
|
|
320
345
|
/**
|
|
321
346
|
* This machine's unpublished joins, straight from the store (never the last-good list a
|
|
322
347
|
* failed read stands in with). What a strand move exports beside the `JoinedStrand`
|
|
323
|
-
* table: a machine publishes only while
|
|
348
|
+
* table: a machine publishes only while a sibling is reachable, so until then its
|
|
324
349
|
* joins live only here.
|
|
325
350
|
*/
|
|
326
351
|
unpublishedRecords(): Promise<JoinedStrandRecord[]> {
|
|
@@ -331,21 +356,32 @@ export class JoinedStrandSession {
|
|
|
331
356
|
async remember(record: JoinedStrandRecord): Promise<void> {
|
|
332
357
|
this.pendingRemovals.delete(record.Id);
|
|
333
358
|
await this.store.record(record);
|
|
359
|
+
this.onLocalChange();
|
|
334
360
|
}
|
|
335
361
|
|
|
336
362
|
/**
|
|
337
363
|
* Record `row`, joined by `addStrand`, unless this party's control database already names
|
|
338
364
|
* it (its own strand, or a join already published) or an identical record is already here
|
|
339
|
-
* (an app re-claiming a remembered join on every start
|
|
365
|
+
* (an app re-claiming a remembered join on every start, or `addStrand` right after
|
|
366
|
+
* `formStrand`). The store is asked first: it answers from memory once loaded, so the
|
|
367
|
+
* common re-claim reads no control row. Keeps the first `joinedAt`. Like
|
|
340
368
|
* {@link remember}, cancels a queued removal.
|
|
341
369
|
*/
|
|
342
370
|
async rememberForeign(row: StrandRow): Promise<void> {
|
|
343
371
|
this.pendingRemovals.delete(row.Id);
|
|
372
|
+
const early = await this.storedRecord(row.Id).catch((error: unknown) => {
|
|
373
|
+
// Asked again below only when the party does not name the strand; a second failure throws.
|
|
374
|
+
log('joined strand %s: store lookup failed; asking the control database first: %o', row.Id, error);
|
|
375
|
+
return null;
|
|
376
|
+
});
|
|
377
|
+
if (early && isSameJoin(early, row)) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
344
380
|
if (await this.namedByParty(row.Id)) {
|
|
345
381
|
return;
|
|
346
382
|
}
|
|
347
|
-
const existing =
|
|
348
|
-
if (existing
|
|
383
|
+
const existing = early === null ? await this.storedRecord(row.Id) : early;
|
|
384
|
+
if (existing && isSameJoin(existing, row)) {
|
|
349
385
|
return;
|
|
350
386
|
}
|
|
351
387
|
await this.store.record({
|
|
@@ -354,6 +390,7 @@ export class JoinedStrandSession {
|
|
|
354
390
|
MemberPrivateKey: row.MemberPrivateKey,
|
|
355
391
|
joinedAt: existing?.joinedAt ?? Date.now()
|
|
356
392
|
});
|
|
393
|
+
this.onLocalChange();
|
|
357
394
|
}
|
|
358
395
|
|
|
359
396
|
/**
|
|
@@ -367,6 +404,7 @@ export class JoinedStrandSession {
|
|
|
367
404
|
this.keptForSession.delete(strandId);
|
|
368
405
|
this.lastPartyWide = this.lastPartyWide.filter((row) => row.Id !== strandId);
|
|
369
406
|
await this.store.forget(strandId);
|
|
407
|
+
this.onLocalChange();
|
|
370
408
|
});
|
|
371
409
|
}
|
|
372
410
|
|
|
@@ -385,13 +423,14 @@ export class JoinedStrandSession {
|
|
|
385
423
|
if (!row) {
|
|
386
424
|
return;
|
|
387
425
|
}
|
|
388
|
-
// Kept BEFORE the store forgets, so no
|
|
426
|
+
// Kept BEFORE the store forgets, so no watcher run can union without it. Queued even for a
|
|
389
427
|
// local-only join, which a sibling may have published already.
|
|
390
428
|
this.keptForSession.set(strandId, row);
|
|
391
429
|
this.pendingRemovals.add(strandId);
|
|
392
430
|
if (local) {
|
|
393
431
|
await this.store.forget(strandId);
|
|
394
432
|
}
|
|
433
|
+
this.onLocalChange();
|
|
395
434
|
});
|
|
396
435
|
}
|
|
397
436
|
|
|
@@ -423,15 +462,16 @@ export class JoinedStrandSession {
|
|
|
423
462
|
}
|
|
424
463
|
|
|
425
464
|
// NOTE: a sibling that joined the same strand and has not yet published publishes it after
|
|
426
|
-
// a leave, bringing the strand back party-wide. Needs
|
|
427
|
-
// a reconcile
|
|
465
|
+
// a leave, bringing the strand back party-wide. Needs a second device to have joined the
|
|
466
|
+
// strand and not yet run a connected reconcile pass when the leave lands (a join starts no
|
|
467
|
+
// pass, so up to the 2-minute backstop); if it is ever seen, check the strand's `Revocation`
|
|
428
468
|
// tombstone before publishing.
|
|
429
469
|
private async publish(record: JoinedStrandRecord): Promise<void> {
|
|
430
470
|
try {
|
|
431
471
|
if (!(await this.ledger.names(record.Id))) {
|
|
432
472
|
await this.ledger.publish(record);
|
|
433
473
|
// Into the last-good list too: its local record goes next, so a party-wide read that
|
|
434
|
-
// fails before one succeeds would otherwise drop the strand from the
|
|
474
|
+
// fails before one succeeds would otherwise drop the strand from the watcher's read and hide it
|
|
435
475
|
// from a self-revocation.
|
|
436
476
|
this.lastPartyWide = [...this.lastPartyWide.filter((row) => row.Id !== record.Id), joinedStrandRow(record)];
|
|
437
477
|
log('joined strand %s published party-wide (party=%s)', record.Id, this.store.partyId);
|
|
@@ -457,19 +497,20 @@ export class JoinedStrandSession {
|
|
|
457
497
|
private async partyWide(): Promise<{ rows: StrandRow[]; fresh: boolean }> {
|
|
458
498
|
try {
|
|
459
499
|
this.lastPartyWide = await this.ledger.list();
|
|
460
|
-
|
|
500
|
+
this.lastPartyWideFresh = true;
|
|
461
501
|
} catch (error) {
|
|
462
502
|
log('party-wide joined-strand read failed; offering the last list it answered (%d join(s)): %o',
|
|
463
503
|
this.lastPartyWide.length, error);
|
|
464
|
-
|
|
504
|
+
this.lastPartyWideFresh = false;
|
|
465
505
|
}
|
|
506
|
+
return { rows: this.lastPartyWide, fresh: this.lastPartyWideFresh };
|
|
466
507
|
}
|
|
467
508
|
|
|
468
509
|
/**
|
|
469
510
|
* {@link PartyJoinedStrandLedger.names}, answering false when the control database cannot
|
|
470
511
|
* be read (a machine cut off from its party before it received the `JoinedStrand` block).
|
|
471
512
|
* Recording is the safe side: a local record the party turns out to name is forgotten by
|
|
472
|
-
* the next
|
|
513
|
+
* the next watcher run that reads it, while a join with no record is not offered after a restart.
|
|
473
514
|
*/
|
|
474
515
|
private async namedByParty(strandId: string): Promise<boolean> {
|
|
475
516
|
try {
|
|
@@ -480,6 +521,10 @@ export class JoinedStrandSession {
|
|
|
480
521
|
}
|
|
481
522
|
}
|
|
482
523
|
|
|
524
|
+
private async storedRecord(strandId: string): Promise<JoinedStrandRecord | undefined> {
|
|
525
|
+
return (await this.store.list()).find((record) => record.Id === strandId);
|
|
526
|
+
}
|
|
527
|
+
|
|
483
528
|
/** Run `work` after every earlier serialized move settles; see {@link exclusive}. */
|
|
484
529
|
private serialized<T>(work: () => Promise<T>): Promise<T> {
|
|
485
530
|
const result = this.exclusive.then(work);
|
|
@@ -502,7 +547,7 @@ export class JoinedStrandSession {
|
|
|
502
547
|
await this.store.forget(strandId);
|
|
503
548
|
log('joined strand %s is named by this party\'s control database — local record forgotten', strandId);
|
|
504
549
|
} catch (error) {
|
|
505
|
-
log('forgetting superseded joined strand %s failed; the next
|
|
550
|
+
log('forgetting superseded joined strand %s failed; the next watcher run retries: %o', strandId, error);
|
|
506
551
|
}
|
|
507
552
|
}
|
|
508
553
|
}
|
package/src/key-store.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Backend-agnostic store for raw private key material.
|
|
3
3
|
*
|
|
4
4
|
* Mobile cadre nodes hold sensitive key material at rest: the libp2p
|
|
5
|
-
* peer/node identity key and
|
|
6
|
-
*
|
|
7
|
-
* through so a platform-secure backend (iOS Keychain / Android Keystore)
|
|
8
|
-
* plugged in without `@serfab/cadre-core` taking any platform dependency.
|
|
5
|
+
* peer/node identity key and, on a device the user made an owner, an owner key
|
|
6
|
+
* in a slot of its own (`owner-key-store.ts`). This module defines the seam those
|
|
7
|
+
* keys flow through so a platform-secure backend (iOS Keychain / Android Keystore)
|
|
8
|
+
* can be plugged in without `@serfab/cadre-core` taking any platform dependency.
|
|
9
9
|
*
|
|
10
10
|
* This file is dependency-free (no node:/RN imports) so it is safe in every
|
|
11
11
|
* entry graph. The Node reference {@link FileKeyStore} lives in a separate
|
|
@@ -23,6 +23,12 @@ export type KeyId = string;
|
|
|
23
23
|
*/
|
|
24
24
|
export const DEFAULT_IDENTITY_KEY_ID: KeyId = 'cadre/identity';
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Default slot id for an owner key (`owner-key-store.ts`). Not party-scoped: one
|
|
28
|
+
* owner key may own several cadres, each owner signature binding its party id.
|
|
29
|
+
*/
|
|
30
|
+
export const DEFAULT_OWNER_KEY_ID: KeyId = 'cadre/owner';
|
|
31
|
+
|
|
26
32
|
/**
|
|
27
33
|
* Backend-agnostic store for raw private key material. Implementations may be
|
|
28
34
|
* file-based, OS-keyring, or platform secure-enclave backed (iOS Keychain /
|
package/src/link-budget.ts
CHANGED
|
@@ -392,13 +392,15 @@ export const PUSH_TRANSFER_ALLOWANCE_MS = 6000;
|
|
|
392
392
|
* every members-only protocol from the materialized snapshot, and unplaced
|
|
393
393
|
* relay reservations stay capped (`MAX_UNAUTHORIZED_RELAY_RESERVATIONS`).
|
|
394
394
|
* - **The slow case is bring-up, not steady state.** A membership read consults
|
|
395
|
-
* the cohort only for a block this node does not hold
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
395
|
+
* the cohort only for a block this node does not hold: before a joining
|
|
396
|
+
* machine has read it from the cohort, or for a table nobody has written on a
|
|
397
|
+
* party founded before genesis created every control collection. Otherwise
|
|
398
|
+
* every block the decision reads is held
|
|
399
|
+
* (`control-founding-consult-budget.spec.ts` pins that). So the gate always
|
|
400
|
+
* fails open only on a node whose membership reads still consult: one consult
|
|
401
|
+
* costs about two link round trips, and asking a silent peer costs the
|
|
402
|
+
* per-peer read deadline ({@link cohortReadDeadlineMs}, 7 s at the default
|
|
403
|
+
* declaration).
|
|
402
404
|
* - **The decision is spent on the dialing machine's clock.** libp2p's listener
|
|
403
405
|
* runs this gate before it answers the multiplexer negotiation the dialer is
|
|
404
406
|
* waiting on (`libp2p/dist/src/upgrader.js`), and the listener's own
|
package/src/machine-removal.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Removing a machine from a cadre: the listing an app shows (`CadreNode.listMachines`)
|
|
3
|
-
* read-only plan behind its confirmation step (`CadreNode.planMachineRemoval`)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `ControlDatabase.removeMachine`'s.
|
|
2
|
+
* Removing a machine from a cadre: the listing an app shows (`CadreNode.listMachines`) and the
|
|
3
|
+
* read-only plan behind its confirmation step (`CadreNode.planMachineRemoval`). Pure
|
|
4
|
+
* projections over rows the node has already read; the reads and writes themselves are
|
|
5
|
+
* `CadreNode.removeMachine`'s and `ControlDatabase.removeMachine`'s.
|
|
7
6
|
*
|
|
8
|
-
* A machine
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* and an owner key with no machine row is still listed, as the machine it names.
|
|
7
|
+
* A machine is a `CadrePeer` row and nothing more: an owner key is the person's, so removing a
|
|
8
|
+
* machine leaves every owner key standing, the one the machine's user signed with included
|
|
9
|
+
* (removing one is `CadreNode.removeOwner`, `owner-keys.ts`).
|
|
12
10
|
*/
|
|
13
11
|
|
|
14
|
-
import { ed25519PublicKeyB64FromPeerId
|
|
12
|
+
import { ed25519PublicKeyB64FromPeerId } from './ed25519-key.js';
|
|
15
13
|
import { verifyPeerRecordSignature } from './peer-record.js';
|
|
16
14
|
import type {
|
|
17
15
|
CadreInviteStatus,
|
|
@@ -20,8 +18,6 @@ import type {
|
|
|
20
18
|
CadrePeerRow,
|
|
21
19
|
MachineRemovalPlan,
|
|
22
20
|
MachineRemovalRefusal,
|
|
23
|
-
OwnerKeyRow,
|
|
24
|
-
RemoveMachineOptions,
|
|
25
21
|
} from './types.js';
|
|
26
22
|
|
|
27
23
|
/**
|
|
@@ -42,8 +38,7 @@ export class MachineRemovalRefusedError extends Error {
|
|
|
42
38
|
|
|
43
39
|
const MACHINE_REMOVAL_REFUSAL_REASON: Record<MachineRemovalRefusal, string> = {
|
|
44
40
|
'not-an-owner': 'this node holds no enrolled owner key',
|
|
45
|
-
'self': '
|
|
46
|
-
'last-owner': 'it holds the only live owner key',
|
|
41
|
+
'self': 'it is this node; leaving a cadre is stopping the node and deleting its data',
|
|
47
42
|
};
|
|
48
43
|
|
|
49
44
|
/** What {@link describeMachines} projects from: one node's live rows and its own predicate. */
|
|
@@ -52,45 +47,21 @@ export interface MachineListingInput {
|
|
|
52
47
|
selfPeerId: string | null;
|
|
53
48
|
/** Live `CadrePeer` rows (`ControlDatabase.queryCadrePeers`). */
|
|
54
49
|
rows: readonly CadrePeerRow[];
|
|
55
|
-
/** Live `OwnerKey` rows (`ControlDatabase.queryOwnerKeyRows`). */
|
|
56
|
-
owners: readonly OwnerKeyRow[];
|
|
57
50
|
/** This node's authorized-membership predicate for one row (`CadreNode.listAuthorizedMembers`'s). */
|
|
58
51
|
trusted: (row: CadrePeerRow) => boolean;
|
|
59
52
|
}
|
|
60
53
|
|
|
61
54
|
/**
|
|
62
|
-
* One entry per live `CadrePeer` row
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* bump `UpdatedAt` without re-signing.
|
|
55
|
+
* One entry per live `CadrePeer` row. `profile` and `lastPublishedAt` are reported only when
|
|
56
|
+
* the row's self-signature verifies under the key behind its peer id, as
|
|
57
|
+
* `CadreNode.resolvePeerAddrs` judges the same record: no insert rule constrains `Profile`, and
|
|
58
|
+
* an owner re-vouch may bump `UpdatedAt` without re-signing.
|
|
67
59
|
*/
|
|
68
60
|
export function describeMachines(input: MachineListingInput): CadreMachine[] {
|
|
69
|
-
|
|
70
|
-
const machines = input.rows.map((row) => describeMemberRow(input, row, ownerKeys));
|
|
71
|
-
const listed = new Set(machines.map((machine) => machine.peerId));
|
|
72
|
-
for (const owner of input.owners) {
|
|
73
|
-
const peerId = peerIdFromEd25519PublicKeyB64(owner.key);
|
|
74
|
-
if (peerId === null || listed.has(peerId)) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
listed.add(peerId);
|
|
78
|
-
machines.push({
|
|
79
|
-
peerId,
|
|
80
|
-
publicKey: owner.key,
|
|
81
|
-
isSelf: peerId === input.selfPeerId,
|
|
82
|
-
isOwner: true,
|
|
83
|
-
isMember: false,
|
|
84
|
-
trusted: false,
|
|
85
|
-
profile: null,
|
|
86
|
-
lastPublishedAt: null,
|
|
87
|
-
vouchedBy: owner.vouchOwner,
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
return machines;
|
|
61
|
+
return input.rows.map((row) => describeMemberRow(input, row));
|
|
91
62
|
}
|
|
92
63
|
|
|
93
|
-
function describeMemberRow(input: MachineListingInput, row: CadrePeerRow
|
|
64
|
+
function describeMemberRow(input: MachineListingInput, row: CadrePeerRow): CadreMachine {
|
|
94
65
|
const publicKey = ed25519PublicKeyB64FromPeerId(row.peerId);
|
|
95
66
|
const selfPublished = publicKey !== null && row.publicKey === publicKey && row.sig !== null
|
|
96
67
|
&& verifyPeerRecordSignature(input.partyId, {
|
|
@@ -105,8 +76,6 @@ function describeMemberRow(input: MachineListingInput, row: CadrePeerRow, ownerK
|
|
|
105
76
|
peerId: row.peerId,
|
|
106
77
|
publicKey,
|
|
107
78
|
isSelf: row.peerId === input.selfPeerId,
|
|
108
|
-
isOwner: publicKey !== null && ownerKeys.has(publicKey),
|
|
109
|
-
isMember: true,
|
|
110
79
|
trusted: input.trusted(row),
|
|
111
80
|
profile: selfPublished ? row.profile : null,
|
|
112
81
|
lastPublishedAt: selfPublished ? row.updatedAt : null,
|
|
@@ -118,99 +87,45 @@ function describeMemberRow(input: MachineListingInput, row: CadrePeerRow, ownerK
|
|
|
118
87
|
export interface MachineRemovalPlanInput {
|
|
119
88
|
peerId: string;
|
|
120
89
|
machines: readonly CadreMachine[];
|
|
121
|
-
rows: readonly CadrePeerRow[];
|
|
122
|
-
owners: readonly OwnerKeyRow[];
|
|
123
90
|
selfPeerId: string | null;
|
|
124
|
-
/**
|
|
125
|
-
|
|
91
|
+
/** Whether this node's owner signer is an enrolled owner (`CadreNode.ownerSignerEnrolledIn`), so it can sign the removal. */
|
|
92
|
+
canSign: boolean;
|
|
126
93
|
invites: readonly CadreInviteStatus[];
|
|
127
94
|
usages: readonly CadreInviteUsageRow[];
|
|
128
95
|
}
|
|
129
96
|
|
|
130
97
|
/**
|
|
131
|
-
* The plan for removing `peerId` from these rows. The refusals come first and by name: a
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* owner, but the local count can be wrong under concurrency, so the name stays.
|
|
136
|
-
*
|
|
137
|
-
* `dependents` are the rows the machine's owner key vouched, which every reader judges
|
|
138
|
-
* against its anchor through `VouchOwner`: once the key is removed they stop passing the
|
|
139
|
-
* membership predicate on every machine unless re-vouched first. This node's own rows are
|
|
140
|
-
* reported as `ownDependency` instead, since it cannot re-vouch them itself.
|
|
98
|
+
* The plan for removing `peerId` from these rows. The refusals come first and by name: a node
|
|
99
|
+
* that cannot sign, and the node itself (leaving a cadre is the embedder's flow, which stops
|
|
100
|
+
* the node and deletes its data; the removal notice a machine acts on is built around another
|
|
101
|
+
* machine removing it).
|
|
141
102
|
*
|
|
142
103
|
* `invitations` are the live ones the machine could redeem to come back: targeted at it,
|
|
143
104
|
* or already redeemed by it and still redeemable (a multi-use invitation, or an open
|
|
144
|
-
*
|
|
105
|
+
* invitation inside its lifetime). The schema's rule: keeping a device out is removing
|
|
145
106
|
* its row and withdrawing the invitation.
|
|
146
107
|
*/
|
|
147
108
|
export function planMachineRemoval(input: MachineRemovalPlanInput): MachineRemovalPlan {
|
|
148
|
-
const machine = input.machines.find((candidate) => candidate.peerId === input.peerId) ?? null;
|
|
149
|
-
const key = machine?.isOwner ? machine.publicKey : null;
|
|
150
|
-
const refusal = refusalFor(input, machine);
|
|
151
|
-
const removesOwnerKey = key !== null;
|
|
152
109
|
return {
|
|
153
|
-
machine,
|
|
154
|
-
refusal,
|
|
155
|
-
removesOwnerKey,
|
|
156
|
-
dependents: removesOwnerKey ? dependentsOf(input, key) : { owners: [], peers: [] },
|
|
110
|
+
machine: input.machines.find((candidate) => candidate.peerId === input.peerId) ?? null,
|
|
111
|
+
refusal: refusalFor(input),
|
|
157
112
|
invitations: invitationsOpenTo(input),
|
|
158
|
-
ownDependency: removesOwnerKey && isOwnDependency(input, key),
|
|
159
113
|
};
|
|
160
114
|
}
|
|
161
115
|
|
|
162
|
-
function refusalFor(input: MachineRemovalPlanInput
|
|
163
|
-
if (input.
|
|
116
|
+
function refusalFor(input: MachineRemovalPlanInput): MachineRemovalRefusal | null {
|
|
117
|
+
if (!input.canSign) {
|
|
164
118
|
return 'not-an-owner';
|
|
165
119
|
}
|
|
166
120
|
if (input.peerId === input.selfPeerId) {
|
|
167
121
|
return 'self';
|
|
168
122
|
}
|
|
169
|
-
if (machine?.isOwner && input.owners.length <= 1) {
|
|
170
|
-
return 'last-owner';
|
|
171
|
-
}
|
|
172
123
|
return null;
|
|
173
124
|
}
|
|
174
125
|
|
|
175
|
-
function dependentsOf(input: MachineRemovalPlanInput, key: string): MachineRemovalPlan['dependents'] {
|
|
176
|
-
return {
|
|
177
|
-
owners: input.owners
|
|
178
|
-
.filter((owner) => owner.vouchOwner === key && owner.key !== key && owner.key !== input.selfOwnerKey)
|
|
179
|
-
.map((owner) => owner.key),
|
|
180
|
-
peers: input.rows
|
|
181
|
-
.filter((row) => row.vouchOwner === key && row.peerId !== input.peerId && row.peerId !== input.selfPeerId)
|
|
182
|
-
.map((row) => row.peerId),
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function isOwnDependency(input: MachineRemovalPlanInput, key: string): boolean {
|
|
187
|
-
const ownRow = input.rows.find((row) => row.peerId === input.selfPeerId);
|
|
188
|
-
const ownOwner = input.owners.find((owner) => owner.key === input.selfOwnerKey);
|
|
189
|
-
return ownRow?.vouchOwner === key || ownOwner?.vouchOwner === key;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
126
|
function invitationsOpenTo(input: MachineRemovalPlanInput): string[] {
|
|
193
127
|
const redeemed = new Set(input.usages.filter((usage) => usage.peerId === input.peerId).map((usage) => usage.inviteKey));
|
|
194
128
|
return input.invites
|
|
195
129
|
.filter((status) => status.live && (status.invite.peerId === input.peerId || redeemed.has(status.invite.key)))
|
|
196
130
|
.map((status) => status.invite.key);
|
|
197
131
|
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* The re-vouch a caller asked for, checked against the plan: every key and peer must be in
|
|
201
|
-
* `plan.dependents`, so a caller cannot re-vouch a row the removed owner never vouched (nor
|
|
202
|
-
* the machine itself, which `dependents.peers` excludes). Throws naming the offenders, before
|
|
203
|
-
* `CadreNode.removeMachine` writes anything. Duplicates are dropped.
|
|
204
|
-
*/
|
|
205
|
-
export function revouchWithinPlan(plan: MachineRemovalPlan, revouch: RemoveMachineOptions['revouch']): MachineRemovalPlan['dependents'] {
|
|
206
|
-
const owners = [...new Set(revouch?.owners ?? [])];
|
|
207
|
-
const peers = [...new Set(revouch?.peers ?? [])];
|
|
208
|
-
const strayOwners = owners.filter((key) => !plan.dependents.owners.includes(key));
|
|
209
|
-
const strayPeers = peers.filter((peerId) => !plan.dependents.peers.includes(peerId));
|
|
210
|
-
if (strayOwners.length > 0 || strayPeers.length > 0) {
|
|
211
|
-
throw new Error(
|
|
212
|
-
`removeMachine: re-vouch names rows the removed machine did not vouch: ${[...strayOwners, ...strayPeers].join(', ')}`,
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
return { owners, peers };
|
|
216
|
-
}
|