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