@interop/was-react 0.4.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/README.md +114 -35
- package/dist/auth/loginFlow.d.ts +6 -0
- package/dist/auth/loginFlow.d.ts.map +1 -1
- package/dist/auth/loginFlow.js +28 -9
- package/dist/auth/loginFlow.js.map +1 -1
- package/dist/auth/loginRequest.d.ts +22 -1
- package/dist/auth/loginRequest.d.ts.map +1 -1
- package/dist/auth/loginRequest.js +17 -1
- package/dist/auth/loginRequest.js.map +1 -1
- package/dist/config.d.ts +51 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +0 -0
- package/dist/config.js.map +1 -1
- package/dist/identity/agents.d.ts +38 -11
- package/dist/identity/agents.d.ts.map +1 -1
- package/dist/identity/agents.js +35 -11
- package/dist/identity/agents.js.map +1 -1
- package/dist/identity/documentLoader.d.ts.map +1 -1
- package/dist/identity/documentLoader.js +21 -6
- package/dist/identity/documentLoader.js.map +1 -1
- package/dist/identity/initAppSession.d.ts +6 -6
- package/dist/identity/initAppSession.js +6 -6
- package/dist/identity/seedCredential.d.ts +29 -5
- package/dist/identity/seedCredential.d.ts.map +1 -1
- package/dist/identity/seedCredential.js +47 -11
- package/dist/identity/seedCredential.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/react/hooks.d.ts +12 -0
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/hooks.js +14 -0
- package/dist/react/hooks.js.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/session/authStore.d.ts +9 -0
- package/dist/session/authStore.d.ts.map +1 -1
- package/dist/session/authStore.js +41 -16
- package/dist/session/authStore.js.map +1 -1
- package/dist/storage/adopt.js +2 -2
- package/dist/storage/localStore.d.ts +27 -16
- package/dist/storage/localStore.d.ts.map +1 -1
- package/dist/storage/localStore.js +38 -31
- package/dist/storage/localStore.js.map +1 -1
- package/dist/storage/sharedCollectionReader.d.ts +95 -0
- package/dist/storage/sharedCollectionReader.d.ts.map +1 -0
- package/dist/storage/sharedCollectionReader.js +338 -0
- package/dist/storage/sharedCollectionReader.js.map +1 -0
- package/dist/storage/wasSync.d.ts +37 -5
- package/dist/storage/wasSync.d.ts.map +1 -1
- package/dist/storage/wasSync.js +45 -5
- package/dist/storage/wasSync.js.map +1 -1
- package/dist/sync/docCipher.d.ts +3 -2
- package/dist/sync/docCipher.d.ts.map +1 -1
- package/dist/sync/docCipher.js +3 -2
- package/dist/sync/docCipher.js.map +1 -1
- package/package.json +6 -5
|
@@ -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":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAMzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAC3D,OAAO,EACL,eAAe,EACf,YAAY,EACZ,mBAAmB,EAGpB,MAAM,kBAAkB,CAAA;AAGzB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,CAAA;AAmB3C;;;;;GAKG;AACH,MAAM,OAAO,gCAAiC,SAAQ,KAAK;IACzD,YAAY,OAAe,EAAE,OAA6B;QACxD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,kCAAkC,CAAA;IAChD,CAAC;CACF;AAED,MAAM,OAAO,sBAAsB;IACxB,YAAY,CAAQ;IACpB,YAAY,CAAgB;IAC5B,gBAAgB,CAAkB;IAClC,YAAY,CAAc;IACnC,OAAO,CAAW;IAClB,8EAA8E;IAC9E,sEAAsE;IACtE,8EAA8E;IAC9E,6DAA6D;IAC7D,UAAU,GAAG,KAAK,CAAA;IAClB,0EAA0E;IAC1E,wCAAwC;IACxC,eAAe,GAAG,KAAK,CAAA;IACd,SAAS,CAAQ;IAE1B,YAAoB,EAClB,YAAY,EACZ,WAAW,EACX,eAAe,EACf,WAAW,EACX,MAAM,EACN,QAAQ,EAQT;QACC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAA;QAChC,IAAI,CAAC,YAAY,GAAG,WAAW,CAAA;QAC/B,IAAI,CAAC,gBAAgB,GAAG,eAAe,CAAA;QACvC,IAAI,CAAC,YAAY,GAAG,WAAW,CAAA;QAC/B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAA;QACrB,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAA;IAC3B,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAChB,WAAW,EACX,eAAe,EACf,WAAW,EACX,YAAY,EACZ,QAAQ,GAAG,wBAAwB,EAOpC;QACC,MAAM,UAAU,GAAG,MAAM,WAAW,CAAC,wBAAwB,CAAC,YAAY,CAAC,CAAA;QAC3E,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC;YACrC,YAAY;YACZ,eAAe;YACf,WAAW;YACX,UAAU;SACX,CAAC,CAAA;QACF,OAAO,IAAI,sBAAsB,CAAC;YAChC,YAAY;YACZ,WAAW;YACX,eAAe;YACf,WAAW;YACX,MAAM;YACN,QAAQ;SACT,CAAC,CAAA;IACJ,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,IAAI;QACR,MAAM,UAAU,GAAG,IAAI,CAAC,iBAAiB,EAAE,CAAA;QAC3C,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,eAAe,CAAC,UAAU,CAAC,CAAA;QAC/C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,CAAC,GAAG,YAAY,mBAAmB,CAAC,EAAE,CAAC;gBAC1C,MAAM,GAAG,CAAA;YACX,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;gBAC1B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAA;gBAC3B,OAAO,CAAC,IAAI,CACV,sBAAsB,IAAI,CAAC,YAAY,2BAA2B;oBAChE,mEAAmE;oBACnE,oDAAoD,CACvD,CAAA;YACH,CAAC;YACD,OAAO,MAAM,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAA;QACjD,CAAC;IACH,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,eAAe,CAAC,UAAsB;QAC1C,MAAM,MAAM,GAAG,IAAI,GAAG,EAA4B,CAAA;QAClD,IAAI,UAAyC,CAAA;QAC7C,SAAS,CAAC;YACR,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC;gBACpC,GAAG,CAAC,UAAU,IAAI,EAAE,UAAU,EAAE,CAAC;gBACjC,KAAK,EAAE,IAAI,CAAC,SAAS;aACtB,CAAC,CAAA;YACF,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;gBACtC,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;oBACtB,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;oBAC1B,SAAQ;gBACV,CAAC;gBACD,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,IAAwB,CAAC,CAAA;YAC5D,CAAC;YACD,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;gBAC/D,MAAK;YACP,CAAC;YACD,UAAU,GAAG,IAAI,CAAC,UAAU,CAAA;QAC9B,CAAC;QACD,MAAM,SAAS,GAAqB,EAAE,CAAA;QACtC,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC;YAChC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,CAAA;YACrE,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;YACzC,CAAC;QACH,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,iBAAiB,CAAC,UAAsB;QAC5C,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,CAAA;QACvC,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,IAAI,EAAE,CAAA;QAClC,MAAM,SAAS,GAAqB,EAAE,CAAA;QACtC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,CAAC,MAAM,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAgB,CAAA;YAC3D,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAA;YAChE,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;YAClD,CAAC;QACH,CAAC;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,GAAG,CAAC,UAAkB;QAC1B,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,CAAgB,CAAA;QAC5E,OAAO,MAAM,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAA;IAC1D,CAAC;IAED;;;;;OAKG;IACH,iBAAiB;QACf,MAAM,UAAU,GAAG,IAAI,CAAC,YAAY,CAAC,oBAAoB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;QAC5E,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,gCAAgC,CACxC,uDAAuD;gBACrD,IAAI,IAAI,CAAC,YAAY,IAAI,CAC5B,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAC,YAAY,CAAC,GAAG;aACzB,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC;aAChC,UAAU,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAA;IAC3E,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,YAAY,CAAC,EACjB,EAAE,EACF,IAAI,EAIL;QACC,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACxC,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,CACV,sBAAsB,EAAE,yBAAyB;gBAC/C,IAAI,IAAI,CAAC,YAAY,qCAAqC,CAC7D,CAAA;YACD,OAAO,SAAS,CAAA;QAClB,CAAC;QACD,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;QACvD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,iBAAiB,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;gBACzD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAA;gBACtB,MAAM,IAAI,CAAC,cAAc,EAAE,CAAA;gBAC3B,IAAI,CAAC;oBACH,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;gBACvD,CAAC;gBAAC,OAAO,QAAQ,EAAE,CAAC;oBAClB,OAAO,IAAI,CAAC,kBAAkB,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAA;gBACvD,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAC,kBAAkB,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAA;QAC7C,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,cAAc;QAClB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,wBAAwB,CACjE,IAAI,CAAC,YAAY,CAClB,CAAA;QACD,IAAI,CAAC,OAAO,GAAG,MAAM,iBAAiB,CAAC;YACrC,YAAY,EAAE,IAAI,CAAC,YAAY;YAC/B,eAAe,EAAE,IAAI,CAAC,gBAAgB;YACtC,WAAW,EAAE,IAAI,CAAC,YAAY;YAC9B,UAAU;SACX,CAAC,CAAA;IACJ,CAAC;IAED;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,EAAE,EAAE,GAAG,EAAgC;QAC1D,OAAO,CAAC,IAAI,CACV,sBAAsB,EAAE,yBAAyB;YAC/C,IAAI,IAAI,CAAC,YAAY,iDAAiD;YACtE,kEAAkE;YAClE,8DAA8D;YAC9D,kBAAkB,YAAY,CAAC,GAAG,CAAC,EAAE,CACxC,CAAA;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,KAAK,UAAU,iBAAiB,CAAC,EAC/B,YAAY,EACZ,eAAe,EACf,WAAW,EACX,UAAU,EAMX;IACC,IAAI,CAAC,UAAU,EAAE,MAAM,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,gCAAgC,CACxC,sBAAsB,YAAY,uCAAuC;YACvE,uEAAuE;YACvE,yCAAyC,CAC5C,CAAA;IACH,CAAC;IACD,IAAI,CAAC;QACH,OAAO,MAAM,eAAe,CAAC;YAC3B,eAAe;YACf,WAAW;YACX,YAAY;YACZ,UAAU;SACX,CAAC,CAAA;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,gCAAgC,CACxC,kCAAkC,YAAY,uBAAuB;YACnE,mEAAmE;YACnE,oEAAoE;YACpE,4BAA4B,EAC9B,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAA;IACH,CAAC;AACH,CAAC"}
|
|
@@ -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
|
-
*
|
|
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<
|
|
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<
|
|
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
|
|
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"}
|
package/dist/storage/wasSync.js
CHANGED
|
@@ -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
|
|
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<
|
|
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
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"wasSync.js","sourceRoot":"","sources":["../../src/storage/wasSync.ts"],"names":[],"mappings":"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"}
|
package/dist/sync/docCipher.d.ts
CHANGED
|
@@ -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
|
|
75
|
-
* (the
|
|
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).
|