@interop/wallet-core 0.56.0 → 0.58.0

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