@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.
- package/dist/clientAnnex/establish.d.ts +401 -0
- package/dist/clientAnnex/establish.d.ts.map +1 -0
- package/dist/clientAnnex/establish.js +660 -0
- package/dist/clientAnnex/establish.js.map +1 -0
- package/dist/clientAnnex/heal.d.ts +45 -1
- package/dist/clientAnnex/heal.d.ts.map +1 -1
- package/dist/clientAnnex/heal.js +39 -12
- package/dist/clientAnnex/heal.js.map +1 -1
- package/dist/clientAnnex/index.d.ts +7 -1
- package/dist/clientAnnex/index.d.ts.map +1 -1
- package/dist/clientAnnex/index.js +4 -1
- package/dist/clientAnnex/index.js.map +1 -1
- package/dist/clientAnnex/mend.d.ts +322 -0
- package/dist/clientAnnex/mend.d.ts.map +1 -0
- package/dist/clientAnnex/mend.js +761 -0
- package/dist/clientAnnex/mend.js.map +1 -0
- package/dist/clientAnnex/rosterDeliveredEpochs.d.ts +114 -0
- package/dist/clientAnnex/rosterDeliveredEpochs.d.ts.map +1 -0
- package/dist/clientAnnex/rosterDeliveredEpochs.js +118 -0
- package/dist/clientAnnex/rosterDeliveredEpochs.js.map +1 -0
- package/dist/space/ceremony.d.ts +17 -0
- package/dist/space/ceremony.d.ts.map +1 -0
- package/dist/space/ceremony.js +26 -0
- package/dist/space/ceremony.js.map +1 -0
- package/dist/space/index.d.ts +4 -0
- package/dist/space/index.d.ts.map +1 -1
- package/dist/space/index.js +3 -0
- package/dist/space/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|