@interop/wallet-core 0.49.0 → 0.50.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/README.md +5 -2
- package/dist/clientAnnex/credentialAnchoredGenesis.d.ts +118 -0
- package/dist/clientAnnex/credentialAnchoredGenesis.d.ts.map +1 -0
- package/dist/clientAnnex/credentialAnchoredGenesis.js +174 -0
- package/dist/clientAnnex/credentialAnchoredGenesis.js.map +1 -0
- package/dist/clientAnnex/forget.d.ts +138 -0
- package/dist/clientAnnex/forget.d.ts.map +1 -0
- package/dist/clientAnnex/forget.js +133 -0
- package/dist/clientAnnex/forget.js.map +1 -0
- package/dist/clientAnnex/forgetLast.d.ts +267 -0
- package/dist/clientAnnex/forgetLast.d.ts.map +1 -0
- package/dist/clientAnnex/forgetLast.js +374 -0
- package/dist/clientAnnex/forgetLast.js.map +1 -0
- package/dist/{webvh/companionGc.d.ts → clientAnnex/gc.d.ts} +50 -16
- package/dist/clientAnnex/gc.d.ts.map +1 -0
- package/dist/{webvh/companionGc.js → clientAnnex/gc.js} +103 -71
- package/dist/clientAnnex/gc.js.map +1 -0
- package/dist/clientAnnex/index.d.ts +60 -0
- package/dist/clientAnnex/index.d.ts.map +1 -0
- package/dist/clientAnnex/index.js +60 -0
- package/dist/clientAnnex/index.js.map +1 -0
- package/dist/{unlock → clientAnnex}/ladder.d.ts +89 -15
- package/dist/clientAnnex/ladder.d.ts.map +1 -0
- package/dist/clientAnnex/ladder.js +474 -0
- package/dist/clientAnnex/ladder.js.map +1 -0
- package/dist/clientAnnex/ladderAnchored.d.ts +276 -0
- package/dist/clientAnnex/ladderAnchored.d.ts.map +1 -0
- package/dist/clientAnnex/ladderAnchored.js +631 -0
- package/dist/clientAnnex/ladderAnchored.js.map +1 -0
- package/dist/{webvh/companion.d.ts → clientAnnex/log.d.ts} +333 -119
- package/dist/clientAnnex/log.d.ts.map +1 -0
- package/dist/{webvh/companion.js → clientAnnex/log.js} +583 -199
- package/dist/clientAnnex/log.js.map +1 -0
- package/dist/clientAnnex/recoveryLadderAnchored.d.ts +103 -0
- package/dist/clientAnnex/recoveryLadderAnchored.d.ts.map +1 -0
- package/dist/clientAnnex/recoveryLadderAnchored.js +263 -0
- package/dist/clientAnnex/recoveryLadderAnchored.js.map +1 -0
- package/dist/{unlock → clientAnnex}/selfEnroll.d.ts +1 -1
- package/dist/clientAnnex/selfEnroll.d.ts.map +1 -0
- package/dist/{unlock → clientAnnex}/selfEnroll.js +1 -1
- package/dist/clientAnnex/selfEnroll.js.map +1 -0
- package/dist/clientAnnex/zcap.d.ts +49 -0
- package/dist/clientAnnex/zcap.d.ts.map +1 -0
- package/dist/clientAnnex/zcap.js +101 -0
- package/dist/clientAnnex/zcap.js.map +1 -0
- package/dist/clients/index.d.ts +9 -6
- package/dist/clients/index.d.ts.map +1 -1
- package/dist/clients/index.js +8 -5
- package/dist/clients/index.js.map +1 -1
- package/dist/clients/listing.d.ts +32 -0
- package/dist/clients/listing.d.ts.map +1 -1
- package/dist/clients/listing.js +47 -1
- package/dist/clients/listing.js.map +1 -1
- package/dist/clients/revocation.d.ts +33 -1
- package/dist/clients/revocation.d.ts.map +1 -1
- package/dist/clients/revocation.js +16 -4
- package/dist/clients/revocation.js.map +1 -1
- package/dist/enrollment/index.d.ts +6 -4
- package/dist/enrollment/index.d.ts.map +1 -1
- package/dist/enrollment/index.js +6 -4
- package/dist/enrollment/index.js.map +1 -1
- package/dist/enrollment/onboardingInvite.d.ts +5 -88
- package/dist/enrollment/onboardingInvite.d.ts.map +1 -1
- package/dist/enrollment/onboardingInvite.js +5 -168
- package/dist/enrollment/onboardingInvite.js.map +1 -1
- package/dist/genesis/index.d.ts +5 -0
- package/dist/genesis/index.d.ts.map +1 -1
- package/dist/genesis/index.js +5 -0
- package/dist/genesis/index.js.map +1 -1
- package/dist/keys/index.d.ts +1 -1
- package/dist/keys/index.d.ts.map +1 -1
- package/dist/keys/index.js +1 -1
- package/dist/keys/index.js.map +1 -1
- package/dist/keys/spaceEpochs.d.ts +7 -2
- package/dist/keys/spaceEpochs.d.ts.map +1 -1
- package/dist/keys/spaceEpochs.js +8 -2
- package/dist/keys/spaceEpochs.js.map +1 -1
- package/dist/keys/userKeyRoster.d.ts +39 -0
- package/dist/keys/userKeyRoster.d.ts.map +1 -1
- package/dist/keys/userKeyRoster.js +43 -1
- package/dist/keys/userKeyRoster.js.map +1 -1
- package/dist/recovery/index.d.ts +5 -3
- package/dist/recovery/index.d.ts.map +1 -1
- package/dist/recovery/index.js +4 -2
- package/dist/recovery/index.js.map +1 -1
- package/dist/recovery/recoveryDelegation.d.ts +35 -12
- package/dist/recovery/recoveryDelegation.d.ts.map +1 -1
- package/dist/recovery/recoveryDelegation.js +84 -86
- package/dist/recovery/recoveryDelegation.js.map +1 -1
- package/dist/recovery/recoveryWebvh.d.ts +32 -1
- package/dist/recovery/recoveryWebvh.d.ts.map +1 -1
- package/dist/recovery/recoveryWebvh.js +2 -2
- package/dist/recovery/recoveryWebvh.js.map +1 -1
- package/dist/request/capabilityRequest.d.ts +33 -0
- package/dist/request/capabilityRequest.d.ts.map +1 -0
- package/dist/request/capabilityRequest.js +35 -0
- package/dist/request/capabilityRequest.js.map +1 -0
- package/dist/request/ephemeralExchange.d.ts +118 -0
- package/dist/request/ephemeralExchange.d.ts.map +1 -0
- package/dist/request/ephemeralExchange.js +224 -0
- package/dist/request/ephemeralExchange.js.map +1 -0
- package/dist/request/index.d.ts +6 -0
- package/dist/request/index.d.ts.map +1 -1
- package/dist/request/index.js +6 -0
- package/dist/request/index.js.map +1 -1
- package/dist/space/activity.d.ts +2 -2
- package/dist/space/activity.js +3 -3
- package/dist/space/activity.js.map +1 -1
- package/dist/unlock/index.d.ts +11 -23
- package/dist/unlock/index.d.ts.map +1 -1
- package/dist/unlock/index.js +10 -21
- package/dist/unlock/index.js.map +1 -1
- package/dist/unlock/retire.d.ts +47 -3
- package/dist/unlock/retire.d.ts.map +1 -1
- package/dist/unlock/retire.js +21 -5
- package/dist/unlock/retire.js.map +1 -1
- package/dist/unlock/standingWebvh.d.ts +37 -82
- package/dist/unlock/standingWebvh.d.ts.map +1 -1
- package/dist/unlock/standingWebvh.js +64 -247
- package/dist/unlock/standingWebvh.js.map +1 -1
- package/dist/unlock/unlockRecord.d.ts +13 -6
- package/dist/unlock/unlockRecord.d.ts.map +1 -1
- package/dist/unlock/unlockRecord.js +14 -8
- package/dist/unlock/unlockRecord.js.map +1 -1
- package/dist/webvh/delegatedLogStore.d.ts +5 -5
- package/dist/webvh/delegatedLogStore.js +1 -1
- package/dist/webvh/didWebvh.d.ts +19 -9
- package/dist/webvh/didWebvh.d.ts.map +1 -1
- package/dist/webvh/didWebvh.js +16 -16
- package/dist/webvh/didWebvh.js.map +1 -1
- package/dist/webvh/index.d.ts +9 -25
- package/dist/webvh/index.d.ts.map +1 -1
- package/dist/webvh/index.js +9 -23
- package/dist/webvh/index.js.map +1 -1
- package/dist/webvh/revokeClient.d.ts +77 -5
- package/dist/webvh/revokeClient.d.ts.map +1 -1
- package/dist/webvh/revokeClient.js +154 -61
- package/dist/webvh/revokeClient.js.map +1 -1
- package/dist/webvh/standingZcap.d.ts +11 -1
- package/dist/webvh/standingZcap.d.ts.map +1 -1
- package/dist/webvh/standingZcap.js +13 -14
- package/dist/webvh/standingZcap.js.map +1 -1
- package/dist/webvh/wasIdStore.d.ts +13 -7
- package/dist/webvh/wasIdStore.d.ts.map +1 -1
- package/dist/webvh/wasIdStore.js +8 -4
- package/dist/webvh/wasIdStore.js.map +1 -1
- package/dist/webvh/zcap.d.ts +0 -24
- package/dist/webvh/zcap.d.ts.map +1 -1
- package/dist/webvh/zcap.js +0 -42
- package/dist/webvh/zcap.js.map +1 -1
- package/package.json +40 -34
- package/dist/unlock/ladder.d.ts.map +0 -1
- package/dist/unlock/ladder.js +0 -258
- package/dist/unlock/ladder.js.map +0 -1
- package/dist/unlock/selfEnroll.d.ts.map +0 -1
- package/dist/unlock/selfEnroll.js.map +0 -1
- package/dist/webvh/companion.d.ts.map +0 -1
- package/dist/webvh/companion.js.map +0 -1
- package/dist/webvh/companionGc.d.ts.map +0 -1
- package/dist/webvh/companionGc.js.map +0 -1
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
3
|
*/
|
|
4
4
|
/**
|
|
5
|
-
* The
|
|
5
|
+
* The client annex did:webvh: the disposable sidecar log holding transient
|
|
6
6
|
* per-visit verification methods, one generation per flat `gen-` collection
|
|
7
|
-
* inside the account's stable auxiliary
|
|
7
|
+
* inside the account's stable auxiliary annex Space -- so per-visit facts
|
|
8
8
|
* stay out of the account's identity log entirely. This module is the
|
|
9
9
|
* generation's identity, genesis, and enrollment machinery: the
|
|
10
10
|
* `gen-<random>` generation id convention, the typed auxiliary Space ensure,
|
|
11
|
-
* the genesis parameters, the pin-slot key for
|
|
11
|
+
* the genesis parameters, the pin-slot key for annex continuity, the atomic
|
|
12
12
|
* transient-enrollment entry, and the account document's delegated-clients
|
|
13
13
|
* service entry (the pointer at the current generation).
|
|
14
14
|
*
|
|
15
|
-
* The
|
|
15
|
+
* The annex's posture differs from the account log's on purpose:
|
|
16
16
|
*
|
|
17
|
-
* - Update authority is each standing credential's static
|
|
17
|
+
* - Update authority is each standing credential's static annex rung 0
|
|
18
18
|
* (chain length one, no rung advancement, no attribution scan). Genesis
|
|
19
19
|
* states the minting credential's rung-0 key in `updateKeys` and commits
|
|
20
20
|
* every standing credential's rung-0 hash in `nextKeyHashes` -- the minting
|
|
21
21
|
* key's own carry-over hash included, or no later entry could re-state it.
|
|
22
22
|
* - Prerotation stays on (the rung-0 hashes are the commitment chain),
|
|
23
|
-
* witnesses stay off, and portability is off:
|
|
23
|
+
* witnesses stay off, and portability is off: an annex is
|
|
24
24
|
* generation-scoped and host-bound, and replacement is a GC swap, never a
|
|
25
25
|
* portability move.
|
|
26
26
|
* - The genesis document is bare -- no verification methods, no service
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
* - No `did:web` projection exists: the generation collection holds only its
|
|
33
33
|
* `did.jsonl`, capability-gated rather than world-readable.
|
|
34
34
|
*
|
|
35
|
-
* Ordering rule: the
|
|
35
|
+
* Ordering rule: the annex log publishes FIRST; only then does the
|
|
36
36
|
* caller re-point the account document's `#DelegatedClients` service entry at
|
|
37
|
-
* the new
|
|
37
|
+
* the new annex DID. An annex nobody points at is authorization-inert
|
|
38
38
|
* (no delegation ever names it), so a tear or a double-genesis race leaks
|
|
39
39
|
* storage, never authority -- and the standing orphan discovery is a plain
|
|
40
40
|
* `gen-` prefix match over the auxiliary Space's collection listing, with no
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
* Generation identity is random, never a counter: a reused generation id would
|
|
44
44
|
* re-derive the same rung-0 update key for a new generation, and no counter
|
|
45
45
|
* carrier survives GC deleting the old collection. The generation id is also
|
|
46
|
-
* the generation-identifying half of the
|
|
46
|
+
* the generation-identifying half of the annex rung HKDF labels
|
|
47
47
|
* (`<generationId>/rung/<k>` under the unlock ladder's one salt), so there is
|
|
48
|
-
* exactly one spelling of a generation's identity -- the one the
|
|
48
|
+
* exactly one spelling of a generation's identity -- the one the annex
|
|
49
49
|
* DID string already embeds.
|
|
50
50
|
*/
|
|
51
51
|
import { createDID, deriveNextKeyHash, updateDID } from '@interop/did-method-webvh';
|
|
@@ -53,31 +53,32 @@ import { rootCapabilityId, spaceItems, spacePath, toUrl } from '@interop/was-cli
|
|
|
53
53
|
import { base64urlnopad } from '@scure/base';
|
|
54
54
|
import { DID_LOG_RESOURCE } from '../space/collections.js';
|
|
55
55
|
import { resourceLogPinId } from '../resourceLog/pin.js';
|
|
56
|
-
import {
|
|
57
|
-
import { assertCarryOverCommitments, concludeWithPublishedLog, didWebvhControllerTemplate, MULTIKEY_VM_TYPE, publishUpdatedLog, putLogResource, readPublishedLog, relationIds, updateKeyMultibase, updateKeySigner, withLogConflictRetry } from '
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
56
|
+
import { clientAnnexRung } from './ladder.js';
|
|
57
|
+
import { assertCarryOverCommitments, concludeWithPublishedLog, didWebvhControllerTemplate, MULTIKEY_VM_TYPE, publishUpdatedLog, putLogResource, readPublishedLog, relationIds, updateKeyMultibase, updateKeySigner, withLogConflictRetry } from '../webvh/didWebvh.js';
|
|
58
|
+
import { delegationKeyInDocument } from '../webvh/listClients.js';
|
|
59
|
+
import { delegationProofKeyId, STANDING_ZCAP_TTL_MS, zcapExpiring } from '../webvh/standingZcap.js';
|
|
60
|
+
import { wasWebvhLogStore } from '../webvh/wasIdStore.js';
|
|
60
61
|
/**
|
|
61
|
-
* The Space Description `type` array of the auxiliary
|
|
62
|
+
* The Space Description `type` array of the auxiliary annex Space, set at
|
|
62
63
|
* creation (the server treats a Space's `type` as immutable afterwards).
|
|
63
64
|
* Wire-level and permanent: the server's inspector clause recognizes the
|
|
64
65
|
* `DelegatedClientsSpace` member, and user-data surfaces exclude auxiliary
|
|
65
66
|
* Spaces by it.
|
|
66
67
|
*/
|
|
67
|
-
export const
|
|
68
|
+
export const CLIENT_ANNEX_SPACE_TYPE = [
|
|
68
69
|
'Space',
|
|
69
70
|
'AuxiliarySpace',
|
|
70
71
|
'DelegatedClientsSpace'
|
|
71
72
|
];
|
|
72
73
|
/**
|
|
73
74
|
* The `type` member that marks a Space as the delegated-clients auxiliary
|
|
74
|
-
* Space (the last entry of {@link
|
|
75
|
+
* Space (the last entry of {@link CLIENT_ANNEX_SPACE_TYPE}).
|
|
75
76
|
*/
|
|
76
77
|
const DELEGATED_CLIENTS_SPACE_TYPE = 'DelegatedClientsSpace';
|
|
77
78
|
/**
|
|
78
79
|
* The literal prefix of every generation collection's name. Wire-level and
|
|
79
80
|
* permanent: orphan discovery is a plain prefix match over the auxiliary
|
|
80
|
-
* Space's collection listing, and the generation id embeds in every
|
|
81
|
+
* Space's collection listing, and the generation id embeds in every annex
|
|
81
82
|
* DID string ever published.
|
|
82
83
|
*/
|
|
83
84
|
export const GENERATION_ID_PREFIX = 'gen-';
|
|
@@ -107,7 +108,7 @@ export function mintGenerationId() {
|
|
|
107
108
|
}
|
|
108
109
|
/**
|
|
109
110
|
* Refuses anything that is not a well-formed generation id. Run by every
|
|
110
|
-
*
|
|
111
|
+
* annex builder that takes a generation id, so a malformed one is refused
|
|
111
112
|
* before it can reach a DID string, an HKDF label, or a collection id.
|
|
112
113
|
*
|
|
113
114
|
* @param generationId {string}
|
|
@@ -119,19 +120,19 @@ export function assertGenerationId(generationId) {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
/**
|
|
122
|
-
* The pin-slot key for one
|
|
123
|
+
* The pin-slot key for one annex generation's log -- host-free like every
|
|
123
124
|
* pin-slot key, keyed by the auxiliary Space id and the generation id.
|
|
124
125
|
* A transient session keeps this slot in an in-memory pin store (a durable
|
|
125
126
|
* pin is the wrong lifetime for a disposable log, and a transient session
|
|
126
127
|
* must not durably create the pin store on a read); a durable client's store
|
|
127
|
-
* clears
|
|
128
|
+
* clears annex slots when the generation is collected.
|
|
128
129
|
*
|
|
129
130
|
* @param options {object}
|
|
130
|
-
* @param options.spaceId {string} the auxiliary
|
|
131
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
131
132
|
* @param options.generationId {string} the generation collection's name
|
|
132
133
|
* @returns {string}
|
|
133
134
|
*/
|
|
134
|
-
export function
|
|
135
|
+
export function clientAnnexLogPinId({ spaceId, generationId }) {
|
|
135
136
|
return resourceLogPinId({
|
|
136
137
|
spaceId,
|
|
137
138
|
collectionId: generationId,
|
|
@@ -139,7 +140,7 @@ export function companionLogPinId({ spaceId, generationId }) {
|
|
|
139
140
|
});
|
|
140
141
|
}
|
|
141
142
|
/**
|
|
142
|
-
* The WAS-backed store
|
|
143
|
+
* The WAS-backed store an annex generation's ceremonies read and publish
|
|
143
144
|
* through with controller-tier signing (an enrolled client). A transient
|
|
144
145
|
* session writes through the delegated store instead
|
|
145
146
|
* (`delegatedWebvhLogStore`, invoking the credential's sibling delegation);
|
|
@@ -147,16 +148,24 @@ export function companionLogPinId({ spaceId, generationId }) {
|
|
|
147
148
|
*
|
|
148
149
|
* @param options {object}
|
|
149
150
|
* @param options.was {WasClient}
|
|
150
|
-
* @param options.spaceId {string} the auxiliary
|
|
151
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
151
152
|
* @param options.generationId {string} the generation collection's name
|
|
153
|
+
* @param [options.capability] {IZcap} an invocation capability every request
|
|
154
|
+
* rides (the sibling delegation, where the caller is not an enrolled
|
|
155
|
+
* invoker); absent, requests invoke the root capability
|
|
152
156
|
* @returns {WebvhLogResourceStore}
|
|
153
157
|
*/
|
|
154
|
-
export function
|
|
158
|
+
export function clientAnnexLogStore({ was, spaceId, generationId, capability }) {
|
|
155
159
|
assertGenerationId(generationId);
|
|
156
|
-
return wasWebvhLogStore({
|
|
160
|
+
return wasWebvhLogStore({
|
|
161
|
+
was,
|
|
162
|
+
spaceId,
|
|
163
|
+
collectionId: generationId,
|
|
164
|
+
...(capability !== undefined ? { capability } : {})
|
|
165
|
+
});
|
|
157
166
|
}
|
|
158
167
|
/**
|
|
159
|
-
* The DID core context -- the
|
|
168
|
+
* The DID core context -- the annex genesis document's whole `@context`.
|
|
160
169
|
* The document carries no verification methods and no service entries at
|
|
161
170
|
* genesis, so no other vocabulary is in scope; the entry that first publishes
|
|
162
171
|
* a typed member extends the context then (a did:webvh entry replaces the
|
|
@@ -164,23 +173,23 @@ export function companionLogStore({ was, spaceId, generationId }) {
|
|
|
164
173
|
*/
|
|
165
174
|
const DID_CORE_CONTEXT = 'https://www.w3.org/ns/did/v1';
|
|
166
175
|
/**
|
|
167
|
-
* The Multikey context, appended to the
|
|
176
|
+
* The Multikey context, appended to the annex document's `@context` by
|
|
168
177
|
* the entry that first publishes a transient verification method (genesis
|
|
169
178
|
* carries the DID core context only, having no typed members to define).
|
|
170
179
|
*/
|
|
171
180
|
const MULTIKEY_CONTEXT_URL = 'https://w3id.org/security/multikey/v1';
|
|
172
181
|
/**
|
|
173
|
-
* Creates the one-entry
|
|
174
|
-
* the
|
|
182
|
+
* Creates the one-entry annex generation log. The genesis parameters are
|
|
183
|
+
* the annex posture (see the module doc): prerotation on via the rung-0
|
|
175
184
|
* hash commitments, no witnesses, portability off (the library's default,
|
|
176
185
|
* stated explicitly in the emitted entry), and a bare document -- id and the
|
|
177
186
|
* DID core context, nothing else.
|
|
178
187
|
*
|
|
179
188
|
* The caller supplies the update authority: the minting credential's
|
|
180
|
-
*
|
|
189
|
+
* annex rung-0 key as the sole `updateKeys` member, `nextKeyHashes` as
|
|
181
190
|
* every standing credential's rung-0 hash (restated explicitly on every later
|
|
182
191
|
* entry, never inherited), and rung 0's signer. The minting key's own
|
|
183
|
-
* carry-over hash MUST be among the commitments -- every
|
|
192
|
+
* carry-over hash MUST be among the commitments -- every annex entry
|
|
184
193
|
* re-states `updateKeys` containing the revealed rung-0 keys, and the
|
|
185
194
|
* resolver checks the re-statement against the previous entry's commitments
|
|
186
195
|
* -- so a `nextKeyHashes` that omits it is refused here rather than
|
|
@@ -188,20 +197,20 @@ const MULTIKEY_CONTEXT_URL = 'https://w3id.org/security/multikey/v1';
|
|
|
188
197
|
*
|
|
189
198
|
* @param options {object}
|
|
190
199
|
* @param options.wasServerUrl {string}
|
|
191
|
-
* @param options.spaceId {string} the auxiliary
|
|
200
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
192
201
|
* @param options.generationId {string} the generation collection's name
|
|
193
202
|
* @param options.updateKeyPublicKeyMultibase {string} the minting
|
|
194
|
-
* credential's
|
|
203
|
+
* credential's annex rung-0 key
|
|
195
204
|
* @param options.nextKeyHashes {string[]} every standing credential's
|
|
196
205
|
* rung-0 hash, the minting credential's included
|
|
197
206
|
* @param options.signer {Signer} the minting credential's rung-0 signer
|
|
198
207
|
* @returns {Promise<{ log: DIDLog; did: string; doc: DIDDoc }>}
|
|
199
208
|
*/
|
|
200
|
-
export async function
|
|
209
|
+
export async function createClientAnnexLog({ wasServerUrl, spaceId, generationId, updateKeyPublicKeyMultibase, nextKeyHashes, signer }) {
|
|
201
210
|
assertGenerationId(generationId);
|
|
202
211
|
const carryOverHash = await deriveNextKeyHash(updateKeyPublicKeyMultibase);
|
|
203
212
|
if (!nextKeyHashes.includes(carryOverHash)) {
|
|
204
|
-
throw new Error('
|
|
213
|
+
throw new Error('client annex genesis: `nextKeyHashes` must include the minting ' +
|
|
205
214
|
"credential's own rung-0 hash (the carry-over commitment), or no " +
|
|
206
215
|
'later entry could ever re-state the revealed key.');
|
|
207
216
|
}
|
|
@@ -220,13 +229,13 @@ export async function createCompanionLog({ wasServerUrl, spaceId, generationId,
|
|
|
220
229
|
didDocument: { '@context': [DID_CORE_CONTEXT], id: controllerTemplate }
|
|
221
230
|
});
|
|
222
231
|
if (!result.did || !result.doc) {
|
|
223
|
-
throw new Error('
|
|
232
|
+
throw new Error('client annex genesis: createDID returned no DID document.');
|
|
224
233
|
}
|
|
225
234
|
return { log: result.log, did: result.did, doc: result.doc };
|
|
226
235
|
}
|
|
227
236
|
/**
|
|
228
|
-
* Ensures the auxiliary
|
|
229
|
-
* Description ({@link
|
|
237
|
+
* Ensures the auxiliary annex Space exists: created with the typed
|
|
238
|
+
* Description ({@link CLIENT_ANNEX_SPACE_TYPE}) under the given controller when
|
|
230
239
|
* absent, verified when present. The `type` array must ride the create -- the
|
|
231
240
|
* server accepts it at creation only and treats it as immutable afterwards --
|
|
232
241
|
* which is also why an existing Space at this id that is NOT typed as the
|
|
@@ -242,19 +251,19 @@ export async function createCompanionLog({ wasServerUrl, spaceId, generationId,
|
|
|
242
251
|
*
|
|
243
252
|
* @param options {object}
|
|
244
253
|
* @param options.was {WasClient}
|
|
245
|
-
* @param options.spaceId {string} the auxiliary
|
|
254
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
246
255
|
* @param options.controller {string} the Space controller (the account
|
|
247
|
-
* did:webvh where it exists; a bootstrap did:key on a
|
|
256
|
+
* did:webvh where it exists; a bootstrap did:key on a ladder-anchored signup,
|
|
248
257
|
* promoted the same way the account Space's controller is)
|
|
249
258
|
* @returns {Promise<void>}
|
|
250
259
|
*/
|
|
251
|
-
export async function
|
|
260
|
+
export async function ensureClientAnnexSpace({ was, spaceId, controller }) {
|
|
252
261
|
const space = was.space(spaceId);
|
|
253
262
|
const current = await space.describe();
|
|
254
263
|
if (current === null) {
|
|
255
264
|
await space.configure({
|
|
256
265
|
controller,
|
|
257
|
-
type:
|
|
266
|
+
type: CLIENT_ANNEX_SPACE_TYPE,
|
|
258
267
|
force: true
|
|
259
268
|
});
|
|
260
269
|
return;
|
|
@@ -262,11 +271,11 @@ export async function ensureCompanionSpace({ was, spaceId, controller }) {
|
|
|
262
271
|
if (!current.type?.includes(DELEGATED_CLIENTS_SPACE_TYPE)) {
|
|
263
272
|
throw new Error(`The Space "${spaceId}" exists but is not typed as the ` +
|
|
264
273
|
'delegated-clients auxiliary Space; its type is immutable, so it ' +
|
|
265
|
-
'cannot hold
|
|
274
|
+
'cannot hold client-annex generations.');
|
|
266
275
|
}
|
|
267
276
|
}
|
|
268
277
|
/**
|
|
269
|
-
* Mints a fresh
|
|
278
|
+
* Mints a fresh annex generation with controller-tier signing: ensures
|
|
270
279
|
* the typed auxiliary Space, mints a fresh random generation id, creates the
|
|
271
280
|
* generation collection, and publishes the genesis `did.jsonl` as a
|
|
272
281
|
* create-if-absent -- the same conditional-publish discipline as every log
|
|
@@ -274,7 +283,7 @@ export async function ensureCompanionSpace({ was, spaceId, controller }) {
|
|
|
274
283
|
* negligible.
|
|
275
284
|
*
|
|
276
285
|
* The account document's `#DelegatedClients` service entry is deliberately
|
|
277
|
-
* NOT written here: the
|
|
286
|
+
* NOT written here: the annex log publishes first, and the caller
|
|
278
287
|
* re-points the account document at the returned DID afterwards. A run torn
|
|
279
288
|
* between the two leaves an unpointed generation -- authorization-inert (no
|
|
280
289
|
* delegation names it), collected by the standing `gen-` prefix orphan
|
|
@@ -287,23 +296,23 @@ export async function ensureCompanionSpace({ was, spaceId, controller }) {
|
|
|
287
296
|
*
|
|
288
297
|
* @param options {object}
|
|
289
298
|
* @param options.was {WasClient} the storage client, signing as an enrolled
|
|
290
|
-
* client (or the bootstrap controller on a
|
|
299
|
+
* client (or the bootstrap controller on a ladder-anchored signup)
|
|
291
300
|
* @param options.wasServerUrl {string}
|
|
292
|
-
* @param options.spaceId {string} the auxiliary
|
|
301
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
293
302
|
* @param options.controller {string} the auxiliary Space's controller, used
|
|
294
303
|
* only when the Space does not exist yet
|
|
295
304
|
* @param options.updateKeyPublicKeyMultibase {string} the minting
|
|
296
|
-
* credential's
|
|
305
|
+
* credential's annex rung-0 key
|
|
297
306
|
* @param options.nextKeyHashes {string[]} every standing credential's
|
|
298
307
|
* rung-0 hash, the minting credential's included
|
|
299
308
|
* @param options.signer {Signer} the minting credential's rung-0 signer
|
|
300
309
|
* @returns {Promise<{ did: string; generationId: string; log: DIDLog;
|
|
301
310
|
* doc: DIDDoc }>}
|
|
302
311
|
*/
|
|
303
|
-
export async function
|
|
304
|
-
await
|
|
312
|
+
export async function mintClientAnnexGeneration({ was, wasServerUrl, spaceId, controller, updateKeyPublicKeyMultibase, nextKeyHashes, signer }) {
|
|
313
|
+
await ensureClientAnnexSpace({ was, spaceId, controller });
|
|
305
314
|
const generationId = mintGenerationId();
|
|
306
|
-
return
|
|
315
|
+
return publishClientAnnexGenesis({
|
|
307
316
|
was,
|
|
308
317
|
wasServerUrl,
|
|
309
318
|
spaceId,
|
|
@@ -321,25 +330,27 @@ export async function mintCompanionGeneration({ was, wasServerUrl, spaceId, cont
|
|
|
321
330
|
* @param options {object}
|
|
322
331
|
* @param options.was {WasClient}
|
|
323
332
|
* @param options.wasServerUrl {string}
|
|
324
|
-
* @param options.spaceId {string} the auxiliary
|
|
333
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
325
334
|
* @param options.generationId {string} the freshly minted generation id
|
|
326
335
|
* @param options.updateKeyPublicKeyMultibase {string}
|
|
327
336
|
* @param options.nextKeyHashes {string[]}
|
|
328
337
|
* @param options.signer {Signer}
|
|
338
|
+
* @param [options.capability] {IZcap} an invocation capability the
|
|
339
|
+
* collection create and the genesis publish ride (a delegated minter)
|
|
329
340
|
* @returns {Promise<{ did: string; generationId: string; log: DIDLog;
|
|
330
341
|
* doc: DIDDoc }>}
|
|
331
342
|
*/
|
|
332
|
-
async function
|
|
343
|
+
async function publishClientAnnexGenesis({ was, wasServerUrl, spaceId, generationId, updateKeyPublicKeyMultibase, nextKeyHashes, signer, capability }) {
|
|
333
344
|
// The generation collection must exist before its first resource PUT; a
|
|
334
345
|
// fresh random generation id means this is always a create. Plaintext on
|
|
335
346
|
// purpose:
|
|
336
|
-
// the server resolves the
|
|
347
|
+
// the server resolves the annex DID out of its own storage, and the
|
|
337
348
|
// collection is capability-gated rather than encrypted.
|
|
338
349
|
await was
|
|
339
|
-
.space(spaceId)
|
|
350
|
+
.space(spaceId, capability !== undefined ? { capability } : {})
|
|
340
351
|
.collection(generationId, { encryption: 'plaintext' })
|
|
341
352
|
.configure({ name: generationId, force: true });
|
|
342
|
-
const created = await
|
|
353
|
+
const created = await createClientAnnexLog({
|
|
343
354
|
wasServerUrl,
|
|
344
355
|
spaceId,
|
|
345
356
|
generationId,
|
|
@@ -348,30 +359,35 @@ async function publishCompanionGenesis({ was, wasServerUrl, spaceId, generationI
|
|
|
348
359
|
signer
|
|
349
360
|
});
|
|
350
361
|
await putLogResource({
|
|
351
|
-
store:
|
|
362
|
+
store: clientAnnexLogStore({
|
|
363
|
+
was,
|
|
364
|
+
spaceId,
|
|
365
|
+
generationId,
|
|
366
|
+
...(capability !== undefined ? { capability } : {})
|
|
367
|
+
}),
|
|
352
368
|
log: created.log,
|
|
353
369
|
ifNoneMatch: true
|
|
354
370
|
});
|
|
355
371
|
return { ...created, generationId };
|
|
356
372
|
}
|
|
357
373
|
/**
|
|
358
|
-
* Mints a fresh
|
|
359
|
-
*
|
|
374
|
+
* Mints a fresh annex generation signed by a standing CREDENTIAL's
|
|
375
|
+
* annex rung 0 -- the mint a ladder-seed holder runs (a credential-in-hand
|
|
360
376
|
* login, or a test harness standing in for one). The generation id must exist
|
|
361
377
|
* before the update authority can: the rung-0 key derives from the ladder
|
|
362
|
-
* seed AND the generation id (`
|
|
378
|
+
* seed AND the generation id (`clientAnnexRung`), so this helper mints the
|
|
363
379
|
* generation id
|
|
364
380
|
* first, derives the rung, and states its own carry-over hash in
|
|
365
|
-
* `nextKeyHashes` -- {@link
|
|
381
|
+
* `nextKeyHashes` -- {@link mintClientAnnexGeneration}'s caller-supplied-key
|
|
366
382
|
* shape cannot express that ordering. Everything else matches it: the typed
|
|
367
383
|
* Space ensure, the collection create, the create-if-absent genesis publish,
|
|
368
384
|
* and the pointer deliberately left to the caller.
|
|
369
385
|
*
|
|
370
386
|
* @param options {object}
|
|
371
387
|
* @param options.was {WasClient} the storage client, signing as an enrolled
|
|
372
|
-
* client (or the bootstrap controller on a
|
|
388
|
+
* client (or the bootstrap controller on a ladder-anchored signup)
|
|
373
389
|
* @param options.wasServerUrl {string}
|
|
374
|
-
* @param options.spaceId {string} the auxiliary
|
|
390
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
375
391
|
* @param options.controller {string} the auxiliary Space's controller, used
|
|
376
392
|
* only when the Space does not exist yet
|
|
377
393
|
* @param options.ladderSeed {Uint8Array} the minting credential's ladder
|
|
@@ -380,14 +396,22 @@ async function publishCompanionGenesis({ was, wasServerUrl, spaceId, generationI
|
|
|
380
396
|
* credentials' rung-0 hashes for this generation id, when the account has
|
|
381
397
|
* more
|
|
382
398
|
* than one; the minting credential's own carry-over hash is always included
|
|
399
|
+
* @param [options.capability] {IZcap} an invocation capability the mint
|
|
400
|
+
* rides -- the transient-recovery continuation minting its fresh generation
|
|
401
|
+
* through the credential's sibling delegation (the auxiliary Space's items
|
|
402
|
+
* subtree). The typed-Space ensure is then skipped: the delegation's target
|
|
403
|
+
* covers the collections beneath the Space, never the Space Description,
|
|
404
|
+
* and a standing sibling delegation presupposes the auxiliary Space
|
|
383
405
|
* @returns {Promise<{ did: string; generationId: string; log: DIDLog;
|
|
384
406
|
* doc: DIDDoc }>}
|
|
385
407
|
*/
|
|
386
|
-
export async function
|
|
387
|
-
|
|
408
|
+
export async function mintCredentialClientAnnexGeneration({ was, wasServerUrl, spaceId, controller, ladderSeed, extraNextKeyHashes = [], capability }) {
|
|
409
|
+
if (capability === undefined) {
|
|
410
|
+
await ensureClientAnnexSpace({ was, spaceId, controller });
|
|
411
|
+
}
|
|
388
412
|
const generationId = mintGenerationId();
|
|
389
|
-
const rung = await
|
|
390
|
-
return
|
|
413
|
+
const rung = await clientAnnexRung({ ladderSeed, generationId });
|
|
414
|
+
return publishClientAnnexGenesis({
|
|
391
415
|
was,
|
|
392
416
|
wasServerUrl,
|
|
393
417
|
spaceId,
|
|
@@ -397,14 +421,15 @@ export async function mintCredentialCompanionGeneration({ was, wasServerUrl, spa
|
|
|
397
421
|
await deriveNextKeyHash(rung.keyMultibase),
|
|
398
422
|
...extraNextKeyHashes
|
|
399
423
|
],
|
|
400
|
-
signer: await updateKeySigner({ seed: rung.seed })
|
|
424
|
+
signer: await updateKeySigner({ seed: rung.seed }),
|
|
425
|
+
...(capability !== undefined ? { capability } : {})
|
|
401
426
|
});
|
|
402
427
|
}
|
|
403
428
|
/**
|
|
404
429
|
* The type IRI of the account document's delegated-clients service entry --
|
|
405
|
-
* the pointer at the current
|
|
430
|
+
* the pointer at the current annex generation's DID. Wire-level and
|
|
406
431
|
* permanent: readers (this module's {@link delegatedClientsPointer}, the
|
|
407
|
-
* server's
|
|
432
|
+
* server's annex-chain inspector clause) dispatch on this IRI, never on
|
|
408
433
|
* the entry's fragment id, which is non-semantic by convention.
|
|
409
434
|
*/
|
|
410
435
|
export const DELEGATED_CLIENTS_SERVICE_TYPE = 'https://w3id.org/byoe#DelegatedClients';
|
|
@@ -417,25 +442,25 @@ export const DELEGATED_CLIENTS_SERVICE_TYPE = 'https://w3id.org/byoe#DelegatedCl
|
|
|
417
442
|
const DELEGATED_CLIENTS_SERVICE_FRAGMENT = 'delegated-clients';
|
|
418
443
|
/**
|
|
419
444
|
* Builds a fresh delegated-clients service entry for the account document.
|
|
420
|
-
* The `serviceEndpoint` is the
|
|
445
|
+
* The `serviceEndpoint` is the annex DID STRING, deliberately not a URL:
|
|
421
446
|
* the DID is self-certifying and host-independent, and the account pointer
|
|
422
447
|
* already carries the host.
|
|
423
448
|
*
|
|
424
449
|
* @param options {object}
|
|
425
450
|
* @param options.accountDid {string} the account did:webvh
|
|
426
|
-
* @param options.
|
|
451
|
+
* @param options.clientAnnexDid {string} the current generation's annex
|
|
427
452
|
* DID
|
|
428
453
|
* @returns {ServiceEndpoint}
|
|
429
454
|
*/
|
|
430
|
-
export function delegatedClientsServiceEntry({ accountDid,
|
|
455
|
+
export function delegatedClientsServiceEntry({ accountDid, clientAnnexDid }) {
|
|
431
456
|
return {
|
|
432
457
|
id: `${accountDid}#${DELEGATED_CLIENTS_SERVICE_FRAGMENT}`,
|
|
433
458
|
type: DELEGATED_CLIENTS_SERVICE_TYPE,
|
|
434
|
-
serviceEndpoint:
|
|
459
|
+
serviceEndpoint: clientAnnexDid
|
|
435
460
|
};
|
|
436
461
|
}
|
|
437
462
|
/**
|
|
438
|
-
* The
|
|
463
|
+
* The annex DID the account document currently points at: the
|
|
439
464
|
* `serviceEndpoint` of the service entry whose `type` names (or includes)
|
|
440
465
|
* {@link DELEGATED_CLIENTS_SERVICE_TYPE}. Only a bare DID-string endpoint
|
|
441
466
|
* counts -- the same predicate the server's inspector clause evaluates, so
|
|
@@ -455,9 +480,42 @@ export function delegatedClientsPointer({ doc }) {
|
|
|
455
480
|
}
|
|
456
481
|
return undefined;
|
|
457
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* The account document's `service` array with the delegated-clients pointer
|
|
485
|
+
* set to `clientAnnexDid`. An existing pointer entry is re-pointed in place,
|
|
486
|
+
* its fragment id preserved verbatim (the id is non-semantic and stable);
|
|
487
|
+
* absent one, a fresh entry is appended. Every other service entry is carried
|
|
488
|
+
* through untouched.
|
|
489
|
+
*
|
|
490
|
+
* Shared by the two writers of the pointer: the standalone
|
|
491
|
+
* {@link setDelegatedClientsPointer} entry, and the transient-recovery
|
|
492
|
+
* continuation, which folds the pointer into its own add-and-retire entry so
|
|
493
|
+
* the pointer can never lag the entry that retires the standing ladder VMs.
|
|
494
|
+
*
|
|
495
|
+
* @param options {object}
|
|
496
|
+
* @param options.doc {DIDDoc} the current account document
|
|
497
|
+
* @param options.accountDid {string} the account did:webvh
|
|
498
|
+
* @param options.clientAnnexDid {string} the generation to point at
|
|
499
|
+
* @returns {ServiceEndpoint[]}
|
|
500
|
+
*/
|
|
501
|
+
export function servicesPointedAtClientAnnex({ doc, accountDid, clientAnnexDid }) {
|
|
502
|
+
const existing = (doc.service ?? []);
|
|
503
|
+
const isPointerEntry = (entry) => {
|
|
504
|
+
const types = Array.isArray(entry.type) ? entry.type : [entry.type];
|
|
505
|
+
return types.includes(DELEGATED_CLIENTS_SERVICE_TYPE);
|
|
506
|
+
};
|
|
507
|
+
return existing.some(isPointerEntry)
|
|
508
|
+
? existing.map(entry => isPointerEntry(entry)
|
|
509
|
+
? { ...entry, serviceEndpoint: clientAnnexDid }
|
|
510
|
+
: entry)
|
|
511
|
+
: [
|
|
512
|
+
...existing,
|
|
513
|
+
delegatedClientsServiceEntry({ accountDid, clientAnnexDid })
|
|
514
|
+
];
|
|
515
|
+
}
|
|
458
516
|
/**
|
|
459
517
|
* The unlock-record sibling delegation's `allowedAction` set: GET beside PUT,
|
|
460
|
-
* so an enrolling transient client can read the
|
|
518
|
+
* so an enrolling transient client can read the annex head it appends to.
|
|
461
519
|
* Wire-level and permanent (wallet-core decision 0005): the server's
|
|
462
520
|
* inspector clause admits a delegated-clients delegation with `allowedAction`
|
|
463
521
|
* a subset of exactly this pair.
|
|
@@ -471,13 +529,13 @@ export const DELEGATED_CLIENTS_DELEGATION_ACTIONS = ['GET', 'PUT'];
|
|
|
471
529
|
*/
|
|
472
530
|
export const DELEGATED_CLIENTS_DELEGATION_TTL_MS = STANDING_ZCAP_TTL_MS;
|
|
473
531
|
/**
|
|
474
|
-
* Mints one delegated-clients (
|
|
532
|
+
* Mints one delegated-clients (annex Space) delegation: the pre-minted
|
|
475
533
|
* zcap sealed into a standing credential's unlock record beside the account
|
|
476
|
-
* bridge, which is what lets a transient login reach the
|
|
534
|
+
* bridge, which is what lets a transient login reach the annex log with
|
|
477
535
|
* nothing but the credential. The shape is a permanent wire artifact
|
|
478
536
|
* (wallet-core decision 0005):
|
|
479
537
|
*
|
|
480
|
-
* - `invocationTarget` is the AUXILIARY
|
|
538
|
+
* - `invocationTarget` is the AUXILIARY annex Space's items subtree --
|
|
481
539
|
* the Space URL with a trailing slash, built with was-client's paths
|
|
482
540
|
* helpers so the bytes match the server's target check on a sub-path
|
|
483
541
|
* deployment. Generation coverage comes from generation-id-bounded
|
|
@@ -495,22 +553,22 @@ export const DELEGATED_CLIENTS_DELEGATION_TTL_MS = STANDING_ZCAP_TTL_MS;
|
|
|
495
553
|
* enrolled client's promoted signer, or the account ladder VM)
|
|
496
554
|
* @param options.wasServerUrl {string} the auxiliary Space's storage
|
|
497
555
|
* server (the account pointer's host)
|
|
498
|
-
* @param options.
|
|
556
|
+
* @param options.clientAnnexSpaceId {string} the auxiliary annex
|
|
499
557
|
* Space's id
|
|
500
558
|
* @param options.controller {string} the credential-derived signing DID
|
|
501
559
|
* @param [options.now] {number} epoch milliseconds, for tests
|
|
502
560
|
* @returns {Promise<IZcap>}
|
|
503
561
|
*/
|
|
504
|
-
export async function mintDelegatedClientsDelegation({ zcapClient, wasServerUrl,
|
|
562
|
+
export async function mintDelegatedClientsDelegation({ zcapClient, wasServerUrl, clientAnnexSpaceId, controller, now = Date.now() }) {
|
|
505
563
|
const spaceUrl = toUrl({
|
|
506
564
|
serverUrl: wasServerUrl,
|
|
507
|
-
path: spacePath(
|
|
565
|
+
path: spacePath(clientAnnexSpaceId)
|
|
508
566
|
});
|
|
509
567
|
return (await zcapClient.delegate({
|
|
510
568
|
capability: rootCapabilityId(spaceUrl),
|
|
511
569
|
invocationTarget: toUrl({
|
|
512
570
|
serverUrl: wasServerUrl,
|
|
513
|
-
path: spaceItems(
|
|
571
|
+
path: spaceItems(clientAnnexSpaceId)
|
|
514
572
|
}),
|
|
515
573
|
controller,
|
|
516
574
|
allowedActions: [...DELEGATED_CLIENTS_DELEGATION_ACTIONS],
|
|
@@ -518,9 +576,51 @@ export async function mintDelegatedClientsDelegation({ zcapClient, wasServerUrl,
|
|
|
518
576
|
}));
|
|
519
577
|
}
|
|
520
578
|
/**
|
|
521
|
-
*
|
|
579
|
+
* Builds the annex-side sibling-delegation minter the durable record re-mint
|
|
580
|
+
* orchestrator (`recovery/remintRecoveryDelegations`) takes as an injected
|
|
581
|
+
* closure -- the boundary keeping that base orchestrator free of annex
|
|
582
|
+
* imports. The returned closure reads the auxiliary annex Space id off the
|
|
583
|
+
* verified document's delegated-clients service entry (the annex DID string
|
|
584
|
+
* embeds it) and mints a fresh {@link mintDelegatedClientsDelegation} to the
|
|
585
|
+
* named controller; it resolves `undefined` while the document points at no
|
|
586
|
+
* generation, which the orchestrator reads as "carry the old sealed member
|
|
587
|
+
* verbatim".
|
|
588
|
+
*
|
|
589
|
+
* @param options {object}
|
|
590
|
+
* @param options.doc {object} the locally verified account document
|
|
591
|
+
* @param options.zcapClient {ZcapClient} the acting client's promoted
|
|
592
|
+
* signer, which mints the fresh delegations
|
|
593
|
+
* @param options.wasServerUrl {string} the auxiliary Space's storage
|
|
594
|
+
* server (the account pointer's host)
|
|
595
|
+
* @returns {Function} `({ controller }) => Promise<IZcap | undefined>`
|
|
596
|
+
*/
|
|
597
|
+
export function delegatedClientsDelegationMinter({ doc, zcapClient, wasServerUrl }) {
|
|
598
|
+
return async ({ controller }) => {
|
|
599
|
+
const clientAnnexDid = delegatedClientsPointer({
|
|
600
|
+
doc: doc
|
|
601
|
+
});
|
|
602
|
+
if (!clientAnnexDid) {
|
|
603
|
+
return undefined;
|
|
604
|
+
}
|
|
605
|
+
let clientAnnexSpaceId;
|
|
606
|
+
try {
|
|
607
|
+
clientAnnexSpaceId = clientAnnexDidParts({ did: clientAnnexDid }).spaceId;
|
|
608
|
+
}
|
|
609
|
+
catch {
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
612
|
+
return mintDelegatedClientsDelegation({
|
|
613
|
+
zcapClient,
|
|
614
|
+
wasServerUrl,
|
|
615
|
+
clientAnnexSpaceId,
|
|
616
|
+
controller
|
|
617
|
+
});
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* The auxiliary annex Space id a delegated-clients delegation targets,
|
|
522
622
|
* read out of its `invocationTarget` (the items-subtree URL,
|
|
523
|
-
* `.../space/<
|
|
623
|
+
* `.../space/<clientAnnexSpaceId>/`). The id has no other home -- a transient
|
|
524
624
|
* login learns the Space from the delegation it unwraps, and a refresh pass
|
|
525
625
|
* that holds the old delegation rebuilds the target from it -- so this parse
|
|
526
626
|
* is the one reader. Returns `undefined` on anything that is not an
|
|
@@ -554,7 +654,7 @@ export function delegatedClientsDelegationSpaceId({ delegation }) {
|
|
|
554
654
|
return spaceId ? decodeURIComponent(spaceId) : undefined;
|
|
555
655
|
}
|
|
556
656
|
/**
|
|
557
|
-
* The type IRI of the
|
|
657
|
+
* The type IRI of the annex document's generation-delegation service
|
|
558
658
|
* entry -- the generation's standing Space-scoped zcap, embedded where an
|
|
559
659
|
* enrolling transient client can reach it before it holds any other
|
|
560
660
|
* authority. Wire-level and permanent: readers (this module's
|
|
@@ -607,8 +707,8 @@ export const GENERATION_DELEGATION_TTL_MS = STANDING_ZCAP_TTL_MS;
|
|
|
607
707
|
* bytes match the server's target check on a sub-path deployment. The
|
|
608
708
|
* bare Space URL sits outside the capability bytes (see
|
|
609
709
|
* {@link GENERATION_DELEGATION_ACTIONS} for what that excludes).
|
|
610
|
-
* - `controller` is the bare
|
|
611
|
-
* `<
|
|
710
|
+
* - `controller` is the bare annex DID string. Transient keys invoke as
|
|
711
|
+
* `<clientAnnexDid>#<vm>`, and the server's inspector clause compares this
|
|
612
712
|
* string against the account document's delegated-clients pointer.
|
|
613
713
|
* - The chain is rooted directly in the account Space's root zcap, so an
|
|
614
714
|
* App Connect grant delegated under it forms the depth-3 chain
|
|
@@ -624,12 +724,12 @@ export const GENERATION_DELEGATION_TTL_MS = STANDING_ZCAP_TTL_MS;
|
|
|
624
724
|
* or a durable client's promoted signer)
|
|
625
725
|
* @param options.wasServerUrl {string} the ACCOUNT Space's storage server
|
|
626
726
|
* @param options.spaceId {string} the ACCOUNT Space's id
|
|
627
|
-
* @param options.
|
|
727
|
+
* @param options.clientAnnexDid {string} the generation's annex DID
|
|
628
728
|
* @param [options.now] {number} epoch milliseconds, for tests
|
|
629
729
|
* @returns {Promise<IZcap>}
|
|
630
730
|
*/
|
|
631
|
-
export async function mintGenerationDelegation({ zcapClient, wasServerUrl, spaceId,
|
|
632
|
-
|
|
731
|
+
export async function mintGenerationDelegation({ zcapClient, wasServerUrl, spaceId, clientAnnexDid, now = Date.now() }) {
|
|
732
|
+
clientAnnexDidParts({ did: clientAnnexDid });
|
|
633
733
|
const spaceUrl = toUrl({ serverUrl: wasServerUrl, path: spacePath(spaceId) });
|
|
634
734
|
return (await zcapClient.delegate({
|
|
635
735
|
capability: rootCapabilityId(spaceUrl),
|
|
@@ -637,7 +737,7 @@ export async function mintGenerationDelegation({ zcapClient, wasServerUrl, space
|
|
|
637
737
|
serverUrl: wasServerUrl,
|
|
638
738
|
path: spaceItems(spaceId)
|
|
639
739
|
}),
|
|
640
|
-
controller:
|
|
740
|
+
controller: clientAnnexDid,
|
|
641
741
|
allowedActions: [...GENERATION_DELEGATION_ACTIONS],
|
|
642
742
|
expires: new Date(now + GENERATION_DELEGATION_TTL_MS)
|
|
643
743
|
}));
|
|
@@ -667,33 +767,33 @@ export function clampGrantExpires({ ttlMs, delegation, now = Date.now() }) {
|
|
|
667
767
|
return new Date(Math.min(now + ttlMs, parentExpires));
|
|
668
768
|
}
|
|
669
769
|
/**
|
|
670
|
-
* Builds a fresh generation-delegation service entry for
|
|
770
|
+
* Builds a fresh generation-delegation service entry for an annex
|
|
671
771
|
* document. The `serviceEndpoint` is the full delegated-zcap JSON as a
|
|
672
772
|
* single map, byte-identical to what `zcapClient.delegate` produced -- the
|
|
673
|
-
*
|
|
773
|
+
* annex entry proof (JCS canonicalization) then covers it byte for byte,
|
|
674
774
|
* so host tampering with the stored delegation is client-visible.
|
|
675
775
|
*
|
|
676
776
|
* @param options {object}
|
|
677
|
-
* @param options.
|
|
777
|
+
* @param options.clientAnnexDid {string} the generation's annex DID
|
|
678
778
|
* @param options.delegation {IZcap} the minted generation delegation
|
|
679
779
|
* @returns {ServiceEndpoint}
|
|
680
780
|
*/
|
|
681
|
-
export function generationDelegationServiceEntry({
|
|
781
|
+
export function generationDelegationServiceEntry({ clientAnnexDid, delegation }) {
|
|
682
782
|
return {
|
|
683
|
-
id: `${
|
|
783
|
+
id: `${clientAnnexDid}#${GENERATION_DELEGATION_SERVICE_FRAGMENT}`,
|
|
684
784
|
type: GENERATION_DELEGATION_SERVICE_TYPE,
|
|
685
785
|
serviceEndpoint: delegation
|
|
686
786
|
};
|
|
687
787
|
}
|
|
688
788
|
/**
|
|
689
|
-
* The generation delegation
|
|
789
|
+
* The generation delegation an annex document carries: the
|
|
690
790
|
* `serviceEndpoint` map of the service entry whose `type` names (or
|
|
691
791
|
* includes) {@link GENERATION_DELEGATION_SERVICE_TYPE}. Only a map-form
|
|
692
792
|
* endpoint counts (the delegation is embedded as the zcap JSON itself,
|
|
693
793
|
* never as a URL or an encoded string).
|
|
694
794
|
*
|
|
695
795
|
* @param options {object}
|
|
696
|
-
* @param options.doc {DIDDoc} the resolved (and verified)
|
|
796
|
+
* @param options.doc {DIDDoc} the resolved (and verified) annex
|
|
697
797
|
* document
|
|
698
798
|
* @returns {IZcap | undefined}
|
|
699
799
|
*/
|
|
@@ -712,18 +812,77 @@ export function embeddedGenerationDelegation({ doc }) {
|
|
|
712
812
|
return undefined;
|
|
713
813
|
}
|
|
714
814
|
/**
|
|
715
|
-
*
|
|
815
|
+
* Every generation delegation a generation's log has ever embedded, in log
|
|
816
|
+
* order and deduplicated by zcap id -- the annex-log HISTORY WALK the
|
|
817
|
+
* last-durable-client forget revokes from (decision 0004's 2026-08-19
|
|
818
|
+
* amendment): a renewal replaces the head service entry's endpoint in place,
|
|
819
|
+
* so a superseded delegation's bytes survive only in earlier entries'
|
|
820
|
+
* re-stated full state, and a renewal inside the 30-day window can leave TWO
|
|
821
|
+
* still-unexpired ladder-signed delegations. The caller filters (signer,
|
|
822
|
+
* expiry) and revokes; this walk only recovers the bytes.
|
|
823
|
+
*
|
|
824
|
+
* @param options {object}
|
|
825
|
+
* @param options.log {DIDLog} the generation's VERIFIED log
|
|
826
|
+
* @returns {IZcap[]}
|
|
827
|
+
*/
|
|
828
|
+
export function generationDelegationHistory({ log }) {
|
|
829
|
+
const seen = new Set();
|
|
830
|
+
const delegations = [];
|
|
831
|
+
for (const entry of log) {
|
|
832
|
+
const embedded = embeddedGenerationDelegation({
|
|
833
|
+
doc: entry.state
|
|
834
|
+
});
|
|
835
|
+
if (embedded === undefined) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const id = embedded.id;
|
|
839
|
+
if (typeof id !== 'string' || seen.has(id)) {
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
seen.add(id);
|
|
843
|
+
delegations.push(embedded);
|
|
844
|
+
}
|
|
845
|
+
return delegations;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Submits the revocation of a generation delegation, reading the server's
|
|
849
|
+
* 400 answer as success: an already-revoked chain (a resumed ceremony's
|
|
850
|
+
* blind re-POST) and an expired delegation (which no longer needs revoking)
|
|
851
|
+
* both land there, and the revocation protocol exposes no read endpoint to
|
|
852
|
+
* distinguish them beforehand. Matched on `err.name` -- error classes do not
|
|
853
|
+
* survive crossing package copies. The `revoke` seam is was-client's
|
|
854
|
+
* `WasClient#revoke`, bound by the caller.
|
|
855
|
+
*
|
|
856
|
+
* @param options {object}
|
|
857
|
+
* @param options.revoke {Function} `(delegation) => Promise<void>` --
|
|
858
|
+
* POSTs the revocation (`was.revoke`)
|
|
859
|
+
* @param options.delegation {IZcap}
|
|
860
|
+
* @returns {Promise<void>}
|
|
861
|
+
*/
|
|
862
|
+
export async function revokeTreatingAlreadyRevokedAsSuccess({ revoke, delegation }) {
|
|
863
|
+
try {
|
|
864
|
+
await revoke(delegation);
|
|
865
|
+
}
|
|
866
|
+
catch (err) {
|
|
867
|
+
if (err.name === 'ValidationError') {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
throw err;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* The annex document's service list with the generation delegation
|
|
716
875
|
* installed: an existing entry's endpoint is replaced in place, its fragment
|
|
717
876
|
* id preserved verbatim (the id is non-semantic and stable); absent one, a
|
|
718
877
|
* fresh entry is appended. Every other service entry is preserved untouched.
|
|
719
878
|
*
|
|
720
879
|
* @param options {object}
|
|
721
|
-
* @param options.doc {DIDDoc} the
|
|
722
|
-
* @param options.
|
|
880
|
+
* @param options.doc {DIDDoc} the annex document as published
|
|
881
|
+
* @param options.clientAnnexDid {string}
|
|
723
882
|
* @param options.delegation {IZcap}
|
|
724
883
|
* @returns {ServiceEndpoint[]}
|
|
725
884
|
*/
|
|
726
|
-
function withGenerationDelegationEntry({ doc,
|
|
885
|
+
function withGenerationDelegationEntry({ doc, clientAnnexDid, delegation }) {
|
|
727
886
|
const existing = (doc.service ?? []);
|
|
728
887
|
const isDelegationEntry = (entry) => {
|
|
729
888
|
const types = Array.isArray(entry.type) ? entry.type : [entry.type];
|
|
@@ -738,23 +897,23 @@ function withGenerationDelegationEntry({ doc, companionDid, delegation }) {
|
|
|
738
897
|
: entry)
|
|
739
898
|
: [
|
|
740
899
|
...existing,
|
|
741
|
-
generationDelegationServiceEntry({
|
|
900
|
+
generationDelegationServiceEntry({ clientAnnexDid, delegation })
|
|
742
901
|
];
|
|
743
902
|
}
|
|
744
903
|
/**
|
|
745
|
-
* Parses the auxiliary Space id and generation id out of
|
|
746
|
-
* string. Both are permanent substrings of every
|
|
747
|
-
* construction: the generation id is the final path segment of the
|
|
904
|
+
* Parses the auxiliary Space id and generation id out of an annex DID
|
|
905
|
+
* string. Both are permanent substrings of every annex DID by
|
|
906
|
+
* construction: the generation id is the final path segment of the annex
|
|
748
907
|
* DID (`did:webvh:<scid>:<host>:...:space:<spaceId>:<generationId>`), and it
|
|
749
|
-
* is the generation-identifying half of the
|
|
908
|
+
* is the generation-identifying half of the annex rung HKDF
|
|
750
909
|
* labels, so this parse is what lets an enrollee derive its writing key from
|
|
751
910
|
* the pointer alone -- no log read, no registry.
|
|
752
911
|
*
|
|
753
912
|
* @param options {object}
|
|
754
|
-
* @param options.did {string}
|
|
913
|
+
* @param options.did {string} an annex did:webvh string
|
|
755
914
|
* @returns {{ spaceId: string, generationId: string }}
|
|
756
915
|
*/
|
|
757
|
-
export function
|
|
916
|
+
export function clientAnnexDidParts({ did }) {
|
|
758
917
|
const parts = did.split(':');
|
|
759
918
|
const generationId = parts[parts.length - 1];
|
|
760
919
|
const spaceId = parts[parts.length - 2];
|
|
@@ -765,39 +924,39 @@ export function companionDidParts({ did }) {
|
|
|
765
924
|
generationId === undefined ||
|
|
766
925
|
spaceId === undefined ||
|
|
767
926
|
spaceId.length === 0) {
|
|
768
|
-
throw new Error(`Not a
|
|
927
|
+
throw new Error(`Not a client annex did:webvh: "${did}".`);
|
|
769
928
|
}
|
|
770
929
|
assertGenerationId(generationId);
|
|
771
930
|
return { spaceId, generationId };
|
|
772
931
|
}
|
|
773
932
|
/**
|
|
774
|
-
* Thrown when the published
|
|
933
|
+
* Thrown when the published annex log commits neither the writing
|
|
775
934
|
* credential's rung-0 key nor its hash -- the mid-generation lockout: a
|
|
776
|
-
* credential bound after the generation's genesis cannot write the
|
|
935
|
+
* credential bound after the generation's genesis cannot write the annex
|
|
777
936
|
* until an existing writer commits its rung-0 hash or the next GC swap's
|
|
778
937
|
* genesis does. Typed so callers can map it to the fresh-generation path
|
|
779
938
|
* where one is licensed (the transient-recovery continuation) or to honest
|
|
780
939
|
* copy where none is.
|
|
781
940
|
*/
|
|
782
|
-
export class
|
|
941
|
+
export class ClientAnnexRungUncommittedError extends Error {
|
|
783
942
|
constructor(message) {
|
|
784
943
|
super(message);
|
|
785
|
-
this.name = '
|
|
944
|
+
this.name = 'ClientAnnexRungUncommittedError';
|
|
786
945
|
}
|
|
787
946
|
}
|
|
788
947
|
/**
|
|
789
|
-
* Reads and resolves the published
|
|
948
|
+
* Reads and resolves the published annex log through the narrow seam, or
|
|
790
949
|
* throws when the generation's `did.jsonl` is missing (an unpointed or
|
|
791
950
|
* deleted generation -- nothing to enroll into).
|
|
792
951
|
*
|
|
793
952
|
* @param options {object}
|
|
794
|
-
* @param options.store {
|
|
953
|
+
* @param options.store {ClientAnnexWriteStore}
|
|
795
954
|
* @param [options.expectedDid] {string}
|
|
796
955
|
* @param [options.pinStore] {ResourceLogPinStore}
|
|
797
956
|
* @param [options.logId] {string}
|
|
798
957
|
* @returns {Promise<PublishedWebvhLog>}
|
|
799
958
|
*/
|
|
800
|
-
async function
|
|
959
|
+
async function readClientAnnexLogOrThrow({ store, expectedDid, pinStore, logId }) {
|
|
801
960
|
// readPublishedLog only calls getIdResourceRaw, so the narrow seam is safe.
|
|
802
961
|
const published = await readPublishedLog({
|
|
803
962
|
idStore: store,
|
|
@@ -806,19 +965,19 @@ async function readCompanionLogOrThrow({ store, expectedDid, pinStore, logId })
|
|
|
806
965
|
...(logId !== undefined ? { logId } : {})
|
|
807
966
|
});
|
|
808
967
|
if (!published) {
|
|
809
|
-
throw new Error('
|
|
968
|
+
throw new Error('client annex: did.jsonl is missing; the generation was never minted or ' +
|
|
810
969
|
'has been collected.');
|
|
811
970
|
}
|
|
812
971
|
return published;
|
|
813
972
|
}
|
|
814
973
|
/**
|
|
815
974
|
* TRANSIENT ENROLLMENT: publishes one per-visit verification method into a
|
|
816
|
-
*
|
|
975
|
+
* annex generation's log -- one atomic entry, signed by the writing
|
|
817
976
|
* credential's static rung 0 (derived from the ladder seed and the generation
|
|
818
977
|
* id;
|
|
819
|
-
* see `
|
|
978
|
+
* see `clientAnnexRung`). The entry:
|
|
820
979
|
*
|
|
821
|
-
* - reveals the writer's rung-0 key into `updateKeys` at its first
|
|
980
|
+
* - reveals the writer's rung-0 key into `updateKeys` at its first annex
|
|
822
981
|
* write (later writes re-state it unchanged);
|
|
823
982
|
* - re-states `nextKeyHashes` verbatim -- every standing credential's rung-0
|
|
824
983
|
* hash, the writer's own carry-over hash included -- explicitly on the
|
|
@@ -827,7 +986,7 @@ async function readCompanionLogOrThrow({ store, expectedDid, pinStore, logId })
|
|
|
827
986
|
* relationship arrays stated explicitly (no `authentication`, no
|
|
828
987
|
* `assertionMethod`, no `keyAgreement` twin -- the DIDAuth path signs as
|
|
829
988
|
* the bare did:key, and the controller-marker convention does not arise in
|
|
830
|
-
* the
|
|
989
|
+
* the annex at all).
|
|
831
990
|
*
|
|
832
991
|
* The transient key set carries no update key, and nothing here touches the
|
|
833
992
|
* ACCOUNT log's `updateKeys` or `nextKeyHashes`. There is no two-entry
|
|
@@ -836,12 +995,12 @@ async function readCompanionLogOrThrow({ store, expectedDid, pinStore, logId })
|
|
|
836
995
|
* published document's own state -- a VM already present is a no-op.
|
|
837
996
|
*
|
|
838
997
|
* A writer whose rung-0 key is neither revealed nor committed is refused
|
|
839
|
-
* ({@link
|
|
998
|
+
* ({@link ClientAnnexRungUncommittedError}): annex entries verify against
|
|
840
999
|
* the log's own hash-commitment chain, so no admission rule can make an
|
|
841
1000
|
* uncommitted key verify mid-log.
|
|
842
1001
|
*
|
|
843
1002
|
* @param options {object}
|
|
844
|
-
* @param options.store {
|
|
1003
|
+
* @param options.store {ClientAnnexWriteStore} the generation's log store
|
|
845
1004
|
* (delegated through the credential's sibling delegation, or
|
|
846
1005
|
* controller-tier)
|
|
847
1006
|
* @param options.ladderSeed {Uint8Array} the credential's ladder seed, from
|
|
@@ -849,41 +1008,41 @@ async function readCompanionLogOrThrow({ store, expectedDid, pinStore, logId })
|
|
|
849
1008
|
* @param options.generationId {string} the generation collection's name
|
|
850
1009
|
* @param options.transientKeyMultibase {string} the visit's in-memory
|
|
851
1010
|
* Ed25519 signing key, public multibase
|
|
852
|
-
* @param [options.services] {ServiceEndpoint[]} the
|
|
1011
|
+
* @param [options.services] {ServiceEndpoint[]} the annex document's
|
|
853
1012
|
* full service-entry list, replacing the published one wholesale; omitted,
|
|
854
1013
|
* the prior entries are preserved verbatim (or extended by
|
|
855
1014
|
* `mintGenerationDelegation` below). Supplying both is refused in favor of
|
|
856
1015
|
* the explicit list
|
|
857
1016
|
* @param [options.mintGenerationDelegation] {Function}
|
|
858
|
-
* `({
|
|
1017
|
+
* `({ clientAnnexDid }) => Promise<IZcap>` -- mints the generation
|
|
859
1018
|
* delegation this entry installs when it publishes the generation's FIRST
|
|
860
1019
|
* transient verification method (and the document carries no delegation
|
|
861
1020
|
* entry yet). Never invoked otherwise: the delegation is installed with
|
|
862
1021
|
* the first transient VM or by the GC ceremony's own install stage, never
|
|
863
1022
|
* by genesis (a genesis-embedded signed zcap can never verify -- its
|
|
864
1023
|
* `controller` embeds the SCID the genesis hash derives from)
|
|
865
|
-
* @param [options.expectedDid] {string} the
|
|
1024
|
+
* @param [options.expectedDid] {string} the annex DID the log must
|
|
866
1025
|
* resolve to, from the account document's pointer
|
|
867
1026
|
* @param [options.pinStore] {ResourceLogPinStore} chain-head pins (a
|
|
868
1027
|
* transient session passes an in-memory store)
|
|
869
1028
|
* @param [options.logId] {string} the generation's pin-slot key, from
|
|
870
|
-
* {@link
|
|
1029
|
+
* {@link clientAnnexLogPinId}; required whenever a `pinStore` is supplied
|
|
871
1030
|
* @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
|
|
872
1031
|
*/
|
|
873
|
-
export async function
|
|
874
|
-
return withLogConflictRetry(() =>
|
|
1032
|
+
export async function enrollClientAnnexTransientClient(options) {
|
|
1033
|
+
return withLogConflictRetry(() => enrollClientAnnexTransientClientOnce(options));
|
|
875
1034
|
}
|
|
876
1035
|
/**
|
|
877
|
-
* One attempt of {@link
|
|
1036
|
+
* One attempt of {@link enrollClientAnnexTransientClient}, re-invoked by the
|
|
878
1037
|
* conflict retry (with the same signing key -- static rung 0 has no
|
|
879
1038
|
* advanced-rung retry shape).
|
|
880
1039
|
*
|
|
881
|
-
* @param options {object} see {@link
|
|
1040
|
+
* @param options {object} see {@link enrollClientAnnexTransientClient}
|
|
882
1041
|
* @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
|
|
883
1042
|
*/
|
|
884
|
-
async function
|
|
1043
|
+
async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generationId, transientKeyMultibase, services, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId }) {
|
|
885
1044
|
assertGenerationId(generationId);
|
|
886
|
-
const published = await
|
|
1045
|
+
const published = await readClientAnnexLogOrThrow({
|
|
887
1046
|
store,
|
|
888
1047
|
...(expectedDid !== undefined ? { expectedDid } : {}),
|
|
889
1048
|
...(pinStore !== undefined ? { pinStore } : {}),
|
|
@@ -898,13 +1057,13 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, generatio
|
|
|
898
1057
|
if (existingMethods.some(method => method.id === vmId)) {
|
|
899
1058
|
return { did, doc, log: published.log };
|
|
900
1059
|
}
|
|
901
|
-
const rung = await
|
|
1060
|
+
const rung = await clientAnnexRung({ ladderSeed, generationId });
|
|
902
1061
|
const rungHash = await deriveNextKeyHash(rung.keyMultibase);
|
|
903
1062
|
const revealed = published.updateKeys.includes(rung.keyMultibase);
|
|
904
1063
|
if (!revealed && !published.nextKeyHashes.includes(rungHash)) {
|
|
905
|
-
throw new
|
|
1064
|
+
throw new ClientAnnexRungUncommittedError("client annex: the log commits neither this credential's rung-0 key nor " +
|
|
906
1065
|
'its hash; a credential bound mid-generation cannot write the ' +
|
|
907
|
-
'
|
|
1066
|
+
'annex until a writer commits its hash or the next GC swap does.');
|
|
908
1067
|
}
|
|
909
1068
|
// A non-rotating entry re-states `updateKeys`, which the resolver checks
|
|
910
1069
|
// against the previous entry's commitments -- genesis enforces the
|
|
@@ -918,10 +1077,10 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, generatio
|
|
|
918
1077
|
mintDelegation !== undefined &&
|
|
919
1078
|
existingMethods.length === 0 &&
|
|
920
1079
|
embeddedGenerationDelegation({ doc }) === undefined) {
|
|
921
|
-
const delegation = await mintDelegation({
|
|
1080
|
+
const delegation = await mintDelegation({ clientAnnexDid: did });
|
|
922
1081
|
services = withGenerationDelegationEntry({
|
|
923
1082
|
doc,
|
|
924
|
-
|
|
1083
|
+
clientAnnexDid: did,
|
|
925
1084
|
delegation
|
|
926
1085
|
});
|
|
927
1086
|
}
|
|
@@ -955,17 +1114,17 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, generatio
|
|
|
955
1114
|
capabilityDelegation: relationIds(doc.capabilityDelegation),
|
|
956
1115
|
...(services !== undefined ? { services } : {})
|
|
957
1116
|
});
|
|
958
|
-
// The log only --
|
|
1117
|
+
// The log only -- an annex has no did:web projection -- conditional on
|
|
959
1118
|
// the read this entry was built on.
|
|
960
1119
|
await putLogResource({ store, log: updated.log, ifMatch: published.etag });
|
|
961
1120
|
return { did: updated.did, doc: updated.doc, log: updated.log };
|
|
962
1121
|
}
|
|
963
1122
|
/**
|
|
964
1123
|
* Points the account document's delegated-clients service entry at a
|
|
965
|
-
*
|
|
1124
|
+
* annex DID -- the first install after a generation's genesis, and the GC
|
|
966
1125
|
* swap's re-point alike. One ordinary document-update entry, signed by an
|
|
967
|
-
* enrolled durable client's active update key; the
|
|
968
|
-
* publishes FIRST (see {@link
|
|
1126
|
+
* enrolled durable client's active update key; the annex log always
|
|
1127
|
+
* publishes FIRST (see {@link mintClientAnnexGeneration}), so a tear leaves an
|
|
969
1128
|
* unpointed, authorization-inert generation, never a dangling pointer.
|
|
970
1129
|
*
|
|
971
1130
|
* An existing delegated-clients entry is re-pointed in place, its fragment id
|
|
@@ -976,10 +1135,14 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, generatio
|
|
|
976
1135
|
* a no-op on the log (it still heals a lagging `did.json`).
|
|
977
1136
|
*
|
|
978
1137
|
* @param options {object}
|
|
979
|
-
* @param options.idStore {WebvhIdStore} the ACCOUNT log's store
|
|
1138
|
+
* @param options.idStore {WebvhIdStore} the ACCOUNT log's store; with
|
|
1139
|
+
* `logOnly`, only its log read and `did.jsonl` PUT are used, so the narrow
|
|
1140
|
+
* delegated seam satisfies it
|
|
980
1141
|
* @param options.updateKeys {ClientWebvhUpdateKeys} this durable client's
|
|
981
|
-
* update-key seeds
|
|
982
|
-
*
|
|
1142
|
+
* update-key seeds -- or the ladder-rung idiom on a ladder-anchored
|
|
1143
|
+
* account (`{ updateSeed: rung0.seed, stagedSeed: rung1.seed }`), as the
|
|
1144
|
+
* credential-anchored genesis and the transient-recovery continuation pass
|
|
1145
|
+
* @param options.clientAnnexDid {string} the generation to point at
|
|
983
1146
|
* @param [options.expectedDid] {string} the account DID the log must
|
|
984
1147
|
* resolve to, from the account pointer
|
|
985
1148
|
* @param [options.pinStore] {ResourceLogPinStore} this client's chain-head
|
|
@@ -987,6 +1150,11 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, generatio
|
|
|
987
1150
|
* @param [options.logId] {string} the account log's pin-slot key, from
|
|
988
1151
|
* `accountLogPinId({ spaceId })`; required whenever a `pinStore` is
|
|
989
1152
|
* supplied
|
|
1153
|
+
* @param [options.logOnly] {boolean} publish `did.jsonl` only, never the
|
|
1154
|
+
* `did.json` projection -- the transient-recovery continuation writing
|
|
1155
|
+
* through the record's bridge delegation, whose narrow scope covers nothing
|
|
1156
|
+
* but the log. The projection heals at the next authorized write (the log
|
|
1157
|
+
* is the source of truth)
|
|
990
1158
|
* @returns {Promise<{ did: string, doc: DIDDoc }>}
|
|
991
1159
|
*/
|
|
992
1160
|
export async function setDelegatedClientsPointer(options) {
|
|
@@ -999,9 +1167,9 @@ export async function setDelegatedClientsPointer(options) {
|
|
|
999
1167
|
* @param options {object} see {@link setDelegatedClientsPointer}
|
|
1000
1168
|
* @returns {Promise<{ did: string, doc: DIDDoc }>}
|
|
1001
1169
|
*/
|
|
1002
|
-
async function setDelegatedClientsPointerOnce({ idStore, updateKeys,
|
|
1170
|
+
async function setDelegatedClientsPointerOnce({ idStore, updateKeys, clientAnnexDid, expectedDid, pinStore, logId, logOnly = false }) {
|
|
1003
1171
|
// Refuses a malformed target before anything is read or written.
|
|
1004
|
-
|
|
1172
|
+
clientAnnexDidParts({ did: clientAnnexDid });
|
|
1005
1173
|
const published = await readPublishedLog({
|
|
1006
1174
|
idStore,
|
|
1007
1175
|
...(expectedDid !== undefined ? { expectedDid } : {}),
|
|
@@ -1009,11 +1177,13 @@ async function setDelegatedClientsPointerOnce({ idStore, updateKeys, companionDi
|
|
|
1009
1177
|
...(logId !== undefined ? { logId } : {})
|
|
1010
1178
|
});
|
|
1011
1179
|
if (!published) {
|
|
1012
|
-
throw new Error('did:webvh: did.jsonl is missing; nothing to point at a
|
|
1180
|
+
throw new Error('did:webvh: did.jsonl is missing; nothing to point at a client annex.');
|
|
1013
1181
|
}
|
|
1014
1182
|
const { did, doc } = published;
|
|
1015
|
-
if (delegatedClientsPointer({ doc }) ===
|
|
1016
|
-
|
|
1183
|
+
if (delegatedClientsPointer({ doc }) === clientAnnexDid) {
|
|
1184
|
+
if (!logOnly) {
|
|
1185
|
+
await concludeWithPublishedLog({ idStore, published });
|
|
1186
|
+
}
|
|
1017
1187
|
return { did, doc };
|
|
1018
1188
|
}
|
|
1019
1189
|
// The entry is signed by this client's active update key; a log that does
|
|
@@ -1025,19 +1195,11 @@ async function setDelegatedClientsPointerOnce({ idStore, updateKeys, companionDi
|
|
|
1025
1195
|
'delegated-clients entry.');
|
|
1026
1196
|
}
|
|
1027
1197
|
await assertCarryOverCommitments({ published });
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
};
|
|
1033
|
-
const services = existing.some(isPointerEntry)
|
|
1034
|
-
? existing.map(entry => isPointerEntry(entry)
|
|
1035
|
-
? { ...entry, serviceEndpoint: companionDid }
|
|
1036
|
-
: entry)
|
|
1037
|
-
: [
|
|
1038
|
-
...existing,
|
|
1039
|
-
delegatedClientsServiceEntry({ accountDid: did, companionDid })
|
|
1040
|
-
];
|
|
1198
|
+
const services = servicesPointedAtClientAnnex({
|
|
1199
|
+
doc,
|
|
1200
|
+
accountDid: did,
|
|
1201
|
+
clientAnnexDid
|
|
1202
|
+
});
|
|
1041
1203
|
const signer = await updateKeySigner({ seed: updateKeys.updateSeed });
|
|
1042
1204
|
const updated = await updateDID({
|
|
1043
1205
|
log: published.log,
|
|
@@ -1050,7 +1212,16 @@ async function setDelegatedClientsPointerOnce({ idStore, updateKeys, companionDi
|
|
|
1050
1212
|
nextKeyHashes: published.nextKeyHashes,
|
|
1051
1213
|
services
|
|
1052
1214
|
});
|
|
1053
|
-
|
|
1215
|
+
if (logOnly) {
|
|
1216
|
+
await putLogResource({
|
|
1217
|
+
store: idStore,
|
|
1218
|
+
log: updated.log,
|
|
1219
|
+
ifMatch: published.etag
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
await publishUpdatedLog({ idStore, updated, ifMatch: published.etag });
|
|
1224
|
+
}
|
|
1054
1225
|
return { did: updated.did, doc: updated.doc };
|
|
1055
1226
|
}
|
|
1056
1227
|
/**
|
|
@@ -1076,69 +1247,80 @@ async function setDelegatedClientsPointerOnce({ idStore, updateKeys, companionDi
|
|
|
1076
1247
|
* @param options.transientKeyMultibase {string} the visit's in-memory
|
|
1077
1248
|
* signing key, public multibase
|
|
1078
1249
|
* @param [options.mintGenerationDelegation] {Function}
|
|
1079
|
-
* `({
|
|
1250
|
+
* `({ clientAnnexDid }) => Promise<IZcap>` -- forwarded to the enrollment
|
|
1080
1251
|
* entry, which installs the minted delegation when it publishes the
|
|
1081
1252
|
* generation's first transient VM (see
|
|
1082
|
-
* {@link
|
|
1083
|
-
*
|
|
1253
|
+
* {@link enrollClientAnnexTransientClient}). The closure receives whichever
|
|
1254
|
+
* annex DID the round enrolls into, so a GC-race re-enroll mints for
|
|
1084
1255
|
* the fresh generation
|
|
1085
1256
|
* @param [options.pinStore] {ResourceLogPinStore} chain-head pins for the
|
|
1086
1257
|
* generation logs (a transient session passes an in-memory store); slot
|
|
1087
|
-
* keys are derived per generation with {@link
|
|
1258
|
+
* keys are derived per generation with {@link clientAnnexLogPinId}
|
|
1088
1259
|
* @param [options.maxRounds] {number} how many pointer moves to chase
|
|
1089
1260
|
* before giving up (a GC pass is quarterly, so more than one mid-ceremony
|
|
1090
1261
|
* move means something else is wrong)
|
|
1091
|
-
* @returns {Promise<{
|
|
1262
|
+
* @returns {Promise<{ clientAnnexDid: string, doc: DIDDoc, log: DIDLog }>}
|
|
1092
1263
|
*/
|
|
1093
1264
|
export async function enrollTransientClient({ readAccountDocument, storeForGenerationId, ladderSeed, transientKeyMultibase, mintGenerationDelegation: mintDelegation, pinStore, maxRounds = 3 }) {
|
|
1094
1265
|
let accountDoc = await readAccountDocument();
|
|
1095
1266
|
for (let round = 0; round < maxRounds; round++) {
|
|
1096
|
-
const
|
|
1097
|
-
if (
|
|
1098
|
-
throw new Error('
|
|
1267
|
+
const clientAnnexDid = delegatedClientsPointer({ doc: accountDoc });
|
|
1268
|
+
if (clientAnnexDid === undefined) {
|
|
1269
|
+
throw new Error('client annex: the account document carries no delegated-clients ' +
|
|
1099
1270
|
'service entry; no generation exists to enroll into.');
|
|
1100
1271
|
}
|
|
1101
|
-
const { spaceId, generationId } =
|
|
1102
|
-
|
|
1272
|
+
const { spaceId, generationId } = clientAnnexDidParts({
|
|
1273
|
+
did: clientAnnexDid
|
|
1274
|
+
});
|
|
1275
|
+
const enrolled = await enrollClientAnnexTransientClient({
|
|
1103
1276
|
store: storeForGenerationId(generationId),
|
|
1104
1277
|
ladderSeed,
|
|
1105
1278
|
generationId,
|
|
1106
1279
|
transientKeyMultibase,
|
|
1107
|
-
expectedDid:
|
|
1280
|
+
expectedDid: clientAnnexDid,
|
|
1108
1281
|
...(mintDelegation !== undefined
|
|
1109
1282
|
? { mintGenerationDelegation: mintDelegation }
|
|
1110
1283
|
: {}),
|
|
1111
1284
|
...(pinStore !== undefined
|
|
1112
|
-
? { pinStore, logId:
|
|
1285
|
+
? { pinStore, logId: clientAnnexLogPinId({ spaceId, generationId }) }
|
|
1113
1286
|
: {})
|
|
1114
1287
|
});
|
|
1115
1288
|
// The GC-race re-read: an unchanged pointer means the enrollment stands
|
|
1116
1289
|
// in the pointed generation; a moved one means a concurrent GC abandoned
|
|
1117
1290
|
// it, and the next round enrolls into the fresh generation.
|
|
1118
1291
|
accountDoc = await readAccountDocument();
|
|
1119
|
-
if (delegatedClientsPointer({ doc: accountDoc }) ===
|
|
1120
|
-
return {
|
|
1292
|
+
if (delegatedClientsPointer({ doc: accountDoc }) === clientAnnexDid) {
|
|
1293
|
+
return { clientAnnexDid, doc: enrolled.doc, log: enrolled.log };
|
|
1121
1294
|
}
|
|
1122
1295
|
}
|
|
1123
|
-
throw new Error('
|
|
1296
|
+
throw new Error('client annex: the delegated-clients pointer kept moving across ' +
|
|
1124
1297
|
`${String(maxRounds)} enrollment rounds; giving up.`);
|
|
1125
1298
|
}
|
|
1126
1299
|
/**
|
|
1127
1300
|
* RENEW PRECEDES MINT: the blocking pre-mint stage a transient App Connect
|
|
1128
|
-
* approval runs before delegating any grant. Reads the
|
|
1301
|
+
* approval runs before delegating any grant. Reads the annex document
|
|
1129
1302
|
* and hands back its embedded generation delegation -- renewing it first
|
|
1130
1303
|
* when it is expired or inside the 30-day renewal window ({@link
|
|
1131
1304
|
* zcapExpiring}): a fresh delegation is minted through the caller's closure
|
|
1132
1305
|
* (ladder-signed -- the renewal must not depend on the very delegation it
|
|
1133
1306
|
* replaces; published through the store, which in a transient session is
|
|
1134
1307
|
* the credential's sibling delegation, so even a hard-expired delegation is
|
|
1135
|
-
* recoverable), and one
|
|
1308
|
+
* recoverable), and one annex entry replaces the service entry's
|
|
1136
1309
|
* endpoint in place, signed by the writing credential's static rung 0.
|
|
1137
1310
|
*
|
|
1138
|
-
*
|
|
1311
|
+
* An annex document carrying no delegation entry at all installs one the
|
|
1139
1312
|
* same way (the GC ceremony's own install stage and the first-VM install
|
|
1140
1313
|
* make this rare; a heal, not a policy).
|
|
1141
1314
|
*
|
|
1315
|
+
* Beside the expiry axis, an `accountDoc` adds the SIGNER-DEATH axis: a
|
|
1316
|
+
* standing delegation whose proof key is no longer in the supplied verified
|
|
1317
|
+
* account document has rotted under the current-key-set rule (the durable
|
|
1318
|
+
* client that minted it was revoked, or the ladder VM that signed it left
|
|
1319
|
+
* with the first durable self-enrollment) and is replaced the same way. No
|
|
1320
|
+
* revocation POST accompanies the replacement: a rotted chain no longer
|
|
1321
|
+
* verifies at the revocation endpoint, and the expiry-renewal path never
|
|
1322
|
+
* revoked either.
|
|
1323
|
+
*
|
|
1142
1324
|
* Failure is the caller's failure: a renewal that cannot complete throws,
|
|
1143
1325
|
* and the App Connect approval fails with the standard retryable-ceremony
|
|
1144
1326
|
* posture -- deliberately no clamp-on-failure fallback, which would deliver
|
|
@@ -1148,21 +1330,28 @@ export async function enrollTransientClient({ readAccountDocument, storeForGener
|
|
|
1148
1330
|
* 30 or more days remaining ({@link clampGrantExpires}).
|
|
1149
1331
|
*
|
|
1150
1332
|
* @param options {object}
|
|
1151
|
-
* @param options.store {
|
|
1333
|
+
* @param options.store {ClientAnnexWriteStore} the generation's log store
|
|
1152
1334
|
* (delegated through the credential's sibling delegation, or
|
|
1153
1335
|
* controller-tier)
|
|
1154
1336
|
* @param options.ladderSeed {Uint8Array} the credential's ladder seed, from
|
|
1155
1337
|
* its unlock record
|
|
1156
1338
|
* @param options.generationId {string} the generation collection's name
|
|
1157
1339
|
* @param options.mintGenerationDelegation {Function}
|
|
1158
|
-
* `({
|
|
1340
|
+
* `({ clientAnnexDid }) => Promise<IZcap>` -- mints the replacement
|
|
1159
1341
|
* delegation (ladder-signed in a transient session)
|
|
1160
|
-
* @param [options.expectedDid] {string} the
|
|
1342
|
+
* @param [options.expectedDid] {string} the annex DID the log must
|
|
1161
1343
|
* resolve to, from the account document's pointer
|
|
1162
1344
|
* @param [options.pinStore] {ResourceLogPinStore} chain-head pins (a
|
|
1163
1345
|
* transient session passes an in-memory store)
|
|
1164
1346
|
* @param [options.logId] {string} the generation's pin-slot key, from
|
|
1165
|
-
* {@link
|
|
1347
|
+
* {@link clientAnnexLogPinId}; required whenever a `pinStore` is supplied
|
|
1348
|
+
* @param [options.accountDoc] {PublishedKeyDocument} the locally VERIFIED
|
|
1349
|
+
* account document; supplied, a standing delegation whose proof key it no
|
|
1350
|
+
* longer lists is replaced (the signer-death axis above)
|
|
1351
|
+
* @param [options.force] {boolean} replace the embedded delegation
|
|
1352
|
+
* unconditionally, however healthy it looks -- the last-durable-client
|
|
1353
|
+
* forget's replacement stage, where the standing delegation has just been
|
|
1354
|
+
* revoked server-side (a state no client-side predicate can read)
|
|
1166
1355
|
* @param [options.now] {number} epoch milliseconds, for tests
|
|
1167
1356
|
* @returns {Promise<{ delegation: IZcap, renewed: boolean }>}
|
|
1168
1357
|
*/
|
|
@@ -1177,9 +1366,9 @@ export async function ensureGenerationDelegationCurrent(options) {
|
|
|
1177
1366
|
* @param options {object} see {@link ensureGenerationDelegationCurrent}
|
|
1178
1367
|
* @returns {Promise<{ delegation: IZcap, renewed: boolean }>}
|
|
1179
1368
|
*/
|
|
1180
|
-
async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, generationId, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId, now }) {
|
|
1369
|
+
async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, generationId, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId, accountDoc, force = false, now }) {
|
|
1181
1370
|
assertGenerationId(generationId);
|
|
1182
|
-
const published = await
|
|
1371
|
+
const published = await readClientAnnexLogOrThrow({
|
|
1183
1372
|
store,
|
|
1184
1373
|
...(expectedDid !== undefined ? { expectedDid } : {}),
|
|
1185
1374
|
...(pinStore !== undefined ? { pinStore } : {}),
|
|
@@ -1187,7 +1376,17 @@ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, genera
|
|
|
1187
1376
|
});
|
|
1188
1377
|
const { did, doc } = published;
|
|
1189
1378
|
const standing = embeddedGenerationDelegation({ doc });
|
|
1190
|
-
|
|
1379
|
+
const signerRotted = standing !== undefined &&
|
|
1380
|
+
accountDoc !== undefined &&
|
|
1381
|
+
!delegationKeyInDocument({
|
|
1382
|
+
doc: accountDoc,
|
|
1383
|
+
...(delegationProofKeyId(standing) !== undefined
|
|
1384
|
+
? { delegationKeyId: delegationProofKeyId(standing) }
|
|
1385
|
+
: {})
|
|
1386
|
+
});
|
|
1387
|
+
if (!force &&
|
|
1388
|
+
standing !== undefined &&
|
|
1389
|
+
!signerRotted &&
|
|
1191
1390
|
!zcapExpiring({
|
|
1192
1391
|
...(standing.expires !== undefined
|
|
1193
1392
|
? { expires: standing.expires }
|
|
@@ -1198,22 +1397,22 @@ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, genera
|
|
|
1198
1397
|
}
|
|
1199
1398
|
// The rung refusal precedes the mint: nothing is delegated for a writer
|
|
1200
1399
|
// who cannot publish the entry that would carry it.
|
|
1201
|
-
const rung = await
|
|
1400
|
+
const rung = await clientAnnexRung({ ladderSeed, generationId });
|
|
1202
1401
|
const rungHash = await deriveNextKeyHash(rung.keyMultibase);
|
|
1203
1402
|
const revealed = published.updateKeys.includes(rung.keyMultibase);
|
|
1204
1403
|
if (!revealed && !published.nextKeyHashes.includes(rungHash)) {
|
|
1205
|
-
throw new
|
|
1404
|
+
throw new ClientAnnexRungUncommittedError("client annex: the log commits neither this credential's rung-0 key nor " +
|
|
1206
1405
|
'its hash; a credential bound mid-generation cannot renew the ' +
|
|
1207
1406
|
'generation delegation until a writer commits its hash or the next ' +
|
|
1208
1407
|
'GC swap does.');
|
|
1209
1408
|
}
|
|
1210
1409
|
await assertCarryOverCommitments({ published });
|
|
1211
|
-
const fresh = await mintDelegation({
|
|
1410
|
+
const fresh = await mintDelegation({ clientAnnexDid: did });
|
|
1212
1411
|
const signer = await updateKeySigner({ seed: rung.seed });
|
|
1213
1412
|
const updated = await updateDID({
|
|
1214
1413
|
log: published.log,
|
|
1215
1414
|
signer,
|
|
1216
|
-
// The writer's rung-0 key reveals at its first
|
|
1415
|
+
// The writer's rung-0 key reveals at its first annex write, exactly
|
|
1217
1416
|
// as the enrollment entry does; `nextKeyHashes` is re-stated verbatim,
|
|
1218
1417
|
// never inherited. Verification methods, relationship arrays, and every
|
|
1219
1418
|
// other service entry ride the library's prior-state clone untouched.
|
|
@@ -1221,11 +1420,196 @@ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, genera
|
|
|
1221
1420
|
nextKeyHashes: [...published.nextKeyHashes],
|
|
1222
1421
|
services: withGenerationDelegationEntry({
|
|
1223
1422
|
doc,
|
|
1224
|
-
|
|
1423
|
+
clientAnnexDid: did,
|
|
1225
1424
|
delegation: fresh
|
|
1226
1425
|
})
|
|
1227
1426
|
});
|
|
1228
1427
|
await putLogResource({ store, log: updated.log, ifMatch: published.etag });
|
|
1229
1428
|
return { delegation: fresh, renewed: true };
|
|
1230
1429
|
}
|
|
1231
|
-
|
|
1430
|
+
/**
|
|
1431
|
+
* THE CLIENT-ANNEX RUNG STRIKE: drops a retired credential's annex posture
|
|
1432
|
+
* from a generation's log -- its revealed rung-0 key out of `updateKeys` and
|
|
1433
|
+
* its standing rung-0 hash out of `nextKeyHashes` -- in one atomic entry
|
|
1434
|
+
* signed by ANOTHER credential's committed rung 0 (an annex entry cannot
|
|
1435
|
+
* remove its own signing key: the entry verifies against its own re-stated
|
|
1436
|
+
* `updateKeys`). The credential-rotation ceremony's annex reach.
|
|
1437
|
+
*
|
|
1438
|
+
* A log committing neither the retired rung's key nor its hash is already
|
|
1439
|
+
* clean and the strike no-ops (`struck: false`) -- the resumable shape, and
|
|
1440
|
+
* the common one: a credential that never minted or wrote this generation
|
|
1441
|
+
* has no posture in it. An acting rung the log does not commit (after the
|
|
1442
|
+
* retired members are excluded -- so the retired credential can never sign
|
|
1443
|
+
* its own strike) is refused with {@link ClientAnnexRungUncommittedError},
|
|
1444
|
+
* which the caller maps to the generation-swap fallback: a fresh generation
|
|
1445
|
+
* minted from a surviving credential's seed retires the rung with the whole
|
|
1446
|
+
* generation.
|
|
1447
|
+
*
|
|
1448
|
+
* @param options {object}
|
|
1449
|
+
* @param options.store {ClientAnnexWriteStore} the pointed generation's log
|
|
1450
|
+
* store (controller-tier, or delegated through a sibling delegation)
|
|
1451
|
+
* @param options.retiredLadderSeed {Uint8Array} the RETIRED credential's
|
|
1452
|
+
* ladder seed (its rung is derived per generation, so the seed is the only
|
|
1453
|
+
* way to name what to strike)
|
|
1454
|
+
* @param options.actingLadderSeed {Uint8Array} a surviving credential's
|
|
1455
|
+
* ladder seed, whose committed rung 0 signs the strike entry
|
|
1456
|
+
* @param options.generationId {string} the generation collection's name
|
|
1457
|
+
* @param [options.expectedDid] {string} the annex DID the log must
|
|
1458
|
+
* resolve to, from the account document's pointer
|
|
1459
|
+
* @param [options.pinStore] {ResourceLogPinStore}
|
|
1460
|
+
* @param [options.logId] {string} the generation's pin-slot key, from
|
|
1461
|
+
* {@link clientAnnexLogPinId}; required whenever a `pinStore` is supplied
|
|
1462
|
+
* @returns {Promise<{ struck: boolean }>}
|
|
1463
|
+
*/
|
|
1464
|
+
export async function retireClientAnnexRung(options) {
|
|
1465
|
+
return withLogConflictRetry(() => retireClientAnnexRungOnce(options));
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* One attempt of {@link retireClientAnnexRung}, re-invoked by the conflict
|
|
1469
|
+
* retry (with the same signing key -- static rung 0 has no advanced-rung
|
|
1470
|
+
* retry shape).
|
|
1471
|
+
*
|
|
1472
|
+
* @param options {object} see {@link retireClientAnnexRung}
|
|
1473
|
+
* @returns {Promise<{ struck: boolean }>}
|
|
1474
|
+
*/
|
|
1475
|
+
async function retireClientAnnexRungOnce({ store, retiredLadderSeed, actingLadderSeed, generationId, expectedDid, pinStore, logId }) {
|
|
1476
|
+
assertGenerationId(generationId);
|
|
1477
|
+
const published = await readClientAnnexLogOrThrow({
|
|
1478
|
+
store,
|
|
1479
|
+
...(expectedDid !== undefined ? { expectedDid } : {}),
|
|
1480
|
+
...(pinStore !== undefined ? { pinStore } : {}),
|
|
1481
|
+
...(logId !== undefined ? { logId } : {})
|
|
1482
|
+
});
|
|
1483
|
+
const retired = await clientAnnexRung({
|
|
1484
|
+
ladderSeed: retiredLadderSeed,
|
|
1485
|
+
generationId
|
|
1486
|
+
});
|
|
1487
|
+
const retiredHash = await deriveNextKeyHash(retired.keyMultibase);
|
|
1488
|
+
const remainingKeys = published.updateKeys.filter(key => key !== retired.keyMultibase);
|
|
1489
|
+
const remainingHashes = published.nextKeyHashes.filter(hash => hash !== retiredHash);
|
|
1490
|
+
if (remainingKeys.length === published.updateKeys.length &&
|
|
1491
|
+
remainingHashes.length === published.nextKeyHashes.length) {
|
|
1492
|
+
// Already clean: the retired credential holds no posture in this
|
|
1493
|
+
// generation (never committed, or a completed earlier strike).
|
|
1494
|
+
return { struck: false };
|
|
1495
|
+
}
|
|
1496
|
+
// The acting rung must be committed AFTER the retired members are
|
|
1497
|
+
// excluded, so the retired credential can never sign its own strike.
|
|
1498
|
+
const acting = await clientAnnexRung({
|
|
1499
|
+
ladderSeed: actingLadderSeed,
|
|
1500
|
+
generationId
|
|
1501
|
+
});
|
|
1502
|
+
const actingHash = await deriveNextKeyHash(acting.keyMultibase);
|
|
1503
|
+
const revealed = remainingKeys.includes(acting.keyMultibase);
|
|
1504
|
+
if (!revealed && !remainingHashes.includes(actingHash)) {
|
|
1505
|
+
throw new ClientAnnexRungUncommittedError("client annex: the log commits neither the acting credential's rung-0 " +
|
|
1506
|
+
'key nor its hash (or it is the retired rung itself); the strike ' +
|
|
1507
|
+
'needs a distinct committed writer -- swap the generation instead.');
|
|
1508
|
+
}
|
|
1509
|
+
await assertCarryOverCommitments({ published });
|
|
1510
|
+
const signer = await updateKeySigner({ seed: acting.seed });
|
|
1511
|
+
const updated = await updateDID({
|
|
1512
|
+
log: published.log,
|
|
1513
|
+
signer,
|
|
1514
|
+
// The acting rung reveals at its first annex write, exactly as the
|
|
1515
|
+
// enrollment entry does; the retired rung's key and hash are dropped by
|
|
1516
|
+
// explicit re-statement (never parameter inheritance). Verification
|
|
1517
|
+
// methods, relationship arrays, and the service entries ride the
|
|
1518
|
+
// library's prior-state clone untouched.
|
|
1519
|
+
updateKeys: [...new Set([...remainingKeys, acting.keyMultibase])],
|
|
1520
|
+
nextKeyHashes: [...remainingHashes]
|
|
1521
|
+
});
|
|
1522
|
+
await putLogResource({ store, log: updated.log, ifMatch: published.etag });
|
|
1523
|
+
return { struck: true };
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* THE CLIENT-ANNEX RUNG COMMIT: adds a freshly bound credential's rung-0
|
|
1527
|
+
* hash to a generation's `nextKeyHashes` -- one atomic hash-restating entry
|
|
1528
|
+
* signed by an already-committed credential's rung 0. The bind ceremonies'
|
|
1529
|
+
* annex reach (passkey add, passphrase change): a bind runs from a logged-in
|
|
1530
|
+
* session whose own login credential's rung 0 is committed, so committing the
|
|
1531
|
+
* new credential's hash here is what keeps it out of the mid-generation
|
|
1532
|
+
* lockout ({@link ClientAnnexRungUncommittedError} at its first transient
|
|
1533
|
+
* login, otherwise standing until the next GC swap's genesis).
|
|
1534
|
+
*
|
|
1535
|
+
* A log already committing the bound rung's hash (or carrying its revealed
|
|
1536
|
+
* key) is a no-op (`committed: false`) -- the resumable shape. An acting rung
|
|
1537
|
+
* the log does not commit is refused with
|
|
1538
|
+
* {@link ClientAnnexRungUncommittedError}: the bind ceremony maps that to an
|
|
1539
|
+
* honest skip (nothing licenses it to mint a generation), and the lockout
|
|
1540
|
+
* consequence stands as documented.
|
|
1541
|
+
*
|
|
1542
|
+
* @param options {object}
|
|
1543
|
+
* @param options.store {ClientAnnexWriteStore} the pointed generation's log
|
|
1544
|
+
* store (controller-tier, or delegated through a sibling delegation)
|
|
1545
|
+
* @param options.boundLadderSeed {Uint8Array} the freshly bound
|
|
1546
|
+
* credential's ladder seed (its rung is derived per generation, so the seed
|
|
1547
|
+
* is the only way to name what to commit)
|
|
1548
|
+
* @param options.actingLadderSeed {Uint8Array} the logged-in session's
|
|
1549
|
+
* login credential's ladder seed, whose committed rung 0 signs the entry
|
|
1550
|
+
* @param options.generationId {string} the generation collection's name
|
|
1551
|
+
* @param [options.expectedDid] {string} the annex DID the log must
|
|
1552
|
+
* resolve to, from the account document's pointer
|
|
1553
|
+
* @param [options.pinStore] {ResourceLogPinStore}
|
|
1554
|
+
* @param [options.logId] {string} the generation's pin-slot key, from
|
|
1555
|
+
* {@link clientAnnexLogPinId}; required whenever a `pinStore` is supplied
|
|
1556
|
+
* @returns {Promise<{ committed: boolean }>}
|
|
1557
|
+
*/
|
|
1558
|
+
export async function commitClientAnnexRung(options) {
|
|
1559
|
+
return withLogConflictRetry(() => commitClientAnnexRungOnce(options));
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* One attempt of {@link commitClientAnnexRung}, re-invoked by the conflict
|
|
1563
|
+
* retry (with the same signing key -- static rung 0 has no advanced-rung
|
|
1564
|
+
* retry shape).
|
|
1565
|
+
*
|
|
1566
|
+
* @param options {object} see {@link commitClientAnnexRung}
|
|
1567
|
+
* @returns {Promise<{ committed: boolean }>}
|
|
1568
|
+
*/
|
|
1569
|
+
async function commitClientAnnexRungOnce({ store, boundLadderSeed, actingLadderSeed, generationId, expectedDid, pinStore, logId }) {
|
|
1570
|
+
assertGenerationId(generationId);
|
|
1571
|
+
const published = await readClientAnnexLogOrThrow({
|
|
1572
|
+
store,
|
|
1573
|
+
...(expectedDid !== undefined ? { expectedDid } : {}),
|
|
1574
|
+
...(pinStore !== undefined ? { pinStore } : {}),
|
|
1575
|
+
...(logId !== undefined ? { logId } : {})
|
|
1576
|
+
});
|
|
1577
|
+
const bound = await clientAnnexRung({
|
|
1578
|
+
ladderSeed: boundLadderSeed,
|
|
1579
|
+
generationId
|
|
1580
|
+
});
|
|
1581
|
+
const boundHash = await deriveNextKeyHash(bound.keyMultibase);
|
|
1582
|
+
if (published.updateKeys.includes(bound.keyMultibase) ||
|
|
1583
|
+
published.nextKeyHashes.includes(boundHash)) {
|
|
1584
|
+
// Already committed (or even revealed): a completed earlier run, or a
|
|
1585
|
+
// generation the bound credential itself minted.
|
|
1586
|
+
return { committed: false };
|
|
1587
|
+
}
|
|
1588
|
+
const acting = await clientAnnexRung({
|
|
1589
|
+
ladderSeed: actingLadderSeed,
|
|
1590
|
+
generationId
|
|
1591
|
+
});
|
|
1592
|
+
const actingHash = await deriveNextKeyHash(acting.keyMultibase);
|
|
1593
|
+
const revealed = published.updateKeys.includes(acting.keyMultibase);
|
|
1594
|
+
if (!revealed && !published.nextKeyHashes.includes(actingHash)) {
|
|
1595
|
+
throw new ClientAnnexRungUncommittedError("client annex: the log commits neither the acting credential's rung-0 " +
|
|
1596
|
+
'key nor its hash; a commit entry needs a committed writer -- the ' +
|
|
1597
|
+
'bound credential stays locked out until the next GC swap.');
|
|
1598
|
+
}
|
|
1599
|
+
await assertCarryOverCommitments({ published });
|
|
1600
|
+
const signer = await updateKeySigner({ seed: acting.seed });
|
|
1601
|
+
const updated = await updateDID({
|
|
1602
|
+
log: published.log,
|
|
1603
|
+
signer,
|
|
1604
|
+
// The acting rung reveals at its first annex write, exactly as the
|
|
1605
|
+
// enrollment entry does; the bound rung's hash is added by explicit
|
|
1606
|
+
// re-statement (never parameter inheritance). Verification methods,
|
|
1607
|
+
// relationship arrays, and the service entries ride the library's
|
|
1608
|
+
// prior-state clone untouched.
|
|
1609
|
+
updateKeys: [...new Set([...published.updateKeys, acting.keyMultibase])],
|
|
1610
|
+
nextKeyHashes: [...published.nextKeyHashes, boundHash]
|
|
1611
|
+
});
|
|
1612
|
+
await putLogResource({ store, log: updated.log, ifMatch: published.etag });
|
|
1613
|
+
return { committed: true };
|
|
1614
|
+
}
|
|
1615
|
+
//# sourceMappingURL=log.js.map
|