@interop/wallet-core 0.5.0 → 0.7.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 +16 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -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/markers/acquire.d.ts +107 -0
- package/dist/markers/acquire.d.ts.map +1 -0
- package/dist/markers/acquire.js +83 -0
- package/dist/markers/acquire.js.map +1 -0
- package/dist/markers/cipher.d.ts +56 -0
- package/dist/markers/cipher.d.ts.map +1 -0
- package/dist/markers/cipher.js +73 -0
- package/dist/markers/cipher.js.map +1 -0
- package/dist/markers/index.d.ts +30 -0
- package/dist/markers/index.d.ts.map +1 -0
- package/dist/markers/index.js +29 -0
- package/dist/markers/index.js.map +1 -0
- package/dist/markers/refresh.d.ts +60 -0
- package/dist/markers/refresh.d.ts.map +1 -0
- package/dist/markers/refresh.js +67 -0
- package/dist/markers/refresh.js.map +1 -0
- package/dist/recovery/index.d.ts +31 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/index.js +28 -0
- package/dist/recovery/index.js.map +1 -0
- package/dist/recovery/recoveryCode.d.ts +105 -0
- package/dist/recovery/recoveryCode.d.ts.map +1 -0
- package/dist/recovery/recoveryCode.js +155 -0
- package/dist/recovery/recoveryCode.js.map +1 -0
- package/dist/recovery/recoveryRecord.d.ts +75 -0
- package/dist/recovery/recoveryRecord.d.ts.map +1 -0
- package/dist/recovery/recoveryRecord.js +89 -0
- package/dist/recovery/recoveryRecord.js.map +1 -0
- package/dist/recovery/recoveryWebvh.d.ts +126 -0
- package/dist/recovery/recoveryWebvh.d.ts.map +1 -0
- package/dist/recovery/recoveryWebvh.js +379 -0
- package/dist/recovery/recoveryWebvh.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 +330 -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 +42 -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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Collection-encryption marker acquisition: reading a collection's
|
|
6
|
+
* `CollectionEncryption` marker (its key-epoch roster) from the Collection
|
|
7
|
+
* Description, caching each success, and falling back to the cached copy when
|
|
8
|
+
* the description cannot be fetched -- offline, a previously-shared collection
|
|
9
|
+
* must keep encrypting under its current epoch. A successful fetch that
|
|
10
|
+
* returns no marker (an unshared collection) yields `undefined`: the
|
|
11
|
+
* single-key path.
|
|
12
|
+
*
|
|
13
|
+
* The two seams are deliberately narrow, so a wallet app's own classes satisfy
|
|
14
|
+
* them structurally -- no adapter needed. A {@link MarkerSource} is one signed
|
|
15
|
+
* describe; a {@link MarkerCache} is a durable get/put the host has already
|
|
16
|
+
* scoped to one account's Space (a web wallet: a localStorage pair keyed by
|
|
17
|
+
* Space id; a mobile wallet: a per-(profile, collection) table column), so no
|
|
18
|
+
* scope key appears in the interface.
|
|
19
|
+
*/
|
|
20
|
+
import type { CollectionEncryption, WasClient } from '@interop/was-client';
|
|
21
|
+
/**
|
|
22
|
+
* Where markers come from: one signed read of the collection's Description.
|
|
23
|
+
* Resolves `undefined` for a collection that is plaintext or has no marker;
|
|
24
|
+
* network errors throw through (callers treat the fetch as best-effort and
|
|
25
|
+
* fall back to a cached copy).
|
|
26
|
+
*/
|
|
27
|
+
export interface MarkerSource {
|
|
28
|
+
collectionEncryption(options: {
|
|
29
|
+
collectionId: string;
|
|
30
|
+
}): Promise<CollectionEncryption | undefined>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Where fetched markers survive offline: a durable get/put pre-scoped by the
|
|
34
|
+
* host to one account's Space. `readMarker` resolves `undefined` when nothing
|
|
35
|
+
* is cached (never throws for absence).
|
|
36
|
+
*/
|
|
37
|
+
export interface MarkerCache {
|
|
38
|
+
readMarker(options: {
|
|
39
|
+
collectionId: string;
|
|
40
|
+
}): Promise<CollectionEncryption | undefined>;
|
|
41
|
+
writeMarker(options: {
|
|
42
|
+
collectionId: string;
|
|
43
|
+
marker: CollectionEncryption;
|
|
44
|
+
}): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The {@link MarkerSource} over a was-client handle: reads the collection's
|
|
48
|
+
* Description in the given Space and returns its `encryption` marker.
|
|
49
|
+
*
|
|
50
|
+
* @param options {object}
|
|
51
|
+
* @param options.was {WasClient} a client whose signer can read the Space
|
|
52
|
+
* @param options.spaceId {string}
|
|
53
|
+
* @returns {MarkerSource}
|
|
54
|
+
*/
|
|
55
|
+
export declare function wasMarkerSource({ was, spaceId }: {
|
|
56
|
+
was: WasClient;
|
|
57
|
+
spaceId: string;
|
|
58
|
+
}): MarkerSource;
|
|
59
|
+
/**
|
|
60
|
+
* Acquires one collection's marker: fetches it from the source, caching a
|
|
61
|
+
* success; falls back to the cached copy when the fetch fails; and with no
|
|
62
|
+
* source at all (a purely local code path) reads the cache alone. A
|
|
63
|
+
* successful fetch that returns no marker resolves `undefined` -- an unshared
|
|
64
|
+
* collection stays on the single-key path -- and deliberately leaves any
|
|
65
|
+
* cached copy in place, mirroring the fetch-failure fallback.
|
|
66
|
+
*
|
|
67
|
+
* @param options {object}
|
|
68
|
+
* @param [options.source] {MarkerSource} omit for cache-only acquisition
|
|
69
|
+
* @param options.cache {MarkerCache}
|
|
70
|
+
* @param options.collectionId {string}
|
|
71
|
+
* @param [options.onFetchError] {function} observes a swallowed fetch
|
|
72
|
+
* failure (the cached-fallback branch); errors from the cache itself throw
|
|
73
|
+
* through
|
|
74
|
+
* @returns {Promise<CollectionEncryption | undefined>}
|
|
75
|
+
*/
|
|
76
|
+
export declare function acquireMarker({ source, cache, collectionId, onFetchError }: {
|
|
77
|
+
source?: MarkerSource;
|
|
78
|
+
cache: MarkerCache;
|
|
79
|
+
collectionId: string;
|
|
80
|
+
onFetchError?: (err: unknown, info: {
|
|
81
|
+
collectionId: string;
|
|
82
|
+
}) => void;
|
|
83
|
+
}): Promise<CollectionEncryption | undefined>;
|
|
84
|
+
/**
|
|
85
|
+
* Acquires markers for a set of collections concurrently (each fetch is an
|
|
86
|
+
* independent signed round trip, so a session start is not gated on a serial
|
|
87
|
+
* chain of describes). Collections that resolve no marker are simply absent
|
|
88
|
+
* from the result.
|
|
89
|
+
*
|
|
90
|
+
* @param options {object}
|
|
91
|
+
* @param [options.source] {MarkerSource} omit for cache-only acquisition
|
|
92
|
+
* @param options.cache {MarkerCache}
|
|
93
|
+
* @param options.collectionIds {string[]}
|
|
94
|
+
* @param [options.onFetchError] {function} observes each swallowed fetch
|
|
95
|
+
* failure
|
|
96
|
+
* @returns {Promise<Record<string, CollectionEncryption>>} keyed by
|
|
97
|
+
* collection id
|
|
98
|
+
*/
|
|
99
|
+
export declare function acquireMarkers({ source, cache, collectionIds, onFetchError }: {
|
|
100
|
+
source?: MarkerSource;
|
|
101
|
+
cache: MarkerCache;
|
|
102
|
+
collectionIds: string[];
|
|
103
|
+
onFetchError?: (err: unknown, info: {
|
|
104
|
+
collectionId: string;
|
|
105
|
+
}) => void;
|
|
106
|
+
}): Promise<Record<string, CollectionEncryption>>;
|
|
107
|
+
//# sourceMappingURL=acquire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acquire.d.ts","sourceRoot":"","sources":["../../src/markers/acquire.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE1E;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,oBAAoB,CAAC,OAAO,EAAE;QAC5B,YAAY,EAAE,MAAM,CAAA;KACrB,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAA;CAC9C;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,OAAO,EAAE;QAClB,YAAY,EAAE,MAAM,CAAA;KACrB,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAA;IAC7C,WAAW,CAAC,OAAO,EAAE;QACnB,YAAY,EAAE,MAAM,CAAA;QACpB,MAAM,EAAE,oBAAoB,CAAA;KAC7B,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAC9B,GAAG,EACH,OAAO,EACR,EAAE;IACD,GAAG,EAAE,SAAS,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CAChB,GAAG,YAAY,CAUf;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,aAAa,CAAC,EAClC,MAAM,EACN,KAAK,EACL,YAAY,EACZ,YAAY,EACb,EAAE;IACD,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACtE,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAe5C;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAAC,EACnC,MAAM,EACN,KAAK,EACL,aAAa,EACb,YAAY,EACb,EAAE;IACD,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,KAAK,EAAE,WAAW,CAAA;IAClB,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACtE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAiBhD"}
|