@interop/wallet-core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/enrollment/enrollment.d.ts +144 -0
- package/dist/enrollment/enrollment.d.ts.map +1 -0
- package/dist/enrollment/enrollment.js +296 -0
- package/dist/enrollment/enrollment.js.map +1 -0
- package/dist/enrollment/index.d.ts +20 -0
- package/dist/enrollment/index.d.ts.map +1 -0
- package/dist/enrollment/index.js +19 -0
- package/dist/enrollment/index.js.map +1 -0
- package/dist/identity/index.d.ts +0 -5
- package/dist/identity/index.d.ts.map +1 -1
- package/dist/identity/index.js +0 -4
- package/dist/identity/index.js.map +1 -1
- package/dist/index.d.ts +13 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/keyring/fetch.d.ts +22 -0
- package/dist/keyring/fetch.d.ts.map +1 -0
- package/dist/keyring/fetch.js +35 -0
- package/dist/keyring/fetch.js.map +1 -0
- package/dist/keyring/index.d.ts +30 -0
- package/dist/keyring/index.d.ts.map +1 -0
- package/dist/keyring/index.js +28 -0
- package/dist/keyring/index.js.map +1 -0
- package/dist/keyring/kdf.d.ts +102 -0
- package/dist/keyring/kdf.d.ts.map +1 -0
- package/dist/keyring/kdf.js +148 -0
- package/dist/keyring/kdf.js.map +1 -0
- package/dist/keyring/record.d.ts +98 -0
- package/dist/keyring/record.d.ts.map +1 -0
- package/dist/keyring/record.js +119 -0
- package/dist/keyring/record.js.map +1 -0
- package/dist/keyring/unlockSpace.d.ts +101 -0
- package/dist/keyring/unlockSpace.d.ts.map +1 -0
- package/dist/keyring/unlockSpace.js +226 -0
- package/dist/keyring/unlockSpace.js.map +1 -0
- package/dist/keys/index.d.ts +24 -0
- package/dist/keys/index.d.ts.map +1 -0
- package/dist/keys/index.js +22 -0
- package/dist/keys/index.js.map +1 -0
- package/dist/keys/puk.d.ts +42 -0
- package/dist/keys/puk.d.ts.map +1 -0
- package/dist/keys/puk.js +56 -0
- package/dist/keys/puk.js.map +1 -0
- package/dist/keys/pukRoster.d.ts +210 -0
- package/dist/keys/pukRoster.d.ts.map +1 -0
- package/dist/keys/pukRoster.js +263 -0
- package/dist/keys/pukRoster.js.map +1 -0
- package/dist/keys/rosterStore.d.ts +18 -0
- package/dist/keys/rosterStore.d.ts.map +1 -0
- package/dist/keys/rosterStore.js +38 -0
- package/dist/keys/rosterStore.js.map +1 -0
- package/dist/space/collections.d.ts +57 -0
- package/dist/space/collections.d.ts.map +1 -1
- package/dist/space/collections.js +48 -0
- package/dist/space/collections.js.map +1 -1
- package/dist/space/index.d.ts +4 -0
- package/dist/space/index.d.ts.map +1 -1
- package/dist/space/index.js +4 -0
- package/dist/space/index.js.map +1 -1
- package/dist/webvh/didWeb.d.ts +41 -0
- package/dist/webvh/didWeb.d.ts.map +1 -0
- package/dist/webvh/didWeb.js +24 -0
- package/dist/webvh/didWeb.js.map +1 -0
- package/dist/webvh/didWebvh.d.ts +251 -0
- package/dist/webvh/didWebvh.d.ts.map +1 -0
- package/dist/webvh/didWebvh.js +775 -0
- package/dist/webvh/didWebvh.js.map +1 -0
- package/dist/webvh/index.d.ts +27 -0
- package/dist/webvh/index.d.ts.map +1 -0
- package/dist/webvh/index.js +24 -0
- package/dist/webvh/index.js.map +1 -0
- package/dist/webvh/zcap.d.ts +104 -0
- package/dist/webvh/zcap.d.ts.map +1 -0
- package/dist/webvh/zcap.js +117 -0
- package/dist/webvh/zcap.js.map +1 -0
- package/package.json +30 -4
- package/dist/identity/collectionKeys.d.ts +0 -36
- package/dist/identity/collectionKeys.d.ts.map +0 -1
- package/dist/identity/collectionKeys.js +0 -92
- package/dist/identity/collectionKeys.js.map +0 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The PUK wrap set: the `key-map/puk.json` roster resource. Its body is a
|
|
6
|
+
* `CollectionEncryption` marker verbatim, whose current epoch IS the current
|
|
7
|
+
* per-user key -- the epoch id is the PUK's did:key and the wrapped secret is
|
|
8
|
+
* the PUK's raw 32-byte key, wrapped to each enrolled client's key-agreement
|
|
9
|
+
* key. The roster is the delivery channel for PUK rotation: each client keeps
|
|
10
|
+
* the PUK in its own local state under the unlock layer, and the roster's
|
|
11
|
+
* epoch stamp marks a cached copy stale.
|
|
12
|
+
*
|
|
13
|
+
* Everything mutates through was-client's marker-store seam (the
|
|
14
|
+
* plain-resource adapter): read-with-etag, compare-and-swap writes, and a
|
|
15
|
+
* guarded create for the initially-absent roster. No marker logic is
|
|
16
|
+
* reimplemented here.
|
|
17
|
+
*
|
|
18
|
+
* A resource-hosted marker gets NONE of the server-side epoch invariants a
|
|
19
|
+
* Collection Description enforces (append-only epochs, monotone
|
|
20
|
+
* `currentEpoch`), so three client-side compensations are load-bearing alone
|
|
21
|
+
* against a tampering host:
|
|
22
|
+
*
|
|
23
|
+
* - **`epochsMac`** -- the epoch configuration is authenticated under the
|
|
24
|
+
* current epoch's secret, which the server never holds; a fabricated
|
|
25
|
+
* configuration fails the MAC (`PukRosterIntegrityError`).
|
|
26
|
+
* - **The epoch pin** -- the latest-seen roster epoch is pinned locally by the
|
|
27
|
+
* consuming app (beside the account-pointer pin); a served
|
|
28
|
+
* roster that rolls back behind the pin is refused
|
|
29
|
+
* (`PukRosterContinuityError`) rather than followed. Stale-roster replay
|
|
30
|
+
* thereby lands in the same accepted continuity class as a substituted
|
|
31
|
+
* account pointer.
|
|
32
|
+
* - **The roster delivers, never sources** -- the recipient-key source of
|
|
33
|
+
* record is the locally verified did:webvh document (one `keyAgreement`
|
|
34
|
+
* verification method per enrolled client). When an epoch rotates, each
|
|
35
|
+
* remaining recipient's key is resolved from that document
|
|
36
|
+
* (`pukRosterRecipientResolver`); a roster entry with no matching document
|
|
37
|
+
* verification method is dropped and never receives a wrap, so a
|
|
38
|
+
* server-injected entry sits ignored. Wraps are minted only by enrolled
|
|
39
|
+
* clients, against log-verified keys.
|
|
40
|
+
*/
|
|
41
|
+
import type { IKeyAgreementKey } from '@interop/data-integrity-core';
|
|
42
|
+
import type { CollectionEncryption } from '@interop/was-client';
|
|
43
|
+
import { type MarkerStore, type RecipientPublicKey } from '@interop/was-client/edv';
|
|
44
|
+
import type { Puk } from './puk.js';
|
|
45
|
+
/**
|
|
46
|
+
* Thrown when a served roster fails its client-side authentication: a
|
|
47
|
+
* missing/unsupported/invalid `epochsMac`, or a marker whose `currentEpoch`
|
|
48
|
+
* names no epoch in its own list. The server (or whoever can write to it) has
|
|
49
|
+
* produced a configuration no enrolled client authenticated.
|
|
50
|
+
*/
|
|
51
|
+
export declare class PukRosterIntegrityError extends Error {
|
|
52
|
+
constructor(message: string);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Thrown when a served roster conflicts with the locally pinned latest-seen
|
|
56
|
+
* epoch -- the epochs list no longer contains the pinned epoch, or
|
|
57
|
+
* `currentEpoch` precedes it in the (append-only) list. A rollback/replay of
|
|
58
|
+
* an older consistent configuration, which a valid `epochsMac` alone cannot
|
|
59
|
+
* catch; refused rather than followed.
|
|
60
|
+
*/
|
|
61
|
+
export declare class PukRosterContinuityError extends Error {
|
|
62
|
+
pinnedEpochId: string;
|
|
63
|
+
constructor({ pinnedEpochId }: {
|
|
64
|
+
pinnedEpochId: string;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Thrown when this client holds no usable wrap in the roster's current epoch
|
|
69
|
+
* (no recipient entry for its key-agreement key, or the entry fails to
|
|
70
|
+
* unwrap). The client cannot obtain the current PUK -- it may have been
|
|
71
|
+
* rotated off the roster.
|
|
72
|
+
*/
|
|
73
|
+
export declare class PukRosterUnwrapError extends Error {
|
|
74
|
+
constructor(message: string);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The subset of a resolved DID document the recipient resolver consumes: the
|
|
78
|
+
* `keyAgreement` relation (VM references or embedded VMs) and the
|
|
79
|
+
* `verificationMethod` list references resolve against.
|
|
80
|
+
*/
|
|
81
|
+
export interface RosterRecipientDocument {
|
|
82
|
+
keyAgreement?: Array<string | {
|
|
83
|
+
id?: string;
|
|
84
|
+
publicKeyMultibase?: string;
|
|
85
|
+
}>;
|
|
86
|
+
verificationMethod?: Array<{
|
|
87
|
+
id?: string;
|
|
88
|
+
publicKeyMultibase?: string;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Builds the recipient resolver for roster rotations, backed by a locally
|
|
93
|
+
* verified did:webvh document -- the enforcement point for "the roster
|
|
94
|
+
* delivers, never sources". Given a remaining recipient's `kid`, it answers
|
|
95
|
+
* with that recipient's public key ONLY when the document carries a matching
|
|
96
|
+
* `keyAgreement` verification method (matched on the public-key multibase, so
|
|
97
|
+
* a did:key-form kid matches its `<did:webvh>#<multibase>` VM); otherwise it
|
|
98
|
+
* resolves `null` -- the was-client skip contract -- so the entry is dropped
|
|
99
|
+
* from the fresh epoch and never receives a wrap.
|
|
100
|
+
*
|
|
101
|
+
* @param options {object}
|
|
102
|
+
* @param options.document {RosterRecipientDocument} the locally verified
|
|
103
|
+
* did:webvh document (never a server-supplied roster field)
|
|
104
|
+
* @returns {function} a `resolveRecipientKey` for `removeRecipient`
|
|
105
|
+
*/
|
|
106
|
+
export declare function pukRosterRecipientResolver({ document }: {
|
|
107
|
+
document: RosterRecipientDocument;
|
|
108
|
+
}): (kid: string) => Promise<RecipientPublicKey | null>;
|
|
109
|
+
/**
|
|
110
|
+
* Ensures the roster exists, create-if-absent: an absent roster is
|
|
111
|
+
* initialized with the account's existing PUK installed as the first epoch,
|
|
112
|
+
* wrapped to this client's key-agreement key; an existing roster is returned
|
|
113
|
+
* as-is (authentication is the read path's job, and provisioning must never
|
|
114
|
+
* clobber an established roster). Idempotent -- losing the guarded-create
|
|
115
|
+
* race to a concurrent first init converges on the winner's roster.
|
|
116
|
+
*
|
|
117
|
+
* @param options {object}
|
|
118
|
+
* @param options.store {MarkerStore} the roster's marker store
|
|
119
|
+
* @param options.puk {Puk} the account's per-user key
|
|
120
|
+
* @param options.clientKeyAgreementKey {IKeyAgreementKey} this client's own
|
|
121
|
+
* (identity) key-agreement key -- the roster recipient
|
|
122
|
+
* @returns {Promise<CollectionEncryption>} the roster marker
|
|
123
|
+
*/
|
|
124
|
+
export declare function ensurePukRoster({ store, puk, clientKeyAgreementKey }: {
|
|
125
|
+
store: MarkerStore;
|
|
126
|
+
puk: Puk;
|
|
127
|
+
clientKeyAgreementKey: IKeyAgreementKey;
|
|
128
|
+
}): Promise<CollectionEncryption>;
|
|
129
|
+
/**
|
|
130
|
+
* Wraps the PUK to a client being enrolled -- the roster half of the
|
|
131
|
+
* enrollment ceremony, and deliberately its FIRST write (decryption material
|
|
132
|
+
* before authorization, the push order): the wrap lands before the did:webvh
|
|
133
|
+
* log entries, so no enrolled client is ever authorized but blind, and a tear
|
|
134
|
+
* right after this write leaves only an orphan wrap -- invisible to
|
|
135
|
+
* authorization, harmless, resumed by re-running the ceremony.
|
|
136
|
+
*
|
|
137
|
+
* Escrow semantics ride on was-client's `addRecipient`: the new client
|
|
138
|
+
* receives EVERY epoch's key, current and prior, so it decrypts
|
|
139
|
+
* pre-enrollment history. The recipient key arrives over the point-to-point
|
|
140
|
+
* enrollment channel and is verified there by the enrolling client (the
|
|
141
|
+
* document VM it writes next comes from the same exchange) -- never sourced
|
|
142
|
+
* from the roster. Idempotent: a wrap already standing in the current epoch
|
|
143
|
+
* is returned as-is.
|
|
144
|
+
*
|
|
145
|
+
* @param options {object}
|
|
146
|
+
* @param options.store {MarkerStore} the roster's marker store
|
|
147
|
+
* @param options.recipient {RecipientPublicKey} the enrollee's public
|
|
148
|
+
* key-agreement key; `id` is the kid its own roster reads will look for
|
|
149
|
+
* @param options.ownerKeyAgreementKey {IKeyAgreementKey} the enrolling
|
|
150
|
+
* client's own (identity) key-agreement key, unwrapping each epoch for
|
|
151
|
+
* re-wrapping
|
|
152
|
+
* @returns {Promise<CollectionEncryption>} the refreshed roster marker
|
|
153
|
+
*/
|
|
154
|
+
export declare function addPukRosterRecipient({ store, recipient, ownerKeyAgreementKey }: {
|
|
155
|
+
store: MarkerStore;
|
|
156
|
+
recipient: RecipientPublicKey;
|
|
157
|
+
ownerKeyAgreementKey: IKeyAgreementKey;
|
|
158
|
+
}): Promise<CollectionEncryption>;
|
|
159
|
+
/**
|
|
160
|
+
* What a roster read resolves to: the authenticated marker, the current PUK
|
|
161
|
+
* (the cached one confirmed current, or a fresh one unwrapped from a rotated
|
|
162
|
+
* epoch -- `rotated` says which), and the epoch id the caller must pin as the
|
|
163
|
+
* new latest-seen.
|
|
164
|
+
*/
|
|
165
|
+
export interface PukRosterReadResult {
|
|
166
|
+
marker: CollectionEncryption;
|
|
167
|
+
puk: Puk;
|
|
168
|
+
rotated: boolean;
|
|
169
|
+
latestEpochId: string;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Reads and authenticates the roster -- the direct read at login and on epoch
|
|
173
|
+
* mismatch. Resolves `null` when the roster does not exist yet (an account
|
|
174
|
+
* provisioned before the roster, or provisioning still in flight); otherwise:
|
|
175
|
+
*
|
|
176
|
+
* 1. **Continuity**: the served epochs must contain the pinned latest-seen
|
|
177
|
+
* epoch, and `currentEpoch` must not precede it in the append-only list
|
|
178
|
+
* (`PukRosterContinuityError` -- the rollback/replay refusal).
|
|
179
|
+
* 2. **Possession**: `currentEpoch === puk.id` confirms the cached PUK
|
|
180
|
+
* current; otherwise the current epoch was rotated by another client and
|
|
181
|
+
* this client's wrap is unwrapped with its own key-agreement key
|
|
182
|
+
* (`PukRosterUnwrapError` when it holds none).
|
|
183
|
+
* 3. **Authentication**: the marker's `epochsMac` is verified under the
|
|
184
|
+
* current epoch's secret (`PukRosterIntegrityError` on any mismatch -- a
|
|
185
|
+
* fabricated configuration).
|
|
186
|
+
*
|
|
187
|
+
* A rotated read returns the fresh PUK; its Ed25519 signing seed does not
|
|
188
|
+
* travel through the roster (the roster wraps the key-agreement secret
|
|
189
|
+
* alone), so the returned PUK carries none.
|
|
190
|
+
*
|
|
191
|
+
* A caller with no cached PUK at all -- a freshly enrolled client making its
|
|
192
|
+
* first post-enrollment read -- omits `puk` and always takes the unwrap path;
|
|
193
|
+
* the result's `rotated` is then true (the PUK was adopted from the roster).
|
|
194
|
+
*
|
|
195
|
+
* @param options {object}
|
|
196
|
+
* @param options.store {MarkerStore} the roster's marker store
|
|
197
|
+
* @param [options.puk] {Puk} this client's cached PUK, when it holds one
|
|
198
|
+
* @param options.clientKeyAgreementKey {IKeyAgreementKey} this client's own
|
|
199
|
+
* (identity) key-agreement key, unwrapping a rotated epoch
|
|
200
|
+
* @param [options.pinnedEpochId] {string} the locally pinned latest-seen
|
|
201
|
+
* roster epoch, when this client has seen the roster before
|
|
202
|
+
* @returns {Promise<PukRosterReadResult | null>}
|
|
203
|
+
*/
|
|
204
|
+
export declare function readPukRoster({ store, puk, clientKeyAgreementKey, pinnedEpochId }: {
|
|
205
|
+
store: MarkerStore;
|
|
206
|
+
puk?: Puk;
|
|
207
|
+
clientKeyAgreementKey: IKeyAgreementKey;
|
|
208
|
+
pinnedEpochId?: string | null;
|
|
209
|
+
}): Promise<PukRosterReadResult | null>;
|
|
210
|
+
//# sourceMappingURL=pukRoster.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pukRoster.d.ts","sourceRoot":"","sources":["../../src/keys/pukRoster.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,EAML,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACxB,MAAM,yBAAyB,CAAA;AAChC,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAEnC;;;;;GAKG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;GAMG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;IACjD,aAAa,EAAE,MAAM,CAAA;gBACT,EAAE,aAAa,EAAE,EAAE;QAAE,aAAa,EAAE,MAAM,CAAA;KAAE;CAQzD;AAED;;;;;GAKG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,YAAY,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC3E,kBAAkB,CAAC,EAAE,KAAK,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACzE;AAgBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,0BAA0B,CAAC,EACzC,QAAQ,EACT,EAAE;IACD,QAAQ,EAAE,uBAAuB,CAAA;CAClC,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAsCtD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CAAC,EACpC,KAAK,EACL,GAAG,EACH,qBAAqB,EACtB,EAAE;IACD,KAAK,EAAE,WAAW,CAAA;IAClB,GAAG,EAAE,GAAG,CAAA;IACR,qBAAqB,EAAE,gBAAgB,CAAA;CACxC,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAUhC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,qBAAqB,CAAC,EAC1C,KAAK,EACL,SAAS,EACT,oBAAoB,EACrB,EAAE;IACD,KAAK,EAAE,WAAW,CAAA;IAClB,SAAS,EAAE,kBAAkB,CAAA;IAC7B,oBAAoB,EAAE,gBAAgB,CAAA;CACvC,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA0BhC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,oBAAoB,CAAA;IAC5B,GAAG,EAAE,GAAG,CAAA;IACR,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,aAAa,CAAC,EAClC,KAAK,EACL,GAAG,EACH,qBAAqB,EACrB,aAAa,EACd,EAAE;IACD,KAAK,EAAE,WAAW,CAAA;IAClB,GAAG,CAAC,EAAE,GAAG,CAAA;IACT,qBAAqB,EAAE,gBAAgB,CAAA;IACvC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAwEtC"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { addRecipient, initRecipients, ownerRecipient, unwrapEpochSecret, verifyEpochsMac } from '@interop/was-client/edv';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when a served roster fails its client-side authentication: a
|
|
4
|
+
* missing/unsupported/invalid `epochsMac`, or a marker whose `currentEpoch`
|
|
5
|
+
* names no epoch in its own list. The server (or whoever can write to it) has
|
|
6
|
+
* produced a configuration no enrolled client authenticated.
|
|
7
|
+
*/
|
|
8
|
+
export class PukRosterIntegrityError extends Error {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'PukRosterIntegrityError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Thrown when a served roster conflicts with the locally pinned latest-seen
|
|
16
|
+
* epoch -- the epochs list no longer contains the pinned epoch, or
|
|
17
|
+
* `currentEpoch` precedes it in the (append-only) list. A rollback/replay of
|
|
18
|
+
* an older consistent configuration, which a valid `epochsMac` alone cannot
|
|
19
|
+
* catch; refused rather than followed.
|
|
20
|
+
*/
|
|
21
|
+
export class PukRosterContinuityError extends Error {
|
|
22
|
+
pinnedEpochId;
|
|
23
|
+
constructor({ pinnedEpochId }) {
|
|
24
|
+
super('The PUK roster no longer carries the epoch this client has pinned -- ' +
|
|
25
|
+
'a rolled-back or replayed roster.');
|
|
26
|
+
this.name = 'PukRosterContinuityError';
|
|
27
|
+
this.pinnedEpochId = pinnedEpochId;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Thrown when this client holds no usable wrap in the roster's current epoch
|
|
32
|
+
* (no recipient entry for its key-agreement key, or the entry fails to
|
|
33
|
+
* unwrap). The client cannot obtain the current PUK -- it may have been
|
|
34
|
+
* rotated off the roster.
|
|
35
|
+
*/
|
|
36
|
+
export class PukRosterUnwrapError extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = 'PukRosterUnwrapError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The fragment after the last `#` of a key/VM id -- for the ids this roster
|
|
44
|
+
* handles (did:key KAK ids, `<did>#<multibase>` VM ids) that fragment is the
|
|
45
|
+
* key's public multibase, which is what makes kid-to-VM matching key-material
|
|
46
|
+
* equality rather than string equality across id formats.
|
|
47
|
+
*
|
|
48
|
+
* @param id {string}
|
|
49
|
+
* @returns {string | undefined}
|
|
50
|
+
*/
|
|
51
|
+
function multibaseFragmentOf(id) {
|
|
52
|
+
const hash = id.lastIndexOf('#');
|
|
53
|
+
return hash === -1 ? undefined : id.slice(hash + 1);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds the recipient resolver for roster rotations, backed by a locally
|
|
57
|
+
* verified did:webvh document -- the enforcement point for "the roster
|
|
58
|
+
* delivers, never sources". Given a remaining recipient's `kid`, it answers
|
|
59
|
+
* with that recipient's public key ONLY when the document carries a matching
|
|
60
|
+
* `keyAgreement` verification method (matched on the public-key multibase, so
|
|
61
|
+
* a did:key-form kid matches its `<did:webvh>#<multibase>` VM); otherwise it
|
|
62
|
+
* resolves `null` -- the was-client skip contract -- so the entry is dropped
|
|
63
|
+
* from the fresh epoch and never receives a wrap.
|
|
64
|
+
*
|
|
65
|
+
* @param options {object}
|
|
66
|
+
* @param options.document {RosterRecipientDocument} the locally verified
|
|
67
|
+
* did:webvh document (never a server-supplied roster field)
|
|
68
|
+
* @returns {function} a `resolveRecipientKey` for `removeRecipient`
|
|
69
|
+
*/
|
|
70
|
+
export function pukRosterRecipientResolver({ document }) {
|
|
71
|
+
// Materialize the document's keyAgreement VMs once: embedded VMs verbatim,
|
|
72
|
+
// string references resolved against `verificationMethod`.
|
|
73
|
+
const byId = new Map();
|
|
74
|
+
for (const method of document.verificationMethod ?? []) {
|
|
75
|
+
if (typeof method?.id === 'string') {
|
|
76
|
+
byId.set(method.id, method);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const keyAgreementMethods = [];
|
|
80
|
+
for (const entry of document.keyAgreement ?? []) {
|
|
81
|
+
const method = typeof entry === 'string' ? byId.get(entry) : entry;
|
|
82
|
+
if (method && typeof method.publicKeyMultibase === 'string') {
|
|
83
|
+
keyAgreementMethods.push(method);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return async function resolveRecipientKey(kid) {
|
|
87
|
+
const fragment = multibaseFragmentOf(kid);
|
|
88
|
+
if (!fragment) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const match = keyAgreementMethods.find(method => method.publicKeyMultibase === fragment ||
|
|
92
|
+
(typeof method.id === 'string' &&
|
|
93
|
+
multibaseFragmentOf(method.id) === fragment));
|
|
94
|
+
if (!match) {
|
|
95
|
+
// No document verification method backs this roster entry: drop it.
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return { id: kid, publicKeyMultibase: match.publicKeyMultibase };
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Ensures the roster exists, create-if-absent: an absent roster is
|
|
103
|
+
* initialized with the account's existing PUK installed as the first epoch,
|
|
104
|
+
* wrapped to this client's key-agreement key; an existing roster is returned
|
|
105
|
+
* as-is (authentication is the read path's job, and provisioning must never
|
|
106
|
+
* clobber an established roster). Idempotent -- losing the guarded-create
|
|
107
|
+
* race to a concurrent first init converges on the winner's roster.
|
|
108
|
+
*
|
|
109
|
+
* @param options {object}
|
|
110
|
+
* @param options.store {MarkerStore} the roster's marker store
|
|
111
|
+
* @param options.puk {Puk} the account's per-user key
|
|
112
|
+
* @param options.clientKeyAgreementKey {IKeyAgreementKey} this client's own
|
|
113
|
+
* (identity) key-agreement key -- the roster recipient
|
|
114
|
+
* @returns {Promise<CollectionEncryption>} the roster marker
|
|
115
|
+
*/
|
|
116
|
+
export async function ensurePukRoster({ store, puk, clientKeyAgreementKey }) {
|
|
117
|
+
const current = await store.read();
|
|
118
|
+
if (current !== null) {
|
|
119
|
+
return current.marker;
|
|
120
|
+
}
|
|
121
|
+
return initRecipients({
|
|
122
|
+
store,
|
|
123
|
+
recipients: [ownerRecipient({ keyAgreementKey: clientKeyAgreementKey })],
|
|
124
|
+
epoch: { epochId: puk.id, secret: puk.secret }
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Wraps the PUK to a client being enrolled -- the roster half of the
|
|
129
|
+
* enrollment ceremony, and deliberately its FIRST write (decryption material
|
|
130
|
+
* before authorization, the push order): the wrap lands before the did:webvh
|
|
131
|
+
* log entries, so no enrolled client is ever authorized but blind, and a tear
|
|
132
|
+
* right after this write leaves only an orphan wrap -- invisible to
|
|
133
|
+
* authorization, harmless, resumed by re-running the ceremony.
|
|
134
|
+
*
|
|
135
|
+
* Escrow semantics ride on was-client's `addRecipient`: the new client
|
|
136
|
+
* receives EVERY epoch's key, current and prior, so it decrypts
|
|
137
|
+
* pre-enrollment history. The recipient key arrives over the point-to-point
|
|
138
|
+
* enrollment channel and is verified there by the enrolling client (the
|
|
139
|
+
* document VM it writes next comes from the same exchange) -- never sourced
|
|
140
|
+
* from the roster. Idempotent: a wrap already standing in the current epoch
|
|
141
|
+
* is returned as-is.
|
|
142
|
+
*
|
|
143
|
+
* @param options {object}
|
|
144
|
+
* @param options.store {MarkerStore} the roster's marker store
|
|
145
|
+
* @param options.recipient {RecipientPublicKey} the enrollee's public
|
|
146
|
+
* key-agreement key; `id` is the kid its own roster reads will look for
|
|
147
|
+
* @param options.ownerKeyAgreementKey {IKeyAgreementKey} the enrolling
|
|
148
|
+
* client's own (identity) key-agreement key, unwrapping each epoch for
|
|
149
|
+
* re-wrapping
|
|
150
|
+
* @returns {Promise<CollectionEncryption>} the refreshed roster marker
|
|
151
|
+
*/
|
|
152
|
+
export async function addPukRosterRecipient({ store, recipient, ownerKeyAgreementKey }) {
|
|
153
|
+
const current = await store.read();
|
|
154
|
+
if (current === null) {
|
|
155
|
+
throw new Error('The PUK roster does not exist yet; the account must finish ' +
|
|
156
|
+
'provisioning before a client can be enrolled.');
|
|
157
|
+
}
|
|
158
|
+
const marker = current.marker;
|
|
159
|
+
const currentEpoch = (marker.epochs ?? []).find(epoch => epoch.id === marker.currentEpoch);
|
|
160
|
+
const wrapped = currentEpoch?.recipients.some(entry => entry.header.kid === recipient.id);
|
|
161
|
+
if (wrapped) {
|
|
162
|
+
// A completed (or torn-after-the-wrap) earlier run: addRecipient writes
|
|
163
|
+
// every epoch's wrap in one marker write, so the current epoch standing
|
|
164
|
+
// means the escrow set is complete.
|
|
165
|
+
return marker;
|
|
166
|
+
}
|
|
167
|
+
return addRecipient({
|
|
168
|
+
store,
|
|
169
|
+
recipient,
|
|
170
|
+
owner: { keyAgreementKey: ownerKeyAgreementKey }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Reads and authenticates the roster -- the direct read at login and on epoch
|
|
175
|
+
* mismatch. Resolves `null` when the roster does not exist yet (an account
|
|
176
|
+
* provisioned before the roster, or provisioning still in flight); otherwise:
|
|
177
|
+
*
|
|
178
|
+
* 1. **Continuity**: the served epochs must contain the pinned latest-seen
|
|
179
|
+
* epoch, and `currentEpoch` must not precede it in the append-only list
|
|
180
|
+
* (`PukRosterContinuityError` -- the rollback/replay refusal).
|
|
181
|
+
* 2. **Possession**: `currentEpoch === puk.id` confirms the cached PUK
|
|
182
|
+
* current; otherwise the current epoch was rotated by another client and
|
|
183
|
+
* this client's wrap is unwrapped with its own key-agreement key
|
|
184
|
+
* (`PukRosterUnwrapError` when it holds none).
|
|
185
|
+
* 3. **Authentication**: the marker's `epochsMac` is verified under the
|
|
186
|
+
* current epoch's secret (`PukRosterIntegrityError` on any mismatch -- a
|
|
187
|
+
* fabricated configuration).
|
|
188
|
+
*
|
|
189
|
+
* A rotated read returns the fresh PUK; its Ed25519 signing seed does not
|
|
190
|
+
* travel through the roster (the roster wraps the key-agreement secret
|
|
191
|
+
* alone), so the returned PUK carries none.
|
|
192
|
+
*
|
|
193
|
+
* A caller with no cached PUK at all -- a freshly enrolled client making its
|
|
194
|
+
* first post-enrollment read -- omits `puk` and always takes the unwrap path;
|
|
195
|
+
* the result's `rotated` is then true (the PUK was adopted from the roster).
|
|
196
|
+
*
|
|
197
|
+
* @param options {object}
|
|
198
|
+
* @param options.store {MarkerStore} the roster's marker store
|
|
199
|
+
* @param [options.puk] {Puk} this client's cached PUK, when it holds one
|
|
200
|
+
* @param options.clientKeyAgreementKey {IKeyAgreementKey} this client's own
|
|
201
|
+
* (identity) key-agreement key, unwrapping a rotated epoch
|
|
202
|
+
* @param [options.pinnedEpochId] {string} the locally pinned latest-seen
|
|
203
|
+
* roster epoch, when this client has seen the roster before
|
|
204
|
+
* @returns {Promise<PukRosterReadResult | null>}
|
|
205
|
+
*/
|
|
206
|
+
export async function readPukRoster({ store, puk, clientKeyAgreementKey, pinnedEpochId }) {
|
|
207
|
+
const current = await store.read();
|
|
208
|
+
if (current === null) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const marker = current.marker;
|
|
212
|
+
const epochIds = (marker.epochs ?? []).map(epoch => epoch.id);
|
|
213
|
+
const currentIndex = marker.currentEpoch
|
|
214
|
+
? epochIds.indexOf(marker.currentEpoch)
|
|
215
|
+
: -1;
|
|
216
|
+
if (currentIndex === -1) {
|
|
217
|
+
throw new PukRosterIntegrityError('The PUK roster names no current epoch in its own epoch list.');
|
|
218
|
+
}
|
|
219
|
+
if (pinnedEpochId) {
|
|
220
|
+
const pinnedIndex = epochIds.indexOf(pinnedEpochId);
|
|
221
|
+
if (pinnedIndex === -1 || currentIndex < pinnedIndex) {
|
|
222
|
+
throw new PukRosterContinuityError({ pinnedEpochId });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// The epochsMac construction's own version/alg are the caller's to check.
|
|
226
|
+
const epochsMac = marker.epochsMac;
|
|
227
|
+
if (!epochsMac || epochsMac.v !== 1 || epochsMac.alg !== 'HS256') {
|
|
228
|
+
throw new PukRosterIntegrityError('The PUK roster carries no supported epoch-configuration MAC.');
|
|
229
|
+
}
|
|
230
|
+
let currentPuk;
|
|
231
|
+
let rotated;
|
|
232
|
+
if (puk && marker.currentEpoch === puk.id) {
|
|
233
|
+
currentPuk = puk;
|
|
234
|
+
rotated = false;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Rotated by another client: unwrap this client's entry in the current
|
|
238
|
+
// epoch with its own key-agreement key (rotation delivery).
|
|
239
|
+
const entry = marker.epochs[currentIndex].recipients.find(recipient => recipient.header.kid === clientKeyAgreementKey.id);
|
|
240
|
+
if (!entry) {
|
|
241
|
+
throw new PukRosterUnwrapError('The PUK roster current epoch carries no wrap for this client.');
|
|
242
|
+
}
|
|
243
|
+
const secret = await unwrapEpochSecret({
|
|
244
|
+
entry,
|
|
245
|
+
keyAgreementKey: clientKeyAgreementKey
|
|
246
|
+
});
|
|
247
|
+
if (!secret) {
|
|
248
|
+
throw new PukRosterUnwrapError("This client's PUK roster entry failed to unwrap.");
|
|
249
|
+
}
|
|
250
|
+
currentPuk = { id: marker.currentEpoch, secret };
|
|
251
|
+
rotated = true;
|
|
252
|
+
}
|
|
253
|
+
if (!(await verifyEpochsMac({ marker, epochSecret: currentPuk.secret }))) {
|
|
254
|
+
throw new PukRosterIntegrityError('The PUK roster epoch configuration failed authentication.');
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
marker,
|
|
258
|
+
puk: currentPuk,
|
|
259
|
+
rotated,
|
|
260
|
+
latestEpochId: marker.currentEpoch
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=pukRoster.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pukRoster.js","sourceRoot":"","sources":["../../src/keys/pukRoster.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ZcapClient } from '@interop/ezcap';
|
|
2
|
+
import { type MarkerStore } from '@interop/was-client/edv';
|
|
3
|
+
/**
|
|
4
|
+
* Builds the compare-and-swap marker store over `key-map/puk.json` in a data
|
|
5
|
+
* Space.
|
|
6
|
+
*
|
|
7
|
+
* @param options {object}
|
|
8
|
+
* @param options.storageServerUrl {string}
|
|
9
|
+
* @param options.zcapClient {ZcapClient} the session's root signing client
|
|
10
|
+
* @param options.spaceId {string} the data Space id
|
|
11
|
+
* @returns {MarkerStore}
|
|
12
|
+
*/
|
|
13
|
+
export declare function pukRosterMarkerStore({ storageServerUrl, zcapClient, spaceId }: {
|
|
14
|
+
storageServerUrl: string;
|
|
15
|
+
zcapClient: ZcapClient;
|
|
16
|
+
spaceId: string;
|
|
17
|
+
}): MarkerStore;
|
|
18
|
+
//# sourceMappingURL=rosterStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rosterStore.d.ts","sourceRoot":"","sources":["../../src/keys/rosterStore.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAA;AAM/E;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,gBAAgB,EAChB,UAAU,EACV,OAAO,EACR,EAAE;IACD,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,UAAU,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;CAChB,GAAG,WAAW,CAQd"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The marker store over a Space's PUK wrap-set roster (`key-map/puk.json`).
|
|
6
|
+
* Standalone rather than a method on a wallet's remote-store class, because
|
|
7
|
+
* the login-time direct read checks the roster BEFORE any storage client (or
|
|
8
|
+
* cipher) is built: it takes the bare signing client instead of a store
|
|
9
|
+
* instance.
|
|
10
|
+
*
|
|
11
|
+
* The `plaintext` collection override is load-bearing. Without it the client
|
|
12
|
+
* decides plaintext vs encrypted by describing the collection first, and a 404
|
|
13
|
+
* from an absent Space or collection then surfaces as an encryption error
|
|
14
|
+
* rather than as an absent roster.
|
|
15
|
+
*/
|
|
16
|
+
import { WasClient } from '@interop/was-client';
|
|
17
|
+
import { resourceMarkerStore } from '@interop/was-client/edv';
|
|
18
|
+
import { KEY_MAP_COLLECTION, PUK_ROSTER_RESOURCE } from '../space/collections.js';
|
|
19
|
+
/**
|
|
20
|
+
* Builds the compare-and-swap marker store over `key-map/puk.json` in a data
|
|
21
|
+
* Space.
|
|
22
|
+
*
|
|
23
|
+
* @param options {object}
|
|
24
|
+
* @param options.storageServerUrl {string}
|
|
25
|
+
* @param options.zcapClient {ZcapClient} the session's root signing client
|
|
26
|
+
* @param options.spaceId {string} the data Space id
|
|
27
|
+
* @returns {MarkerStore}
|
|
28
|
+
*/
|
|
29
|
+
export function pukRosterMarkerStore({ storageServerUrl, zcapClient, spaceId }) {
|
|
30
|
+
const was = new WasClient({ serverUrl: storageServerUrl, zcapClient });
|
|
31
|
+
return resourceMarkerStore({
|
|
32
|
+
resource: was
|
|
33
|
+
.space(spaceId)
|
|
34
|
+
.collection(KEY_MAP_COLLECTION.id, { encryption: 'plaintext' })
|
|
35
|
+
.resource(PUK_ROSTER_RESOURCE)
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=rosterStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rosterStore.js","sourceRoot":"","sources":["../../src/keys/rosterStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/C,OAAO,EAAE,mBAAmB,EAAoB,MAAM,yBAAyB,CAAA;AAC/E,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,yBAAyB,CAAA;AAEhC;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,EACnC,gBAAgB,EAChB,UAAU,EACV,OAAO,EAKR;IACC,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,EAAE,SAAS,EAAE,gBAAgB,EAAE,UAAU,EAAE,CAAC,CAAA;IACtE,OAAO,mBAAmB,CAAC;QACzB,QAAQ,EAAE,GAAG;aACV,KAAK,CAAC,OAAO,CAAC;aACd,UAAU,CAAC,kBAAkB,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC;aAC9D,QAAQ,CAAC,mBAAmB,CAAC;KACjC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -62,4 +62,61 @@ export declare const PUBLIC_CREDENTIALS_COLLECTION_SPEC: SpaceCollectionSpec;
|
|
|
62
62
|
export declare const WALLET_ACTIVITY_COLLECTION_SPEC: SpaceCollectionSpec;
|
|
63
63
|
/** The wallet Space's own (non-contacts) collection specs, in provision order. */
|
|
64
64
|
export declare const WALLET_SPACE_COLLECTION_SPECS: SpaceCollectionSpec[];
|
|
65
|
+
/**
|
|
66
|
+
* The system collections and resource names that carry an account's identity
|
|
67
|
+
* and key material. They sit deliberately OUTSIDE the synced collection specs
|
|
68
|
+
* above: none of them gets a local replica or background replication, and each
|
|
69
|
+
* is read and written directly.
|
|
70
|
+
*
|
|
71
|
+
* - `id` -- world-readable (a collection-level public-read policy): the
|
|
72
|
+
* published DID document (`did.json`) and the did:webvh history log
|
|
73
|
+
* (`did.jsonl`). The path segments name the collection that holds the
|
|
74
|
+
* document, so the did:web id is `did:web:<host>:space:<spaceId>:id` and
|
|
75
|
+
* resolves to `https://<host>/space/<spaceId>/id/did.json`.
|
|
76
|
+
* - `key-map` -- private and capability-gated: the key-id map (`keys.json`)
|
|
77
|
+
* and the PUK wrap-set roster (`puk.json`). Kept separate from `id` exactly
|
|
78
|
+
* so `id` can be made world-readable without ever exposing key material.
|
|
79
|
+
* - `keyring` -- the unlock Space's single collection, holding the one
|
|
80
|
+
* keyring record (`keyring.json`). It lives in the minimal unlock Space
|
|
81
|
+
* controlled by an unlock identity, never in the wallet data Space.
|
|
82
|
+
*/
|
|
83
|
+
export declare const ID_COLLECTION: {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
};
|
|
87
|
+
export declare const KEY_MAP_COLLECTION: {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
};
|
|
91
|
+
export declare const KEYRING_COLLECTION: {
|
|
92
|
+
id: string;
|
|
93
|
+
name: string;
|
|
94
|
+
};
|
|
95
|
+
/** The world-readable DID document, served as `application/did+json`. */
|
|
96
|
+
export declare const DID_DOCUMENT_RESOURCE = "did.json";
|
|
97
|
+
/**
|
|
98
|
+
* The world-readable did:webvh history log, a raw JSON-Lines string served as
|
|
99
|
+
* `text/jsonl`: one log entry per line, each a full DID-document snapshot in a
|
|
100
|
+
* hash chain. Sibling of `did.json` in the same `id` collection;
|
|
101
|
+
* `did:webvh:<scid>:<host>:space:<spaceId>:id` resolves to
|
|
102
|
+
* `https://<host>/space/<spaceId>/id/did.jsonl`.
|
|
103
|
+
*/
|
|
104
|
+
export declare const DID_LOG_RESOURCE = "did.jsonl";
|
|
105
|
+
/**
|
|
106
|
+
* The (non-public) key-id map: verification method to KMS key id. Lives in the
|
|
107
|
+
* `key-map` collection. The recovery anchor -- written before `did.json` so a
|
|
108
|
+
* torn provisioning resumes from it.
|
|
109
|
+
*/
|
|
110
|
+
export declare const DID_KEYS_RESOURCE = "keys.json";
|
|
111
|
+
/**
|
|
112
|
+
* The per-user-key (PUK) wrap-set roster, sibling of `keys.json` in the same
|
|
113
|
+
* private `key-map` collection: a `CollectionEncryption` marker stored
|
|
114
|
+
* verbatim as the resource body, whose current epoch IS the current PUK (the
|
|
115
|
+
* epoch id is the PUK's did:key; the wrapped secret is the PUK's raw key,
|
|
116
|
+
* wrapped to each enrolled client's key-agreement key). Read directly with a
|
|
117
|
+
* compare-and-swap etag -- never replicated.
|
|
118
|
+
*/
|
|
119
|
+
export declare const PUK_ROSTER_RESOURCE = "puk.json";
|
|
120
|
+
/** The keyring record: the encrypted account pointer, in the unlock Space. */
|
|
121
|
+
export declare const KEYRING_RESOURCE = "keyring.json";
|
|
65
122
|
//# sourceMappingURL=collections.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/space/collections.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;GAaG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,8BAA8B,wBAAwB,CAAA;AACnE,2EAA2E;AAC3E,eAAO,MAAM,6BAA6B,uBAAuB,CAAA;AACjE,0DAA0D;AAC1D,eAAO,MAAM,0BAA0B,oBAAoB,CAAA;AAE3D;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,SAAS,GAAG,QAAQ,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,KAAK,GAAG,WAAW,CAAA;IAC/B,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED;;;GAGG;AACH,eAAO,MAAM,mCAAmC,EAAE,mBAMjD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,EAAE,mBAMhD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,EAAE,mBAM7C,CAAA;AAED,kFAAkF;AAClF,eAAO,MAAM,6BAA6B,EAAE,mBAAmB,EAI9D,CAAA"}
|
|
1
|
+
{"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../../src/space/collections.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;GAaG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,8BAA8B,wBAAwB,CAAA;AACnE,2EAA2E;AAC3E,eAAO,MAAM,6BAA6B,uBAAuB,CAAA;AACjE,0DAA0D;AAC1D,eAAO,MAAM,0BAA0B,oBAAoB,CAAA;AAE3D;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,SAAS,GAAG,QAAQ,CAAA;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,UAAU,EAAE,KAAK,GAAG,WAAW,CAAA;IAC/B,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED;;;GAGG;AACH,eAAO,MAAM,mCAAmC,EAAE,mBAMjD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kCAAkC,EAAE,mBAMhD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,EAAE,mBAM7C,CAAA;AAED,kFAAkF;AAClF,eAAO,MAAM,6BAA6B,EAAE,mBAAmB,EAI9D,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,aAAa;;;CAAiC,CAAA;AAC3D,eAAO,MAAM,kBAAkB;;;CAAqC,CAAA;AACpE,eAAO,MAAM,kBAAkB;;;CAAqC,CAAA;AAEpE,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,aAAa,CAAA;AAC/C;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,cAAc,CAAA;AAC3C;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,cAAc,CAAA;AAC5C;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,aAAa,CAAA;AAC7C,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,iBAAiB,CAAA"}
|