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