@interop/wallet-core 0.48.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/clientAnnex/gc.d.ts +255 -0
- package/dist/clientAnnex/gc.d.ts.map +1 -0
- package/dist/clientAnnex/gc.js +445 -0
- 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 +99 -23
- 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} +373 -152
- package/dist/clientAnnex/log.d.ts.map +1 -0
- package/dist/{webvh/companion.js → clientAnnex/log.js} +654 -261
- 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 +38 -0
- package/dist/space/activity.d.ts.map +1 -1
- package/dist/space/activity.js +41 -1
- package/dist/space/activity.js.map +1 -1
- package/dist/space/index.d.ts +1 -1
- package/dist/space/index.d.ts.map +1 -1
- package/dist/space/index.js +1 -1
- package/dist/space/index.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 +20 -10
- package/dist/webvh/didWebvh.d.ts.map +1 -1
- package/dist/webvh/didWebvh.js +17 -17
- package/dist/webvh/didWebvh.js.map +1 -1
- package/dist/webvh/index.d.ts +9 -23
- package/dist/webvh/index.d.ts.map +1 -1
- package/dist/webvh/index.js +9 -22
- 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 -254
- 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
|
@@ -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
|
-
* `gen-<random>`
|
|
11
|
-
* genesis parameters, the pin-slot key for
|
|
10
|
+
* `gen-<random>` generation id convention, the typed auxiliary Space ensure,
|
|
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,20 +32,20 @@
|
|
|
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
|
|
41
41
|
* registry of generations anywhere.
|
|
42
42
|
*
|
|
43
|
-
* Generation identity is random, never a counter: a reused
|
|
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
|
-
* carrier survives GC deleting the old collection. The
|
|
46
|
-
* generation-identifying half of the
|
|
47
|
-
* (`<
|
|
48
|
-
* exactly one spelling of a generation's identity -- the one the
|
|
45
|
+
* carrier survives GC deleting the old collection. The generation id is also
|
|
46
|
+
* the generation-identifying half of the annex rung HKDF labels
|
|
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 annex
|
|
49
49
|
* DID string already embeds.
|
|
50
50
|
*/
|
|
51
51
|
import { createDID, deriveNextKeyHash, updateDID } from '@interop/did-method-webvh';
|
|
@@ -53,92 +53,94 @@ 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
|
|
81
|
-
* string ever published.
|
|
81
|
+
* Space's collection listing, and the generation id embeds in every annex
|
|
82
|
+
* DID string ever published.
|
|
82
83
|
*/
|
|
83
|
-
export const
|
|
84
|
+
export const GENERATION_ID_PREFIX = 'gen-';
|
|
84
85
|
/**
|
|
85
86
|
* The random suffix: 12 bytes, base64url-no-pad (16 characters), for 20
|
|
86
87
|
* characters total. Every character is inside the server's `[A-Za-z0-9._~-]+`
|
|
87
|
-
* id allowlist, so `encodeURIComponent` is the identity on the
|
|
88
|
-
* the DID path encoding round-trips it.
|
|
88
|
+
* id allowlist, so `encodeURIComponent` is the identity on the generation id
|
|
89
|
+
* and the DID path encoding round-trips it.
|
|
89
90
|
*/
|
|
90
|
-
const
|
|
91
|
+
const GENERATION_ID_SUFFIX_BYTES = 12;
|
|
91
92
|
/**
|
|
92
|
-
* The full
|
|
93
|
+
* The full generation id shape: the literal prefix plus 16 base64url
|
|
94
|
+
* characters.
|
|
93
95
|
*/
|
|
94
|
-
const
|
|
96
|
+
const GENERATION_ID_PATTERN = /^gen-[A-Za-z0-9_-]{16}$/;
|
|
95
97
|
/**
|
|
96
|
-
* Mints a fresh generation
|
|
98
|
+
* Mints a fresh generation id -- the generation collection's name, e.g.
|
|
97
99
|
* `gen-Ux3v0kQf9aPmB2hZ`. Random rather than a counter on purpose: never-reuse
|
|
98
100
|
* is structural (nothing durable survives GC to carry a counter), at the same
|
|
99
101
|
* probabilistic order as every other random-id convention in the system.
|
|
100
102
|
*
|
|
101
103
|
* @returns {string}
|
|
102
104
|
*/
|
|
103
|
-
export function
|
|
104
|
-
return (
|
|
105
|
-
base64urlnopad.encode(crypto.getRandomValues(new Uint8Array(
|
|
105
|
+
export function mintGenerationId() {
|
|
106
|
+
return (GENERATION_ID_PREFIX +
|
|
107
|
+
base64urlnopad.encode(crypto.getRandomValues(new Uint8Array(GENERATION_ID_SUFFIX_BYTES))));
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
108
|
-
* Refuses anything that is not a well-formed generation
|
|
109
|
-
*
|
|
110
|
+
* Refuses anything that is not a well-formed generation id. Run by every
|
|
111
|
+
* annex builder that takes a generation id, so a malformed one is refused
|
|
110
112
|
* before it can reach a DID string, an HKDF label, or a collection id.
|
|
111
113
|
*
|
|
112
|
-
* @param
|
|
114
|
+
* @param generationId {string}
|
|
113
115
|
*/
|
|
114
|
-
export function
|
|
115
|
-
if (!
|
|
116
|
-
throw new Error(`Not a generation
|
|
116
|
+
export function assertGenerationId(generationId) {
|
|
117
|
+
if (!GENERATION_ID_PATTERN.test(generationId)) {
|
|
118
|
+
throw new Error(`Not a generation id: "${generationId}" (expected "gen-" plus 16 ` +
|
|
117
119
|
'base64url characters).');
|
|
118
120
|
}
|
|
119
121
|
}
|
|
120
122
|
/**
|
|
121
|
-
* The pin-slot key for one
|
|
122
|
-
* pin-slot key, keyed by the auxiliary Space id and the generation
|
|
123
|
+
* The pin-slot key for one annex generation's log -- host-free like every
|
|
124
|
+
* pin-slot key, keyed by the auxiliary Space id and the generation id.
|
|
123
125
|
* A transient session keeps this slot in an in-memory pin store (a durable
|
|
124
126
|
* pin is the wrong lifetime for a disposable log, and a transient session
|
|
125
127
|
* must not durably create the pin store on a read); a durable client's store
|
|
126
|
-
* clears
|
|
128
|
+
* clears annex slots when the generation is collected.
|
|
127
129
|
*
|
|
128
130
|
* @param options {object}
|
|
129
|
-
* @param options.spaceId {string} the auxiliary
|
|
130
|
-
* @param options.
|
|
131
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
132
|
+
* @param options.generationId {string} the generation collection's name
|
|
131
133
|
* @returns {string}
|
|
132
134
|
*/
|
|
133
|
-
export function
|
|
135
|
+
export function clientAnnexLogPinId({ spaceId, generationId }) {
|
|
134
136
|
return resourceLogPinId({
|
|
135
137
|
spaceId,
|
|
136
|
-
collectionId:
|
|
138
|
+
collectionId: generationId,
|
|
137
139
|
resourceId: DID_LOG_RESOURCE
|
|
138
140
|
});
|
|
139
141
|
}
|
|
140
142
|
/**
|
|
141
|
-
* The WAS-backed store
|
|
143
|
+
* The WAS-backed store an annex generation's ceremonies read and publish
|
|
142
144
|
* through with controller-tier signing (an enrolled client). A transient
|
|
143
145
|
* session writes through the delegated store instead
|
|
144
146
|
* (`delegatedWebvhLogStore`, invoking the credential's sibling delegation);
|
|
@@ -146,16 +148,24 @@ export function companionLogPinId({ spaceId, segment }) {
|
|
|
146
148
|
*
|
|
147
149
|
* @param options {object}
|
|
148
150
|
* @param options.was {WasClient}
|
|
149
|
-
* @param options.spaceId {string} the auxiliary
|
|
150
|
-
* @param options.
|
|
151
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
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
|
|
151
156
|
* @returns {WebvhLogResourceStore}
|
|
152
157
|
*/
|
|
153
|
-
export function
|
|
154
|
-
|
|
155
|
-
return wasWebvhLogStore({
|
|
158
|
+
export function clientAnnexLogStore({ was, spaceId, generationId, capability }) {
|
|
159
|
+
assertGenerationId(generationId);
|
|
160
|
+
return wasWebvhLogStore({
|
|
161
|
+
was,
|
|
162
|
+
spaceId,
|
|
163
|
+
collectionId: generationId,
|
|
164
|
+
...(capability !== undefined ? { capability } : {})
|
|
165
|
+
});
|
|
156
166
|
}
|
|
157
167
|
/**
|
|
158
|
-
* The DID core context -- the
|
|
168
|
+
* The DID core context -- the annex genesis document's whole `@context`.
|
|
159
169
|
* The document carries no verification methods and no service entries at
|
|
160
170
|
* genesis, so no other vocabulary is in scope; the entry that first publishes
|
|
161
171
|
* a typed member extends the context then (a did:webvh entry replaces the
|
|
@@ -163,23 +173,23 @@ export function companionLogStore({ was, spaceId, segment }) {
|
|
|
163
173
|
*/
|
|
164
174
|
const DID_CORE_CONTEXT = 'https://www.w3.org/ns/did/v1';
|
|
165
175
|
/**
|
|
166
|
-
* The Multikey context, appended to the
|
|
176
|
+
* The Multikey context, appended to the annex document's `@context` by
|
|
167
177
|
* the entry that first publishes a transient verification method (genesis
|
|
168
178
|
* carries the DID core context only, having no typed members to define).
|
|
169
179
|
*/
|
|
170
180
|
const MULTIKEY_CONTEXT_URL = 'https://w3id.org/security/multikey/v1';
|
|
171
181
|
/**
|
|
172
|
-
* Creates the one-entry
|
|
173
|
-
* 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
|
|
174
184
|
* hash commitments, no witnesses, portability off (the library's default,
|
|
175
185
|
* stated explicitly in the emitted entry), and a bare document -- id and the
|
|
176
186
|
* DID core context, nothing else.
|
|
177
187
|
*
|
|
178
188
|
* The caller supplies the update authority: the minting credential's
|
|
179
|
-
*
|
|
189
|
+
* annex rung-0 key as the sole `updateKeys` member, `nextKeyHashes` as
|
|
180
190
|
* every standing credential's rung-0 hash (restated explicitly on every later
|
|
181
191
|
* entry, never inherited), and rung 0's signer. The minting key's own
|
|
182
|
-
* carry-over hash MUST be among the commitments -- every
|
|
192
|
+
* carry-over hash MUST be among the commitments -- every annex entry
|
|
183
193
|
* re-states `updateKeys` containing the revealed rung-0 keys, and the
|
|
184
194
|
* resolver checks the re-statement against the previous entry's commitments
|
|
185
195
|
* -- so a `nextKeyHashes` that omits it is refused here rather than
|
|
@@ -187,20 +197,20 @@ const MULTIKEY_CONTEXT_URL = 'https://w3id.org/security/multikey/v1';
|
|
|
187
197
|
*
|
|
188
198
|
* @param options {object}
|
|
189
199
|
* @param options.wasServerUrl {string}
|
|
190
|
-
* @param options.spaceId {string} the auxiliary
|
|
191
|
-
* @param options.
|
|
200
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
201
|
+
* @param options.generationId {string} the generation collection's name
|
|
192
202
|
* @param options.updateKeyPublicKeyMultibase {string} the minting
|
|
193
|
-
* credential's
|
|
203
|
+
* credential's annex rung-0 key
|
|
194
204
|
* @param options.nextKeyHashes {string[]} every standing credential's
|
|
195
205
|
* rung-0 hash, the minting credential's included
|
|
196
206
|
* @param options.signer {Signer} the minting credential's rung-0 signer
|
|
197
207
|
* @returns {Promise<{ log: DIDLog; did: string; doc: DIDDoc }>}
|
|
198
208
|
*/
|
|
199
|
-
export async function
|
|
200
|
-
|
|
209
|
+
export async function createClientAnnexLog({ wasServerUrl, spaceId, generationId, updateKeyPublicKeyMultibase, nextKeyHashes, signer }) {
|
|
210
|
+
assertGenerationId(generationId);
|
|
201
211
|
const carryOverHash = await deriveNextKeyHash(updateKeyPublicKeyMultibase);
|
|
202
212
|
if (!nextKeyHashes.includes(carryOverHash)) {
|
|
203
|
-
throw new Error('
|
|
213
|
+
throw new Error('client annex genesis: `nextKeyHashes` must include the minting ' +
|
|
204
214
|
"credential's own rung-0 hash (the carry-over commitment), or no " +
|
|
205
215
|
'later entry could ever re-state the revealed key.');
|
|
206
216
|
}
|
|
@@ -208,24 +218,24 @@ export async function createCompanionLog({ wasServerUrl, spaceId, segment, updat
|
|
|
208
218
|
const controllerTemplate = didWebvhControllerTemplate({
|
|
209
219
|
wasServerUrl,
|
|
210
220
|
spaceId,
|
|
211
|
-
collectionId:
|
|
221
|
+
collectionId: generationId
|
|
212
222
|
});
|
|
213
223
|
const result = await createDID({
|
|
214
224
|
address: host,
|
|
215
|
-
paths: ['space', spaceId,
|
|
225
|
+
paths: ['space', spaceId, generationId],
|
|
216
226
|
signer,
|
|
217
227
|
updateKeys: [updateKeyPublicKeyMultibase],
|
|
218
228
|
nextKeyHashes,
|
|
219
229
|
didDocument: { '@context': [DID_CORE_CONTEXT], id: controllerTemplate }
|
|
220
230
|
});
|
|
221
231
|
if (!result.did || !result.doc) {
|
|
222
|
-
throw new Error('
|
|
232
|
+
throw new Error('client annex genesis: createDID returned no DID document.');
|
|
223
233
|
}
|
|
224
234
|
return { log: result.log, did: result.did, doc: result.doc };
|
|
225
235
|
}
|
|
226
236
|
/**
|
|
227
|
-
* Ensures the auxiliary
|
|
228
|
-
* 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
|
|
229
239
|
* absent, verified when present. The `type` array must ride the create -- the
|
|
230
240
|
* server accepts it at creation only and treats it as immutable afterwards --
|
|
231
241
|
* which is also why an existing Space at this id that is NOT typed as the
|
|
@@ -241,19 +251,19 @@ export async function createCompanionLog({ wasServerUrl, spaceId, segment, updat
|
|
|
241
251
|
*
|
|
242
252
|
* @param options {object}
|
|
243
253
|
* @param options.was {WasClient}
|
|
244
|
-
* @param options.spaceId {string} the auxiliary
|
|
254
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
245
255
|
* @param options.controller {string} the Space controller (the account
|
|
246
|
-
* 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,
|
|
247
257
|
* promoted the same way the account Space's controller is)
|
|
248
258
|
* @returns {Promise<void>}
|
|
249
259
|
*/
|
|
250
|
-
export async function
|
|
260
|
+
export async function ensureClientAnnexSpace({ was, spaceId, controller }) {
|
|
251
261
|
const space = was.space(spaceId);
|
|
252
262
|
const current = await space.describe();
|
|
253
263
|
if (current === null) {
|
|
254
264
|
await space.configure({
|
|
255
265
|
controller,
|
|
256
|
-
type:
|
|
266
|
+
type: CLIENT_ANNEX_SPACE_TYPE,
|
|
257
267
|
force: true
|
|
258
268
|
});
|
|
259
269
|
return;
|
|
@@ -261,18 +271,19 @@ export async function ensureCompanionSpace({ was, spaceId, controller }) {
|
|
|
261
271
|
if (!current.type?.includes(DELEGATED_CLIENTS_SPACE_TYPE)) {
|
|
262
272
|
throw new Error(`The Space "${spaceId}" exists but is not typed as the ` +
|
|
263
273
|
'delegated-clients auxiliary Space; its type is immutable, so it ' +
|
|
264
|
-
'cannot hold
|
|
274
|
+
'cannot hold client-annex generations.');
|
|
265
275
|
}
|
|
266
276
|
}
|
|
267
277
|
/**
|
|
268
|
-
* Mints a fresh
|
|
269
|
-
* the typed auxiliary Space, mints a fresh random
|
|
278
|
+
* Mints a fresh annex generation with controller-tier signing: ensures
|
|
279
|
+
* the typed auxiliary Space, mints a fresh random generation id, creates the
|
|
270
280
|
* generation collection, and publishes the genesis `did.jsonl` as a
|
|
271
281
|
* create-if-absent -- the same conditional-publish discipline as every log
|
|
272
|
-
* write, though a fresh random
|
|
282
|
+
* write, though a fresh random generation id makes a create collision
|
|
283
|
+
* negligible.
|
|
273
284
|
*
|
|
274
285
|
* The account document's `#DelegatedClients` service entry is deliberately
|
|
275
|
-
* NOT written here: the
|
|
286
|
+
* NOT written here: the annex log publishes first, and the caller
|
|
276
287
|
* re-points the account document at the returned DID afterwards. A run torn
|
|
277
288
|
* between the two leaves an unpointed generation -- authorization-inert (no
|
|
278
289
|
* delegation names it), collected by the standing `gen-` prefix orphan
|
|
@@ -285,27 +296,27 @@ export async function ensureCompanionSpace({ was, spaceId, controller }) {
|
|
|
285
296
|
*
|
|
286
297
|
* @param options {object}
|
|
287
298
|
* @param options.was {WasClient} the storage client, signing as an enrolled
|
|
288
|
-
* client (or the bootstrap controller on a
|
|
299
|
+
* client (or the bootstrap controller on a ladder-anchored signup)
|
|
289
300
|
* @param options.wasServerUrl {string}
|
|
290
|
-
* @param options.spaceId {string} the auxiliary
|
|
301
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
291
302
|
* @param options.controller {string} the auxiliary Space's controller, used
|
|
292
303
|
* only when the Space does not exist yet
|
|
293
304
|
* @param options.updateKeyPublicKeyMultibase {string} the minting
|
|
294
|
-
* credential's
|
|
305
|
+
* credential's annex rung-0 key
|
|
295
306
|
* @param options.nextKeyHashes {string[]} every standing credential's
|
|
296
307
|
* rung-0 hash, the minting credential's included
|
|
297
308
|
* @param options.signer {Signer} the minting credential's rung-0 signer
|
|
298
|
-
* @returns {Promise<{ did: string;
|
|
309
|
+
* @returns {Promise<{ did: string; generationId: string; log: DIDLog;
|
|
299
310
|
* doc: DIDDoc }>}
|
|
300
311
|
*/
|
|
301
|
-
export async function
|
|
302
|
-
await
|
|
303
|
-
const
|
|
304
|
-
return
|
|
312
|
+
export async function mintClientAnnexGeneration({ was, wasServerUrl, spaceId, controller, updateKeyPublicKeyMultibase, nextKeyHashes, signer }) {
|
|
313
|
+
await ensureClientAnnexSpace({ was, spaceId, controller });
|
|
314
|
+
const generationId = mintGenerationId();
|
|
315
|
+
return publishClientAnnexGenesis({
|
|
305
316
|
was,
|
|
306
317
|
wasServerUrl,
|
|
307
318
|
spaceId,
|
|
308
|
-
|
|
319
|
+
generationId,
|
|
309
320
|
updateKeyPublicKeyMultibase,
|
|
310
321
|
nextKeyHashes,
|
|
311
322
|
signer
|
|
@@ -319,87 +330,106 @@ export async function mintCompanionGeneration({ was, wasServerUrl, spaceId, cont
|
|
|
319
330
|
* @param options {object}
|
|
320
331
|
* @param options.was {WasClient}
|
|
321
332
|
* @param options.wasServerUrl {string}
|
|
322
|
-
* @param options.spaceId {string} the auxiliary
|
|
323
|
-
* @param options.
|
|
333
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
334
|
+
* @param options.generationId {string} the freshly minted generation id
|
|
324
335
|
* @param options.updateKeyPublicKeyMultibase {string}
|
|
325
336
|
* @param options.nextKeyHashes {string[]}
|
|
326
337
|
* @param options.signer {Signer}
|
|
327
|
-
* @
|
|
338
|
+
* @param [options.capability] {IZcap} an invocation capability the
|
|
339
|
+
* collection create and the genesis publish ride (a delegated minter)
|
|
340
|
+
* @returns {Promise<{ did: string; generationId: string; log: DIDLog;
|
|
328
341
|
* doc: DIDDoc }>}
|
|
329
342
|
*/
|
|
330
|
-
async function
|
|
343
|
+
async function publishClientAnnexGenesis({ was, wasServerUrl, spaceId, generationId, updateKeyPublicKeyMultibase, nextKeyHashes, signer, capability }) {
|
|
331
344
|
// The generation collection must exist before its first resource PUT; a
|
|
332
|
-
// fresh random
|
|
333
|
-
//
|
|
345
|
+
// fresh random generation id means this is always a create. Plaintext on
|
|
346
|
+
// purpose:
|
|
347
|
+
// the server resolves the annex DID out of its own storage, and the
|
|
334
348
|
// collection is capability-gated rather than encrypted.
|
|
335
349
|
await was
|
|
336
|
-
.space(spaceId)
|
|
337
|
-
.collection(
|
|
338
|
-
.configure({ name:
|
|
339
|
-
const created = await
|
|
350
|
+
.space(spaceId, capability !== undefined ? { capability } : {})
|
|
351
|
+
.collection(generationId, { encryption: 'plaintext' })
|
|
352
|
+
.configure({ name: generationId, force: true });
|
|
353
|
+
const created = await createClientAnnexLog({
|
|
340
354
|
wasServerUrl,
|
|
341
355
|
spaceId,
|
|
342
|
-
|
|
356
|
+
generationId,
|
|
343
357
|
updateKeyPublicKeyMultibase,
|
|
344
358
|
nextKeyHashes,
|
|
345
359
|
signer
|
|
346
360
|
});
|
|
347
361
|
await putLogResource({
|
|
348
|
-
store:
|
|
362
|
+
store: clientAnnexLogStore({
|
|
363
|
+
was,
|
|
364
|
+
spaceId,
|
|
365
|
+
generationId,
|
|
366
|
+
...(capability !== undefined ? { capability } : {})
|
|
367
|
+
}),
|
|
349
368
|
log: created.log,
|
|
350
369
|
ifNoneMatch: true
|
|
351
370
|
});
|
|
352
|
-
return { ...created,
|
|
371
|
+
return { ...created, generationId };
|
|
353
372
|
}
|
|
354
373
|
/**
|
|
355
|
-
* Mints a fresh
|
|
356
|
-
*
|
|
357
|
-
* login, or a test harness standing in for one). The
|
|
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
|
|
376
|
+
* login, or a test harness standing in for one). The generation id must exist
|
|
358
377
|
* before the update authority can: the rung-0 key derives from the ladder
|
|
359
|
-
* seed AND the
|
|
378
|
+
* seed AND the generation id (`clientAnnexRung`), so this helper mints the
|
|
379
|
+
* generation id
|
|
360
380
|
* first, derives the rung, and states its own carry-over hash in
|
|
361
|
-
* `nextKeyHashes` -- {@link
|
|
381
|
+
* `nextKeyHashes` -- {@link mintClientAnnexGeneration}'s caller-supplied-key
|
|
362
382
|
* shape cannot express that ordering. Everything else matches it: the typed
|
|
363
383
|
* Space ensure, the collection create, the create-if-absent genesis publish,
|
|
364
384
|
* and the pointer deliberately left to the caller.
|
|
365
385
|
*
|
|
366
386
|
* @param options {object}
|
|
367
387
|
* @param options.was {WasClient} the storage client, signing as an enrolled
|
|
368
|
-
* client (or the bootstrap controller on a
|
|
388
|
+
* client (or the bootstrap controller on a ladder-anchored signup)
|
|
369
389
|
* @param options.wasServerUrl {string}
|
|
370
|
-
* @param options.spaceId {string} the auxiliary
|
|
390
|
+
* @param options.spaceId {string} the auxiliary annex Space's id
|
|
371
391
|
* @param options.controller {string} the auxiliary Space's controller, used
|
|
372
392
|
* only when the Space does not exist yet
|
|
373
393
|
* @param options.ladderSeed {Uint8Array} the minting credential's ladder
|
|
374
394
|
* seed, from its unlock record
|
|
375
395
|
* @param [options.extraNextKeyHashes] {string[]} the OTHER standing
|
|
376
|
-
* credentials' rung-0 hashes for this
|
|
396
|
+
* credentials' rung-0 hashes for this generation id, when the account has
|
|
397
|
+
* more
|
|
377
398
|
* than one; the minting credential's own carry-over hash is always included
|
|
378
|
-
* @
|
|
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
|
|
405
|
+
* @returns {Promise<{ did: string; generationId: string; log: DIDLog;
|
|
379
406
|
* doc: DIDDoc }>}
|
|
380
407
|
*/
|
|
381
|
-
export async function
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
408
|
+
export async function mintCredentialClientAnnexGeneration({ was, wasServerUrl, spaceId, controller, ladderSeed, extraNextKeyHashes = [], capability }) {
|
|
409
|
+
if (capability === undefined) {
|
|
410
|
+
await ensureClientAnnexSpace({ was, spaceId, controller });
|
|
411
|
+
}
|
|
412
|
+
const generationId = mintGenerationId();
|
|
413
|
+
const rung = await clientAnnexRung({ ladderSeed, generationId });
|
|
414
|
+
return publishClientAnnexGenesis({
|
|
386
415
|
was,
|
|
387
416
|
wasServerUrl,
|
|
388
417
|
spaceId,
|
|
389
|
-
|
|
418
|
+
generationId,
|
|
390
419
|
updateKeyPublicKeyMultibase: rung.keyMultibase,
|
|
391
420
|
nextKeyHashes: [
|
|
392
421
|
await deriveNextKeyHash(rung.keyMultibase),
|
|
393
422
|
...extraNextKeyHashes
|
|
394
423
|
],
|
|
395
|
-
signer: await updateKeySigner({ seed: rung.seed })
|
|
424
|
+
signer: await updateKeySigner({ seed: rung.seed }),
|
|
425
|
+
...(capability !== undefined ? { capability } : {})
|
|
396
426
|
});
|
|
397
427
|
}
|
|
398
428
|
/**
|
|
399
429
|
* The type IRI of the account document's delegated-clients service entry --
|
|
400
|
-
* the pointer at the current
|
|
430
|
+
* the pointer at the current annex generation's DID. Wire-level and
|
|
401
431
|
* permanent: readers (this module's {@link delegatedClientsPointer}, the
|
|
402
|
-
* server's
|
|
432
|
+
* server's annex-chain inspector clause) dispatch on this IRI, never on
|
|
403
433
|
* the entry's fragment id, which is non-semantic by convention.
|
|
404
434
|
*/
|
|
405
435
|
export const DELEGATED_CLIENTS_SERVICE_TYPE = 'https://w3id.org/byoe#DelegatedClients';
|
|
@@ -412,25 +442,25 @@ export const DELEGATED_CLIENTS_SERVICE_TYPE = 'https://w3id.org/byoe#DelegatedCl
|
|
|
412
442
|
const DELEGATED_CLIENTS_SERVICE_FRAGMENT = 'delegated-clients';
|
|
413
443
|
/**
|
|
414
444
|
* Builds a fresh delegated-clients service entry for the account document.
|
|
415
|
-
* The `serviceEndpoint` is the
|
|
445
|
+
* The `serviceEndpoint` is the annex DID STRING, deliberately not a URL:
|
|
416
446
|
* the DID is self-certifying and host-independent, and the account pointer
|
|
417
447
|
* already carries the host.
|
|
418
448
|
*
|
|
419
449
|
* @param options {object}
|
|
420
450
|
* @param options.accountDid {string} the account did:webvh
|
|
421
|
-
* @param options.
|
|
451
|
+
* @param options.clientAnnexDid {string} the current generation's annex
|
|
422
452
|
* DID
|
|
423
453
|
* @returns {ServiceEndpoint}
|
|
424
454
|
*/
|
|
425
|
-
export function delegatedClientsServiceEntry({ accountDid,
|
|
455
|
+
export function delegatedClientsServiceEntry({ accountDid, clientAnnexDid }) {
|
|
426
456
|
return {
|
|
427
457
|
id: `${accountDid}#${DELEGATED_CLIENTS_SERVICE_FRAGMENT}`,
|
|
428
458
|
type: DELEGATED_CLIENTS_SERVICE_TYPE,
|
|
429
|
-
serviceEndpoint:
|
|
459
|
+
serviceEndpoint: clientAnnexDid
|
|
430
460
|
};
|
|
431
461
|
}
|
|
432
462
|
/**
|
|
433
|
-
* The
|
|
463
|
+
* The annex DID the account document currently points at: the
|
|
434
464
|
* `serviceEndpoint` of the service entry whose `type` names (or includes)
|
|
435
465
|
* {@link DELEGATED_CLIENTS_SERVICE_TYPE}. Only a bare DID-string endpoint
|
|
436
466
|
* counts -- the same predicate the server's inspector clause evaluates, so
|
|
@@ -450,9 +480,42 @@ export function delegatedClientsPointer({ doc }) {
|
|
|
450
480
|
}
|
|
451
481
|
return undefined;
|
|
452
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
|
+
}
|
|
453
516
|
/**
|
|
454
517
|
* The unlock-record sibling delegation's `allowedAction` set: GET beside PUT,
|
|
455
|
-
* so an enrolling transient client can read the
|
|
518
|
+
* so an enrolling transient client can read the annex head it appends to.
|
|
456
519
|
* Wire-level and permanent (wallet-core decision 0005): the server's
|
|
457
520
|
* inspector clause admits a delegated-clients delegation with `allowedAction`
|
|
458
521
|
* a subset of exactly this pair.
|
|
@@ -466,16 +529,17 @@ export const DELEGATED_CLIENTS_DELEGATION_ACTIONS = ['GET', 'PUT'];
|
|
|
466
529
|
*/
|
|
467
530
|
export const DELEGATED_CLIENTS_DELEGATION_TTL_MS = STANDING_ZCAP_TTL_MS;
|
|
468
531
|
/**
|
|
469
|
-
* Mints one delegated-clients (
|
|
532
|
+
* Mints one delegated-clients (annex Space) delegation: the pre-minted
|
|
470
533
|
* zcap sealed into a standing credential's unlock record beside the account
|
|
471
|
-
* bridge, which is what lets a transient login reach the
|
|
534
|
+
* bridge, which is what lets a transient login reach the annex log with
|
|
472
535
|
* nothing but the credential. The shape is a permanent wire artifact
|
|
473
536
|
* (wallet-core decision 0005):
|
|
474
537
|
*
|
|
475
|
-
* - `invocationTarget` is the AUXILIARY
|
|
538
|
+
* - `invocationTarget` is the AUXILIARY annex Space's items subtree --
|
|
476
539
|
* the Space URL with a trailing slash, built with was-client's paths
|
|
477
540
|
* helpers so the bytes match the server's target check on a sub-path
|
|
478
|
-
* deployment. Generation coverage comes from
|
|
541
|
+
* deployment. Generation coverage comes from generation-id-bounded
|
|
542
|
+
* attenuation
|
|
479
543
|
* over the flat `gen-` collection names, so no GC cycle rewrites the
|
|
480
544
|
* record or the registry.
|
|
481
545
|
* - `controller` is the credential-derived signing DID (the same grantee
|
|
@@ -489,22 +553,22 @@ export const DELEGATED_CLIENTS_DELEGATION_TTL_MS = STANDING_ZCAP_TTL_MS;
|
|
|
489
553
|
* enrolled client's promoted signer, or the account ladder VM)
|
|
490
554
|
* @param options.wasServerUrl {string} the auxiliary Space's storage
|
|
491
555
|
* server (the account pointer's host)
|
|
492
|
-
* @param options.
|
|
556
|
+
* @param options.clientAnnexSpaceId {string} the auxiliary annex
|
|
493
557
|
* Space's id
|
|
494
558
|
* @param options.controller {string} the credential-derived signing DID
|
|
495
559
|
* @param [options.now] {number} epoch milliseconds, for tests
|
|
496
560
|
* @returns {Promise<IZcap>}
|
|
497
561
|
*/
|
|
498
|
-
export async function mintDelegatedClientsDelegation({ zcapClient, wasServerUrl,
|
|
562
|
+
export async function mintDelegatedClientsDelegation({ zcapClient, wasServerUrl, clientAnnexSpaceId, controller, now = Date.now() }) {
|
|
499
563
|
const spaceUrl = toUrl({
|
|
500
564
|
serverUrl: wasServerUrl,
|
|
501
|
-
path: spacePath(
|
|
565
|
+
path: spacePath(clientAnnexSpaceId)
|
|
502
566
|
});
|
|
503
567
|
return (await zcapClient.delegate({
|
|
504
568
|
capability: rootCapabilityId(spaceUrl),
|
|
505
569
|
invocationTarget: toUrl({
|
|
506
570
|
serverUrl: wasServerUrl,
|
|
507
|
-
path: spaceItems(
|
|
571
|
+
path: spaceItems(clientAnnexSpaceId)
|
|
508
572
|
}),
|
|
509
573
|
controller,
|
|
510
574
|
allowedActions: [...DELEGATED_CLIENTS_DELEGATION_ACTIONS],
|
|
@@ -512,9 +576,51 @@ export async function mintDelegatedClientsDelegation({ zcapClient, wasServerUrl,
|
|
|
512
576
|
}));
|
|
513
577
|
}
|
|
514
578
|
/**
|
|
515
|
-
*
|
|
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,
|
|
516
622
|
* read out of its `invocationTarget` (the items-subtree URL,
|
|
517
|
-
* `.../space/<
|
|
623
|
+
* `.../space/<clientAnnexSpaceId>/`). The id has no other home -- a transient
|
|
518
624
|
* login learns the Space from the delegation it unwraps, and a refresh pass
|
|
519
625
|
* that holds the old delegation rebuilds the target from it -- so this parse
|
|
520
626
|
* is the one reader. Returns `undefined` on anything that is not an
|
|
@@ -548,7 +654,7 @@ export function delegatedClientsDelegationSpaceId({ delegation }) {
|
|
|
548
654
|
return spaceId ? decodeURIComponent(spaceId) : undefined;
|
|
549
655
|
}
|
|
550
656
|
/**
|
|
551
|
-
* The type IRI of the
|
|
657
|
+
* The type IRI of the annex document's generation-delegation service
|
|
552
658
|
* entry -- the generation's standing Space-scoped zcap, embedded where an
|
|
553
659
|
* enrolling transient client can reach it before it holds any other
|
|
554
660
|
* authority. Wire-level and permanent: readers (this module's
|
|
@@ -601,8 +707,8 @@ export const GENERATION_DELEGATION_TTL_MS = STANDING_ZCAP_TTL_MS;
|
|
|
601
707
|
* bytes match the server's target check on a sub-path deployment. The
|
|
602
708
|
* bare Space URL sits outside the capability bytes (see
|
|
603
709
|
* {@link GENERATION_DELEGATION_ACTIONS} for what that excludes).
|
|
604
|
-
* - `controller` is the bare
|
|
605
|
-
* `<
|
|
710
|
+
* - `controller` is the bare annex DID string. Transient keys invoke as
|
|
711
|
+
* `<clientAnnexDid>#<vm>`, and the server's inspector clause compares this
|
|
606
712
|
* string against the account document's delegated-clients pointer.
|
|
607
713
|
* - The chain is rooted directly in the account Space's root zcap, so an
|
|
608
714
|
* App Connect grant delegated under it forms the depth-3 chain
|
|
@@ -618,12 +724,12 @@ export const GENERATION_DELEGATION_TTL_MS = STANDING_ZCAP_TTL_MS;
|
|
|
618
724
|
* or a durable client's promoted signer)
|
|
619
725
|
* @param options.wasServerUrl {string} the ACCOUNT Space's storage server
|
|
620
726
|
* @param options.spaceId {string} the ACCOUNT Space's id
|
|
621
|
-
* @param options.
|
|
727
|
+
* @param options.clientAnnexDid {string} the generation's annex DID
|
|
622
728
|
* @param [options.now] {number} epoch milliseconds, for tests
|
|
623
729
|
* @returns {Promise<IZcap>}
|
|
624
730
|
*/
|
|
625
|
-
export async function mintGenerationDelegation({ zcapClient, wasServerUrl, spaceId,
|
|
626
|
-
|
|
731
|
+
export async function mintGenerationDelegation({ zcapClient, wasServerUrl, spaceId, clientAnnexDid, now = Date.now() }) {
|
|
732
|
+
clientAnnexDidParts({ did: clientAnnexDid });
|
|
627
733
|
const spaceUrl = toUrl({ serverUrl: wasServerUrl, path: spacePath(spaceId) });
|
|
628
734
|
return (await zcapClient.delegate({
|
|
629
735
|
capability: rootCapabilityId(spaceUrl),
|
|
@@ -631,7 +737,7 @@ export async function mintGenerationDelegation({ zcapClient, wasServerUrl, space
|
|
|
631
737
|
serverUrl: wasServerUrl,
|
|
632
738
|
path: spaceItems(spaceId)
|
|
633
739
|
}),
|
|
634
|
-
controller:
|
|
740
|
+
controller: clientAnnexDid,
|
|
635
741
|
allowedActions: [...GENERATION_DELEGATION_ACTIONS],
|
|
636
742
|
expires: new Date(now + GENERATION_DELEGATION_TTL_MS)
|
|
637
743
|
}));
|
|
@@ -661,33 +767,33 @@ export function clampGrantExpires({ ttlMs, delegation, now = Date.now() }) {
|
|
|
661
767
|
return new Date(Math.min(now + ttlMs, parentExpires));
|
|
662
768
|
}
|
|
663
769
|
/**
|
|
664
|
-
* Builds a fresh generation-delegation service entry for
|
|
770
|
+
* Builds a fresh generation-delegation service entry for an annex
|
|
665
771
|
* document. The `serviceEndpoint` is the full delegated-zcap JSON as a
|
|
666
772
|
* single map, byte-identical to what `zcapClient.delegate` produced -- the
|
|
667
|
-
*
|
|
773
|
+
* annex entry proof (JCS canonicalization) then covers it byte for byte,
|
|
668
774
|
* so host tampering with the stored delegation is client-visible.
|
|
669
775
|
*
|
|
670
776
|
* @param options {object}
|
|
671
|
-
* @param options.
|
|
777
|
+
* @param options.clientAnnexDid {string} the generation's annex DID
|
|
672
778
|
* @param options.delegation {IZcap} the minted generation delegation
|
|
673
779
|
* @returns {ServiceEndpoint}
|
|
674
780
|
*/
|
|
675
|
-
export function generationDelegationServiceEntry({
|
|
781
|
+
export function generationDelegationServiceEntry({ clientAnnexDid, delegation }) {
|
|
676
782
|
return {
|
|
677
|
-
id: `${
|
|
783
|
+
id: `${clientAnnexDid}#${GENERATION_DELEGATION_SERVICE_FRAGMENT}`,
|
|
678
784
|
type: GENERATION_DELEGATION_SERVICE_TYPE,
|
|
679
785
|
serviceEndpoint: delegation
|
|
680
786
|
};
|
|
681
787
|
}
|
|
682
788
|
/**
|
|
683
|
-
* The generation delegation
|
|
789
|
+
* The generation delegation an annex document carries: the
|
|
684
790
|
* `serviceEndpoint` map of the service entry whose `type` names (or
|
|
685
791
|
* includes) {@link GENERATION_DELEGATION_SERVICE_TYPE}. Only a map-form
|
|
686
792
|
* endpoint counts (the delegation is embedded as the zcap JSON itself,
|
|
687
793
|
* never as a URL or an encoded string).
|
|
688
794
|
*
|
|
689
795
|
* @param options {object}
|
|
690
|
-
* @param options.doc {DIDDoc} the resolved (and verified)
|
|
796
|
+
* @param options.doc {DIDDoc} the resolved (and verified) annex
|
|
691
797
|
* document
|
|
692
798
|
* @returns {IZcap | undefined}
|
|
693
799
|
*/
|
|
@@ -706,18 +812,77 @@ export function embeddedGenerationDelegation({ doc }) {
|
|
|
706
812
|
return undefined;
|
|
707
813
|
}
|
|
708
814
|
/**
|
|
709
|
-
*
|
|
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
|
|
710
875
|
* installed: an existing entry's endpoint is replaced in place, its fragment
|
|
711
876
|
* id preserved verbatim (the id is non-semantic and stable); absent one, a
|
|
712
877
|
* fresh entry is appended. Every other service entry is preserved untouched.
|
|
713
878
|
*
|
|
714
879
|
* @param options {object}
|
|
715
|
-
* @param options.doc {DIDDoc} the
|
|
716
|
-
* @param options.
|
|
880
|
+
* @param options.doc {DIDDoc} the annex document as published
|
|
881
|
+
* @param options.clientAnnexDid {string}
|
|
717
882
|
* @param options.delegation {IZcap}
|
|
718
883
|
* @returns {ServiceEndpoint[]}
|
|
719
884
|
*/
|
|
720
|
-
function withGenerationDelegationEntry({ doc,
|
|
885
|
+
function withGenerationDelegationEntry({ doc, clientAnnexDid, delegation }) {
|
|
721
886
|
const existing = (doc.service ?? []);
|
|
722
887
|
const isDelegationEntry = (entry) => {
|
|
723
888
|
const types = Array.isArray(entry.type) ? entry.type : [entry.type];
|
|
@@ -732,65 +897,66 @@ function withGenerationDelegationEntry({ doc, companionDid, delegation }) {
|
|
|
732
897
|
: entry)
|
|
733
898
|
: [
|
|
734
899
|
...existing,
|
|
735
|
-
generationDelegationServiceEntry({
|
|
900
|
+
generationDelegationServiceEntry({ clientAnnexDid, delegation })
|
|
736
901
|
];
|
|
737
902
|
}
|
|
738
903
|
/**
|
|
739
|
-
* Parses the auxiliary Space id and generation
|
|
740
|
-
* string. Both are permanent substrings of every
|
|
741
|
-
* construction
|
|
742
|
-
*
|
|
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
|
|
907
|
+
* DID (`did:webvh:<scid>:<host>:...:space:<spaceId>:<generationId>`), and it
|
|
908
|
+
* is the generation-identifying half of the annex rung HKDF
|
|
743
909
|
* labels, so this parse is what lets an enrollee derive its writing key from
|
|
744
910
|
* the pointer alone -- no log read, no registry.
|
|
745
911
|
*
|
|
746
912
|
* @param options {object}
|
|
747
|
-
* @param options.did {string}
|
|
748
|
-
* @returns {{ spaceId: string,
|
|
913
|
+
* @param options.did {string} an annex did:webvh string
|
|
914
|
+
* @returns {{ spaceId: string, generationId: string }}
|
|
749
915
|
*/
|
|
750
|
-
export function
|
|
916
|
+
export function clientAnnexDidParts({ did }) {
|
|
751
917
|
const parts = did.split(':');
|
|
752
|
-
const
|
|
918
|
+
const generationId = parts[parts.length - 1];
|
|
753
919
|
const spaceId = parts[parts.length - 2];
|
|
754
920
|
if (parts.length < 7 ||
|
|
755
921
|
parts[0] !== 'did' ||
|
|
756
922
|
parts[1] !== 'webvh' ||
|
|
757
923
|
parts[parts.length - 3] !== 'space' ||
|
|
758
|
-
|
|
924
|
+
generationId === undefined ||
|
|
759
925
|
spaceId === undefined ||
|
|
760
926
|
spaceId.length === 0) {
|
|
761
|
-
throw new Error(`Not a
|
|
927
|
+
throw new Error(`Not a client annex did:webvh: "${did}".`);
|
|
762
928
|
}
|
|
763
|
-
|
|
764
|
-
return { spaceId,
|
|
929
|
+
assertGenerationId(generationId);
|
|
930
|
+
return { spaceId, generationId };
|
|
765
931
|
}
|
|
766
932
|
/**
|
|
767
|
-
* Thrown when the published
|
|
933
|
+
* Thrown when the published annex log commits neither the writing
|
|
768
934
|
* credential's rung-0 key nor its hash -- the mid-generation lockout: a
|
|
769
|
-
* credential bound after the generation's genesis cannot write the
|
|
935
|
+
* credential bound after the generation's genesis cannot write the annex
|
|
770
936
|
* until an existing writer commits its rung-0 hash or the next GC swap's
|
|
771
937
|
* genesis does. Typed so callers can map it to the fresh-generation path
|
|
772
938
|
* where one is licensed (the transient-recovery continuation) or to honest
|
|
773
939
|
* copy where none is.
|
|
774
940
|
*/
|
|
775
|
-
export class
|
|
941
|
+
export class ClientAnnexRungUncommittedError extends Error {
|
|
776
942
|
constructor(message) {
|
|
777
943
|
super(message);
|
|
778
|
-
this.name = '
|
|
944
|
+
this.name = 'ClientAnnexRungUncommittedError';
|
|
779
945
|
}
|
|
780
946
|
}
|
|
781
947
|
/**
|
|
782
|
-
* Reads and resolves the published
|
|
948
|
+
* Reads and resolves the published annex log through the narrow seam, or
|
|
783
949
|
* throws when the generation's `did.jsonl` is missing (an unpointed or
|
|
784
950
|
* deleted generation -- nothing to enroll into).
|
|
785
951
|
*
|
|
786
952
|
* @param options {object}
|
|
787
|
-
* @param options.store {
|
|
953
|
+
* @param options.store {ClientAnnexWriteStore}
|
|
788
954
|
* @param [options.expectedDid] {string}
|
|
789
955
|
* @param [options.pinStore] {ResourceLogPinStore}
|
|
790
956
|
* @param [options.logId] {string}
|
|
791
957
|
* @returns {Promise<PublishedWebvhLog>}
|
|
792
958
|
*/
|
|
793
|
-
async function
|
|
959
|
+
async function readClientAnnexLogOrThrow({ store, expectedDid, pinStore, logId }) {
|
|
794
960
|
// readPublishedLog only calls getIdResourceRaw, so the narrow seam is safe.
|
|
795
961
|
const published = await readPublishedLog({
|
|
796
962
|
idStore: store,
|
|
@@ -799,18 +965,19 @@ async function readCompanionLogOrThrow({ store, expectedDid, pinStore, logId })
|
|
|
799
965
|
...(logId !== undefined ? { logId } : {})
|
|
800
966
|
});
|
|
801
967
|
if (!published) {
|
|
802
|
-
throw new Error('
|
|
968
|
+
throw new Error('client annex: did.jsonl is missing; the generation was never minted or ' +
|
|
803
969
|
'has been collected.');
|
|
804
970
|
}
|
|
805
971
|
return published;
|
|
806
972
|
}
|
|
807
973
|
/**
|
|
808
974
|
* TRANSIENT ENROLLMENT: publishes one per-visit verification method into a
|
|
809
|
-
*
|
|
810
|
-
* credential's static rung 0 (derived from the ladder seed and the
|
|
811
|
-
*
|
|
975
|
+
* annex generation's log -- one atomic entry, signed by the writing
|
|
976
|
+
* credential's static rung 0 (derived from the ladder seed and the generation
|
|
977
|
+
* id;
|
|
978
|
+
* see `clientAnnexRung`). The entry:
|
|
812
979
|
*
|
|
813
|
-
* - 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
|
|
814
981
|
* write (later writes re-state it unchanged);
|
|
815
982
|
* - re-states `nextKeyHashes` verbatim -- every standing credential's rung-0
|
|
816
983
|
* hash, the writer's own carry-over hash included -- explicitly on the
|
|
@@ -819,7 +986,7 @@ async function readCompanionLogOrThrow({ store, expectedDid, pinStore, logId })
|
|
|
819
986
|
* relationship arrays stated explicitly (no `authentication`, no
|
|
820
987
|
* `assertionMethod`, no `keyAgreement` twin -- the DIDAuth path signs as
|
|
821
988
|
* the bare did:key, and the controller-marker convention does not arise in
|
|
822
|
-
* the
|
|
989
|
+
* the annex at all).
|
|
823
990
|
*
|
|
824
991
|
* The transient key set carries no update key, and nothing here touches the
|
|
825
992
|
* ACCOUNT log's `updateKeys` or `nextKeyHashes`. There is no two-entry
|
|
@@ -828,54 +995,54 @@ async function readCompanionLogOrThrow({ store, expectedDid, pinStore, logId })
|
|
|
828
995
|
* published document's own state -- a VM already present is a no-op.
|
|
829
996
|
*
|
|
830
997
|
* A writer whose rung-0 key is neither revealed nor committed is refused
|
|
831
|
-
* ({@link
|
|
998
|
+
* ({@link ClientAnnexRungUncommittedError}): annex entries verify against
|
|
832
999
|
* the log's own hash-commitment chain, so no admission rule can make an
|
|
833
1000
|
* uncommitted key verify mid-log.
|
|
834
1001
|
*
|
|
835
1002
|
* @param options {object}
|
|
836
|
-
* @param options.store {
|
|
1003
|
+
* @param options.store {ClientAnnexWriteStore} the generation's log store
|
|
837
1004
|
* (delegated through the credential's sibling delegation, or
|
|
838
1005
|
* controller-tier)
|
|
839
1006
|
* @param options.ladderSeed {Uint8Array} the credential's ladder seed, from
|
|
840
1007
|
* its unlock record
|
|
841
|
-
* @param options.
|
|
1008
|
+
* @param options.generationId {string} the generation collection's name
|
|
842
1009
|
* @param options.transientKeyMultibase {string} the visit's in-memory
|
|
843
1010
|
* Ed25519 signing key, public multibase
|
|
844
|
-
* @param [options.services] {ServiceEndpoint[]} the
|
|
1011
|
+
* @param [options.services] {ServiceEndpoint[]} the annex document's
|
|
845
1012
|
* full service-entry list, replacing the published one wholesale; omitted,
|
|
846
1013
|
* the prior entries are preserved verbatim (or extended by
|
|
847
1014
|
* `mintGenerationDelegation` below). Supplying both is refused in favor of
|
|
848
1015
|
* the explicit list
|
|
849
1016
|
* @param [options.mintGenerationDelegation] {Function}
|
|
850
|
-
* `({
|
|
1017
|
+
* `({ clientAnnexDid }) => Promise<IZcap>` -- mints the generation
|
|
851
1018
|
* delegation this entry installs when it publishes the generation's FIRST
|
|
852
1019
|
* transient verification method (and the document carries no delegation
|
|
853
1020
|
* entry yet). Never invoked otherwise: the delegation is installed with
|
|
854
1021
|
* the first transient VM or by the GC ceremony's own install stage, never
|
|
855
1022
|
* by genesis (a genesis-embedded signed zcap can never verify -- its
|
|
856
1023
|
* `controller` embeds the SCID the genesis hash derives from)
|
|
857
|
-
* @param [options.expectedDid] {string} the
|
|
1024
|
+
* @param [options.expectedDid] {string} the annex DID the log must
|
|
858
1025
|
* resolve to, from the account document's pointer
|
|
859
1026
|
* @param [options.pinStore] {ResourceLogPinStore} chain-head pins (a
|
|
860
1027
|
* transient session passes an in-memory store)
|
|
861
1028
|
* @param [options.logId] {string} the generation's pin-slot key, from
|
|
862
|
-
* {@link
|
|
1029
|
+
* {@link clientAnnexLogPinId}; required whenever a `pinStore` is supplied
|
|
863
1030
|
* @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
|
|
864
1031
|
*/
|
|
865
|
-
export async function
|
|
866
|
-
return withLogConflictRetry(() =>
|
|
1032
|
+
export async function enrollClientAnnexTransientClient(options) {
|
|
1033
|
+
return withLogConflictRetry(() => enrollClientAnnexTransientClientOnce(options));
|
|
867
1034
|
}
|
|
868
1035
|
/**
|
|
869
|
-
* One attempt of {@link
|
|
1036
|
+
* One attempt of {@link enrollClientAnnexTransientClient}, re-invoked by the
|
|
870
1037
|
* conflict retry (with the same signing key -- static rung 0 has no
|
|
871
1038
|
* advanced-rung retry shape).
|
|
872
1039
|
*
|
|
873
|
-
* @param options {object} see {@link
|
|
1040
|
+
* @param options {object} see {@link enrollClientAnnexTransientClient}
|
|
874
1041
|
* @returns {Promise<{ did: string, doc: DIDDoc, log: DIDLog }>}
|
|
875
1042
|
*/
|
|
876
|
-
async function
|
|
877
|
-
|
|
878
|
-
const published = await
|
|
1043
|
+
async function enrollClientAnnexTransientClientOnce({ store, ladderSeed, generationId, transientKeyMultibase, services, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId }) {
|
|
1044
|
+
assertGenerationId(generationId);
|
|
1045
|
+
const published = await readClientAnnexLogOrThrow({
|
|
879
1046
|
store,
|
|
880
1047
|
...(expectedDid !== undefined ? { expectedDid } : {}),
|
|
881
1048
|
...(pinStore !== undefined ? { pinStore } : {}),
|
|
@@ -890,13 +1057,13 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, segment,
|
|
|
890
1057
|
if (existingMethods.some(method => method.id === vmId)) {
|
|
891
1058
|
return { did, doc, log: published.log };
|
|
892
1059
|
}
|
|
893
|
-
const rung = await
|
|
1060
|
+
const rung = await clientAnnexRung({ ladderSeed, generationId });
|
|
894
1061
|
const rungHash = await deriveNextKeyHash(rung.keyMultibase);
|
|
895
1062
|
const revealed = published.updateKeys.includes(rung.keyMultibase);
|
|
896
1063
|
if (!revealed && !published.nextKeyHashes.includes(rungHash)) {
|
|
897
|
-
throw new
|
|
1064
|
+
throw new ClientAnnexRungUncommittedError("client annex: the log commits neither this credential's rung-0 key nor " +
|
|
898
1065
|
'its hash; a credential bound mid-generation cannot write the ' +
|
|
899
|
-
'
|
|
1066
|
+
'annex until a writer commits its hash or the next GC swap does.');
|
|
900
1067
|
}
|
|
901
1068
|
// A non-rotating entry re-states `updateKeys`, which the resolver checks
|
|
902
1069
|
// against the previous entry's commitments -- genesis enforces the
|
|
@@ -910,10 +1077,10 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, segment,
|
|
|
910
1077
|
mintDelegation !== undefined &&
|
|
911
1078
|
existingMethods.length === 0 &&
|
|
912
1079
|
embeddedGenerationDelegation({ doc }) === undefined) {
|
|
913
|
-
const delegation = await mintDelegation({
|
|
1080
|
+
const delegation = await mintDelegation({ clientAnnexDid: did });
|
|
914
1081
|
services = withGenerationDelegationEntry({
|
|
915
1082
|
doc,
|
|
916
|
-
|
|
1083
|
+
clientAnnexDid: did,
|
|
917
1084
|
delegation
|
|
918
1085
|
});
|
|
919
1086
|
}
|
|
@@ -947,17 +1114,17 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, segment,
|
|
|
947
1114
|
capabilityDelegation: relationIds(doc.capabilityDelegation),
|
|
948
1115
|
...(services !== undefined ? { services } : {})
|
|
949
1116
|
});
|
|
950
|
-
// The log only --
|
|
1117
|
+
// The log only -- an annex has no did:web projection -- conditional on
|
|
951
1118
|
// the read this entry was built on.
|
|
952
1119
|
await putLogResource({ store, log: updated.log, ifMatch: published.etag });
|
|
953
1120
|
return { did: updated.did, doc: updated.doc, log: updated.log };
|
|
954
1121
|
}
|
|
955
1122
|
/**
|
|
956
1123
|
* Points the account document's delegated-clients service entry at a
|
|
957
|
-
*
|
|
1124
|
+
* annex DID -- the first install after a generation's genesis, and the GC
|
|
958
1125
|
* swap's re-point alike. One ordinary document-update entry, signed by an
|
|
959
|
-
* enrolled durable client's active update key; the
|
|
960
|
-
* 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
|
|
961
1128
|
* unpointed, authorization-inert generation, never a dangling pointer.
|
|
962
1129
|
*
|
|
963
1130
|
* An existing delegated-clients entry is re-pointed in place, its fragment id
|
|
@@ -968,10 +1135,14 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, segment,
|
|
|
968
1135
|
* a no-op on the log (it still heals a lagging `did.json`).
|
|
969
1136
|
*
|
|
970
1137
|
* @param options {object}
|
|
971
|
-
* @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
|
|
972
1141
|
* @param options.updateKeys {ClientWebvhUpdateKeys} this durable client's
|
|
973
|
-
* update-key seeds
|
|
974
|
-
*
|
|
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
|
|
975
1146
|
* @param [options.expectedDid] {string} the account DID the log must
|
|
976
1147
|
* resolve to, from the account pointer
|
|
977
1148
|
* @param [options.pinStore] {ResourceLogPinStore} this client's chain-head
|
|
@@ -979,6 +1150,11 @@ async function enrollCompanionTransientClientOnce({ store, ladderSeed, segment,
|
|
|
979
1150
|
* @param [options.logId] {string} the account log's pin-slot key, from
|
|
980
1151
|
* `accountLogPinId({ spaceId })`; required whenever a `pinStore` is
|
|
981
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)
|
|
982
1158
|
* @returns {Promise<{ did: string, doc: DIDDoc }>}
|
|
983
1159
|
*/
|
|
984
1160
|
export async function setDelegatedClientsPointer(options) {
|
|
@@ -991,9 +1167,9 @@ export async function setDelegatedClientsPointer(options) {
|
|
|
991
1167
|
* @param options {object} see {@link setDelegatedClientsPointer}
|
|
992
1168
|
* @returns {Promise<{ did: string, doc: DIDDoc }>}
|
|
993
1169
|
*/
|
|
994
|
-
async function setDelegatedClientsPointerOnce({ idStore, updateKeys,
|
|
1170
|
+
async function setDelegatedClientsPointerOnce({ idStore, updateKeys, clientAnnexDid, expectedDid, pinStore, logId, logOnly = false }) {
|
|
995
1171
|
// Refuses a malformed target before anything is read or written.
|
|
996
|
-
|
|
1172
|
+
clientAnnexDidParts({ did: clientAnnexDid });
|
|
997
1173
|
const published = await readPublishedLog({
|
|
998
1174
|
idStore,
|
|
999
1175
|
...(expectedDid !== undefined ? { expectedDid } : {}),
|
|
@@ -1001,11 +1177,13 @@ async function setDelegatedClientsPointerOnce({ idStore, updateKeys, companionDi
|
|
|
1001
1177
|
...(logId !== undefined ? { logId } : {})
|
|
1002
1178
|
});
|
|
1003
1179
|
if (!published) {
|
|
1004
|
-
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.');
|
|
1005
1181
|
}
|
|
1006
1182
|
const { did, doc } = published;
|
|
1007
|
-
if (delegatedClientsPointer({ doc }) ===
|
|
1008
|
-
|
|
1183
|
+
if (delegatedClientsPointer({ doc }) === clientAnnexDid) {
|
|
1184
|
+
if (!logOnly) {
|
|
1185
|
+
await concludeWithPublishedLog({ idStore, published });
|
|
1186
|
+
}
|
|
1009
1187
|
return { did, doc };
|
|
1010
1188
|
}
|
|
1011
1189
|
// The entry is signed by this client's active update key; a log that does
|
|
@@ -1017,19 +1195,11 @@ async function setDelegatedClientsPointerOnce({ idStore, updateKeys, companionDi
|
|
|
1017
1195
|
'delegated-clients entry.');
|
|
1018
1196
|
}
|
|
1019
1197
|
await assertCarryOverCommitments({ published });
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
};
|
|
1025
|
-
const services = existing.some(isPointerEntry)
|
|
1026
|
-
? existing.map(entry => isPointerEntry(entry)
|
|
1027
|
-
? { ...entry, serviceEndpoint: companionDid }
|
|
1028
|
-
: entry)
|
|
1029
|
-
: [
|
|
1030
|
-
...existing,
|
|
1031
|
-
delegatedClientsServiceEntry({ accountDid: did, companionDid })
|
|
1032
|
-
];
|
|
1198
|
+
const services = servicesPointedAtClientAnnex({
|
|
1199
|
+
doc,
|
|
1200
|
+
accountDid: did,
|
|
1201
|
+
clientAnnexDid
|
|
1202
|
+
});
|
|
1033
1203
|
const signer = await updateKeySigner({ seed: updateKeys.updateSeed });
|
|
1034
1204
|
const updated = await updateDID({
|
|
1035
1205
|
log: published.log,
|
|
@@ -1042,7 +1212,16 @@ async function setDelegatedClientsPointerOnce({ idStore, updateKeys, companionDi
|
|
|
1042
1212
|
nextKeyHashes: published.nextKeyHashes,
|
|
1043
1213
|
services
|
|
1044
1214
|
});
|
|
1045
|
-
|
|
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
|
+
}
|
|
1046
1225
|
return { did: updated.did, doc: updated.doc };
|
|
1047
1226
|
}
|
|
1048
1227
|
/**
|
|
@@ -1060,76 +1239,88 @@ async function setDelegatedClientsPointerOnce({ idStore, updateKeys, companionDi
|
|
|
1060
1239
|
* @param options.readAccountDocument {Function} reads the VERIFIED account
|
|
1061
1240
|
* document (the caller's `verifyAccountLog` read, pins and `expectedDid`
|
|
1062
1241
|
* applied there); called once per round
|
|
1063
|
-
* @param options.
|
|
1064
|
-
* store for a
|
|
1242
|
+
* @param options.storeForGenerationId {Function} builds the generation's log
|
|
1243
|
+
* store for a generation id (the delegated store over the credential's
|
|
1244
|
+
* sibling
|
|
1065
1245
|
* delegation, or a controller-tier store)
|
|
1066
1246
|
* @param options.ladderSeed {Uint8Array} the credential's ladder seed
|
|
1067
1247
|
* @param options.transientKeyMultibase {string} the visit's in-memory
|
|
1068
1248
|
* signing key, public multibase
|
|
1069
1249
|
* @param [options.mintGenerationDelegation] {Function}
|
|
1070
|
-
* `({
|
|
1250
|
+
* `({ clientAnnexDid }) => Promise<IZcap>` -- forwarded to the enrollment
|
|
1071
1251
|
* entry, which installs the minted delegation when it publishes the
|
|
1072
1252
|
* generation's first transient VM (see
|
|
1073
|
-
* {@link
|
|
1074
|
-
*
|
|
1253
|
+
* {@link enrollClientAnnexTransientClient}). The closure receives whichever
|
|
1254
|
+
* annex DID the round enrolls into, so a GC-race re-enroll mints for
|
|
1075
1255
|
* the fresh generation
|
|
1076
1256
|
* @param [options.pinStore] {ResourceLogPinStore} chain-head pins for the
|
|
1077
1257
|
* generation logs (a transient session passes an in-memory store); slot
|
|
1078
|
-
* keys are derived per generation with {@link
|
|
1258
|
+
* keys are derived per generation with {@link clientAnnexLogPinId}
|
|
1079
1259
|
* @param [options.maxRounds] {number} how many pointer moves to chase
|
|
1080
1260
|
* before giving up (a GC pass is quarterly, so more than one mid-ceremony
|
|
1081
1261
|
* move means something else is wrong)
|
|
1082
|
-
* @returns {Promise<{
|
|
1262
|
+
* @returns {Promise<{ clientAnnexDid: string, doc: DIDDoc, log: DIDLog }>}
|
|
1083
1263
|
*/
|
|
1084
|
-
export async function enrollTransientClient({ readAccountDocument,
|
|
1264
|
+
export async function enrollTransientClient({ readAccountDocument, storeForGenerationId, ladderSeed, transientKeyMultibase, mintGenerationDelegation: mintDelegation, pinStore, maxRounds = 3 }) {
|
|
1085
1265
|
let accountDoc = await readAccountDocument();
|
|
1086
1266
|
for (let round = 0; round < maxRounds; round++) {
|
|
1087
|
-
const
|
|
1088
|
-
if (
|
|
1089
|
-
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 ' +
|
|
1090
1270
|
'service entry; no generation exists to enroll into.');
|
|
1091
1271
|
}
|
|
1092
|
-
const { spaceId,
|
|
1093
|
-
|
|
1094
|
-
|
|
1272
|
+
const { spaceId, generationId } = clientAnnexDidParts({
|
|
1273
|
+
did: clientAnnexDid
|
|
1274
|
+
});
|
|
1275
|
+
const enrolled = await enrollClientAnnexTransientClient({
|
|
1276
|
+
store: storeForGenerationId(generationId),
|
|
1095
1277
|
ladderSeed,
|
|
1096
|
-
|
|
1278
|
+
generationId,
|
|
1097
1279
|
transientKeyMultibase,
|
|
1098
|
-
expectedDid:
|
|
1280
|
+
expectedDid: clientAnnexDid,
|
|
1099
1281
|
...(mintDelegation !== undefined
|
|
1100
1282
|
? { mintGenerationDelegation: mintDelegation }
|
|
1101
1283
|
: {}),
|
|
1102
1284
|
...(pinStore !== undefined
|
|
1103
|
-
? { pinStore, logId:
|
|
1285
|
+
? { pinStore, logId: clientAnnexLogPinId({ spaceId, generationId }) }
|
|
1104
1286
|
: {})
|
|
1105
1287
|
});
|
|
1106
1288
|
// The GC-race re-read: an unchanged pointer means the enrollment stands
|
|
1107
1289
|
// in the pointed generation; a moved one means a concurrent GC abandoned
|
|
1108
1290
|
// it, and the next round enrolls into the fresh generation.
|
|
1109
1291
|
accountDoc = await readAccountDocument();
|
|
1110
|
-
if (delegatedClientsPointer({ doc: accountDoc }) ===
|
|
1111
|
-
return {
|
|
1292
|
+
if (delegatedClientsPointer({ doc: accountDoc }) === clientAnnexDid) {
|
|
1293
|
+
return { clientAnnexDid, doc: enrolled.doc, log: enrolled.log };
|
|
1112
1294
|
}
|
|
1113
1295
|
}
|
|
1114
|
-
throw new Error('
|
|
1296
|
+
throw new Error('client annex: the delegated-clients pointer kept moving across ' +
|
|
1115
1297
|
`${String(maxRounds)} enrollment rounds; giving up.`);
|
|
1116
1298
|
}
|
|
1117
1299
|
/**
|
|
1118
1300
|
* RENEW PRECEDES MINT: the blocking pre-mint stage a transient App Connect
|
|
1119
|
-
* approval runs before delegating any grant. Reads the
|
|
1301
|
+
* approval runs before delegating any grant. Reads the annex document
|
|
1120
1302
|
* and hands back its embedded generation delegation -- renewing it first
|
|
1121
1303
|
* when it is expired or inside the 30-day renewal window ({@link
|
|
1122
1304
|
* zcapExpiring}): a fresh delegation is minted through the caller's closure
|
|
1123
1305
|
* (ladder-signed -- the renewal must not depend on the very delegation it
|
|
1124
1306
|
* replaces; published through the store, which in a transient session is
|
|
1125
1307
|
* the credential's sibling delegation, so even a hard-expired delegation is
|
|
1126
|
-
* recoverable), and one
|
|
1308
|
+
* recoverable), and one annex entry replaces the service entry's
|
|
1127
1309
|
* endpoint in place, signed by the writing credential's static rung 0.
|
|
1128
1310
|
*
|
|
1129
|
-
*
|
|
1311
|
+
* An annex document carrying no delegation entry at all installs one the
|
|
1130
1312
|
* same way (the GC ceremony's own install stage and the first-VM install
|
|
1131
1313
|
* make this rare; a heal, not a policy).
|
|
1132
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
|
+
*
|
|
1133
1324
|
* Failure is the caller's failure: a renewal that cannot complete throws,
|
|
1134
1325
|
* and the App Connect approval fails with the standard retryable-ceremony
|
|
1135
1326
|
* posture -- deliberately no clamp-on-failure fallback, which would deliver
|
|
@@ -1139,21 +1330,28 @@ export async function enrollTransientClient({ readAccountDocument, storeForSegme
|
|
|
1139
1330
|
* 30 or more days remaining ({@link clampGrantExpires}).
|
|
1140
1331
|
*
|
|
1141
1332
|
* @param options {object}
|
|
1142
|
-
* @param options.store {
|
|
1333
|
+
* @param options.store {ClientAnnexWriteStore} the generation's log store
|
|
1143
1334
|
* (delegated through the credential's sibling delegation, or
|
|
1144
1335
|
* controller-tier)
|
|
1145
1336
|
* @param options.ladderSeed {Uint8Array} the credential's ladder seed, from
|
|
1146
1337
|
* its unlock record
|
|
1147
|
-
* @param options.
|
|
1338
|
+
* @param options.generationId {string} the generation collection's name
|
|
1148
1339
|
* @param options.mintGenerationDelegation {Function}
|
|
1149
|
-
* `({
|
|
1340
|
+
* `({ clientAnnexDid }) => Promise<IZcap>` -- mints the replacement
|
|
1150
1341
|
* delegation (ladder-signed in a transient session)
|
|
1151
|
-
* @param [options.expectedDid] {string} the
|
|
1342
|
+
* @param [options.expectedDid] {string} the annex DID the log must
|
|
1152
1343
|
* resolve to, from the account document's pointer
|
|
1153
1344
|
* @param [options.pinStore] {ResourceLogPinStore} chain-head pins (a
|
|
1154
1345
|
* transient session passes an in-memory store)
|
|
1155
1346
|
* @param [options.logId] {string} the generation's pin-slot key, from
|
|
1156
|
-
* {@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)
|
|
1157
1355
|
* @param [options.now] {number} epoch milliseconds, for tests
|
|
1158
1356
|
* @returns {Promise<{ delegation: IZcap, renewed: boolean }>}
|
|
1159
1357
|
*/
|
|
@@ -1168,9 +1366,9 @@ export async function ensureGenerationDelegationCurrent(options) {
|
|
|
1168
1366
|
* @param options {object} see {@link ensureGenerationDelegationCurrent}
|
|
1169
1367
|
* @returns {Promise<{ delegation: IZcap, renewed: boolean }>}
|
|
1170
1368
|
*/
|
|
1171
|
-
async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed,
|
|
1172
|
-
|
|
1173
|
-
const published = await
|
|
1369
|
+
async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, generationId, mintGenerationDelegation: mintDelegation, expectedDid, pinStore, logId, accountDoc, force = false, now }) {
|
|
1370
|
+
assertGenerationId(generationId);
|
|
1371
|
+
const published = await readClientAnnexLogOrThrow({
|
|
1174
1372
|
store,
|
|
1175
1373
|
...(expectedDid !== undefined ? { expectedDid } : {}),
|
|
1176
1374
|
...(pinStore !== undefined ? { pinStore } : {}),
|
|
@@ -1178,7 +1376,17 @@ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, segmen
|
|
|
1178
1376
|
});
|
|
1179
1377
|
const { did, doc } = published;
|
|
1180
1378
|
const standing = embeddedGenerationDelegation({ doc });
|
|
1181
|
-
|
|
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 &&
|
|
1182
1390
|
!zcapExpiring({
|
|
1183
1391
|
...(standing.expires !== undefined
|
|
1184
1392
|
? { expires: standing.expires }
|
|
@@ -1189,22 +1397,22 @@ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, segmen
|
|
|
1189
1397
|
}
|
|
1190
1398
|
// The rung refusal precedes the mint: nothing is delegated for a writer
|
|
1191
1399
|
// who cannot publish the entry that would carry it.
|
|
1192
|
-
const rung = await
|
|
1400
|
+
const rung = await clientAnnexRung({ ladderSeed, generationId });
|
|
1193
1401
|
const rungHash = await deriveNextKeyHash(rung.keyMultibase);
|
|
1194
1402
|
const revealed = published.updateKeys.includes(rung.keyMultibase);
|
|
1195
1403
|
if (!revealed && !published.nextKeyHashes.includes(rungHash)) {
|
|
1196
|
-
throw new
|
|
1404
|
+
throw new ClientAnnexRungUncommittedError("client annex: the log commits neither this credential's rung-0 key nor " +
|
|
1197
1405
|
'its hash; a credential bound mid-generation cannot renew the ' +
|
|
1198
1406
|
'generation delegation until a writer commits its hash or the next ' +
|
|
1199
1407
|
'GC swap does.');
|
|
1200
1408
|
}
|
|
1201
1409
|
await assertCarryOverCommitments({ published });
|
|
1202
|
-
const fresh = await mintDelegation({
|
|
1410
|
+
const fresh = await mintDelegation({ clientAnnexDid: did });
|
|
1203
1411
|
const signer = await updateKeySigner({ seed: rung.seed });
|
|
1204
1412
|
const updated = await updateDID({
|
|
1205
1413
|
log: published.log,
|
|
1206
1414
|
signer,
|
|
1207
|
-
// The writer's rung-0 key reveals at its first
|
|
1415
|
+
// The writer's rung-0 key reveals at its first annex write, exactly
|
|
1208
1416
|
// as the enrollment entry does; `nextKeyHashes` is re-stated verbatim,
|
|
1209
1417
|
// never inherited. Verification methods, relationship arrays, and every
|
|
1210
1418
|
// other service entry ride the library's prior-state clone untouched.
|
|
@@ -1212,11 +1420,196 @@ async function ensureGenerationDelegationCurrentOnce({ store, ladderSeed, segmen
|
|
|
1212
1420
|
nextKeyHashes: [...published.nextKeyHashes],
|
|
1213
1421
|
services: withGenerationDelegationEntry({
|
|
1214
1422
|
doc,
|
|
1215
|
-
|
|
1423
|
+
clientAnnexDid: did,
|
|
1216
1424
|
delegation: fresh
|
|
1217
1425
|
})
|
|
1218
1426
|
});
|
|
1219
1427
|
await putLogResource({ store, log: updated.log, ifMatch: published.etag });
|
|
1220
1428
|
return { delegation: fresh, renewed: true };
|
|
1221
1429
|
}
|
|
1222
|
-
|
|
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
|