@interop/wallet-core 0.54.0 → 0.56.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.
@@ -0,0 +1,657 @@
1
+ import { ensurePromotedSpaceController, mintSpaceId } from '../genesis/accountGenesis.js';
2
+ import { effectiveParameters, keyAgreementCommitment, readPublishedLog } from '../webvh/didWebvh.js';
3
+ import { accountLogPinId } from '../webvh/verifyLog.js';
4
+ import { didKeyZcapClient } from '../webvh/zcap.js';
5
+ import { delegateLogWrite, delegationProofKeyId } from '../recovery/recoveryDelegation.js';
6
+ import { mintUserKey } from '../keys/index.js';
7
+ import { attributeLadderRung } from './ladder.js';
8
+ import { ladderVmAgent, ladderVmZcapClient } from './zcap.js';
9
+ import { clientAnnexDidParts, clientAnnexLogPinId, clientAnnexLogStore, ensureGenerationDelegationCurrent, mintCredentialClientAnnexGeneration, mintDelegatedClientsDelegation, setDelegatedClientsPointer } from './log.js';
10
+ import { ladderSignedGenerationDelegationMinter, pointerEntryUpdateKeys, resolveClientAnnexSpaceId } from './heal.js';
11
+ import { ensureCredentialAnchoredAccountGenesis } from './credentialAnchoredGenesis.js';
12
+ import { ensureRosterDeliveredEpochs } from './rosterDeliveredEpochs.js';
13
+ /**
14
+ * The stage-3 primitive: ensure the account document points at a client-annex
15
+ * generation, minting and pointing one when it does not. Gated on the
16
+ * pointer: a document already carrying `#DelegatedClients` is returned
17
+ * as-is (`generationMinted: false`). Otherwise the annex Space resolves in
18
+ * the settled order ({@link resolveClientAnnexSpaceId}: the sibling
19
+ * delegation's target, else a fresh Space minted here), and the fold runs in
20
+ * the standing sub-step order: mint the generation, embed the generation
21
+ * delegation while the Space still answers to its creation controller, flip
22
+ * the Space's controller to the account DID, then append the pointer entry
23
+ * -- strictly last in the block, so pointer-present implies every prior
24
+ * sub-step landed. The pointer entry signs by ladder attribution of the
25
+ * currently revealed rung, and every log touch rides the caller's chain-head
26
+ * pin store.
27
+ *
28
+ * Two invocation authorities, heal's pattern. A caller holding a standing
29
+ * invocation authority (`invocation`: an enrolled client's storage handle
30
+ * plus the sibling capability its annex writes ride) converges onto the
31
+ * sibling-named Space as-is -- the Space is already account-controlled, so
32
+ * no flip runs. The bootstrap-only caller (the establishment) can write a
33
+ * sibling-named Space only while it still answers to the bootstrap did:key
34
+ * (a tear before the flip); one an earlier run already flipped refuses its
35
+ * writes, and that authorization refusal falls back to the fresh-mint arm
36
+ * rather than failing the run. The bootstrap arm's own controller flip
37
+ * swallows ONLY an authorization-class refusal (a concurrent run flipped
38
+ * first); a transport failure there aborts BEFORE the pointer entry, since
39
+ * a document pointing at a generation whose Space still answers to the bare
40
+ * ladder did:key would be unreachable forever.
41
+ *
42
+ * The fold shape is fixed (a separate pointer entry); a ceremony whose
43
+ * pointer move must ride another log entry atomically (the transient
44
+ * recovery's add-and-retire) keeps its own inline fold rather than
45
+ * parameterizing this one.
46
+ *
47
+ * @param options {object}
48
+ * @param options.account {object} the VERIFIED account log view
49
+ * (`{ did, doc, log }`; never re-fetched here)
50
+ * @param options.wasServerUrl {string} the account pointer's host
51
+ * @param options.accountSpaceId {string} the ACCOUNT Space's id (the
52
+ * generation delegation's target subtree, and the account-log pin slot)
53
+ * @param options.ladderSeed {Uint8Array} the credential's ladder seed
54
+ * @param options.was {WasClient} the bootstrap storage client (the ladder
55
+ * VM's bare did:key), used by the fresh-mint arm
56
+ * @param options.mintController {string} the annex Space's creation
57
+ * controller (a did:key; the ladder VM's bare did:key here)
58
+ * @param options.mintGenerationDelegation {Function}
59
+ * `({ clientAnnexDid }) => Promise<IZcap>` -- the generation-delegation
60
+ * minter (ladder-VM-signed on a ladder-anchored account)
61
+ * @param options.idStore {WebvhIdStore} the ACCOUNT log's store
62
+ * @param [options.updateKeys] {ClientWebvhUpdateKeys} the pointer entry's
63
+ * signing pair; absent, it is recovered by ladder attribution of the
64
+ * supplied log's current parameters
65
+ * @param [options.delegatedClients] {IZcap} the record's sibling
66
+ * delegation, for the Space resolution's settled order
67
+ * @param [options.invocation] {object} a standing invocation authority for
68
+ * the sibling-named Space's writes: `was` (the standing client's storage
69
+ * handle) and `capability` (the sibling delegation the annex writes ride).
70
+ * Absent, the sibling-named Space is attempted under the bootstrap client
71
+ * and an authorization refusal falls back to a fresh mint
72
+ * @param [options.logOnly] {boolean} pointer entries publish the log only
73
+ * (a bridge-delegated writer has no `did.json` projection rights); the
74
+ * establishment's root window omits it
75
+ * @param [options.pinStore] {ResourceLogPinStore} chain-head pins; slot
76
+ * keys are derived here per log
77
+ * @param [options.now] {number} epoch milliseconds, for tests
78
+ * @returns {Promise<object>} the pointed (or freshly minted) annex DID,
79
+ * the generation delegation when one was installed here, and what ran
80
+ */
81
+ export async function ensurePointedClientAnnexGeneration({ account, wasServerUrl, accountSpaceId, ladderSeed, was, mintController, mintGenerationDelegation, idStore, updateKeys, delegatedClients, invocation, logOnly, pinStore, now }) {
82
+ const { pointer, annexSpaceId } = resolveClientAnnexSpaceId({
83
+ doc: account.doc,
84
+ ...(delegatedClients !== undefined ? { delegatedClients } : {})
85
+ });
86
+ if (pointer !== undefined) {
87
+ return {
88
+ clientAnnexDid: pointer,
89
+ generationMinted: false,
90
+ spaceMinted: false
91
+ };
92
+ }
93
+ // The pre-flight attribution precedes the mint (never mint a generation
94
+ // the pointer entry could not then name), unless the caller supplied the
95
+ // signing pair itself.
96
+ const entryKeys = updateKeys ?? (await pointerEntryUpdateKeys({ ladderSeed, log: account.log }));
97
+ const pointGeneration = async (clientAnnexDid) => setDelegatedClientsPointer({
98
+ idStore,
99
+ updateKeys: entryKeys,
100
+ clientAnnexDid,
101
+ expectedDid: account.did,
102
+ ...(logOnly !== undefined ? { logOnly } : {}),
103
+ ...(pinStore !== undefined
104
+ ? { pinStore, logId: accountLogPinId({ spaceId: accountSpaceId }) }
105
+ : {})
106
+ });
107
+ // The standing-authority arm: the sibling-named Space is already
108
+ // account-controlled, its writes ride the supplied capability, and no
109
+ // flip runs (heal's existing-Space pattern).
110
+ if (annexSpaceId !== undefined && invocation !== undefined) {
111
+ const minted = await mintCredentialClientAnnexGeneration({
112
+ was: invocation.was,
113
+ wasServerUrl,
114
+ spaceId: annexSpaceId,
115
+ controller: account.did,
116
+ ladderSeed,
117
+ capability: invocation.capability
118
+ });
119
+ const ensured = await ensureGenerationDelegationCurrent({
120
+ store: clientAnnexLogStore({
121
+ was: invocation.was,
122
+ spaceId: annexSpaceId,
123
+ generationId: minted.generationId,
124
+ capability: invocation.capability
125
+ }),
126
+ ladderSeed,
127
+ generationId: minted.generationId,
128
+ mintGenerationDelegation,
129
+ expectedDid: minted.did,
130
+ ...(pinStore !== undefined
131
+ ? {
132
+ pinStore,
133
+ logId: clientAnnexLogPinId({
134
+ spaceId: annexSpaceId,
135
+ generationId: minted.generationId
136
+ })
137
+ }
138
+ : {}),
139
+ ...(now !== undefined ? { now } : {})
140
+ });
141
+ await pointGeneration(minted.did);
142
+ return {
143
+ clientAnnexDid: minted.did,
144
+ generationDelegation: ensured.delegation,
145
+ generationMinted: true,
146
+ spaceMinted: false
147
+ };
148
+ }
149
+ // The bootstrap arm: mint, embed the delegation while the Space still
150
+ // answers to its creation controller, flip, then the pointer entry.
151
+ const bootstrapArm = async ({ spaceId, spaceMinted }) => {
152
+ const minted = await mintCredentialClientAnnexGeneration({
153
+ was,
154
+ wasServerUrl,
155
+ spaceId,
156
+ controller: mintController,
157
+ ladderSeed
158
+ });
159
+ const ensured = await ensureGenerationDelegationCurrent({
160
+ store: clientAnnexLogStore({
161
+ was,
162
+ spaceId,
163
+ generationId: minted.generationId
164
+ }),
165
+ ladderSeed,
166
+ generationId: minted.generationId,
167
+ mintGenerationDelegation,
168
+ expectedDid: minted.did,
169
+ ...(pinStore !== undefined
170
+ ? {
171
+ pinStore,
172
+ logId: clientAnnexLogPinId({
173
+ spaceId,
174
+ generationId: minted.generationId
175
+ })
176
+ }
177
+ : {}),
178
+ ...(now !== undefined ? { now } : {})
179
+ });
180
+ // The controller flip. ONLY an authorization-class refusal is swallowed
181
+ // (a concurrent run flipped first, so the Space no longer answers to
182
+ // this client); a transport failure aborts BEFORE the pointer entry --
183
+ // publishing a pointer at a generation whose Space still answers to the
184
+ // bare ladder did:key would leave it unreachable forever, with nothing
185
+ // downstream ever re-running the flip.
186
+ try {
187
+ await was
188
+ .space(spaceId)
189
+ .configure({ controller: account.did, force: true });
190
+ }
191
+ catch (err) {
192
+ if (!authorizationRefusal(err)) {
193
+ throw err;
194
+ }
195
+ }
196
+ await pointGeneration(minted.did);
197
+ return {
198
+ clientAnnexDid: minted.did,
199
+ generationDelegation: ensured.delegation,
200
+ generationMinted: true,
201
+ spaceMinted
202
+ };
203
+ };
204
+ if (annexSpaceId !== undefined) {
205
+ // A sibling-named Space under bootstrap authority alone: writable only
206
+ // while it still answers to the bootstrap did:key (a tear before the
207
+ // flip). One an earlier run already flipped refuses these writes; that
208
+ // refusal falls back to the fresh mint below, and the flipped Space
209
+ // stays the recorded orphan residue.
210
+ try {
211
+ return await bootstrapArm({ spaceId: annexSpaceId, spaceMinted: false });
212
+ }
213
+ catch (err) {
214
+ if (!authorizationRefusal(err)) {
215
+ throw err;
216
+ }
217
+ }
218
+ }
219
+ return bootstrapArm({ spaceId: mintSpaceId(), spaceMinted: true });
220
+ }
221
+ /**
222
+ * Whether an error is an authorization-class refusal (401/403, on the error,
223
+ * its `cause`, or its carried response) rather than a transport or logic
224
+ * failure. Matched structurally: was-client surfaces server refusals with
225
+ * the HTTP status attached, and error classes do not survive crossing
226
+ * package copies.
227
+ *
228
+ * @param err {unknown}
229
+ * @returns {boolean}
230
+ */
231
+ function authorizationRefusal(err) {
232
+ const candidates = [err, err?.cause];
233
+ for (const candidate of candidates) {
234
+ if (candidate === null || typeof candidate !== 'object') {
235
+ continue;
236
+ }
237
+ const carried = candidate;
238
+ const status = carried.status ?? carried.response?.status;
239
+ if (status === 401 || status === 403) {
240
+ return true;
241
+ }
242
+ if (carried.name === 'NotAllowedError' ||
243
+ carried.name === 'ForbiddenError' ||
244
+ carried.name === 'UnauthorizedError') {
245
+ return true;
246
+ }
247
+ }
248
+ return false;
249
+ }
250
+ /**
251
+ * Runs the whole credential-anchored establishment for one unlock credential
252
+ * (see the module doc for the stage order and its whys). Idempotent under
253
+ * re-run from durable state alone; a torn run converges by running again.
254
+ *
255
+ * @param options {object}
256
+ * @param options.wasServerUrl {string} the account pointer's host
257
+ * @param options.spaceId {string} the ACCOUNT Space's id, minted by the
258
+ * caller and carried in the account pointer (never minted here)
259
+ * @param options.ladderSeed {Uint8Array} the credential's ladder seed --
260
+ * freshly minted on a signup, recovered from the record on a heal re-run
261
+ * @param options.standing {object} the credential's standing client
262
+ * identity, from a BINDING-VERIFIED record or a fresh derivation:
263
+ * `clientDid`, `keyAgreementKeyMultibase`, `recipientKid`, and
264
+ * `keyAgreementKey` (the private-half key-agreement key, for the
265
+ * adopted-roster read-back)
266
+ * @param options.bindRecord {CredentialAnchoredBindRecordHook} REQUIRED:
267
+ * the unlock-record codec closure (see its type doc for the hook's
268
+ * standing-layout and freshness-floor obligations)
269
+ * @param options.rosterStoreFor {Function} REQUIRED: `({ did }) =>
270
+ * EncryptionDescriptorStore` -- the user-key roster's store, with a
271
+ * LADDER-signed log signer (the ceremony-tail license's first-entry
272
+ * shape), invoked as the bootstrap did:key
273
+ * @param options.bootstrapWasFor {Function} REQUIRED:
274
+ * `({ keyAgent }) => WasClient` -- the storage client wiring, signing as
275
+ * the ladder VM's bare did:key (the agent is derived here from the seed)
276
+ * @param options.idStore {WebvhIdStore} the account's `id` collection
277
+ * store, signing as the same bootstrap did:key
278
+ * @param [options.expectedDid] {string} the account DID, when the caller's
279
+ * pointer already names one (a heal re-run)
280
+ * @param options.lowEntropy {boolean} whether the credential is
281
+ * low-entropy. REQUIRED (the compiler backstop at the passkey call site,
282
+ * where dropping it would publish the commitment and break the verbatim
283
+ * published key that call site's readers trigger on), and it still FAILS
284
+ * SAFE at runtime: the `keyAgreement` key publishes as a hash commitment
285
+ * unless the value is exactly `false` -- a verbatim KDF-derived key in
286
+ * the world-readable document is a standing offline-grind oracle
287
+ * removable only by credential rotation
288
+ * @param [options.email] {string} -- not taken here; carried inside the
289
+ * caller's `bindRecord` closure
290
+ * @param [options.priorCreatedAt] {string} the previous bind's freshness
291
+ * stamp, from a standing keyring hit. Its presence SKIPS stage 1: the
292
+ * record already carries the ladder seed, and a DID-less re-write could
293
+ * downgrade a sibling browser's completed re-bind
294
+ * @param [options.delegatedClients] {IZcap} the record's sibling
295
+ * delegation, when the caller holds one -- threaded into stage 3's Space
296
+ * resolution; under this ceremony's bootstrap-only authority a
297
+ * sibling-named Space it can no longer write falls back to a fresh mint
298
+ * @param [options.provideDidWebKeys] {Function} `() =>
299
+ * Promise<DidWebKeyMapV2 | undefined>` -- the caller's opaque best-effort
300
+ * KMS/did:web thunk, run inside stage 2 (the caller owns its body and its
301
+ * timeout); a throw is the collected non-fatal `didWebKeys` failure
302
+ * @param [options.promoteKeystore] {Function} `({ did }) => Promise<void>`
303
+ * -- the best-effort keystore-controller promotion, called after the
304
+ * Space's own promotion; the caller's closure no-ops when its KMS stage
305
+ * bound no keystore this run. A throw is collected, never fatal
306
+ * @param [options.beforePromotion] {Function} runs after the re-bind and
307
+ * BEFORE the controller promotion -- the last window where a root
308
+ * invocation under the bootstrap did:key works (the signup's registry
309
+ * write). NOT swallowed here: a throw fails the establishment, so a hook
310
+ * that must be best-effort swallows its own failures
311
+ * @param [options.pinStore] {ResourceLogPinStore} the chain-head pin store
312
+ * for every log read here (a transient visit's in-memory handle, or a
313
+ * durable one when a remembered caller seeds its own pin)
314
+ * @param [options.now] {number} epoch milliseconds, for tests
315
+ * @returns {Promise<CredentialAnchoredEstablishment>}
316
+ * @throws {TypeError} synchronously, when a required hook is missing
317
+ * @throws {Error} when the genesis's roster or epoch stage did not land
318
+ * (the underlying failure as `cause`); the record stays DID-less, so the
319
+ * next login's heal re-runs the establishment
320
+ */
321
+ export function establishCredentialAnchoredAccount(options) {
322
+ // Refused synchronously, before any write: the required hooks are the
323
+ // persist-before-publish seams, and a run without them could publish a
324
+ // rung nobody can re-derive.
325
+ if (typeof options.bindRecord !== 'function') {
326
+ throw new TypeError('establishCredentialAnchoredAccount requires bindRecord: the unlock ' +
327
+ 'record must be durably written before anything publishes.');
328
+ }
329
+ if (typeof options.rosterStoreFor !== 'function') {
330
+ throw new TypeError('establishCredentialAnchoredAccount requires rosterStoreFor: the ' +
331
+ "user-key roster is the account's decryption root.");
332
+ }
333
+ if (typeof options.bootstrapWasFor !== 'function') {
334
+ throw new TypeError('establishCredentialAnchoredAccount requires bootstrapWasFor: every ' +
335
+ 'pre-promotion write signs as the ladder VM\'s bare did:key.');
336
+ }
337
+ return establishCredentialAnchoredAccountChecked(options);
338
+ }
339
+ /**
340
+ * The checked body of {@link establishCredentialAnchoredAccount}.
341
+ *
342
+ * @param options {object} see {@link establishCredentialAnchoredAccount}
343
+ * @returns {Promise<CredentialAnchoredEstablishment>}
344
+ */
345
+ async function establishCredentialAnchoredAccountChecked({ wasServerUrl, spaceId, ladderSeed, standing, bindRecord, rosterStoreFor, bootstrapWasFor, idStore, expectedDid, lowEntropy, priorCreatedAt, delegatedClients, provideDidWebKeys, promoteKeystore, beforePromotion, pinStore, now }) {
346
+ const bootstrapAgent = await ladderVmAgent({ ladderSeed });
347
+ const bootstrapZcap = didKeyZcapClient({ keyAgent: bootstrapAgent });
348
+ const bootstrapWas = bootstrapWasFor({ keyAgent: bootstrapAgent });
349
+ const pointer = { spaceId, host: wasServerUrl };
350
+ const failed = [];
351
+ // The hash-commitment rule, failing safe: a KDF-derived public key
352
+ // published verbatim in the world-readable document is a standing
353
+ // offline-grind oracle, so only an EXPLICIT `lowEntropy: false` publishes
354
+ // the key itself; absent or ambiguous, the commitment publishes.
355
+ const keyAgreement = lowEntropy === false
356
+ ? { publicKeyMultibase: standing.keyAgreementKeyMultibase }
357
+ : {
358
+ commitment: await keyAgreementCommitment({
359
+ keyAgreementKeyMultibase: standing.keyAgreementKeyMultibase
360
+ })
361
+ };
362
+ // 1. The interim bridge and the first bind -- skipped when the caller's
363
+ // record already carries the ladder seed (`priorCreatedAt` from a standing
364
+ // hit): a DID-less re-write here could downgrade a sibling browser's
365
+ // completed re-bind.
366
+ let firstBindCreatedAt = priorCreatedAt;
367
+ if (priorCreatedAt === undefined) {
368
+ const interimBridge = await delegateLogWrite({
369
+ zcapClient: bootstrapZcap,
370
+ pointer,
371
+ recoveryClientDid: standing.clientDid
372
+ });
373
+ const firstBind = await bindRecord({
374
+ controller: bootstrapAgent.id,
375
+ pointer,
376
+ delegation: interimBridge
377
+ });
378
+ assertBindResult({ bind: firstBind, stage: 'first bind' });
379
+ firstBindCreatedAt = firstBind.createdAt;
380
+ }
381
+ // 2. The genesis ceremony under the bootstrap did:key. The candidate user
382
+ // key seeds a fresh roster; an adopted (heal) roster keeps its own.
383
+ const candidateUserKey = await mintUserKey();
384
+ const genesis = await ensureCredentialAnchoredAccountGenesis({
385
+ was: bootstrapWas,
386
+ wasServerUrl,
387
+ spaceId,
388
+ ladderSeed,
389
+ keyAgreement,
390
+ standingRecipient: {
391
+ id: standing.recipientKid,
392
+ publicKeyMultibase: standing.keyAgreementKeyMultibase
393
+ },
394
+ userKey: candidateUserKey,
395
+ idStore,
396
+ rosterStoreFor,
397
+ ...(expectedDid !== undefined ? { expectedDid } : {}),
398
+ ...(provideDidWebKeys ? { provideDidWebKeys } : {}),
399
+ ...(pinStore !== undefined ? { accountLogPinStore: pinStore } : {}),
400
+ promoteController: false
401
+ });
402
+ // The KMS stage stays best-effort: a failed thunk is the ceremony's
403
+ // collected `didWebKeys` stage, reported on the result and never fatal --
404
+ // the account proceeds keystore-less and a later pass heals it. The
405
+ // landing check below deliberately ignores this stage.
406
+ for (const entry of genesis.failed) {
407
+ if (entry.stage === 'didWebKeys') {
408
+ failed.push({ stage: 'didWebKeys', error: entry.error });
409
+ }
410
+ }
411
+ const did = genesis.did;
412
+ const fullPointer = { spaceId, host: wasServerUrl, did };
413
+ // The ceremony collects its roster and epoch failures instead of
414
+ // throwing; here they are fatal. A roster that never landed leaves the
415
+ // candidate key held in this tab's memory alone, and a registry sealed
416
+ // under it would be unreadable forever. Refusing BEFORE the re-bind keeps
417
+ // the record DID-less, which is exactly what routes the next login into
418
+ // the establishment re-run that converges.
419
+ assertGenesisLanded({ failed: genesis.failed, epochs: genesis.epochs });
420
+ if (!genesis.rosterDescriptor) {
421
+ throw new Error('The user-key roster genesis did not land.');
422
+ }
423
+ // 2c. The adopted-roster arm: a re-run that adopted an earlier run's
424
+ // roster recovers the real user key from the credential's standing wrap
425
+ // and completes the collection epochs under it -- the one installer,
426
+ // through the shared mint-policy stage.
427
+ let userKey = candidateUserKey;
428
+ if (genesis.rosterDescriptor.currentEpoch !== candidateUserKey.id) {
429
+ const delivered = await ensureRosterDeliveredEpochs({
430
+ store: rosterStoreFor({ did }),
431
+ candidateUserKey,
432
+ clientKeyAgreementKey: standing.keyAgreementKey,
433
+ was: bootstrapWas,
434
+ spaceId
435
+ });
436
+ if (delivered.outcome === 'no-wrap') {
437
+ throw new Error('The adopted user-key roster could not be read back with this ' +
438
+ 'credential.', { cause: delivered.error });
439
+ }
440
+ userKey = delivered.userKey;
441
+ assertGenesisLanded({ failed: [], epochs: delivered.epochs });
442
+ }
443
+ // 3. The annex generation block, so the very next login can enroll a
444
+ // transient client: reuse the pointed one; resolve, mint, embed the
445
+ // delegation, flip the auxiliary Space's controller, and point otherwise.
446
+ const published = await readPublishedLog({
447
+ idStore,
448
+ expectedDid: did,
449
+ ...(pinStore !== undefined
450
+ ? { pinStore, logId: accountLogPinId({ spaceId }) }
451
+ : {})
452
+ });
453
+ if (published === undefined) {
454
+ throw new Error('The account log the genesis published could not be read.');
455
+ }
456
+ // The bind-time REVEALED rung, attributed from the published log's current
457
+ // parameters (rung 0 on a fresh establishment) -- resolved HERE, before
458
+ // stage 3 and strictly before the re-bind: on an account whose document no
459
+ // longer anchors this ladder (a struck ladder VM), the attribution throws
460
+ // while the record is still in its pre-re-bind shape, rather than after a
461
+ // re-bind that would leave a rebound record with no registry entry and no
462
+ // mender. A committed-only rung refuses too: it cannot sign, and recording
463
+ // it as the registry's update key would misstate the standing rung.
464
+ const attributed = await attributeLadderRung({
465
+ ladderSeed,
466
+ published: currentLogParameters(published)
467
+ });
468
+ if (attributed.state !== 'revealed') {
469
+ throw new Error("No revealed rung of this credential's ladder stands in the account " +
470
+ "log's current update keys; the establishment cannot proceed.");
471
+ }
472
+ const generation = await ensurePointedClientAnnexGeneration({
473
+ account: published,
474
+ wasServerUrl,
475
+ accountSpaceId: spaceId,
476
+ ladderSeed,
477
+ was: bootstrapWas,
478
+ mintController: bootstrapAgent.id,
479
+ mintGenerationDelegation: ladderSignedGenerationDelegationMinter({
480
+ accountDid: did,
481
+ ladderSeed,
482
+ wasServerUrl,
483
+ spaceId,
484
+ ...(now !== undefined ? { now } : {})
485
+ }),
486
+ idStore,
487
+ ...(delegatedClients !== undefined ? { delegatedClients } : {}),
488
+ ...(pinStore !== undefined ? { pinStore } : {}),
489
+ ...(now !== undefined ? { now } : {})
490
+ });
491
+ const clientAnnex = clientAnnexDidParts({ did: generation.clientAnnexDid });
492
+ // 4. The final bridge and sibling, ladder-VM-signed (they must survive
493
+ // promotion, which the interim did:key-signed bridge cannot), and the
494
+ // re-bind: full pointer, both delegations, the management zcap to the
495
+ // account DID.
496
+ const ladderZcap = await ladderVmZcapClient({ accountDid: did, ladderSeed });
497
+ const bridge = await delegateLogWrite({
498
+ zcapClient: ladderZcap,
499
+ pointer: fullPointer,
500
+ recoveryClientDid: standing.clientDid
501
+ });
502
+ const sibling = await mintDelegatedClientsDelegation({
503
+ zcapClient: ladderZcap,
504
+ wasServerUrl,
505
+ clientAnnexSpaceId: clientAnnex.spaceId,
506
+ controller: standing.clientDid,
507
+ ...(now !== undefined ? { now } : {})
508
+ });
509
+ const rebind = await bindRecord({
510
+ controller: bootstrapAgent.id,
511
+ pointer: fullPointer,
512
+ delegation: bridge,
513
+ delegatedClients: sibling,
514
+ delegateManagementTo: did,
515
+ ...(firstBindCreatedAt !== undefined
516
+ ? { priorCreatedAt: firstBindCreatedAt }
517
+ : {})
518
+ });
519
+ assertBindResult({ bind: rebind, stage: 're-bind' });
520
+ const delegationKeyId = delegationProofKeyId(bridge);
521
+ const delegatedClientsKeyId = delegationProofKeyId(sibling);
522
+ const establishment = {
523
+ did,
524
+ unlockSpaceId: rebind.unlockSpaceId,
525
+ ...(rebind.manageCapability
526
+ ? { manageCapability: rebind.manageCapability }
527
+ : {}),
528
+ standingFields: {
529
+ rosterKid: standing.recipientKid,
530
+ keyAgreementKeyMultibase: standing.keyAgreementKeyMultibase,
531
+ updateKeyMultibase: attributed.rung.keyMultibase,
532
+ unlockClientDid: standing.clientDid,
533
+ ...(delegationKeyId ? { delegationKeyId } : {}),
534
+ ...(zcapExpires(bridge) ? { delegationExpires: zcapExpires(bridge) } : {}),
535
+ ...(delegatedClientsKeyId ? { delegatedClientsKeyId } : {}),
536
+ ...(zcapExpires(sibling)
537
+ ? { delegatedClientsExpires: zcapExpires(sibling) }
538
+ : {}),
539
+ ...(rebind.unlockKeyAgreementKeyId
540
+ ? { unlockKeyAgreementKeyId: rebind.unlockKeyAgreementKeyId }
541
+ : {}),
542
+ ...(rebind.unlockKeyAgreementKeyMultibase
543
+ ? {
544
+ unlockKeyAgreementKeyMultibase: rebind.unlockKeyAgreementKeyMultibase
545
+ }
546
+ : {})
547
+ },
548
+ ...(genesis.epochsSkipped ? { epochsSkipped: genesis.epochsSkipped } : {}),
549
+ failed
550
+ };
551
+ // 5. The caller's pre-promotion tail (the signup's registry write): the
552
+ // last window where a root invocation under the bootstrap did:key works.
553
+ // A throw here fails the establishment (some callers' registry writes
554
+ // must land in this window or the credential has no rebuild); a hook that
555
+ // must be best-effort swallows its own failures.
556
+ if (beforePromotion) {
557
+ await beforePromotion({
558
+ was: bootstrapWas,
559
+ zcapClient: bootstrapZcap,
560
+ did,
561
+ userKey,
562
+ establishment
563
+ });
564
+ }
565
+ // 6. The promotion, last: from here on the ladder's authority is exactly
566
+ // its licensed document inventory (delegation and log-anchored signing),
567
+ // and the bootstrap did:key stops verifying.
568
+ await ensurePromotedSpaceController({
569
+ was: bootstrapWas,
570
+ wasAsClient: bootstrapWas,
571
+ spaceId,
572
+ did
573
+ });
574
+ // The keystore half of the promotion, best-effort like every KMS touch
575
+ // here: the caller's closure no-ops when its KMS stage bound no keystore
576
+ // this run, and a throw is collected, never fatal -- the keystore's KMS
577
+ // authority is independent of the Space controller, so a failed promotion
578
+ // is retryable.
579
+ if (promoteKeystore) {
580
+ try {
581
+ await promoteKeystore({ did });
582
+ }
583
+ catch (err) {
584
+ failed.push({ stage: 'keystorePromotion', error: err });
585
+ }
586
+ }
587
+ return establishment;
588
+ }
589
+ /**
590
+ * The current `updateKeys` / `nextKeyHashes` view of a published log, for
591
+ * the bind-time rung attribution.
592
+ *
593
+ * @param published {PublishedWebvhLog}
594
+ * @returns {object}
595
+ */
596
+ function currentLogParameters(published) {
597
+ const params = effectiveParameters(published.log);
598
+ return params[params.length - 1] ?? { updateKeys: [], nextKeyHashes: [] };
599
+ }
600
+ /**
601
+ * A delegation's `expires` caveat, when it carries one.
602
+ *
603
+ * @param zcap {IZcap}
604
+ * @returns {string | undefined}
605
+ */
606
+ function zcapExpires(zcap) {
607
+ return zcap.expires;
608
+ }
609
+ /**
610
+ * Asserts a `bindRecord` result carries the members the ceremony reads --
611
+ * the observable half of the hook's standing-layout obligation (the sealed
612
+ * members are the codec's own duty and cannot be checked from here).
613
+ *
614
+ * @param options {object}
615
+ * @param options.bind {CredentialAnchoredBindResult}
616
+ * @param options.stage {string}
617
+ * @throws {TypeError}
618
+ */
619
+ function assertBindResult({ bind, stage }) {
620
+ if (typeof bind?.createdAt !== 'string' || bind.createdAt.length === 0) {
621
+ throw new TypeError(`The bindRecord hook's ${stage} returned no createdAt stamp; the ` +
622
+ 're-bind cannot advance past it.');
623
+ }
624
+ if (typeof bind.unlockSpaceId !== 'string' ||
625
+ bind.unlockSpaceId.length === 0) {
626
+ throw new TypeError(`The bindRecord hook's ${stage} returned no unlockSpaceId; the ` +
627
+ 'record it wrote cannot be located.');
628
+ }
629
+ }
630
+ /**
631
+ * Refuses a genesis whose roster or epoch stages did not land: the ceremony
632
+ * reports them on `failed` (a stage that could not run) and on
633
+ * `epochs.failed` (a collection the fan-out could not epoch) rather than
634
+ * throwing, and on a credential-anchored account no login-time sweep ever
635
+ * finishes them -- the establishment re-run is the only mender, so the
636
+ * establishment must stop here for it to be reached. A failed `didWebKeys`
637
+ * stage is deliberately NOT refused: the KMS stage is best-effort (reported
638
+ * on the result), and a keystore-less account is complete.
639
+ *
640
+ * @param options {object}
641
+ * @param options.failed {Array} the ceremony's collected stage failures
642
+ * @param [options.epochs] {WalletSpaceEpochsResult} the collection
643
+ * epoch fan-out's result, when the stage ran
644
+ * @throws {Error} carrying the first underlying failure as `cause`
645
+ */
646
+ function assertGenesisLanded({ failed, epochs }) {
647
+ const stage = failed.find(entry => entry.stage === 'roster' || entry.stage === 'epochs');
648
+ if (stage) {
649
+ throw new Error(`The credential-anchored genesis's ${stage.stage} stage failed.`, { cause: stage.error });
650
+ }
651
+ const collection = epochs?.failed[0];
652
+ if (collection) {
653
+ throw new Error('The credential-anchored genesis could not install a key epoch on ' +
654
+ `collection "${collection.collectionId}".`, { cause: collection.error });
655
+ }
656
+ }
657
+ //# sourceMappingURL=establish.js.map