@serfab/cadre-core 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +55 -138
- package/dist/cadre-invite-protocol.js +94 -449
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +530 -247
- package/dist/cadre-node.js +1046 -525
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +22 -23
- package/dist/control-cohort.js +24 -24
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +416 -136
- package/dist/control-database.js +757 -378
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +5 -4
- package/dist/control-protocol-guard.js +6 -3
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +80 -0
- package/dist/control-schema-version.js +203 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +254 -70
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +5 -5
- package/dist/control-write-retry.js +10 -8
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +26 -7
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +3 -3
- package/dist/joined-strand-store.js +3 -3
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +250 -0
- package/dist/member-redemption.js +485 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +247 -0
- package/dist/owner-grant-protocol.js +330 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +131 -109
- package/dist/seed-bootstrap.js +260 -248
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +1 -0
- package/dist/strand-database.js +3 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +4 -3
- package/dist/strand-first-sync-gate.js +4 -3
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +1 -1
- package/dist/strand-membership-writer.d.ts +13 -2
- package/dist/strand-membership-writer.js +52 -16
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +31 -30
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/types.d.ts +286 -103
- package/dist/types.js.map +1 -1
- package/package.json +16 -7
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +131 -531
- package/src/cadre-join-request.ts +188 -167
- package/src/cadre-node.ts +1166 -541
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +127 -128
- package/src/control-database.ts +940 -456
- package/src/control-protocol-guard.ts +7 -4
- package/src/control-schema-version.ts +251 -0
- package/src/control-schema.ts +265 -70
- package/src/control-write-retry.ts +569 -567
- package/src/delegate-admission.ts +37 -0
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/index.ts +102 -5
- package/src/joined-strand-store.ts +4 -4
- package/src/key-store.ts +10 -4
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +642 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +532 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-authorization.ts +558 -488
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +301 -312
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +3 -0
- package/src/strand-first-sync-gate.ts +4 -3
- package/src/strand-instance-manager.ts +1 -1
- package/src/strand-membership-writer.ts +61 -16
- package/src/strand-move.ts +45 -36
- package/src/strand-peer-redial.ts +2 -3
- package/src/types.ts +304 -103
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `EdgeNode.joinCadre`'s sequence: redeem in place on a node already running in the credential's
|
|
3
|
+
* party, or stop the running node, start one in that party, redeem there and go back on any
|
|
4
|
+
* failure (docs/architecture.md → "An app joins an existing cadre"). The credential is a machine
|
|
5
|
+
* invitation, which admits the app as a machine, or an owner grant, which makes it a machine and
|
|
6
|
+
* an owner at once ({@link JoinCredential}). Runs over a narrow view of the runtime
|
|
7
|
+
* ({@link JoinRuntime}), inside one of its transitions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CadreInvitation } from '../cadre-invite-protocol.js';
|
|
11
|
+
import type { CadreNode } from '../cadre-node.js';
|
|
12
|
+
import type { OwnerSigner } from '../owner-signer.js';
|
|
13
|
+
import { exportStrandBundle } from '../strand-move.js';
|
|
14
|
+
import type { RedeemCadreInvitationOptions, RedeemCadreInvitationResult } from '../types.js';
|
|
15
|
+
import type { OwnerGenesisResult } from './config.js';
|
|
16
|
+
import type { EdgeJoinProgress, EdgeNodeStatus, JoinCadreOptions, JoinCadreResult } from './edge-node.js';
|
|
17
|
+
import { isOwnerGrant, type JoinCredential } from './join-credential.js';
|
|
18
|
+
import type { EdgeNodeOptions, SavedStart } from './options.js';
|
|
19
|
+
import {
|
|
20
|
+
adoptMovesInto,
|
|
21
|
+
deferredOutcome,
|
|
22
|
+
joinMoveRecord,
|
|
23
|
+
toError,
|
|
24
|
+
type StrandMoveOutcome,
|
|
25
|
+
type StrandMoveRecords,
|
|
26
|
+
} from './strand-move.js';
|
|
27
|
+
|
|
28
|
+
/** The part of the edge-node runtime a join drives. Every call runs inside the join's own transition. */
|
|
29
|
+
export interface JoinRuntime {
|
|
30
|
+
/** The running node, or null. */
|
|
31
|
+
readonly node: CadreNode | null;
|
|
32
|
+
/** The options the running node was started with; null unless running. */
|
|
33
|
+
readonly runningOptions: EdgeNodeOptions | null;
|
|
34
|
+
readonly moves: StrandMoveRecords;
|
|
35
|
+
readonly status: EdgeNodeStatus;
|
|
36
|
+
setStatus(status: EdgeNodeStatus): void;
|
|
37
|
+
readSavedStart(): Promise<SavedStart | undefined>;
|
|
38
|
+
/** Stop the node and close every store; nothing runs afterwards. */
|
|
39
|
+
shutDown(): Promise<void>;
|
|
40
|
+
/** Build and start the node for `options`, subscriptions attached; nothing saved, nothing reported. */
|
|
41
|
+
bringUp(options: EdgeNodeOptions): Promise<{ node: CadreNode; resolved: EdgeNodeOptions }>;
|
|
42
|
+
/** Start a node for `options`' party only to run `read` on it, then stop it. */
|
|
43
|
+
readAlone<T>(options: EdgeNodeOptions, read: (node: CadreNode) => Promise<T>, purpose: string): Promise<T>;
|
|
44
|
+
/** A full start: bring up, settle the owner key, adopt waiting moves, save and report `running`. */
|
|
45
|
+
buildAndStart(options: EdgeNodeOptions): Promise<CadreNode>;
|
|
46
|
+
/** The owner slot's key, created when the slot is empty: the key an owner grant seats. */
|
|
47
|
+
ownerSlotSigner(): Promise<OwnerSigner>;
|
|
48
|
+
/** `granted`: an owner grant has just seated the slot's key, so wait for the row. */
|
|
49
|
+
wireJoinedOwner(node: CadreNode, granted: boolean): Promise<OwnerGenesisResult>;
|
|
50
|
+
saveRunningStart(resolved: EdgeNodeOptions): Promise<void>;
|
|
51
|
+
reportRunning(node: CadreNode, resolved: EdgeNodeOptions, owner: OwnerGenesisResult): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What redeeming either credential settles: the admitting member, and whether the app is an owner through it. */
|
|
55
|
+
interface Admission extends RedeemCadreInvitationResult {
|
|
56
|
+
/** The credential was an owner grant: the owner slot's key is an owner of the party, its row on its way here when not here yet. */
|
|
57
|
+
asOwner: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function joinCadre(
|
|
61
|
+
runtime: JoinRuntime,
|
|
62
|
+
credential: JoinCredential,
|
|
63
|
+
options: JoinCadreOptions,
|
|
64
|
+
signal: AbortSignal,
|
|
65
|
+
): Promise<JoinCadreResult> {
|
|
66
|
+
signal.throwIfAborted();
|
|
67
|
+
const running = runtime.node;
|
|
68
|
+
if (running?.isRunning && runtime.runningOptions?.partyId === credential.partyId) {
|
|
69
|
+
return redeemInPlace(runtime, running, runtime.runningOptions, credential, options, signal);
|
|
70
|
+
}
|
|
71
|
+
return switchParty(runtime, credential, options, signal);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Redeem on the node already running in the credential's party; nothing to switch or roll back. */
|
|
75
|
+
async function redeemInPlace(
|
|
76
|
+
runtime: JoinRuntime,
|
|
77
|
+
node: CadreNode,
|
|
78
|
+
running: EdgeNodeOptions,
|
|
79
|
+
credential: JoinCredential,
|
|
80
|
+
options: JoinCadreOptions,
|
|
81
|
+
signal: AbortSignal,
|
|
82
|
+
): Promise<JoinCadreResult> {
|
|
83
|
+
const admission = await redeemOnRunningNode(runtime, node, credential, options, signal);
|
|
84
|
+
reportProgress(options, { state: 'admitted', asOwner: admission.asOwner });
|
|
85
|
+
const owner = await runtime.wireJoinedOwner(node, admission.asOwner);
|
|
86
|
+
// An owner grant redeemed on a node that joined as a member finishes the move that join deferred.
|
|
87
|
+
const strandMove = await settleJoinMove(runtime, node, running.formerPartyId, owner);
|
|
88
|
+
if (owner === 'enrolled') runtime.setStatus({ state: 'running', node, owner });
|
|
89
|
+
return joinResult(admission, running.formerPartyId, strandMove);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A machine invitation is redeemed as on a fresh node (a device already admitted is answered as
|
|
94
|
+
* such). An owner grant is redeemed for the owner slot's key: the node is a member of the party,
|
|
95
|
+
* so `redeemOwnerGrant` writes the row in its own store and the key can be wired at once.
|
|
96
|
+
*/
|
|
97
|
+
async function redeemOnRunningNode(
|
|
98
|
+
runtime: JoinRuntime,
|
|
99
|
+
node: CadreNode,
|
|
100
|
+
credential: JoinCredential,
|
|
101
|
+
options: JoinCadreOptions,
|
|
102
|
+
signal: AbortSignal,
|
|
103
|
+
): Promise<Admission> {
|
|
104
|
+
if (!isOwnerGrant(credential)) return redeemInvitation(node, credential, options, signal);
|
|
105
|
+
// NOTE: a member running with no control connection to the party is refused `alone`
|
|
106
|
+
// (`OwnerKeyAddRefusedError`) by the grant's local path, where the invitation path waits for a
|
|
107
|
+
// member; if apps meet that, wait for a control connection before redeeming in place.
|
|
108
|
+
const redeemed = await node.redeemOwnerGrant(credential, await runtime.ownerSlotSigner(), redeemOptions(options, signal));
|
|
109
|
+
return { peerId: redeemed.memberPeerId, redeemedAt: new Date().toISOString(), asOwner: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Stop the running node, start one in the credential's party and redeem there; on any
|
|
114
|
+
* failure, go back to what ran before. Nothing is saved until a member has admitted this app.
|
|
115
|
+
* Leaving a party this app founded, its strands are recorded for the move first, so a refusal
|
|
116
|
+
* leaves that party running and unchanged.
|
|
117
|
+
*/
|
|
118
|
+
async function switchParty(
|
|
119
|
+
runtime: JoinRuntime,
|
|
120
|
+
credential: JoinCredential,
|
|
121
|
+
options: JoinCadreOptions,
|
|
122
|
+
signal: AbortSignal,
|
|
123
|
+
): Promise<JoinCadreResult> {
|
|
124
|
+
const restore = runtime.runningOptions;
|
|
125
|
+
const leaving = restore ?? (await runtime.readSavedStart())?.options ?? null;
|
|
126
|
+
const founded = foundedPartyLeft(credential.partyId, leaving);
|
|
127
|
+
// The party this app founded, carried through any later join.
|
|
128
|
+
// NOTE: leaving a joined party while the move from `formerPartyId` into it is still pending
|
|
129
|
+
// (the key never wired as an owner there) leaves that record naming the party left, adopted
|
|
130
|
+
// only if the app goes back; `pendingStrandMove` keeps reporting it. If apps need the move
|
|
131
|
+
// to follow the app, retarget the record to the credential's party here.
|
|
132
|
+
const formerPartyId = founded?.partyId ?? leaving?.formerPartyId;
|
|
133
|
+
const joined: EdgeNodeOptions = {
|
|
134
|
+
partyId: credential.partyId,
|
|
135
|
+
// None needed: the redemption retains the member that admits this app as a dial
|
|
136
|
+
// target in the party's bootstrap-peer store, which every later start reads.
|
|
137
|
+
bootstrapAddrs: [],
|
|
138
|
+
relayAddrs: options.relayAddrs ?? leaving?.relayAddrs ?? [],
|
|
139
|
+
origin: 'joined',
|
|
140
|
+
};
|
|
141
|
+
const noiseCryptoMode = options.noiseCryptoMode ?? leaving?.noiseCryptoMode;
|
|
142
|
+
if (noiseCryptoMode !== undefined) joined.noiseCryptoMode = noiseCryptoMode;
|
|
143
|
+
if (formerPartyId !== undefined) joined.formerPartyId = formerPartyId;
|
|
144
|
+
const movingFrom = founded ? await recordStrandMove(runtime, founded, credential) : undefined;
|
|
145
|
+
try {
|
|
146
|
+
await runtime.shutDown();
|
|
147
|
+
return await startAndRedeem(runtime, joined, credential, options, signal, movingFrom);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
await returnToLeftParty(runtime, restore, movingFrom);
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Export the founded party's strands and keep them in the key store for the joined party to
|
|
156
|
+
* adopt; the party's id when there was anything to move. Throws `StrandsNeedOwnerJoinError`, or
|
|
157
|
+
* the export's or the write's error: switching without the record would lose the strands.
|
|
158
|
+
* An empty export removes the party's record instead: one left by an earlier join that never
|
|
159
|
+
* finished describes strands the party no longer holds, and adopting it would bring them back.
|
|
160
|
+
*/
|
|
161
|
+
async function recordStrandMove(runtime: JoinRuntime, founded: EdgeNodeOptions, credential: JoinCredential): Promise<string | undefined> {
|
|
162
|
+
const running = runtime.node;
|
|
163
|
+
const bundle = running?.isRunning
|
|
164
|
+
? await exportStrandBundle(running)
|
|
165
|
+
: await runtime.readAlone(founded, exportStrandBundle, 'export strands');
|
|
166
|
+
const record = joinMoveRecord(bundle, credential);
|
|
167
|
+
if (!record) {
|
|
168
|
+
await runtime.moves.remove(founded.partyId);
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
await runtime.moves.save(record);
|
|
172
|
+
return founded.partyId;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Admitted, the start is saved before the strands are adopted, so an app killed mid-adoption
|
|
177
|
+
* relaunches into the joined party and finishes the move there; `running` is reported after.
|
|
178
|
+
*/
|
|
179
|
+
async function startAndRedeem(
|
|
180
|
+
runtime: JoinRuntime,
|
|
181
|
+
joined: EdgeNodeOptions,
|
|
182
|
+
credential: JoinCredential,
|
|
183
|
+
options: JoinCadreOptions,
|
|
184
|
+
signal: AbortSignal,
|
|
185
|
+
movingFrom: string | undefined,
|
|
186
|
+
): Promise<JoinCadreResult> {
|
|
187
|
+
runtime.setStatus({ state: 'starting' });
|
|
188
|
+
const { node, resolved } = await runtime.bringUp(joined);
|
|
189
|
+
runtime.setStatus({ state: 'joining', node });
|
|
190
|
+
const admission = await redeemOnJoinedNode(runtime, node, credential, options, signal);
|
|
191
|
+
reportProgress(options, { state: 'admitted', asOwner: admission.asOwner });
|
|
192
|
+
const owner = await runtime.wireJoinedOwner(node, admission.asOwner);
|
|
193
|
+
await runtime.saveRunningStart(resolved);
|
|
194
|
+
if (movingFrom !== undefined && owner === 'enrolled') reportProgress(options, { state: 'moving-strands' });
|
|
195
|
+
const strandMove = await settleJoinMove(runtime, node, resolved.formerPartyId, owner);
|
|
196
|
+
runtime.reportRunning(node, resolved, owner);
|
|
197
|
+
return joinResult(admission, resolved.formerPartyId, strandMove);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Redeem on the node started in the credential's party: a machine invitation admits the app, an
|
|
202
|
+
* owner grant makes it a machine and an owner (`CadreNode.joinWithOwnerGrant`).
|
|
203
|
+
*
|
|
204
|
+
* The grant seats the owner slot's key, created now when the slot is empty: the app's one owner
|
|
205
|
+
* key, which may already own the party the app founded and is leaving (one key, several cadres).
|
|
206
|
+
* A fresh key would need a second owner slot and a second signer per node, which the kits do not
|
|
207
|
+
* have, and replacing the slot's key would lose the left party, which a rollback restarts.
|
|
208
|
+
* Created before the redemption, the key stays in the slot when the join fails, an owner of
|
|
209
|
+
* nothing until the next founding or grant uses it.
|
|
210
|
+
*/
|
|
211
|
+
async function redeemOnJoinedNode(
|
|
212
|
+
runtime: JoinRuntime,
|
|
213
|
+
node: CadreNode,
|
|
214
|
+
credential: JoinCredential,
|
|
215
|
+
options: JoinCadreOptions,
|
|
216
|
+
signal: AbortSignal,
|
|
217
|
+
): Promise<Admission> {
|
|
218
|
+
if (!isOwnerGrant(credential)) return redeemInvitation(node, credential, options, signal);
|
|
219
|
+
// NOTE: a rollback after the grant was redeemed but the self-invitation refused leaves the
|
|
220
|
+
// slot's key an owner of the credential's party while the app is back in the party it left;
|
|
221
|
+
// a later join there, with a machine invitation too, picks the ownership up through that key.
|
|
222
|
+
// NOTE: `joinWithOwnerGrant` already waits up to its own bound for the key's `OwnerKey` row
|
|
223
|
+
// and wires it; `wireJoinedOwner(node, true)` then asks again for up to the owner-step bound
|
|
224
|
+
// while it is still unwired, so a row that never arrives costs both waits before `timed-out`.
|
|
225
|
+
// If that shows up, carry `joined.ownerWired` in the admission and skip the second wait.
|
|
226
|
+
const joined = await node.joinWithOwnerGrant(credential, await runtime.ownerSlotSigner(), redeemOptions(options, signal));
|
|
227
|
+
return { peerId: joined.peerId, redeemedAt: joined.redeemedAt, asOwner: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function redeemInvitation(node: CadreNode, invitation: CadreInvitation, options: JoinCadreOptions, signal: AbortSignal): Promise<Admission> {
|
|
231
|
+
const redemption = await node.redeemCadreInvitation(invitation, redeemOptions(options, signal));
|
|
232
|
+
return { ...redemption, asOwner: false };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The move from `formerPartyId` as an admitted join reports it: adopted with every other move
|
|
237
|
+
* naming this party once the app's key is wired as its owner, deferred otherwise. Never throws:
|
|
238
|
+
* the app is admitted, and what is not adopted now is adopted by the next start.
|
|
239
|
+
*/
|
|
240
|
+
async function settleJoinMove(
|
|
241
|
+
runtime: JoinRuntime,
|
|
242
|
+
node: CadreNode,
|
|
243
|
+
formerPartyId: string | undefined,
|
|
244
|
+
owner: OwnerGenesisResult,
|
|
245
|
+
): Promise<StrandMoveOutcome | undefined> {
|
|
246
|
+
try {
|
|
247
|
+
if (owner === 'enrolled') {
|
|
248
|
+
const outcomes = await adoptMovesInto(node, runtime.moves);
|
|
249
|
+
return formerPartyId === undefined ? undefined : outcomes.get(formerPartyId);
|
|
250
|
+
}
|
|
251
|
+
const record = formerPartyId === undefined ? undefined : await runtime.moves.load(formerPartyId);
|
|
252
|
+
return record?.targetPartyId === node.partyId ? { state: 'deferred', reason: 'owner-key-not-wired' } : undefined;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.warn('[cadre-core/edge-node] reading the strand move records failed; the next start retries:', err);
|
|
255
|
+
return formerPartyId === undefined ? undefined : deferredOutcome(err);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* After a failed join: discard the move record it wrote, stop the joined party's node and start
|
|
261
|
+
* again what ran before, if anything did.
|
|
262
|
+
*/
|
|
263
|
+
async function returnToLeftParty(runtime: JoinRuntime, restore: EdgeNodeOptions | null, movingFrom: string | undefined): Promise<void> {
|
|
264
|
+
await discardStrandMove(runtime.moves, movingFrom);
|
|
265
|
+
try {
|
|
266
|
+
await runtime.shutDown();
|
|
267
|
+
if (restore) await runtime.buildAndStart(restore);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
// The join's own error is the one its caller gets; this one is only logged.
|
|
270
|
+
console.warn('[cadre-core/edge-node] going back to the party a failed join left failed:', err);
|
|
271
|
+
if (runtime.status.state !== 'failed') runtime.setStatus({ state: 'failed', error: toError(err) });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function discardStrandMove(moves: StrandMoveRecords, fromPartyId: string | undefined): Promise<void> {
|
|
276
|
+
if (fromPartyId === undefined) return;
|
|
277
|
+
try {
|
|
278
|
+
await moves.remove(fromPartyId);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
// Harmless when left: it names the party the join failed to reach, and the next join from
|
|
281
|
+
// this party replaces it.
|
|
282
|
+
console.warn(`[cadre-core/edge-node] could not discard the strand move record of party ${fromPartyId}:`, err);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Wait for a member: an app joining by invitation is often the only machine of the party up. The grant's options are the invitation's. */
|
|
287
|
+
function redeemOptions(options: JoinCadreOptions, signal: AbortSignal): RedeemCadreInvitationOptions {
|
|
288
|
+
return { waitForMember: true, signal, ...(options.onProgress ? { onProgress: options.onProgress } : {}) };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Tell the join's listener; one that throws must not fail a join a member has already admitted. */
|
|
292
|
+
function reportProgress(options: JoinCadreOptions, progress: EdgeJoinProgress): void {
|
|
293
|
+
try {
|
|
294
|
+
options.onProgress?.(progress);
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.warn('[cadre-core/edge-node] join progress listener threw:', err);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* The start a join to `partyId` leaves, when it is of a party this app founded: the party whose
|
|
302
|
+
* strands move and which is kept as `formerPartyId` for clean-up. Null when the join leaves no
|
|
303
|
+
* start, a party this app joined, or the party it joins.
|
|
304
|
+
*/
|
|
305
|
+
function foundedPartyLeft(partyId: string, leaving: EdgeNodeOptions | null): EdgeNodeOptions | null {
|
|
306
|
+
return leaving && leaving.origin !== 'joined' && leaving.partyId !== partyId ? leaving : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function joinResult(
|
|
310
|
+
admission: Admission,
|
|
311
|
+
formerPartyId: string | undefined,
|
|
312
|
+
strandMove: StrandMoveOutcome | undefined,
|
|
313
|
+
): JoinCadreResult {
|
|
314
|
+
return {
|
|
315
|
+
...admission,
|
|
316
|
+
...(formerPartyId !== undefined && { formerPartyId }),
|
|
317
|
+
...(strandMove !== undefined && { strandMove }),
|
|
318
|
+
};
|
|
319
|
+
}
|