@interop/was-react 0.4.0 → 0.5.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.
Files changed (57) hide show
  1. package/README.md +84 -11
  2. package/dist/auth/loginFlow.d.ts +6 -0
  3. package/dist/auth/loginFlow.d.ts.map +1 -1
  4. package/dist/auth/loginFlow.js +28 -9
  5. package/dist/auth/loginFlow.js.map +1 -1
  6. package/dist/auth/loginRequest.d.ts +22 -1
  7. package/dist/auth/loginRequest.d.ts.map +1 -1
  8. package/dist/auth/loginRequest.js +17 -1
  9. package/dist/auth/loginRequest.js.map +1 -1
  10. package/dist/config.d.ts +51 -0
  11. package/dist/config.d.ts.map +1 -1
  12. package/dist/config.js +0 -0
  13. package/dist/config.js.map +1 -1
  14. package/dist/identity/agents.d.ts +38 -11
  15. package/dist/identity/agents.d.ts.map +1 -1
  16. package/dist/identity/agents.js +35 -11
  17. package/dist/identity/agents.js.map +1 -1
  18. package/dist/identity/initAppSession.d.ts +6 -6
  19. package/dist/identity/initAppSession.js +6 -6
  20. package/dist/identity/seedCredential.d.ts +29 -5
  21. package/dist/identity/seedCredential.d.ts.map +1 -1
  22. package/dist/identity/seedCredential.js +47 -11
  23. package/dist/identity/seedCredential.js.map +1 -1
  24. package/dist/index.d.ts +7 -6
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +8 -6
  27. package/dist/index.js.map +1 -1
  28. package/dist/react/hooks.d.ts +12 -0
  29. package/dist/react/hooks.d.ts.map +1 -1
  30. package/dist/react/hooks.js +14 -0
  31. package/dist/react/hooks.js.map +1 -1
  32. package/dist/react/index.d.ts +1 -1
  33. package/dist/react/index.d.ts.map +1 -1
  34. package/dist/react/index.js +1 -1
  35. package/dist/react/index.js.map +1 -1
  36. package/dist/session/authStore.d.ts +9 -0
  37. package/dist/session/authStore.d.ts.map +1 -1
  38. package/dist/session/authStore.js +41 -16
  39. package/dist/session/authStore.js.map +1 -1
  40. package/dist/storage/adopt.js +2 -2
  41. package/dist/storage/localStore.d.ts +27 -16
  42. package/dist/storage/localStore.d.ts.map +1 -1
  43. package/dist/storage/localStore.js +38 -31
  44. package/dist/storage/localStore.js.map +1 -1
  45. package/dist/storage/sharedCollectionReader.d.ts +95 -0
  46. package/dist/storage/sharedCollectionReader.d.ts.map +1 -0
  47. package/dist/storage/sharedCollectionReader.js +338 -0
  48. package/dist/storage/sharedCollectionReader.js.map +1 -0
  49. package/dist/storage/wasSync.d.ts +37 -5
  50. package/dist/storage/wasSync.d.ts.map +1 -1
  51. package/dist/storage/wasSync.js +45 -5
  52. package/dist/storage/wasSync.js.map +1 -1
  53. package/dist/sync/docCipher.d.ts +3 -2
  54. package/dist/sync/docCipher.d.ts.map +1 -1
  55. package/dist/sync/docCipher.js +3 -2
  56. package/dist/sync/docCipher.js.map +1 -1
  57. package/package.json +3 -3
@@ -0,0 +1,95 @@
1
+ import type { IKeyAgreementKey, IKeyResolver } from '@interop/data-integrity-core';
2
+ import { type Json } from '../sync/index.js';
3
+ import type { WasRemoteStore } from './wasRemoteStore.js';
4
+ /**
5
+ * Default page size for the `changes`-feed walk. The server clamps its own
6
+ * maximum, and a page shorter than what was asked for is the feed's end-of-walk
7
+ * signal. Lower it on a collection of large envelopes to bound per-response
8
+ * size.
9
+ */
10
+ export declare const SHARED_CHANGES_PAGE_SIZE = 100;
11
+ /**
12
+ * One decrypted resource of a shared collection: the WAS resource id (the
13
+ * envelope id, opaque) and its decrypted payload.
14
+ */
15
+ export interface SharedResource {
16
+ id: string;
17
+ data: Json;
18
+ }
19
+ /**
20
+ * Thrown when a shared collection cannot be opened for reading: it carries no
21
+ * multi-recipient key-epoch roster, or this app is not a recipient of any epoch
22
+ * on it (the wallet never shared it, or a later un-share rotated the epoch off
23
+ * this app's key).
24
+ */
25
+ export declare class SharedCollectionUnavailableError extends Error {
26
+ constructor(message: string, options?: {
27
+ cause?: unknown;
28
+ });
29
+ }
30
+ export declare class SharedCollectionReader {
31
+ #private;
32
+ readonly collectionId: string;
33
+ private constructor();
34
+ /**
35
+ * Opens a reader over one shared collection: reads its `encryption` marker
36
+ * through the delegated zcap and builds the epoch-aware cipher from it.
37
+ *
38
+ * Throws {@link SharedCollectionUnavailableError} when the collection carries
39
+ * no marker or a marker with no key epochs (it is not multi-recipient, so this
40
+ * app cannot be a recipient of it), and likewise when the cipher cannot unwrap
41
+ * any epoch (this app is not, or is no longer, in the roster).
42
+ *
43
+ * @param options {object}
44
+ * @param options.remoteStore {WasRemoteStore} the delegated remote store
45
+ * @param options.keyAgreementKey {IKeyAgreementKey} this app's IDENTITY KAK
46
+ * @param options.keyResolver {IKeyResolver} its one-key resolver
47
+ * @param options.collectionId {string} the WAS collection id
48
+ * @param [options.pageSize] {number} `changes`-feed page size (defaults to
49
+ * {@link SHARED_CHANGES_PAGE_SIZE})
50
+ * @returns {Promise<SharedCollectionReader>}
51
+ */
52
+ static open({ remoteStore, keyAgreementKey, keyResolver, collectionId, pageSize }: {
53
+ remoteStore: WasRemoteStore;
54
+ keyAgreementKey: IKeyAgreementKey;
55
+ keyResolver: IKeyResolver;
56
+ collectionId: string;
57
+ pageSize?: number;
58
+ }): Promise<SharedCollectionReader>;
59
+ /**
60
+ * Lists the LIVE resources of the shared collection, decrypted. A body that is
61
+ * not an EDV envelope, or one sealed before the collection's first share (a
62
+ * single-recipient envelope this app is not a recipient of), is SKIPPED with a
63
+ * warning rather than failing the whole listing.
64
+ *
65
+ * Two paths, same result. The fast path pages the `changes` feed
66
+ * ({@link SharedCollectionReader.listViaChanges}), which returns whole pages of
67
+ * documents WITH their bodies -- and on an encrypted collection those bodies
68
+ * are exactly the opaque EDV envelopes wanted here, since the feed does not
69
+ * decrypt. That is one round trip per PAGE rather than the one per RESOURCE a
70
+ * listing plus a `get` each would cost, which on a wallet's
71
+ * `private-credentials` or `contacts` is the difference between a handful of
72
+ * requests and one per credential. It is also the same primitive replication
73
+ * pulls with.
74
+ *
75
+ * The slow path ({@link SharedCollectionReader.listViaResources}) is the
76
+ * fallback for a backend that does not advertise the `changes-query` feature
77
+ * (it answers 501, surfaced as `NotImplementedError`): list the resource
78
+ * summaries, then fetch each body. Taken only on that specific error, and
79
+ * warned about once per reader.
80
+ *
81
+ * @returns {Promise<SharedResource[]>}
82
+ */
83
+ list(): Promise<SharedResource[]>;
84
+ /**
85
+ * Reads and decrypts one resource of the shared collection by its WAS
86
+ * resource id. Returns `undefined` for a missing resource, a body that is not
87
+ * an EDV envelope, or a pre-share envelope this app cannot decrypt (each
88
+ * warned about, distinguishably).
89
+ *
90
+ * @param resourceId {string} the WAS resource id
91
+ * @returns {Promise<Json | undefined>}
92
+ */
93
+ get(resourceId: string): Promise<Json | undefined>;
94
+ }
95
+ //# sourceMappingURL=sharedCollectionReader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedCollectionReader.d.ts","sourceRoot":"","sources":["../../src/storage/sharedCollectionReader.ts"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACb,MAAM,8BAA8B,CAAA;AAErC,OAAO,EAKL,KAAK,IAAI,EACV,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEzD;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAA;AAU3C;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,IAAI,CAAA;CACX;AAED;;;;;GAKG;AACH,qBAAa,gCAAiC,SAAQ,KAAK;gBAC7C,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAI3D;AAED,qBAAa,sBAAsB;;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAe7B,OAAO;IAuBP;;;;;;;;;;;;;;;;;OAiBG;WACU,IAAI,CAAC,EAChB,WAAW,EACX,eAAe,EACf,WAAW,EACX,YAAY,EACZ,QAAmC,EACpC,EAAE;QACD,WAAW,EAAE,cAAc,CAAA;QAC3B,eAAe,EAAE,gBAAgB,CAAA;QACjC,WAAW,EAAE,YAAY,CAAA;QACzB,YAAY,EAAE,MAAM,CAAA;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;KAClB,GAAG,OAAO,CAAC,sBAAsB,CAAC;IAkBnC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,IAAI,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IA4EvC;;;;;;;;OAQG;IACG,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC;CAgGzD"}
@@ -0,0 +1,338 @@
1
+ /*!
2
+ * Copyright (c) 2026 Interop Alliance. All rights reserved.
3
+ */
4
+ /**
5
+ * SharedCollectionReader: a READ-ONLY view over one wallet-owned encrypted
6
+ * collection the wallet has shared with this app (the
7
+ * `urn:was:shared-collection` grant). It is the app-side other half of the
8
+ * wallet's share flow, and it is read-only BY CONSTRUCTION -- there is no write
9
+ * verb on this class, the collection is never replicated into RxDB, and the
10
+ * sync bootstrap never PUTs a description onto it.
11
+ *
12
+ * A share fuses two axes, and both are needed here:
13
+ *
14
+ * - **pull** -- the delegated `GET`/`HEAD` zcap on the collection, which is what
15
+ * the server checks at request time;
16
+ * - **read** -- an entry in the collection's key-epoch roster, wrapped to this
17
+ * app's IDENTITY key-agreement key (the X25519 twin of its did:key
18
+ * controller, `IdentityAgents.keyAgreementKey`). That is the same key the
19
+ * app's own collections are encrypted with: one rule for every roster entry,
20
+ * whoever owns the collection.
21
+ *
22
+ * The stored resource body IS the EDV envelope the wallet's replication moved
23
+ * verbatim, so reads must be RAW: the handle is opened with the
24
+ * `encryption: 'plaintext'` override (the `WasClient`'s own encryption provider
25
+ * is a deliberate no-op keystore) and the envelope is decrypted locally with a
26
+ * cipher built from the collection's `encryption` marker.
27
+ *
28
+ * The honest ceiling, the same one the wallet's consent screen states:
29
+ *
30
+ * - access can be removed later, which stops FUTURE reads but cannot take back
31
+ * what has already been read;
32
+ * - resources written BEFORE the collection's first share are single-recipient
33
+ * envelopes sealed to the owner alone. They will not decrypt here. That is
34
+ * expected, not corruption -- they are skipped with a warning.
35
+ */
36
+ import { NotImplementedError } from '@interop/was-client';
37
+ import { UnknownEpochError } from '@interop/was-client/edv';
38
+ import { createDocCipher, errorMessage, isEncryptedEnvelope } from '../sync/index.js';
39
+ /**
40
+ * Default page size for the `changes`-feed walk. The server clamps its own
41
+ * maximum, and a page shorter than what was asked for is the feed's end-of-walk
42
+ * signal. Lower it on a collection of large envelopes to bound per-response
43
+ * size.
44
+ */
45
+ export const SHARED_CHANGES_PAGE_SIZE = 100;
46
+ /**
47
+ * Thrown when a shared collection cannot be opened for reading: it carries no
48
+ * multi-recipient key-epoch roster, or this app is not a recipient of any epoch
49
+ * on it (the wallet never shared it, or a later un-share rotated the epoch off
50
+ * this app's key).
51
+ */
52
+ export class SharedCollectionUnavailableError extends Error {
53
+ constructor(message, options) {
54
+ super(message, options);
55
+ this.name = 'SharedCollectionUnavailableError';
56
+ }
57
+ }
58
+ export class SharedCollectionReader {
59
+ collectionId;
60
+ #remoteStore;
61
+ #keyAgreementKey;
62
+ #keyResolver;
63
+ #cipher;
64
+ // Whether an unknown-epoch marker refresh has already run for this reader. An
65
+ // epoch rotation emits no change-feed entry, so a stale marker is the
66
+ // expected failure mode; one refresh per reader is enough to recover, and the
67
+ // cap keeps a genuinely undecryptable envelope from looping.
68
+ #refreshed = false;
69
+ // Whether the one-time "reading this collection the slow way" warning has
70
+ // already been emitted for this reader.
71
+ #warnedSlowPath = false;
72
+ #pageSize;
73
+ constructor({ collectionId, remoteStore, keyAgreementKey, keyResolver, cipher, pageSize }) {
74
+ this.collectionId = collectionId;
75
+ this.#remoteStore = remoteStore;
76
+ this.#keyAgreementKey = keyAgreementKey;
77
+ this.#keyResolver = keyResolver;
78
+ this.#cipher = cipher;
79
+ this.#pageSize = pageSize;
80
+ }
81
+ /**
82
+ * Opens a reader over one shared collection: reads its `encryption` marker
83
+ * through the delegated zcap and builds the epoch-aware cipher from it.
84
+ *
85
+ * Throws {@link SharedCollectionUnavailableError} when the collection carries
86
+ * no marker or a marker with no key epochs (it is not multi-recipient, so this
87
+ * app cannot be a recipient of it), and likewise when the cipher cannot unwrap
88
+ * any epoch (this app is not, or is no longer, in the roster).
89
+ *
90
+ * @param options {object}
91
+ * @param options.remoteStore {WasRemoteStore} the delegated remote store
92
+ * @param options.keyAgreementKey {IKeyAgreementKey} this app's IDENTITY KAK
93
+ * @param options.keyResolver {IKeyResolver} its one-key resolver
94
+ * @param options.collectionId {string} the WAS collection id
95
+ * @param [options.pageSize] {number} `changes`-feed page size (defaults to
96
+ * {@link SHARED_CHANGES_PAGE_SIZE})
97
+ * @returns {Promise<SharedCollectionReader>}
98
+ */
99
+ static async open({ remoteStore, keyAgreementKey, keyResolver, collectionId, pageSize = SHARED_CHANGES_PAGE_SIZE }) {
100
+ const encryption = await remoteStore.readCollectionEncryption(collectionId);
101
+ const cipher = await buildSharedCipher({
102
+ collectionId,
103
+ keyAgreementKey,
104
+ keyResolver,
105
+ encryption
106
+ });
107
+ return new SharedCollectionReader({
108
+ collectionId,
109
+ remoteStore,
110
+ keyAgreementKey,
111
+ keyResolver,
112
+ cipher,
113
+ pageSize
114
+ });
115
+ }
116
+ /**
117
+ * Lists the LIVE resources of the shared collection, decrypted. A body that is
118
+ * not an EDV envelope, or one sealed before the collection's first share (a
119
+ * single-recipient envelope this app is not a recipient of), is SKIPPED with a
120
+ * warning rather than failing the whole listing.
121
+ *
122
+ * Two paths, same result. The fast path pages the `changes` feed
123
+ * ({@link SharedCollectionReader.listViaChanges}), which returns whole pages of
124
+ * documents WITH their bodies -- and on an encrypted collection those bodies
125
+ * are exactly the opaque EDV envelopes wanted here, since the feed does not
126
+ * decrypt. That is one round trip per PAGE rather than the one per RESOURCE a
127
+ * listing plus a `get` each would cost, which on a wallet's
128
+ * `private-credentials` or `contacts` is the difference between a handful of
129
+ * requests and one per credential. It is also the same primitive replication
130
+ * pulls with.
131
+ *
132
+ * The slow path ({@link SharedCollectionReader.listViaResources}) is the
133
+ * fallback for a backend that does not advertise the `changes-query` feature
134
+ * (it answers 501, surfaced as `NotImplementedError`): list the resource
135
+ * summaries, then fetch each body. Taken only on that specific error, and
136
+ * warned about once per reader.
137
+ *
138
+ * @returns {Promise<SharedResource[]>}
139
+ */
140
+ async list() {
141
+ const collection = this.#collectionHandle();
142
+ try {
143
+ return await this.#listViaChanges(collection);
144
+ }
145
+ catch (err) {
146
+ if (!(err instanceof NotImplementedError)) {
147
+ throw err;
148
+ }
149
+ if (!this.#warnedSlowPath) {
150
+ this.#warnedSlowPath = true;
151
+ console.warn(`Shared collection "${this.collectionId}" is being read the slow ` +
152
+ `way (one request per resource): its backend does not support the ` +
153
+ `"changes-query" feature the paged read path needs.`);
154
+ }
155
+ return await this.#listViaResources(collection);
156
+ }
157
+ }
158
+ /**
159
+ * The fast path: walk the `changes` feed page by page, resuming from each
160
+ * page's returned checkpoint, until a page comes back shorter than the
161
+ * requested limit (the feed's "you have caught up" signal). Tombstones
162
+ * (`_deleted`) are skipped, so the result is the LIVE set -- the same
163
+ * semantics the resource-listing path has. A resource that changes more than
164
+ * once within the walk appears under one id, so the last body seen wins.
165
+ */
166
+ async #listViaChanges(collection) {
167
+ const bodies = new Map();
168
+ let checkpoint;
169
+ for (;;) {
170
+ const page = await collection.changes({
171
+ ...(checkpoint && { checkpoint }),
172
+ limit: this.#pageSize
173
+ });
174
+ for (const document of page.documents) {
175
+ if (document._deleted) {
176
+ bodies.delete(document.id);
177
+ continue;
178
+ }
179
+ bodies.set(document.id, document.data);
180
+ }
181
+ if (page.documents.length < this.#pageSize || !page.checkpoint) {
182
+ break;
183
+ }
184
+ checkpoint = page.checkpoint;
185
+ }
186
+ const resources = [];
187
+ for (const [id, body] of bodies) {
188
+ const decrypted = await this.#decryptBody({ id, body: body ?? null });
189
+ if (decrypted !== undefined) {
190
+ resources.push({ id, data: decrypted });
191
+ }
192
+ }
193
+ return resources;
194
+ }
195
+ /**
196
+ * The slow path: one listing request for the resource summaries, then one
197
+ * request per resource for its body.
198
+ */
199
+ async #listViaResources(collection) {
200
+ const listing = await collection.list();
201
+ const items = listing?.items ?? [];
202
+ const resources = [];
203
+ for (const item of items) {
204
+ const body = (await collection.get(item.id));
205
+ const decrypted = await this.#decryptBody({ id: item.id, body });
206
+ if (decrypted !== undefined) {
207
+ resources.push({ id: item.id, data: decrypted });
208
+ }
209
+ }
210
+ return resources;
211
+ }
212
+ /**
213
+ * Reads and decrypts one resource of the shared collection by its WAS
214
+ * resource id. Returns `undefined` for a missing resource, a body that is not
215
+ * an EDV envelope, or a pre-share envelope this app cannot decrypt (each
216
+ * warned about, distinguishably).
217
+ *
218
+ * @param resourceId {string} the WAS resource id
219
+ * @returns {Promise<Json | undefined>}
220
+ */
221
+ async get(resourceId) {
222
+ const body = (await this.#collectionHandle().get(resourceId));
223
+ return await this.#decryptBody({ id: resourceId, body });
224
+ }
225
+ /**
226
+ * The RAW collection handle: the delegated zcap plus the
227
+ * `encryption: 'plaintext'` override, so `get`/`list` return the stored EDV
228
+ * envelope verbatim instead of routing it through the client's (no-op)
229
+ * encryption provider.
230
+ */
231
+ #collectionHandle() {
232
+ const capability = this.#remoteStore.collectionCapability(this.collectionId);
233
+ if (!capability) {
234
+ throw new SharedCollectionUnavailableError(`No delegated capability covers the shared collection ` +
235
+ `"${this.collectionId}".`);
236
+ }
237
+ return this.#remoteStore.was
238
+ .space(this.#remoteStore.spaceId)
239
+ .collection(this.collectionId, { capability, encryption: 'plaintext' });
240
+ }
241
+ /**
242
+ * Decrypts one fetched body, tolerating the two expected non-results: a body
243
+ * that is not an EDV envelope at all, and a pre-share single-recipient
244
+ * envelope. An {@link UnknownEpochError} drives one marker re-read + cipher
245
+ * rebuild + retry (an epoch rotation emits no change-feed entry, so a stale
246
+ * marker is the expected failure mode).
247
+ */
248
+ async #decryptBody({ id, body }) {
249
+ if (body === null || body === undefined) {
250
+ return undefined;
251
+ }
252
+ if (!isEncryptedEnvelope(body)) {
253
+ console.warn(`Skipping resource "${id}" of shared collection ` +
254
+ `"${this.collectionId}": its body is not an EDV envelope.`);
255
+ return undefined;
256
+ }
257
+ try {
258
+ return await this.#cipher.decrypt({ envelope: body });
259
+ }
260
+ catch (err) {
261
+ if (err instanceof UnknownEpochError && !this.#refreshed) {
262
+ this.#refreshed = true;
263
+ await this.#rebuildCipher();
264
+ try {
265
+ return await this.#cipher.decrypt({ envelope: body });
266
+ }
267
+ catch (retryErr) {
268
+ return this.#skipUndecryptable({ id, err: retryErr });
269
+ }
270
+ }
271
+ return this.#skipUndecryptable({ id, err });
272
+ }
273
+ }
274
+ /**
275
+ * Re-reads the collection's `encryption` marker and rebuilds the cipher from
276
+ * it. Used once per reader on an unknown epoch.
277
+ */
278
+ async #rebuildCipher() {
279
+ const encryption = await this.#remoteStore.readCollectionEncryption(this.collectionId);
280
+ this.#cipher = await buildSharedCipher({
281
+ collectionId: this.collectionId,
282
+ keyAgreementKey: this.#keyAgreementKey,
283
+ keyResolver: this.#keyResolver,
284
+ encryption
285
+ });
286
+ }
287
+ /**
288
+ * Warns about, and skips, one envelope this app cannot decrypt. The
289
+ * overwhelmingly likely cause is a resource written BEFORE the collection's
290
+ * first share -- sealed to the owner's key alone, never re-encrypted -- which
291
+ * is expected rather than corruption, so it is called out by name.
292
+ */
293
+ #skipUndecryptable({ id, err }) {
294
+ console.warn(`Skipping resource "${id}" of shared collection ` +
295
+ `"${this.collectionId}": this app is not a recipient of its envelope ` +
296
+ `(expected for resources written before the collection was first ` +
297
+ `shared -- those are sealed to the owner alone and are never ` +
298
+ `re-encrypted). ${errorMessage(err)}`);
299
+ return undefined;
300
+ }
301
+ }
302
+ /**
303
+ * Builds the epoch-aware cipher for a shared collection, turning the two
304
+ * "cannot read this collection" outcomes into one descriptive error: no
305
+ * multi-recipient roster at all, and a roster this app is not in.
306
+ *
307
+ * Only the cipher's `decrypt` is ever exercised here -- the reader has no write
308
+ * path -- so the seam's write-side id model is irrelevant.
309
+ *
310
+ * @param options {object}
311
+ * @param options.collectionId {string}
312
+ * @param options.keyAgreementKey {IKeyAgreementKey}
313
+ * @param options.keyResolver {IKeyResolver}
314
+ * @param [options.encryption] {CollectionEncryption}
315
+ * @returns {Promise<DocCipher>}
316
+ */
317
+ async function buildSharedCipher({ collectionId, keyAgreementKey, keyResolver, encryption }) {
318
+ if (!encryption?.epochs || encryption.epochs.length === 0) {
319
+ throw new SharedCollectionUnavailableError(`Shared collection "${collectionId}" carries no key-epoch roster, so it ` +
320
+ `is not multi-recipient and this app cannot be a recipient of it. The ` +
321
+ `wallet has not shared it with this app.`);
322
+ }
323
+ try {
324
+ return await createDocCipher({
325
+ keyAgreementKey,
326
+ keyResolver,
327
+ collectionId,
328
+ encryption
329
+ });
330
+ }
331
+ catch (err) {
332
+ throw new SharedCollectionUnavailableError(`Cannot read shared collection "${collectionId}": this app is not a ` +
333
+ `recipient of any of its key epochs. The wallet has not shared it ` +
334
+ `with this app, or access was removed (removing access rotates the ` +
335
+ `epoch off this app's key).`, { cause: err });
336
+ }
337
+ }
338
+ //# sourceMappingURL=sharedCollectionReader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedCollectionReader.js","sourceRoot":"","sources":["../../src/storage/sharedCollectionReader.ts"],"names":[],"mappings":"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"}
@@ -10,18 +10,38 @@
10
10
  * controller, and the per-doc `onRemoteChange` patcher (typically wired to the
11
11
  * rehydrate mechanism over the app's store registry) rather than this module
12
12
  * reaching for app-side globals.
13
+ *
14
+ * SHARED collections are handled apart from all of that. They belong to the
15
+ * wallet, so they never enter replication, never get a local replica, and are
16
+ * excluded from the best-effort description PUTs (a read-only grant would draw
17
+ * nothing but a pointless 403). Instead one {@link SharedCollectionReader} is
18
+ * opened per configured shared collection the grant set actually covers; a
19
+ * shared collection with no covering grant, or one this app turns out not to be
20
+ * a recipient of, is skipped with a warning rather than failing the session.
13
21
  */
14
22
  import type { ZcapClient } from '@interop/ezcap';
15
23
  import type { RxChangeEvent } from 'rxdb/plugins/core';
16
24
  import type { CollectionEncryption } from '@interop/was-client';
25
+ import type { IKeyAgreementKey, IKeyResolver } from '@interop/data-integrity-core';
17
26
  import type { SyncedDoc } from '../sync/index.js';
18
- import type { WasCollectionConfig } from '../config.js';
27
+ import type { SharedCollectionConfig, WasCollectionConfig } from '../config.js';
19
28
  import type { ParsedGrants } from '../grants.js';
20
29
  import { WasRemoteStore } from './wasRemoteStore.js';
30
+ import { SharedCollectionReader } from './sharedCollectionReader.js';
21
31
  import type { LocalStore } from './localStore.js';
22
32
  import type { SyncController } from './syncController.js';
23
33
  /**
24
- * Builds the remote store and starts background replication.
34
+ * What the sync bootstrap hands back: the delegated remote store, plus a
35
+ * read-only {@link SharedCollectionReader} per configured shared collection the
36
+ * grant set covers and this app is a recipient of, keyed by the config `key`.
37
+ */
38
+ export interface WasSyncBootstrap {
39
+ remoteStore: WasRemoteStore;
40
+ sharedCollections: Record<string, SharedCollectionReader>;
41
+ }
42
+ /**
43
+ * Builds the remote store, opens the shared-collection readers, and starts
44
+ * background replication.
25
45
  *
26
46
  * @param options {object}
27
47
  * @param options.parsed {ParsedGrants}
@@ -32,21 +52,33 @@ import type { SyncController } from './syncController.js';
32
52
  * @param options.syncController {SyncController} a fresh per-session controller
33
53
  * @param options.onRemoteChange {(collectionKey, event) => void} per-doc
34
54
  * reactive patcher for pulled/conflict-resolved remote changes
55
+ * @param [options.sharedCollections] {SharedCollectionConfig[]} the shared
56
+ * (read-only, wallet-owned) registry; never replicated, never written to
57
+ * @param [options.identityKeys] {object} this app's IDENTITY key-agreement key
58
+ * and its resolver, the recipient identity a wallet writes into a shared
59
+ * collection's epoch roster. Required to open any shared-collection reader
60
+ * @param [options.identityKeys.keyAgreementKey] {IKeyAgreementKey}
61
+ * @param [options.identityKeys.keyResolver] {IKeyResolver}
35
62
  * @param [options.onAuthError] {() => void} fired when replication hits a
36
63
  * 401/403 (expired/revoked access) -- wired to the reconnect banner
37
64
  * @param [options.onMarkersFetched] {(markers) => void | Promise<void>} given
38
65
  * the freshly fetched per-collection encryption markers (by WAS collection
39
66
  * id), to refresh the offline marker cache
40
- * @returns {Promise<WasRemoteStore>}
67
+ * @returns {Promise<WasSyncBootstrap>}
41
68
  */
42
- export declare function startWasSync({ parsed, zcapClient, collections, localStore, syncController, onRemoteChange, onAuthError, onMarkersFetched }: {
69
+ export declare function startWasSync({ parsed, zcapClient, collections, localStore, syncController, onRemoteChange, sharedCollections, identityKeys, onAuthError, onMarkersFetched }: {
43
70
  parsed: ParsedGrants;
44
71
  zcapClient: ZcapClient;
45
72
  collections: WasCollectionConfig[];
46
73
  localStore: LocalStore;
47
74
  syncController: SyncController;
48
75
  onRemoteChange: (collectionKey: string, event: RxChangeEvent<SyncedDoc>) => void;
76
+ sharedCollections?: SharedCollectionConfig[];
77
+ identityKeys?: {
78
+ keyAgreementKey: IKeyAgreementKey;
79
+ keyResolver: IKeyResolver;
80
+ };
49
81
  onAuthError?: () => void;
50
82
  onMarkersFetched?: (markers: Record<string, CollectionEncryption>) => void | Promise<void>;
51
- }): Promise<WasRemoteStore>;
83
+ }): Promise<WasSyncBootstrap>;
52
84
  //# sourceMappingURL=wasSync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wasSync.d.ts","sourceRoot":"","sources":["../../src/storage/wasSync.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEzD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,YAAY,CAAC,EACjC,MAAM,EACN,UAAU,EACV,WAAW,EACX,UAAU,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,gBAAgB,EACjB,EAAE;IACD,MAAM,EAAE,YAAY,CAAA;IACpB,UAAU,EAAE,UAAU,CAAA;IACtB,WAAW,EAAE,mBAAmB,EAAE,CAAA;IAClC,UAAU,EAAE,UAAU,CAAA;IACtB,cAAc,EAAE,cAAc,CAAA;IAC9B,cAAc,EAAE,CACd,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,aAAa,CAAC,SAAS,CAAC,KAC5B,IAAI,CAAA;IACT,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,gBAAgB,CAAC,EAAE,CACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,KAC1C,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B,GAAG,OAAO,CAAC,cAAc,CAAC,CAkE1B"}
1
+ {"version":3,"file":"wasSync.d.ts","sourceRoot":"","sources":["../../src/storage/wasSync.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACb,MAAM,8BAA8B,CAAA;AACrC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAC/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEzD;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,cAAc,CAAA;IAC3B,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAA;CAC1D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,YAAY,CAAC,EACjC,MAAM,EACN,UAAU,EACV,WAAW,EACX,UAAU,EACV,cAAc,EACd,cAAc,EACd,iBAAsB,EACtB,YAAY,EACZ,WAAW,EACX,gBAAgB,EACjB,EAAE;IACD,MAAM,EAAE,YAAY,CAAA;IACpB,UAAU,EAAE,UAAU,CAAA;IACtB,WAAW,EAAE,mBAAmB,EAAE,CAAA;IAClC,UAAU,EAAE,UAAU,CAAA;IACtB,cAAc,EAAE,cAAc,CAAA;IAC9B,cAAc,EAAE,CACd,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,aAAa,CAAC,SAAS,CAAC,KAC5B,IAAI,CAAA;IACT,iBAAiB,CAAC,EAAE,sBAAsB,EAAE,CAAA;IAC5C,YAAY,CAAC,EAAE;QACb,eAAe,EAAE,gBAAgB,CAAA;QACjC,WAAW,EAAE,YAAY,CAAA;KAC1B,CAAA;IACD,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB,gBAAgB,CAAC,EAAE,CACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,KAC1C,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAqG5B"}
@@ -1,6 +1,8 @@
1
1
  import { WasRemoteStore } from './wasRemoteStore.js';
2
+ import { SharedCollectionReader } from './sharedCollectionReader.js';
2
3
  /**
3
- * Builds the remote store and starts background replication.
4
+ * Builds the remote store, opens the shared-collection readers, and starts
5
+ * background replication.
4
6
  *
5
7
  * @param options {object}
6
8
  * @param options.parsed {ParsedGrants}
@@ -11,14 +13,21 @@ import { WasRemoteStore } from './wasRemoteStore.js';
11
13
  * @param options.syncController {SyncController} a fresh per-session controller
12
14
  * @param options.onRemoteChange {(collectionKey, event) => void} per-doc
13
15
  * reactive patcher for pulled/conflict-resolved remote changes
16
+ * @param [options.sharedCollections] {SharedCollectionConfig[]} the shared
17
+ * (read-only, wallet-owned) registry; never replicated, never written to
18
+ * @param [options.identityKeys] {object} this app's IDENTITY key-agreement key
19
+ * and its resolver, the recipient identity a wallet writes into a shared
20
+ * collection's epoch roster. Required to open any shared-collection reader
21
+ * @param [options.identityKeys.keyAgreementKey] {IKeyAgreementKey}
22
+ * @param [options.identityKeys.keyResolver] {IKeyResolver}
14
23
  * @param [options.onAuthError] {() => void} fired when replication hits a
15
24
  * 401/403 (expired/revoked access) -- wired to the reconnect banner
16
25
  * @param [options.onMarkersFetched] {(markers) => void | Promise<void>} given
17
26
  * the freshly fetched per-collection encryption markers (by WAS collection
18
27
  * id), to refresh the offline marker cache
19
- * @returns {Promise<WasRemoteStore>}
28
+ * @returns {Promise<WasSyncBootstrap>}
20
29
  */
21
- export async function startWasSync({ parsed, zcapClient, collections, localStore, syncController, onRemoteChange, onAuthError, onMarkersFetched }) {
30
+ export async function startWasSync({ parsed, zcapClient, collections, localStore, syncController, onRemoteChange, sharedCollections = [], identityKeys, onAuthError, onMarkersFetched }) {
22
31
  const remoteStore = WasRemoteStore.fromGrants({
23
32
  parsed,
24
33
  zcapClient,
@@ -30,7 +39,10 @@ export async function startWasSync({ parsed, zcapClient, collections, localStore
30
39
  // collections it does not apply to (reported ok + skipped): the encryption
31
40
  // marker skips public collections, the indexes declaration skips private
32
41
  // ones and public ones with no declared indexes.
33
- await Promise.all(Object.keys(parsed.byCollectionId).map(async (collectionId) => {
42
+ const sharedIds = new Set(sharedCollections.map(entry => entry.id));
43
+ await Promise.all(Object.keys(parsed.byCollectionId)
44
+ .filter(collectionId => !sharedIds.has(collectionId))
45
+ .map(async (collectionId) => {
34
46
  const marker = await remoteStore.markCollectionEncrypted(collectionId);
35
47
  if (!marker.ok) {
36
48
  console.warn(`Encryption marker PUT not authorized for "${collectionId}" (status ${marker.status ?? 'n/a'}).`);
@@ -63,12 +75,40 @@ export async function startWasSync({ parsed, zcapClient, collections, localStore
63
75
  if (onMarkersFetched) {
64
76
  await onMarkersFetched(markers);
65
77
  }
78
+ // Shared collections stay entirely out of replication: one read-only reader
79
+ // each, opened over the delegated read zcap and the collection's epoch
80
+ // roster. Every failure mode here is a warn-and-skip -- an uncovered grant,
81
+ // a collection with no roster, an app that is not (or is no longer) a
82
+ // recipient -- so a removed share degrades one reader, never the session.
83
+ const sharedReaders = {};
84
+ for (const { key, id } of sharedCollections) {
85
+ if (!parsed.byCollectionId[id]) {
86
+ console.warn(`Skipping shared collection "${id}": no delegated capability covers it.`);
87
+ continue;
88
+ }
89
+ if (!identityKeys) {
90
+ console.warn(`Skipping shared collection "${id}": no identity key-agreement key was ` +
91
+ `supplied to decrypt it with.`);
92
+ continue;
93
+ }
94
+ try {
95
+ sharedReaders[key] = await SharedCollectionReader.open({
96
+ remoteStore,
97
+ keyAgreementKey: identityKeys.keyAgreementKey,
98
+ keyResolver: identityKeys.keyResolver,
99
+ collectionId: id
100
+ });
101
+ }
102
+ catch (err) {
103
+ console.warn(`Skipping shared collection "${id}":`, err);
104
+ }
105
+ }
66
106
  await syncController.start({
67
107
  remoteStore,
68
108
  localStore,
69
109
  onRemoteChange,
70
110
  ...(onAuthError && { onAuthError })
71
111
  });
72
- return remoteStore;
112
+ return { remoteStore, sharedCollections: sharedReaders };
73
113
  }
74
114
  //# sourceMappingURL=wasSync.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"wasSync.js","sourceRoot":"","sources":["../../src/storage/wasSync.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAIpD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EACjC,MAAM,EACN,UAAU,EACV,WAAW,EACX,UAAU,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACX,gBAAgB,EAejB;IACC,MAAM,WAAW,GAAG,cAAc,CAAC,UAAU,CAAC;QAC5C,MAAM;QACN,UAAU;QACV,WAAW;KACZ,CAAC,CAAA;IAEF,2EAA2E;IAC3E,2EAA2E;IAC3E,0EAA0E;IAC1E,2EAA2E;IAC3E,yEAAyE;IACzE,iDAAiD;IACjD,MAAM,OAAO,CAAC,GAAG,CACf,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,GAAG,CAAC,KAAK,EAAC,YAAY,EAAC,EAAE;QAC1D,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,uBAAuB,CAAC,YAAY,CAAC,CAAA;QACtE,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CACV,6CAA6C,YAAY,aAAa,MAAM,CAAC,MAAM,IAAI,KAAK,IAAI,CACjG,CAAA;QACH,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,wBAAwB,CAAC,YAAY,CAAC,CAAA;QACxE,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CACV,+CAA+C,YAAY,aAAa,OAAO,CAAC,MAAM,IAAI,KAAK,IAAI,CACpG,CAAA;QACH,CAAC;IACH,CAAC,CAAC,CACH,CAAA;IAED,0EAA0E;IAC1E,8EAA8E;IAC9E,2EAA2E;IAC3E,4EAA4E;IAC5E,+CAA+C;IAC/C,MAAM,UAAU,GAAG,WAAW;SAC3B,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,KAAK,QAAQ,CAAC;SACxD,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;SAChC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,KAAK,SAAS,CAAC,CAAA;IACxD,MAAM,OAAO,GAAyC,EAAE,CAAA;IACxD,MAAM,OAAO,CAAC,GAAG,CACf,UAAU,CAAC,GAAG,CAAC,KAAK,EAAC,YAAY,EAAC,EAAE;QAClC,MAAM,UAAU,GACd,MAAM,WAAW,CAAC,wBAAwB,CAAC,YAAY,CAAC,CAAA;QAC1D,IAAI,UAAU,EAAE,CAAC;YACf,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CAAA;YAClC,MAAM,UAAU,CAAC,iBAAiB,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC,CAAA;QAClE,CAAC;IACH,CAAC,CAAC,CACH,CAAA;IACD,+EAA+E;IAC/E,8EAA8E;IAC9E,UAAU,CAAC,iBAAiB,CAAC,YAAY,CAAC,EAAE,CAC1C,WAAW,CAAC,wBAAwB,CAAC,YAAY,CAAC,CACnD,CAAA;IACD,IAAI,gBAAgB,EAAE,CAAC;QACrB,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAA;IACjC,CAAC;IAED,MAAM,cAAc,CAAC,KAAK,CAAC;QACzB,WAAW;QACX,UAAU;QACV,cAAc;QACd,GAAG,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,CAAC;KACpC,CAAC,CAAA;IACF,OAAO,WAAW,CAAA;AACpB,CAAC"}
1
+ {"version":3,"file":"wasSync.js","sourceRoot":"","sources":["../../src/storage/wasSync.ts"],"names":[],"mappings":"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"}
@@ -71,8 +71,9 @@ export declare function createPlaintextDocCodec({ collectionId }: {
71
71
  collectionId: string;
72
72
  }): DocCipher;
73
73
  /**
74
- * Builds a {@link DocCipher} for one collection from its derived key material
75
- * (the per-collection X25519 key agreement key). Keys are supplied directly (no
74
+ * Builds a {@link DocCipher} for one collection from the caller's derived key
75
+ * material (the app's identity X25519 key agreement key, the same one every
76
+ * other collection uses). Keys are supplied directly (no
76
77
  * keystore lookup). `idDerivation: 'random'` mints a stable random id updated in
77
78
  * place via `sequence` -- the mutable head-document model every entity here uses
78
79
  * (constant bump / toggle / re-categorize edits).