@serfab/cadre-core 1.16.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.
Files changed (199) hide show
  1. package/README.md +1 -1
  2. package/dist/bootstrap-peer-store-file.js +1 -1
  3. package/dist/bootstrap-peer-store.d.ts +1 -1
  4. package/dist/bootstrap-peer-store.js +4 -4
  5. package/dist/cadre-invite-protocol.d.ts +55 -138
  6. package/dist/cadre-invite-protocol.js +94 -449
  7. package/dist/cadre-invite-protocol.js.map +1 -1
  8. package/dist/cadre-join-request.d.ts +15 -2
  9. package/dist/cadre-join-request.js +22 -3
  10. package/dist/cadre-join-request.js.map +1 -1
  11. package/dist/cadre-node.d.ts +530 -247
  12. package/dist/cadre-node.js +1046 -525
  13. package/dist/cadre-node.js.map +1 -1
  14. package/dist/control-authorization.d.ts +36 -14
  15. package/dist/control-authorization.js +20 -3
  16. package/dist/control-authorization.js.map +1 -1
  17. package/dist/control-cohort.d.ts +22 -23
  18. package/dist/control-cohort.js +24 -24
  19. package/dist/control-cohort.js.map +1 -1
  20. package/dist/control-database.d.ts +416 -136
  21. package/dist/control-database.js +757 -378
  22. package/dist/control-database.js.map +1 -1
  23. package/dist/control-protocol-guard.d.ts +5 -4
  24. package/dist/control-protocol-guard.js +6 -3
  25. package/dist/control-protocol-guard.js.map +1 -1
  26. package/dist/control-schema-version.d.ts +80 -0
  27. package/dist/control-schema-version.js +203 -0
  28. package/dist/control-schema-version.js.map +1 -0
  29. package/dist/control-schema.d.ts +45 -5
  30. package/dist/control-schema.js +254 -70
  31. package/dist/control-schema.js.map +1 -1
  32. package/dist/control-write-retry.d.ts +5 -5
  33. package/dist/control-write-retry.js +10 -8
  34. package/dist/control-write-retry.js.map +1 -1
  35. package/dist/delegate-admission.d.ts +13 -0
  36. package/dist/delegate-admission.js +27 -0
  37. package/dist/delegate-admission.js.map +1 -1
  38. package/dist/ed25519-key.d.ts +10 -7
  39. package/dist/ed25519-key.js +23 -20
  40. package/dist/ed25519-key.js.map +1 -1
  41. package/dist/edge-node/config.d.ts +122 -0
  42. package/dist/edge-node/config.js +199 -0
  43. package/dist/edge-node/config.js.map +1 -0
  44. package/dist/edge-node/dissolve.d.ts +95 -0
  45. package/dist/edge-node/dissolve.js +184 -0
  46. package/dist/edge-node/dissolve.js.map +1 -0
  47. package/dist/edge-node/edge-join.d.ts +44 -0
  48. package/dist/edge-node/edge-join.js +227 -0
  49. package/dist/edge-node/edge-join.js.map +1 -0
  50. package/dist/edge-node/edge-node.d.ts +295 -0
  51. package/dist/edge-node/edge-node.js +704 -0
  52. package/dist/edge-node/edge-node.js.map +1 -0
  53. package/dist/edge-node/index.d.ts +18 -0
  54. package/dist/edge-node/index.js +18 -0
  55. package/dist/edge-node/index.js.map +1 -0
  56. package/dist/edge-node/join-credential.d.ts +11 -0
  57. package/dist/edge-node/join-credential.js +11 -0
  58. package/dist/edge-node/join-credential.js.map +1 -0
  59. package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
  60. package/dist/edge-node/lazy-raw-storage.js +134 -0
  61. package/dist/edge-node/lazy-raw-storage.js.map +1 -0
  62. package/dist/edge-node/node-local.d.ts +60 -0
  63. package/dist/edge-node/node-local.js +58 -0
  64. package/dist/edge-node/node-local.js.map +1 -0
  65. package/dist/edge-node/options.d.ts +83 -0
  66. package/dist/edge-node/options.js +110 -0
  67. package/dist/edge-node/options.js.map +1 -0
  68. package/dist/edge-node/owner-grant.d.ts +41 -0
  69. package/dist/edge-node/owner-grant.js +38 -0
  70. package/dist/edge-node/owner-grant.js.map +1 -0
  71. package/dist/edge-node/platform.d.ts +140 -0
  72. package/dist/edge-node/platform.js +12 -0
  73. package/dist/edge-node/platform.js.map +1 -0
  74. package/dist/edge-node/strand-move.d.ts +115 -0
  75. package/dist/edge-node/strand-move.js +223 -0
  76. package/dist/edge-node/strand-move.js.map +1 -0
  77. package/dist/edge-node/strands.d.ts +28 -0
  78. package/dist/edge-node/strands.js +60 -0
  79. package/dist/edge-node/strands.js.map +1 -0
  80. package/dist/index.d.ts +16 -7
  81. package/dist/index.js +26 -7
  82. package/dist/index.js.map +1 -1
  83. package/dist/joined-strand-store.d.ts +3 -3
  84. package/dist/joined-strand-store.js +3 -3
  85. package/dist/joined-strand-store.js.map +1 -1
  86. package/dist/key-store.d.ts +9 -4
  87. package/dist/key-store.js +9 -4
  88. package/dist/key-store.js.map +1 -1
  89. package/dist/machine-removal.d.ts +19 -39
  90. package/dist/machine-removal.js +24 -98
  91. package/dist/machine-removal.js.map +1 -1
  92. package/dist/member-redemption.d.ts +250 -0
  93. package/dist/member-redemption.js +485 -0
  94. package/dist/member-redemption.js.map +1 -0
  95. package/dist/owner-anchor-sync.d.ts +4 -4
  96. package/dist/owner-anchor-sync.js +16 -15
  97. package/dist/owner-anchor-sync.js.map +1 -1
  98. package/dist/owner-grant-protocol.d.ts +247 -0
  99. package/dist/owner-grant-protocol.js +330 -0
  100. package/dist/owner-grant-protocol.js.map +1 -0
  101. package/dist/owner-key-export.d.ts +70 -0
  102. package/dist/owner-key-export.js +279 -0
  103. package/dist/owner-key-export.js.map +1 -0
  104. package/dist/owner-key-flows.d.ts +55 -0
  105. package/dist/owner-key-flows.js +60 -0
  106. package/dist/owner-key-flows.js.map +1 -0
  107. package/dist/owner-key-store.d.ts +31 -0
  108. package/dist/owner-key-store.js +81 -0
  109. package/dist/owner-key-store.js.map +1 -0
  110. package/dist/owner-keys.d.ts +78 -0
  111. package/dist/owner-keys.js +133 -0
  112. package/dist/owner-keys.js.map +1 -0
  113. package/dist/owner-signer.d.ts +54 -0
  114. package/dist/owner-signer.js +51 -0
  115. package/dist/owner-signer.js.map +1 -0
  116. package/dist/peer-authorization.d.ts +83 -18
  117. package/dist/peer-authorization.js +123 -53
  118. package/dist/peer-authorization.js.map +1 -1
  119. package/dist/provider/rent-node.js +1 -1
  120. package/dist/provider/rent-node.js.map +1 -1
  121. package/dist/seed-bootstrap.d.ts +131 -109
  122. package/dist/seed-bootstrap.js +260 -248
  123. package/dist/seed-bootstrap.js.map +1 -1
  124. package/dist/seed-trust-policy.js +4 -4
  125. package/dist/strand-addr-protocol.d.ts +4 -1
  126. package/dist/strand-addr-protocol.js +8 -2
  127. package/dist/strand-addr-protocol.js.map +1 -1
  128. package/dist/strand-cohort.d.ts +3 -0
  129. package/dist/strand-cohort.js.map +1 -1
  130. package/dist/strand-database.d.ts +1 -0
  131. package/dist/strand-database.js +3 -0
  132. package/dist/strand-database.js.map +1 -1
  133. package/dist/strand-first-sync-gate.d.ts +4 -3
  134. package/dist/strand-first-sync-gate.js +4 -3
  135. package/dist/strand-first-sync-gate.js.map +1 -1
  136. package/dist/strand-instance-manager.d.ts +1 -1
  137. package/dist/strand-membership-writer.d.ts +13 -2
  138. package/dist/strand-membership-writer.js +52 -16
  139. package/dist/strand-membership-writer.js.map +1 -1
  140. package/dist/strand-move.d.ts +6 -5
  141. package/dist/strand-move.js +31 -30
  142. package/dist/strand-move.js.map +1 -1
  143. package/dist/strand-peer-redial.js +2 -3
  144. package/dist/strand-peer-redial.js.map +1 -1
  145. package/dist/types.d.ts +286 -103
  146. package/dist/types.js.map +1 -1
  147. package/package.json +11 -2
  148. package/src/bootstrap-peer-store-file.ts +44 -44
  149. package/src/bootstrap-peer-store.ts +214 -214
  150. package/src/cadre-invite-protocol.ts +652 -1052
  151. package/src/cadre-join-request.ts +24 -3
  152. package/src/cadre-node.ts +1166 -541
  153. package/src/control-authorization.ts +47 -14
  154. package/src/control-cohort.ts +127 -128
  155. package/src/control-database.ts +940 -456
  156. package/src/control-protocol-guard.ts +7 -4
  157. package/src/control-schema-version.ts +251 -0
  158. package/src/control-schema.ts +265 -70
  159. package/src/control-write-retry.ts +569 -567
  160. package/src/delegate-admission.ts +37 -0
  161. package/src/ed25519-key.ts +174 -171
  162. package/src/edge-node/config.ts +277 -0
  163. package/src/edge-node/dissolve.ts +251 -0
  164. package/src/edge-node/edge-join.ts +319 -0
  165. package/src/edge-node/edge-node.ts +1045 -0
  166. package/src/edge-node/index.ts +59 -0
  167. package/src/edge-node/join-credential.ts +16 -0
  168. package/src/edge-node/lazy-raw-storage.ts +178 -0
  169. package/src/edge-node/node-local.ts +84 -0
  170. package/src/edge-node/options.ts +179 -0
  171. package/src/edge-node/owner-grant.ts +63 -0
  172. package/src/edge-node/platform.ts +151 -0
  173. package/src/edge-node/strand-move.ts +270 -0
  174. package/src/edge-node/strands.ts +72 -0
  175. package/src/index.ts +886 -789
  176. package/src/joined-strand-store.ts +4 -4
  177. package/src/key-store.ts +10 -4
  178. package/src/machine-removal.ts +26 -111
  179. package/src/member-redemption.ts +642 -0
  180. package/src/owner-anchor-sync.ts +19 -18
  181. package/src/owner-grant-protocol.ts +532 -0
  182. package/src/owner-key-export.ts +348 -0
  183. package/src/owner-key-flows.ts +109 -0
  184. package/src/owner-key-store.ts +85 -0
  185. package/src/owner-keys.ts +178 -0
  186. package/src/owner-signer.ts +94 -0
  187. package/src/peer-authorization.ts +558 -488
  188. package/src/provider/rent-node.ts +1 -1
  189. package/src/seed-bootstrap.ts +301 -312
  190. package/src/seed-trust-policy.ts +4 -4
  191. package/src/strand-addr-protocol.ts +547 -538
  192. package/src/strand-cohort.ts +4 -0
  193. package/src/strand-database.ts +3 -0
  194. package/src/strand-first-sync-gate.ts +4 -3
  195. package/src/strand-instance-manager.ts +1 -1
  196. package/src/strand-membership-writer.ts +61 -16
  197. package/src/strand-move.ts +45 -36
  198. package/src/strand-peer-redial.ts +2 -3
  199. 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
+ }