@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,761 @@
1
+ import { ensurePromotedSpaceController } from '../genesis/accountGenesis.js';
2
+ import { keyAgreementCommitment, readPublishedLog, relationIds } from '../webvh/didWebvh.js';
3
+ import { isWebvhDid } from '../webvh/did.js';
4
+ import { accountLogPinId } from '../webvh/verifyLog.js';
5
+ import { delegateLogWrite, delegationProofKeyId } from '../recovery/recoveryDelegation.js';
6
+ import { mintUserKey } from '../keys/index.js';
7
+ import { WALLET_SPACE_PROVISION_ROSTER } from '../space/collections.js';
8
+ import { attributeLadderRung } from './ladder.js';
9
+ import { ladderVmAgent, ladderVmZcapClient } from './zcap.js';
10
+ import { clientAnnexDidParts, delegatedClientsPointer, mintDelegatedClientsDelegation } from './log.js';
11
+ import { assertBindResult, currentLogParameters, establishCredentialAnchoredAccount, zcapExpires } from './establish.js';
12
+ import { ensureRosterDeliveredEpochs } from './rosterDeliveredEpochs.js';
13
+ /**
14
+ * Mends a credential-anchored account from whatever tear state its durable
15
+ * artifacts show (see the module doc for the arms, their order, the caller
16
+ * obligations, and the mixed return contract). The options are a superset of
17
+ * {@link establishCredentialAnchoredAccount}'s hooks -- the establishment arm
18
+ * hands them through verbatim -- so a caller passes one bundle.
19
+ *
20
+ * @param options {object}
21
+ * @param options.account {CredentialAnchoredAccountCore} the
22
+ * BINDING-VERIFIED account core (controller, pointer, ladder seed) from
23
+ * the caller's standing record; never loose or unverified fields
24
+ * @param options.standing {object} the credential's standing client
25
+ * identity (`clientDid`, `keyAgreementKeyMultibase`, `recipientKid`,
26
+ * `keyAgreementKey`), as the establishment takes it
27
+ * @param options.bindRecord {CredentialAnchoredBindRecordHook} REQUIRED:
28
+ * the unlock-record codec closure; the establishment arm's re-run and the
29
+ * record-downgrade re-bind both write through it
30
+ * @param options.rosterStoreFor {Function} REQUIRED: the establishment's
31
+ * bootstrap-invoked roster store builder (`({ did }) => store`); used only
32
+ * inside the establishment arm's re-run
33
+ * @param options.bootstrapWasFor {Function} REQUIRED:
34
+ * `({ keyAgent }) => WasClient` signing as the ladder VM's bare did:key;
35
+ * the promotion arm's mend and the establishment arm ride it
36
+ * @param options.idStore {WebvhIdStore} the account's `id` collection
37
+ * store (the establishment arm's probe and the registry arm's rung
38
+ * attribution read through it)
39
+ * @param options.lowEntropy {boolean} threaded to the establishment arm
40
+ * (the hash-commitment fail-safe is the establishment's)
41
+ * @param [options.priorCreatedAt] {string} the standing record's freshness
42
+ * stamp; threads to the establishment re-run (skipping its first bind) and
43
+ * to the record-downgrade re-bind
44
+ * @param [options.delegatedClients] {IZcap} the record's sibling
45
+ * delegation, for the establishment arm's stage-3 Space resolution
46
+ * @param [options.provideDidWebKeys] {Function} the establishment's
47
+ * best-effort KMS/did:web thunk, handed through
48
+ * @param [options.promoteKeystore] {Function} the establishment's
49
+ * best-effort keystore promotion, handed through
50
+ * @param [options.beforePromotion] {Function} the caller's read-first
51
+ * registry hook. The establishment arm's re-run fires it in its own root
52
+ * window; the registry arm re-fires it under the post-promotion
53
+ * `invocation` authority on a repair-shaped entry. It must encapsulate its
54
+ * own read-first-and-skip-on-refused-read rule
55
+ * @param [options.invocation] {object} the post-promotion authority
56
+ * triple: `was` and `zcapClient` signing as the caller's live invocation
57
+ * identity (a transient visit's annex identity, or a durable client), and
58
+ * `capability` (the generation delegation) their requests ride. Required
59
+ * by the roster-and-epochs and registry arms
60
+ * @param [options.rosterStore] {EncryptionDescriptorStore} the user-key
61
+ * roster's store under the SAME post-promotion authority, with a
62
+ * ladder-signed log signer -- the roster arm's store (the bootstrap
63
+ * `rosterStoreFor` cannot serve it: the promoted Space refuses bootstrap
64
+ * invocations)
65
+ * @param [options.delegatedRead] {object} the promotion arm's
66
+ * failed-delegated-read trigger: `error` (the original failure, rethrown
67
+ * unchanged on a non-converging mend) and `retry` (re-runs the caller's
68
+ * read once after the promotion lands; its result stays caller-side)
69
+ * @param options.hasRosterEpochPin {Function} REQUIRED:
70
+ * `() => Promise<boolean>` -- the mint precondition port: whether this
71
+ * caller holds a durable roster-epoch pin for the account. A caller with
72
+ * no durable pins (the transient visit's in-memory pins) passes
73
+ * `async () => false` explicitly, so "no pin" is always a statement, never
74
+ * a dropped option
75
+ * @param [options.registry] {CredentialAnchoredRegistryContext} the
76
+ * registry arm's context, from the caller's own standing record
77
+ * @param [options.userKey] {UserKey} the session's user key, when the
78
+ * caller already holds one -- the registry arm's context when the roster
79
+ * arm did not deliver a key this invocation
80
+ * @param [options.repairShaped] {boolean} the caller's explicit
81
+ * repair-shaped flag: fires the roster completion and registry arms even
82
+ * when no earlier arm found a tear (the registry-only tear's entry, and
83
+ * the re-entry after a `reenterRepairShaped` report)
84
+ * @param [options.collectionIds] {string[]} the encrypted-collection set
85
+ * the roster arm covers (the completion probe, the mint preconditions, and
86
+ * the epoch fan-out); defaults to the wallet Space roster's encrypted
87
+ * collections
88
+ * @param [options.pinStore] {ResourceLogPinStore} chain-head pins for the
89
+ * log reads here
90
+ * @param [options.now] {number} epoch milliseconds, for tests
91
+ * @returns {Promise<CredentialAnchoredMendReport>}
92
+ * @throws {TypeError} synchronously, when a required hook is missing
93
+ * @throws the original `delegatedRead.error`, unchanged, when that trigger's
94
+ * promotion mend or read retry fails
95
+ * @throws the establishment-arm probe's `ResourceLogContinuityError`,
96
+ * unchanged: a served log refused against the chain-head pin must surface
97
+ * as the continuity refusal it is, never be swallowed into a full
98
+ * establishment re-run over a rolled-back or substituted log
99
+ */
100
+ export function mendCredentialAnchoredAccount(options) {
101
+ if (typeof options.bindRecord !== 'function') {
102
+ throw new TypeError('mendCredentialAnchoredAccount requires bindRecord: both the ' +
103
+ 'establishment re-run and the record-downgrade re-bind write the ' +
104
+ 'record through it.');
105
+ }
106
+ if (typeof options.rosterStoreFor !== 'function') {
107
+ throw new TypeError('mendCredentialAnchoredAccount requires rosterStoreFor: the ' +
108
+ "establishment arm's re-run cannot land a roster without it.");
109
+ }
110
+ if (typeof options.bootstrapWasFor !== 'function') {
111
+ throw new TypeError('mendCredentialAnchoredAccount requires bootstrapWasFor: the ' +
112
+ "promotion arm signs as the ladder VM's bare did:key.");
113
+ }
114
+ if (typeof options.hasRosterEpochPin !== 'function') {
115
+ throw new TypeError('mendCredentialAnchoredAccount requires hasRosterEpochPin: the mint ' +
116
+ 'preconditions must be told whether a durable roster-epoch pin is ' +
117
+ 'held (a caller with no durable pins passes async () => false).');
118
+ }
119
+ return mendCredentialAnchoredAccountChecked(options);
120
+ }
121
+ /**
122
+ * The checked body of {@link mendCredentialAnchoredAccount}.
123
+ *
124
+ * @param options {object} see {@link mendCredentialAnchoredAccount}
125
+ * @returns {Promise<CredentialAnchoredMendReport>}
126
+ */
127
+ async function mendCredentialAnchoredAccountChecked(options) {
128
+ const { account, standing, idStore, pinStore } = options;
129
+ const { pointer, ladderSeed } = account;
130
+ const report = { reenter: false };
131
+ // The bootstrap identity every arm needs. A failure here (a malformed
132
+ // ladder seed, a throwing factory) rides the report of whichever arm the
133
+ // pointer shape selects, keeping the documented throw surface -- except
134
+ // under the delegated-read trigger, whose contract is the original error.
135
+ let bootstrapAgent;
136
+ let bootstrapWas;
137
+ try {
138
+ bootstrapAgent = await ladderVmAgent({ ladderSeed });
139
+ bootstrapWas = options.bootstrapWasFor({ keyAgent: bootstrapAgent });
140
+ }
141
+ catch (err) {
142
+ if (!isWebvhDid(pointer.did)) {
143
+ report.establishment = {
144
+ converged: false,
145
+ outcome: 'established',
146
+ error: err
147
+ };
148
+ return report;
149
+ }
150
+ if (options.delegatedRead !== undefined) {
151
+ throw options.delegatedRead.error;
152
+ }
153
+ report.promotion = { converged: false, error: err };
154
+ return report;
155
+ }
156
+ const pinned = pinStore !== undefined
157
+ ? { pinStore, logId: accountLogPinId({ spaceId: pointer.spaceId }) }
158
+ : {};
159
+ // The establishment arm: a DID-less pointer. Probe durable state before
160
+ // re-running -- an already-resolvable, ladder-attributing log means the
161
+ // record was DOWNGRADED and the mend is a re-bind, never a stage-1 re-run.
162
+ if (!isWebvhDid(pointer.did)) {
163
+ let published;
164
+ try {
165
+ published = await readPublishedLog({ idStore, ...pinned });
166
+ }
167
+ catch (err) {
168
+ // A continuity refusal against the chain-head pin is a served
169
+ // rollback, fork, or identity switch: surface it as itself, never
170
+ // swallow it into "no log" and re-run an establishment over it.
171
+ if (err.name === 'ResourceLogContinuityError') {
172
+ throw err;
173
+ }
174
+ // Any other unreadable log probes as unresolved; the establishment
175
+ // run below meets (and reports) whatever the store's real state is.
176
+ published = undefined;
177
+ }
178
+ const attributed = published === undefined
179
+ ? undefined
180
+ : await attributeLadderRungSafely({
181
+ ladderSeed,
182
+ published
183
+ });
184
+ // The re-bind classifies ONLY a fully established account: the log
185
+ // resolves, the ladder attributes with a revealed rung, AND the
186
+ // document already points at a generation. A revealed rung with no
187
+ // pointer is the stage-3 tear (the pointer entry never landed), whose
188
+ // mender is the establishment run itself -- its pointer stage is the
189
+ // ensure -- never a re-bind that has no sibling target to name.
190
+ const pointed = published === undefined
191
+ ? undefined
192
+ : delegatedClientsPointer({ doc: published.doc });
193
+ if (published !== undefined &&
194
+ attributed?.state === 'revealed' &&
195
+ pointed !== undefined) {
196
+ try {
197
+ await rebindDowngradedRecord({
198
+ options,
199
+ published,
200
+ pointed,
201
+ bootstrapAgent
202
+ });
203
+ report.establishment = {
204
+ converged: true,
205
+ outcome: 'rebound',
206
+ did: published.did
207
+ };
208
+ report.reenter = true;
209
+ report.reenterRepairShaped = true;
210
+ }
211
+ catch (err) {
212
+ report.establishment = {
213
+ converged: false,
214
+ outcome: 'rebound',
215
+ error: err
216
+ };
217
+ }
218
+ return report;
219
+ }
220
+ try {
221
+ const established = await establishCredentialAnchoredAccount({
222
+ wasServerUrl: pointer.host,
223
+ spaceId: pointer.spaceId,
224
+ ladderSeed,
225
+ standing,
226
+ bindRecord: options.bindRecord,
227
+ rosterStoreFor: options.rosterStoreFor,
228
+ bootstrapWasFor: options.bootstrapWasFor,
229
+ idStore,
230
+ lowEntropy: options.lowEntropy,
231
+ ...(published !== undefined ? { expectedDid: published.did } : {}),
232
+ ...(options.priorCreatedAt !== undefined
233
+ ? { priorCreatedAt: options.priorCreatedAt }
234
+ : {}),
235
+ ...(options.delegatedClients !== undefined
236
+ ? { delegatedClients: options.delegatedClients }
237
+ : {}),
238
+ ...(options.provideDidWebKeys
239
+ ? { provideDidWebKeys: options.provideDidWebKeys }
240
+ : {}),
241
+ ...(options.promoteKeystore
242
+ ? { promoteKeystore: options.promoteKeystore }
243
+ : {}),
244
+ ...(options.beforePromotion
245
+ ? { beforePromotion: options.beforePromotion }
246
+ : {}),
247
+ ...(pinStore !== undefined ? { pinStore } : {}),
248
+ ...(options.now !== undefined ? { now: options.now } : {})
249
+ });
250
+ report.establishment = {
251
+ converged: true,
252
+ outcome: 'established',
253
+ did: established.did
254
+ };
255
+ report.reenter = true;
256
+ }
257
+ catch (err) {
258
+ report.establishment = {
259
+ converged: false,
260
+ outcome: 'established',
261
+ error: err
262
+ };
263
+ }
264
+ return report;
265
+ }
266
+ // The promoted-pointer arms.
267
+ const did = pointer.did;
268
+ let mended = false;
269
+ // The promotion arm, delegated-read trigger: the caller's read already
270
+ // failed, so mend and retry -- and rethrow the ORIGINAL error unchanged
271
+ // when either the mend or the retry still fails (a healthy account's
272
+ // doomed promotion attempt refuses too, landing exactly here).
273
+ if (options.delegatedRead !== undefined) {
274
+ const delegatedRead = options.delegatedRead;
275
+ let promotionOutcome;
276
+ try {
277
+ promotionOutcome = await ensurePromotedSpaceController({
278
+ was: bootstrapWas,
279
+ wasAsClient: bootstrapWas,
280
+ spaceId: pointer.spaceId,
281
+ did
282
+ });
283
+ }
284
+ catch {
285
+ throw delegatedRead.error;
286
+ }
287
+ try {
288
+ await delegatedRead.retry();
289
+ }
290
+ catch {
291
+ throw delegatedRead.error;
292
+ }
293
+ report.promotion = { converged: true, outcome: 'retried' };
294
+ // Only a promotion that WROTE marks the entry repair-shaped: a
295
+ // `confirmed` outcome means the account was healthy all along (the
296
+ // failed read was a flap), and firing the roster completion and
297
+ // registry arms off it would repair nothing.
298
+ mended = promotionOutcome !== 'confirmed';
299
+ }
300
+ else {
301
+ // The authority-neutral probe: a null read under the bootstrap did:key
302
+ // is evidence of promotion (WAS masks "unauthorized" as absence), and
303
+ // only an AUTHORIZED read showing a non-account controller classifies
304
+ // the tear. Non-convergence here reports; there is no antecedent error
305
+ // to rethrow.
306
+ let described = null;
307
+ let probeFailed = false;
308
+ try {
309
+ described = (await bootstrapWas.space(pointer.spaceId).describe());
310
+ }
311
+ catch (err) {
312
+ probeFailed = true;
313
+ report.promotion = { converged: false, error: err };
314
+ }
315
+ if (!probeFailed && described !== null && described.controller !== did) {
316
+ try {
317
+ const promotionOutcome = await ensurePromotedSpaceController({
318
+ was: bootstrapWas,
319
+ wasAsClient: bootstrapWas,
320
+ spaceId: pointer.spaceId,
321
+ did
322
+ });
323
+ report.promotion = { converged: true, outcome: 'promoted' };
324
+ // A concurrent run may have promoted between the probe and the
325
+ // ensure; `confirmed` then repairs nothing here either.
326
+ mended = promotionOutcome !== 'confirmed';
327
+ }
328
+ catch (err) {
329
+ report.promotion = { converged: false, error: err };
330
+ }
331
+ }
332
+ }
333
+ // The roster-and-epochs arm, gated on the ratified completion test --
334
+ // "roster delivered AND every encrypted collection carries epoch[0]",
335
+ // durable state alone, never roster presence alone. Fires on: an absent
336
+ // roster head, an epoch-less encrypted collection behind a present roster
337
+ // (the completion probe, ~one Description read per encrypted collection,
338
+ // the stated budget), a prior arm's mend, or the caller's repair-shaped
339
+ // flag.
340
+ const rosterStore = options.rosterStore;
341
+ const invocation = options.invocation;
342
+ if (rosterStore !== undefined && invocation !== undefined) {
343
+ let rosterAbsent = false;
344
+ let detectionFailed = false;
345
+ try {
346
+ rosterAbsent = (await rosterStore.read()) === null;
347
+ }
348
+ catch (err) {
349
+ // Transport, never incompleteness: report, mint nothing, refuse
350
+ // nothing.
351
+ detectionFailed = true;
352
+ report.rosterEpochs = { converged: false, error: err };
353
+ }
354
+ let collectionEpochless = false;
355
+ if (!detectionFailed && !rosterAbsent && !mended && !options.repairShaped) {
356
+ try {
357
+ collectionEpochless = await hasEpochlessEncryptedCollection({
358
+ options,
359
+ invocation
360
+ });
361
+ }
362
+ catch (err) {
363
+ detectionFailed = true;
364
+ report.rosterEpochs = { converged: false, error: err };
365
+ }
366
+ }
367
+ if (!detectionFailed &&
368
+ (rosterAbsent || collectionEpochless || mended || options.repairShaped)) {
369
+ report.rosterEpochs = await runRosterEpochsArm({
370
+ options,
371
+ did,
372
+ rosterStore,
373
+ invocation
374
+ });
375
+ if (report.rosterEpochs.converged) {
376
+ mended = true;
377
+ }
378
+ }
379
+ }
380
+ // The registry arm: a repair-shaped entry re-fires the caller's
381
+ // read-first hook under the post-promotion authority (the root window the
382
+ // establishment's own write used is permanently closed). The hook owns
383
+ // the registry protocol; a hook that skips on a refused read is the
384
+ // caller's own rule.
385
+ if (options.beforePromotion !== undefined &&
386
+ (mended || options.repairShaped)) {
387
+ report.registry = await runRegistryArm({ options, did, report });
388
+ }
389
+ return report;
390
+ }
391
+ /**
392
+ * The ladder attribution over a published log, resolved as a value: the
393
+ * establishment-arm probe treats an attribution failure (another ladder's
394
+ * account, a struck inventory) the same as an unresolved log -- the
395
+ * establishment run is what meets and reports the real state.
396
+ *
397
+ * @param options {object}
398
+ * @param options.ladderSeed {Uint8Array}
399
+ * @param options.published {PublishedWebvhLog}
400
+ * @returns {Promise<object | undefined>} the attributed rung and its
401
+ * state, or undefined when the attribution refuses
402
+ */
403
+ async function attributeLadderRungSafely({ ladderSeed, published }) {
404
+ try {
405
+ return await attributeLadderRung({
406
+ ladderSeed,
407
+ published: currentLogParameters(published)
408
+ });
409
+ }
410
+ catch {
411
+ return undefined;
412
+ }
413
+ }
414
+ /**
415
+ * The record-downgrade re-bind (the establishment's stage 4, standalone):
416
+ * fresh ladder-VM-signed bridge and sibling delegations, then the record
417
+ * re-bound to the published DID with the management delegation. Runs only
418
+ * when the ladder attributes on the published log AND the document already
419
+ * carries the delegated-clients pointer (the caller's gate), so the
420
+ * delegations it signs verify under the standing document and the sibling
421
+ * has a target to name.
422
+ *
423
+ * @param options {object}
424
+ * @param options.options {object} the mend options
425
+ * @param options.published {PublishedWebvhLog} the resolved account log
426
+ * @param options.pointed {string} the document's delegated-clients pointer
427
+ * (the annex DID the sibling delegation targets)
428
+ * @param options.bootstrapAgent {ICapabilityAgent} the ladder VM's bare
429
+ * did:key agent (the record's controller field, matching the
430
+ * establishment's own binds)
431
+ * @returns {Promise<void>}
432
+ */
433
+ async function rebindDowngradedRecord({ options, published, pointed, bootstrapAgent }) {
434
+ const { account, standing } = options;
435
+ const { pointer, ladderSeed } = account;
436
+ const fullPointer = {
437
+ spaceId: pointer.spaceId,
438
+ host: pointer.host,
439
+ did: published.did
440
+ };
441
+ const ladderZcap = await ladderVmZcapClient({
442
+ accountDid: published.did,
443
+ ladderSeed
444
+ });
445
+ const bridge = await delegateLogWrite({
446
+ zcapClient: ladderZcap,
447
+ pointer: fullPointer,
448
+ recoveryClientDid: standing.clientDid
449
+ });
450
+ const sibling = await mintDelegatedClientsDelegation({
451
+ zcapClient: ladderZcap,
452
+ wasServerUrl: pointer.host,
453
+ clientAnnexSpaceId: clientAnnexDidParts({ did: pointed }).spaceId,
454
+ controller: standing.clientDid,
455
+ ...(options.now !== undefined ? { now: options.now } : {})
456
+ });
457
+ const rebind = await options.bindRecord({
458
+ controller: bootstrapAgent.id,
459
+ pointer: fullPointer,
460
+ delegation: bridge,
461
+ delegatedClients: sibling,
462
+ delegateManagementTo: published.did,
463
+ ...(options.priorCreatedAt !== undefined
464
+ ? { priorCreatedAt: options.priorCreatedAt }
465
+ : {})
466
+ });
467
+ assertBindResult({ bind: rebind, stage: 'downgrade re-bind' });
468
+ }
469
+ /**
470
+ * The refusing mint preconditions, carried as a throwable so the shared
471
+ * mint-policy stage's `beforeMint` seam can surface them: thrown and caught
472
+ * inside this module only, so `instanceof` is safe here.
473
+ */
474
+ class RosterMintRefusedSignal extends Error {
475
+ refusal;
476
+ constructor(options) {
477
+ super('The roster mint preconditions refused.');
478
+ this.name = 'RosterMintRefusedSignal';
479
+ this.refusal = options.refusal;
480
+ }
481
+ }
482
+ /**
483
+ * The encrypted-collection set the roster arm covers: the caller's
484
+ * `collectionIds` override, or the wallet Space roster's encrypted
485
+ * collections.
486
+ *
487
+ * @param options {object} the mend options
488
+ * @returns {string[]}
489
+ */
490
+ function encryptedCollectionIds(options) {
491
+ return (options.collectionIds ??
492
+ WALLET_SPACE_PROVISION_ROSTER.filter(spec => spec.encryption === 'edv').map(spec => spec.collectionId));
493
+ }
494
+ /**
495
+ * The completion probe's per-collection half: whether any encrypted
496
+ * collection lacks epoch[0], read under the caller's post-promotion
497
+ * authority. A null Description counts as epoch-less here: with the roster
498
+ * present no mint can follow, so firing the arm on an absent (or masked)
499
+ * collection only drives the create-if-absent fan-out, whose own refusals
500
+ * ride the report's `epochsFailed` list. A thrown read propagates to the
501
+ * caller as transport.
502
+ *
503
+ * @param options {object}
504
+ * @param options.options {object} the mend options
505
+ * @param options.invocation {object} the post-promotion authority triple
506
+ * @returns {Promise<boolean>}
507
+ */
508
+ async function hasEpochlessEncryptedCollection({ options, invocation }) {
509
+ const space = invocation.was.space(options.account.pointer.spaceId, {
510
+ capability: invocation.capability
511
+ });
512
+ for (const collectionId of encryptedCollectionIds(options)) {
513
+ const description = (await space.collection(collectionId).describe());
514
+ if ((description?.encryption?.epochs?.length ?? 0) === 0) {
515
+ return true;
516
+ }
517
+ }
518
+ return false;
519
+ }
520
+ /**
521
+ * The roster-and-epochs arm's body: the shared mint-policy stage under the
522
+ * caller's post-promotion authority, with the mint preconditions threaded
523
+ * into the stage's own mint decision (`beforeMint`) -- checked against the
524
+ * SAME absent-roster observation the mint acts on, whichever trigger fired
525
+ * the arm, never against the arm's earlier detection read.
526
+ *
527
+ * @param options {object}
528
+ * @param options.options {object} the mend options
529
+ * @param options.did {string} the account DID
530
+ * @param options.rosterStore {EncryptionDescriptorStore}
531
+ * @param options.invocation {object} the post-promotion authority triple
532
+ * @returns {Promise<object>} the report member
533
+ */
534
+ async function runRosterEpochsArm({ options, did, rosterStore, invocation }) {
535
+ const { account, standing } = options;
536
+ let delivered;
537
+ try {
538
+ delivered = await ensureRosterDeliveredEpochs({
539
+ store: rosterStore,
540
+ candidateUserKey: await mintUserKey(),
541
+ clientKeyAgreementKey: standing.keyAgreementKey,
542
+ was: invocation.was,
543
+ spaceId: account.pointer.spaceId,
544
+ capability: invocation.capability,
545
+ ...(options.collectionIds !== undefined
546
+ ? { collectionIds: options.collectionIds }
547
+ : {}),
548
+ beforeMint: async () => {
549
+ const refusal = await rosterMintRefusal({ options, did, invocation });
550
+ if (refusal !== undefined) {
551
+ throw new RosterMintRefusedSignal({ refusal });
552
+ }
553
+ }
554
+ });
555
+ }
556
+ catch (err) {
557
+ if (err instanceof RosterMintRefusedSignal) {
558
+ return err.refusal;
559
+ }
560
+ // The shared stage rethrows transport errors unchanged; here they are
561
+ // the arm's report, never a refusal shape of their own.
562
+ return { converged: false, error: err };
563
+ }
564
+ if (delivered.outcome === 'no-wrap') {
565
+ return { converged: false, outcome: 'no-wrap', error: delivered.error };
566
+ }
567
+ if (delivered.epochs.failed.length > 0) {
568
+ return {
569
+ converged: false,
570
+ outcome: delivered.outcome,
571
+ userKey: delivered.userKey,
572
+ epochsFailed: delivered.epochs.failed
573
+ };
574
+ }
575
+ return {
576
+ converged: true,
577
+ outcome: delivered.outcome,
578
+ userKey: delivered.userKey
579
+ };
580
+ }
581
+ /**
582
+ * The mint preconditions, fired from the shared stage's `beforeMint` seam --
583
+ * on the same absent-roster observation the mint acts on: a
584
+ * fabricated-absent roster must not become a single-recipient genesis. Each
585
+ * refusal (and a precondition read that failed or masked, which cannot
586
+ * prove the mint safe) reports as `mint-refused` with the reason as its
587
+ * error.
588
+ *
589
+ * @param options {object}
590
+ * @param options.options {object} the mend options
591
+ * @param options.did {string}
592
+ * @param options.invocation {object}
593
+ * @returns {Promise<object | undefined>} the refusing report member, or
594
+ * undefined when the mint may proceed
595
+ */
596
+ async function rosterMintRefusal({ options, did, invocation }) {
597
+ const { account, standing, idStore, pinStore } = options;
598
+ const refused = (error) => ({
599
+ converged: false,
600
+ outcome: 'mint-refused',
601
+ error
602
+ });
603
+ try {
604
+ if (await options.hasRosterEpochPin()) {
605
+ return refused(new Error('A durable roster-epoch pin is held for this account; a served ' +
606
+ 'absent roster reads as a rollback, not a mint license.'));
607
+ }
608
+ // No OTHER standing credential in the verified document: every
609
+ // keyAgreement entry must be this credential's own publication
610
+ // (verbatim, or its hash commitment).
611
+ const published = await readPublishedLog({
612
+ idStore,
613
+ expectedDid: did,
614
+ ...(pinStore !== undefined
615
+ ? {
616
+ pinStore,
617
+ logId: accountLogPinId({ spaceId: account.pointer.spaceId })
618
+ }
619
+ : {})
620
+ });
621
+ if (published === undefined) {
622
+ return refused(new Error('The account log did not resolve; the mint preconditions cannot ' +
623
+ 'be verified.'));
624
+ }
625
+ const commitment = await keyAgreementCommitment({
626
+ keyAgreementKeyMultibase: standing.keyAgreementKeyMultibase
627
+ });
628
+ const own = new Set([
629
+ `${did}#${standing.keyAgreementKeyMultibase}`,
630
+ `${did}#${commitment}`
631
+ ]);
632
+ const doc = published.doc;
633
+ const foreign = relationIds(doc.keyAgreement).filter(id => !own.has(id));
634
+ if (foreign.length > 0) {
635
+ return refused(new Error('The verified document publishes another standing credential ' +
636
+ `(${foreign.join(', ')}); minting a fresh roster would evict it.`));
637
+ }
638
+ // No encrypted collection already epoch'd: a collection keyed under an
639
+ // earlier user key beside an "absent" roster is fabricated absence.
640
+ const space = invocation.was.space(account.pointer.spaceId, {
641
+ capability: invocation.capability
642
+ });
643
+ for (const collectionId of encryptedCollectionIds(options)) {
644
+ const description = (await space.collection(collectionId).describe());
645
+ if (description === null) {
646
+ // WAS masks an unauthorized read as absence, so a null Description
647
+ // cannot prove the collection epoch-less: refuse as unreadable
648
+ // rather than license the mint on it.
649
+ return refused(new Error(`Collection "${collectionId}" has no readable Description ` +
650
+ 'under this authority; the mint preconditions cannot be ' +
651
+ 'verified.'));
652
+ }
653
+ if ((description.encryption?.epochs?.length ?? 0) > 0) {
654
+ return refused(new Error(`Collection "${collectionId}" already carries a key epoch; a ` +
655
+ 'served absent roster beside it is fabricated absence.'));
656
+ }
657
+ }
658
+ }
659
+ catch (err) {
660
+ return refused(err);
661
+ }
662
+ return undefined;
663
+ }
664
+ /**
665
+ * The registry arm's body: synthesize the establishment-shaped context from
666
+ * the caller's standing record and the log-attributed rung, then fire the
667
+ * caller's read-first hook under the post-promotion authority.
668
+ *
669
+ * @param options {object}
670
+ * @param options.options {object} the mend options
671
+ * @param options.did {string}
672
+ * @param options.report {CredentialAnchoredMendReport} the report so far
673
+ * (the roster arm's delivered key feeds the hook context)
674
+ * @returns {Promise<object>} the report member
675
+ */
676
+ async function runRegistryArm({ options, did, report }) {
677
+ const { account, standing, idStore, pinStore } = options;
678
+ const invocation = options.invocation;
679
+ if (invocation === undefined) {
680
+ return { converged: false, skipped: 'no-invocation' };
681
+ }
682
+ const registry = options.registry;
683
+ if (registry === undefined) {
684
+ return { converged: false, skipped: 'no-registry-context' };
685
+ }
686
+ const userKey = report.rosterEpochs?.userKey ?? options.userKey;
687
+ if (userKey === undefined) {
688
+ return { converged: false, skipped: 'no-user-key' };
689
+ }
690
+ try {
691
+ const published = await readPublishedLog({
692
+ idStore,
693
+ expectedDid: did,
694
+ ...(pinStore !== undefined
695
+ ? {
696
+ pinStore,
697
+ logId: accountLogPinId({ spaceId: account.pointer.spaceId })
698
+ }
699
+ : {})
700
+ });
701
+ if (published === undefined) {
702
+ return { converged: false, skipped: 'no-account-log' };
703
+ }
704
+ const attributed = await attributeLadderRungSafely({
705
+ ladderSeed: account.ladderSeed,
706
+ published
707
+ });
708
+ if (attributed?.state !== 'revealed') {
709
+ return { converged: false, skipped: 'no-revealed-rung' };
710
+ }
711
+ const delegationKeyId = registry.delegation
712
+ ? delegationProofKeyId(registry.delegation)
713
+ : undefined;
714
+ const delegatedClientsKeyId = registry.delegatedClients
715
+ ? delegationProofKeyId(registry.delegatedClients)
716
+ : undefined;
717
+ const standingFields = {
718
+ rosterKid: standing.recipientKid,
719
+ keyAgreementKeyMultibase: standing.keyAgreementKeyMultibase,
720
+ updateKeyMultibase: attributed.rung.keyMultibase,
721
+ unlockClientDid: standing.clientDid,
722
+ ...(delegationKeyId ? { delegationKeyId } : {}),
723
+ ...(registry.delegation && zcapExpires(registry.delegation)
724
+ ? { delegationExpires: zcapExpires(registry.delegation) }
725
+ : {}),
726
+ ...(delegatedClientsKeyId ? { delegatedClientsKeyId } : {}),
727
+ ...(registry.delegatedClients && zcapExpires(registry.delegatedClients)
728
+ ? { delegatedClientsExpires: zcapExpires(registry.delegatedClients) }
729
+ : {}),
730
+ ...(registry.unlockKeyAgreementKeyId
731
+ ? { unlockKeyAgreementKeyId: registry.unlockKeyAgreementKeyId }
732
+ : {}),
733
+ ...(registry.unlockKeyAgreementKeyMultibase
734
+ ? {
735
+ unlockKeyAgreementKeyMultibase: registry.unlockKeyAgreementKeyMultibase
736
+ }
737
+ : {})
738
+ };
739
+ const establishment = {
740
+ did,
741
+ unlockSpaceId: registry.unlockSpaceId,
742
+ ...(registry.manageCapability
743
+ ? { manageCapability: registry.manageCapability }
744
+ : {}),
745
+ standingFields,
746
+ failed: []
747
+ };
748
+ await options.beforePromotion({
749
+ was: invocation.was,
750
+ zcapClient: invocation.zcapClient,
751
+ did,
752
+ userKey,
753
+ establishment
754
+ });
755
+ return { converged: true };
756
+ }
757
+ catch (err) {
758
+ return { converged: false, error: err };
759
+ }
760
+ }
761
+ //# sourceMappingURL=mend.js.map