@interop/was-react 0.1.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/LICENSE.md +20 -0
- package/README.md +410 -0
- package/dist/auth/chapi.d.ts +43 -0
- package/dist/auth/chapi.d.ts.map +1 -0
- package/dist/auth/chapi.js +107 -0
- package/dist/auth/chapi.js.map +1 -0
- package/dist/auth/loginFlow.d.ts +93 -0
- package/dist/auth/loginFlow.d.ts.map +1 -0
- package/dist/auth/loginFlow.js +149 -0
- package/dist/auth/loginFlow.js.map +1 -0
- package/dist/auth/loginRequest.d.ts +66 -0
- package/dist/auth/loginRequest.d.ts.map +1 -0
- package/dist/auth/loginRequest.js +83 -0
- package/dist/auth/loginRequest.js.map +1 -0
- package/dist/auth/verifyResponse.d.ts +52 -0
- package/dist/auth/verifyResponse.d.ts.map +1 -0
- package/dist/auth/verifyResponse.js +138 -0
- package/dist/auth/verifyResponse.js.map +1 -0
- package/dist/auth/verifyResponse.test.d.ts +2 -0
- package/dist/auth/verifyResponse.test.d.ts.map +1 -0
- package/dist/auth/verifyResponse.test.js +282 -0
- package/dist/auth/verifyResponse.test.js.map +1 -0
- package/dist/auth/walletRequestTypes.d.ts +143 -0
- package/dist/auth/walletRequestTypes.d.ts.map +1 -0
- package/dist/auth/walletRequestTypes.js +2 -0
- package/dist/auth/walletRequestTypes.js.map +1 -0
- package/dist/config.d.ts +150 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -0
- package/dist/dev/cli.d.ts +16 -0
- package/dist/dev/cli.d.ts.map +1 -0
- package/dist/dev/cli.js +122 -0
- package/dist/dev/cli.js.map +1 -0
- package/dist/dev/cli.test.d.ts +2 -0
- package/dist/dev/cli.test.d.ts.map +1 -0
- package/dist/dev/cli.test.js +39 -0
- package/dist/dev/cli.test.js.map +1 -0
- package/dist/dev/index.d.ts +10 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +10 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/dev/provisionDevGrants.d.ts +73 -0
- package/dist/dev/provisionDevGrants.d.ts.map +1 -0
- package/dist/dev/provisionDevGrants.js +176 -0
- package/dist/dev/provisionDevGrants.js.map +1 -0
- package/dist/grants.d.ts +59 -0
- package/dist/grants.d.ts.map +1 -0
- package/dist/grants.js +82 -0
- package/dist/grants.js.map +1 -0
- package/dist/grants.test.d.ts +2 -0
- package/dist/grants.test.d.ts.map +1 -0
- package/dist/grants.test.js +79 -0
- package/dist/grants.test.js.map +1 -0
- package/dist/identity/agents.d.ts +99 -0
- package/dist/identity/agents.d.ts.map +1 -0
- package/dist/identity/agents.js +132 -0
- package/dist/identity/agents.js.map +1 -0
- package/dist/identity/appSession.d.ts +78 -0
- package/dist/identity/appSession.d.ts.map +1 -0
- package/dist/identity/appSession.js +93 -0
- package/dist/identity/appSession.js.map +1 -0
- package/dist/identity/documentLoader.d.ts +20 -0
- package/dist/identity/documentLoader.d.ts.map +1 -0
- package/dist/identity/documentLoader.js +99 -0
- package/dist/identity/documentLoader.js.map +1 -0
- package/dist/identity/initAppSession.d.ts +27 -0
- package/dist/identity/initAppSession.d.ts.map +1 -0
- package/dist/identity/initAppSession.js +30 -0
- package/dist/identity/initAppSession.js.map +1 -0
- package/dist/identity/seedCredential.d.ts +74 -0
- package/dist/identity/seedCredential.d.ts.map +1 -0
- package/dist/identity/seedCredential.js +165 -0
- package/dist/identity/seedCredential.js.map +1 -0
- package/dist/identity/seedCredential.test.d.ts +2 -0
- package/dist/identity/seedCredential.test.d.ts.map +1 -0
- package/dist/identity/seedCredential.test.js +205 -0
- package/dist/identity/seedCredential.test.js.map +1 -0
- package/dist/identity/seedStore.d.ts +27 -0
- package/dist/identity/seedStore.d.ts.map +1 -0
- package/dist/identity/seedStore.js +74 -0
- package/dist/identity/seedStore.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/mui/ProtectedRoute.d.ts +10 -0
- package/dist/mui/ProtectedRoute.d.ts.map +1 -0
- package/dist/mui/ProtectedRoute.js +54 -0
- package/dist/mui/ProtectedRoute.js.map +1 -0
- package/dist/mui/ReconnectBanner.d.ts +2 -0
- package/dist/mui/ReconnectBanner.d.ts.map +1 -0
- package/dist/mui/ReconnectBanner.js +20 -0
- package/dist/mui/ReconnectBanner.js.map +1 -0
- package/dist/mui/SyncStatusChip.d.ts +2 -0
- package/dist/mui/SyncStatusChip.d.ts.map +1 -0
- package/dist/mui/SyncStatusChip.js +27 -0
- package/dist/mui/SyncStatusChip.js.map +1 -0
- package/dist/mui/index.d.ts +12 -0
- package/dist/mui/index.d.ts.map +1 -0
- package/dist/mui/index.js +12 -0
- package/dist/mui/index.js.map +1 -0
- package/dist/react/WasSessionProvider.d.ts +36 -0
- package/dist/react/WasSessionProvider.d.ts.map +1 -0
- package/dist/react/WasSessionProvider.js +35 -0
- package/dist/react/WasSessionProvider.js.map +1 -0
- package/dist/react/hooks.d.ts +66 -0
- package/dist/react/hooks.d.ts.map +1 -0
- package/dist/react/hooks.js +121 -0
- package/dist/react/hooks.js.map +1 -0
- package/dist/react/index.d.ts +11 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +11 -0
- package/dist/react/index.js.map +1 -0
- package/dist/session/appReadyStore.d.ts +10 -0
- package/dist/session/appReadyStore.d.ts.map +1 -0
- package/dist/session/appReadyStore.js +22 -0
- package/dist/session/appReadyStore.js.map +1 -0
- package/dist/session/authStore.d.ts +71 -0
- package/dist/session/authStore.d.ts.map +1 -0
- package/dist/session/authStore.js +347 -0
- package/dist/session/authStore.js.map +1 -0
- package/dist/session/index.d.ts +10 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +10 -0
- package/dist/session/index.js.map +1 -0
- package/dist/storage/entityStore.d.ts +51 -0
- package/dist/storage/entityStore.d.ts.map +1 -0
- package/dist/storage/entityStore.js +78 -0
- package/dist/storage/entityStore.js.map +1 -0
- package/dist/storage/localStore.d.ts +144 -0
- package/dist/storage/localStore.d.ts.map +1 -0
- package/dist/storage/localStore.js +289 -0
- package/dist/storage/localStore.js.map +1 -0
- package/dist/storage/rehydrate.d.ts +56 -0
- package/dist/storage/rehydrate.d.ts.map +1 -0
- package/dist/storage/rehydrate.js +87 -0
- package/dist/storage/rehydrate.js.map +1 -0
- package/dist/storage/rehydrate.test.d.ts +2 -0
- package/dist/storage/rehydrate.test.d.ts.map +1 -0
- package/dist/storage/rehydrate.test.js +172 -0
- package/dist/storage/rehydrate.test.js.map +1 -0
- package/dist/storage/storageManager.d.ts +39 -0
- package/dist/storage/storageManager.d.ts.map +1 -0
- package/dist/storage/storageManager.js +68 -0
- package/dist/storage/storageManager.js.map +1 -0
- package/dist/storage/syncController.d.ts +79 -0
- package/dist/storage/syncController.d.ts.map +1 -0
- package/dist/storage/syncController.js +189 -0
- package/dist/storage/syncController.js.map +1 -0
- package/dist/storage/syncStatusStore.d.ts +17 -0
- package/dist/storage/syncStatusStore.d.ts.map +1 -0
- package/dist/storage/syncStatusStore.js +19 -0
- package/dist/storage/syncStatusStore.js.map +1 -0
- package/dist/storage/wasRemoteStore.d.ts +73 -0
- package/dist/storage/wasRemoteStore.d.ts.map +1 -0
- package/dist/storage/wasRemoteStore.js +76 -0
- package/dist/storage/wasRemoteStore.js.map +1 -0
- package/dist/storage/wasSync.d.ts +42 -0
- package/dist/storage/wasSync.d.ts.map +1 -0
- package/dist/storage/wasSync.js +34 -0
- package/dist/storage/wasSync.js.map +1 -0
- package/dist/sync/changesQuery.d.ts +44 -0
- package/dist/sync/changesQuery.d.ts.map +1 -0
- package/dist/sync/changesQuery.js +61 -0
- package/dist/sync/changesQuery.js.map +1 -0
- package/dist/sync/changesQuery.test.d.ts +2 -0
- package/dist/sync/changesQuery.test.d.ts.map +1 -0
- package/dist/sync/changesQuery.test.js +145 -0
- package/dist/sync/changesQuery.test.js.map +1 -0
- package/dist/sync/docCipher.d.ts +68 -0
- package/dist/sync/docCipher.d.ts.map +1 -0
- package/dist/sync/docCipher.js +89 -0
- package/dist/sync/docCipher.js.map +1 -0
- package/dist/sync/feedMasterPort.d.ts +30 -0
- package/dist/sync/feedMasterPort.d.ts.map +1 -0
- package/dist/sync/feedMasterPort.js +58 -0
- package/dist/sync/feedMasterPort.js.map +1 -0
- package/dist/sync/index.d.ts +21 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +21 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/lww.d.ts +29 -0
- package/dist/sync/lww.d.ts.map +1 -0
- package/dist/sync/lww.js +28 -0
- package/dist/sync/lww.js.map +1 -0
- package/dist/sync/lww.test.d.ts +2 -0
- package/dist/sync/lww.test.d.ts.map +1 -0
- package/dist/sync/lww.test.js +28 -0
- package/dist/sync/lww.test.js.map +1 -0
- package/dist/sync/lwwConflictHandler.d.ts +49 -0
- package/dist/sync/lwwConflictHandler.d.ts.map +1 -0
- package/dist/sync/lwwConflictHandler.js +65 -0
- package/dist/sync/lwwConflictHandler.js.map +1 -0
- package/dist/sync/lwwConflictHandler.test.d.ts +2 -0
- package/dist/sync/lwwConflictHandler.test.d.ts.map +1 -0
- package/dist/sync/lwwConflictHandler.test.js +78 -0
- package/dist/sync/lwwConflictHandler.test.js.map +1 -0
- package/dist/sync/pushWrites.d.ts +57 -0
- package/dist/sync/pushWrites.d.ts.map +1 -0
- package/dist/sync/pushWrites.js +159 -0
- package/dist/sync/pushWrites.js.map +1 -0
- package/dist/sync/pushWrites.test.d.ts +2 -0
- package/dist/sync/pushWrites.test.d.ts.map +1 -0
- package/dist/sync/pushWrites.test.js +287 -0
- package/dist/sync/pushWrites.test.js.map +1 -0
- package/dist/sync/syncedDocSchema.d.ts +22 -0
- package/dist/sync/syncedDocSchema.d.ts.map +1 -0
- package/dist/sync/syncedDocSchema.js +26 -0
- package/dist/sync/syncedDocSchema.js.map +1 -0
- package/dist/sync/types.d.ts +194 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +36 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/sync/wasReplication.d.ts +47 -0
- package/dist/sync/wasReplication.d.ts.map +1 -0
- package/dist/sync/wasReplication.js +55 -0
- package/dist/sync/wasReplication.js.map +1 -0
- package/dist/sync/wasSyncPort.d.ts +45 -0
- package/dist/sync/wasSyncPort.d.ts.map +1 -0
- package/dist/sync/wasSyncPort.js +170 -0
- package/dist/sync/wasSyncPort.js.map +1 -0
- package/package.json +142 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* WasRemoteStore (delegated-only): the relying-party view of the user's WAS
|
|
6
|
+
* Space, stripped to the RP model -- this app NEVER provisions the Space, never
|
|
7
|
+
* derives the spaceId, and never touches the `id` collection or DID publishing.
|
|
8
|
+
* It receives a set of wallet-delegated zcaps, reads the server URL + space id
|
|
9
|
+
* straight out of their `invocationTarget`s ({@link parseGrants}), and holds:
|
|
10
|
+
*
|
|
11
|
+
* - a `WasClient` wrapping the app's own `ZcapClient` (its invocation signer is
|
|
12
|
+
* the seed-derived controller the grants were delegated to);
|
|
13
|
+
* - per-collection capability routing, so each sync request invokes the exact
|
|
14
|
+
* collection grant;
|
|
15
|
+
* - a best-effort encryption-marker PUT (whether a delegated collection-scoped
|
|
16
|
+
* RW zcap authorizes writing the collection description). It is non-fatal
|
|
17
|
+
* either way -- envelopes replicate into an unmarked (plaintext) collection
|
|
18
|
+
* just the same.
|
|
19
|
+
*/
|
|
20
|
+
import type { ZcapClient } from '@interop/ezcap';
|
|
21
|
+
import type { IZcap } from '@interop/data-integrity-core';
|
|
22
|
+
import { WasClient } from '@interop/was-client';
|
|
23
|
+
import type { ParsedGrants } from '../grants.js';
|
|
24
|
+
/** The outcome of a best-effort encryption-marker PUT, for diagnostics. */
|
|
25
|
+
export interface MarkerResult {
|
|
26
|
+
collectionId: string;
|
|
27
|
+
ok: boolean;
|
|
28
|
+
status?: number;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare class WasRemoteStore {
|
|
32
|
+
readonly was: WasClient;
|
|
33
|
+
readonly serverUrl: string;
|
|
34
|
+
readonly spaceId: string;
|
|
35
|
+
private readonly _byCollectionId;
|
|
36
|
+
private constructor();
|
|
37
|
+
/**
|
|
38
|
+
* Builds a delegated remote store from a parsed grant set and the app's
|
|
39
|
+
* ZcapClient (whose invocation signer is the controller the grants target).
|
|
40
|
+
* The EDV encryption provider is a no-op keystore: replication moves opaque
|
|
41
|
+
* envelopes verbatim, and encrypt/decrypt is a local read/write concern.
|
|
42
|
+
*
|
|
43
|
+
* @param options {object}
|
|
44
|
+
* @param options.parsed {ParsedGrants}
|
|
45
|
+
* @param options.zcapClient {ZcapClient}
|
|
46
|
+
* @returns {WasRemoteStore}
|
|
47
|
+
*/
|
|
48
|
+
static fromGrants({ parsed, zcapClient }: {
|
|
49
|
+
parsed: ParsedGrants;
|
|
50
|
+
zcapClient: ZcapClient;
|
|
51
|
+
}): WasRemoteStore;
|
|
52
|
+
/**
|
|
53
|
+
* The delegated capability for one WAS collection, or `undefined` when no
|
|
54
|
+
* grant covers it (the sync port then invokes without a capability and the
|
|
55
|
+
* server denies it -- the intended fail-closed mode).
|
|
56
|
+
*
|
|
57
|
+
* @param collectionId {string} the WAS collection id
|
|
58
|
+
* @returns {IZcap | undefined}
|
|
59
|
+
*/
|
|
60
|
+
collectionCapability(collectionId: string): IZcap | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Best-effort declaration of the `{ encryption: { scheme: 'edv' } }` marker on
|
|
63
|
+
* one collection, invoked with that collection's delegated RW zcap. Non-fatal:
|
|
64
|
+
* returns the outcome rather than throwing, so a server that does not authorize
|
|
65
|
+
* a delegated description write leaves replication untouched (the collection
|
|
66
|
+
* simply stays unmarked / plaintext, which still stores envelopes).
|
|
67
|
+
*
|
|
68
|
+
* @param collectionId {string} the WAS collection id
|
|
69
|
+
* @returns {Promise<MarkerResult>}
|
|
70
|
+
*/
|
|
71
|
+
markCollectionEncrypted(collectionId: string): Promise<MarkerResult>;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=wasRemoteStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasRemoteStore.d.ts","sourceRoot":"","sources":["../../src/storage/wasRemoteStore.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;GAeG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,8BAA8B,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,2EAA2E;AAC3E,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,qBAAa,cAAc;IACzB,SAAgB,GAAG,EAAE,SAAS,CAAA;IAC9B,SAAgB,SAAS,EAAE,MAAM,CAAA;IACjC,SAAgB,OAAO,EAAE,MAAM,CAAA;IAC/B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAuB;IAEvD,OAAO;IAaP;;;;;;;;;;OAUG;IACH,MAAM,CAAC,UAAU,CAAC,EAChB,MAAM,EACN,UAAU,EACX,EAAE;QACD,MAAM,EAAE,YAAY,CAAA;QACpB,UAAU,EAAE,UAAU,CAAA;KACvB,GAAG,cAAc;IASlB;;;;;;;OAOG;IACH,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS;IAI7D;;;;;;;;;OASG;IACG,uBAAuB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAqB3E"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { WasClient } from '@interop/was-client';
|
|
2
|
+
import { createEdvEncryption } from '@interop/was-client/edv';
|
|
3
|
+
export class WasRemoteStore {
|
|
4
|
+
was;
|
|
5
|
+
serverUrl;
|
|
6
|
+
spaceId;
|
|
7
|
+
_byCollectionId;
|
|
8
|
+
constructor({ was, parsed }) {
|
|
9
|
+
this.was = was;
|
|
10
|
+
this.serverUrl = parsed.serverUrl;
|
|
11
|
+
this.spaceId = parsed.spaceId;
|
|
12
|
+
this._byCollectionId = parsed.byCollectionId;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Builds a delegated remote store from a parsed grant set and the app's
|
|
16
|
+
* ZcapClient (whose invocation signer is the controller the grants target).
|
|
17
|
+
* The EDV encryption provider is a no-op keystore: replication moves opaque
|
|
18
|
+
* envelopes verbatim, and encrypt/decrypt is a local read/write concern.
|
|
19
|
+
*
|
|
20
|
+
* @param options {object}
|
|
21
|
+
* @param options.parsed {ParsedGrants}
|
|
22
|
+
* @param options.zcapClient {ZcapClient}
|
|
23
|
+
* @returns {WasRemoteStore}
|
|
24
|
+
*/
|
|
25
|
+
static fromGrants({ parsed, zcapClient }) {
|
|
26
|
+
const was = new WasClient({
|
|
27
|
+
serverUrl: parsed.serverUrl,
|
|
28
|
+
zcapClient,
|
|
29
|
+
encryption: createEdvEncryption({ resolveKeys: async () => null })
|
|
30
|
+
});
|
|
31
|
+
return new WasRemoteStore({ was, parsed });
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The delegated capability for one WAS collection, or `undefined` when no
|
|
35
|
+
* grant covers it (the sync port then invokes without a capability and the
|
|
36
|
+
* server denies it -- the intended fail-closed mode).
|
|
37
|
+
*
|
|
38
|
+
* @param collectionId {string} the WAS collection id
|
|
39
|
+
* @returns {IZcap | undefined}
|
|
40
|
+
*/
|
|
41
|
+
collectionCapability(collectionId) {
|
|
42
|
+
return this._byCollectionId[collectionId];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Best-effort declaration of the `{ encryption: { scheme: 'edv' } }` marker on
|
|
46
|
+
* one collection, invoked with that collection's delegated RW zcap. Non-fatal:
|
|
47
|
+
* returns the outcome rather than throwing, so a server that does not authorize
|
|
48
|
+
* a delegated description write leaves replication untouched (the collection
|
|
49
|
+
* simply stays unmarked / plaintext, which still stores envelopes).
|
|
50
|
+
*
|
|
51
|
+
* @param collectionId {string} the WAS collection id
|
|
52
|
+
* @returns {Promise<MarkerResult>}
|
|
53
|
+
*/
|
|
54
|
+
async markCollectionEncrypted(collectionId) {
|
|
55
|
+
const capability = this.collectionCapability(collectionId);
|
|
56
|
+
if (!capability) {
|
|
57
|
+
return { collectionId, ok: false, error: 'no capability' };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const response = await this.was.request({
|
|
61
|
+
capability,
|
|
62
|
+
path: `/space/${this.spaceId}/${collectionId}`,
|
|
63
|
+
method: 'PUT',
|
|
64
|
+
json: { id: collectionId, encryption: { scheme: 'edv' } }
|
|
65
|
+
});
|
|
66
|
+
return { collectionId, ok: true, status: response.status };
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const status = err.status ??
|
|
70
|
+
err.response?.status;
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
return { collectionId, ok: false, status, error: message };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=wasRemoteStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasRemoteStore.js","sourceRoot":"","sources":["../../src/storage/wasRemoteStore.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAA;AAW7D,MAAM,OAAO,cAAc;IACT,GAAG,CAAW;IACd,SAAS,CAAQ;IACjB,OAAO,CAAQ;IACd,eAAe,CAAuB;IAEvD,YAAoB,EAClB,GAAG,EACH,MAAM,EAIP;QACC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAA;QACjC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;QAC7B,IAAI,CAAC,eAAe,GAAG,MAAM,CAAC,cAAc,CAAA;IAC9C,CAAC;IAED;;;;;;;;;;OAUG;IACH,MAAM,CAAC,UAAU,CAAC,EAChB,MAAM,EACN,UAAU,EAIX;QACC,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,UAAU;YACV,UAAU,EAAE,mBAAmB,CAAC,EAAE,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;SACnE,CAAC,CAAA;QACF,OAAO,IAAI,cAAc,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAA;IAC5C,CAAC;IAED;;;;;;;OAOG;IACH,oBAAoB,CAAC,YAAoB;QACvC,OAAO,IAAI,CAAC,eAAe,CAAC,YAAY,CAAC,CAAA;IAC3C,CAAC;IAED;;;;;;;;;OASG;IACH,KAAK,CAAC,uBAAuB,CAAC,YAAoB;QAChD,MAAM,UAAU,GAAG,IAAI,CAAC,oBAAoB,CAAC,YAAY,CAAC,CAAA;QAC1D,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,eAAe,EAAE,CAAA;QAC5D,CAAC;QACD,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;gBACtC,UAAU;gBACV,IAAI,EAAE,UAAU,IAAI,CAAC,OAAO,IAAI,YAAY,EAAE;gBAC9C,MAAM,EAAE,KAAK;gBACb,IAAI,EAAE,EAAE,EAAE,EAAE,YAAY,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;aAC1D,CAAC,CAAA;YACF,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAA;QAC5D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GACT,GAA2B,CAAC,MAAM;gBAClC,GAA0C,CAAC,QAAQ,EAAE,MAAM,CAAA;YAC9D,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAChE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAA;QAC5D,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Shared WAS replication bootstrap: given a parsed grant set and the invoking
|
|
6
|
+
* ZcapClient, builds the delegated {@link WasRemoteStore}, best-effort marks
|
|
7
|
+
* each collection encrypted, and starts the supplied {@link SyncController} with
|
|
8
|
+
* reactive store patching. The caller injects the opened localStore, the
|
|
9
|
+
* controller, and the per-doc `onRemoteChange` patcher (typically wired to the
|
|
10
|
+
* rehydrate mechanism over the app's store registry) rather than this module
|
|
11
|
+
* reaching for app-side globals.
|
|
12
|
+
*/
|
|
13
|
+
import type { ZcapClient } from '@interop/ezcap';
|
|
14
|
+
import type { RxChangeEvent } from 'rxdb/plugins/core';
|
|
15
|
+
import type { SyncedDoc } from '../sync/index.js';
|
|
16
|
+
import type { ParsedGrants } from '../grants.js';
|
|
17
|
+
import { WasRemoteStore } from './wasRemoteStore.js';
|
|
18
|
+
import type { LocalStore } from './localStore.js';
|
|
19
|
+
import type { SyncController } from './syncController.js';
|
|
20
|
+
/**
|
|
21
|
+
* Builds the remote store and starts background replication.
|
|
22
|
+
*
|
|
23
|
+
* @param options {object}
|
|
24
|
+
* @param options.parsed {ParsedGrants}
|
|
25
|
+
* @param options.zcapClient {ZcapClient} invocation signer = grants' controller
|
|
26
|
+
* @param options.localStore {LocalStore} the opened local encrypted replica
|
|
27
|
+
* @param options.syncController {SyncController} a fresh per-session controller
|
|
28
|
+
* @param options.onRemoteChange {(collectionKey, event) => void} per-doc
|
|
29
|
+
* reactive patcher for pulled/conflict-resolved remote changes
|
|
30
|
+
* @param [options.onAuthError] {() => void} fired when replication hits a
|
|
31
|
+
* 401/403 (expired/revoked access) -- wired to the reconnect banner
|
|
32
|
+
* @returns {Promise<WasRemoteStore>}
|
|
33
|
+
*/
|
|
34
|
+
export declare function startWasSync({ parsed, zcapClient, localStore, syncController, onRemoteChange, onAuthError }: {
|
|
35
|
+
parsed: ParsedGrants;
|
|
36
|
+
zcapClient: ZcapClient;
|
|
37
|
+
localStore: LocalStore;
|
|
38
|
+
syncController: SyncController;
|
|
39
|
+
onRemoteChange: (collectionKey: string, event: RxChangeEvent<SyncedDoc>) => void;
|
|
40
|
+
onAuthError?: () => void;
|
|
41
|
+
}): Promise<WasRemoteStore>;
|
|
42
|
+
//# sourceMappingURL=wasSync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasSync.d.ts","sourceRoot":"","sources":["../../src/storage/wasSync.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjD,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;;;;;;;;;;;;;GAaG;AACH,wBAAsB,YAAY,CAAC,EACjC,MAAM,EACN,UAAU,EACV,UAAU,EACV,cAAc,EACd,cAAc,EACd,WAAW,EACZ,EAAE;IACD,MAAM,EAAE,YAAY,CAAA;IACpB,UAAU,EAAE,UAAU,CAAA;IACtB,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;CACzB,GAAG,OAAO,CAAC,cAAc,CAAC,CAuB1B"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { WasRemoteStore } from './wasRemoteStore.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the remote store and starts background replication.
|
|
4
|
+
*
|
|
5
|
+
* @param options {object}
|
|
6
|
+
* @param options.parsed {ParsedGrants}
|
|
7
|
+
* @param options.zcapClient {ZcapClient} invocation signer = grants' controller
|
|
8
|
+
* @param options.localStore {LocalStore} the opened local encrypted replica
|
|
9
|
+
* @param options.syncController {SyncController} a fresh per-session controller
|
|
10
|
+
* @param options.onRemoteChange {(collectionKey, event) => void} per-doc
|
|
11
|
+
* reactive patcher for pulled/conflict-resolved remote changes
|
|
12
|
+
* @param [options.onAuthError] {() => void} fired when replication hits a
|
|
13
|
+
* 401/403 (expired/revoked access) -- wired to the reconnect banner
|
|
14
|
+
* @returns {Promise<WasRemoteStore>}
|
|
15
|
+
*/
|
|
16
|
+
export async function startWasSync({ parsed, zcapClient, localStore, syncController, onRemoteChange, onAuthError }) {
|
|
17
|
+
const remoteStore = WasRemoteStore.fromGrants({ parsed, zcapClient });
|
|
18
|
+
// Best-effort encryption marker; non-fatal either way (envelopes replicate
|
|
19
|
+
// into an unmarked collection just the same).
|
|
20
|
+
await Promise.all(Object.keys(parsed.byCollectionId).map(async (collectionId) => {
|
|
21
|
+
const result = await remoteStore.markCollectionEncrypted(collectionId);
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
console.warn(`Encryption marker PUT not authorized for "${collectionId}" (status ${result.status ?? 'n/a'}).`);
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
await syncController.start({
|
|
27
|
+
remoteStore,
|
|
28
|
+
localStore,
|
|
29
|
+
onRemoteChange,
|
|
30
|
+
...(onAuthError && { onAuthError })
|
|
31
|
+
});
|
|
32
|
+
return remoteStore;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=wasSync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wasSync.js","sourceRoot":"","sources":["../../src/storage/wasSync.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAIpD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,EACjC,MAAM,EACN,UAAU,EACV,UAAU,EACV,cAAc,EACd,cAAc,EACd,WAAW,EAWZ;IACC,MAAM,WAAW,GAAG,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;IAErE,2EAA2E;IAC3E,8CAA8C;IAC9C,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;IACH,CAAC,CAAC,CACH,CAAA;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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The pull side of the WAS replication adapter: the `changes`-feed request /
|
|
6
|
+
* response mapping and the RxDB pull handler built from it. Maps each wire
|
|
7
|
+
* document (`{ id, _deleted, updatedAt, version, metaVersion?, data?, custom? }`)
|
|
8
|
+
* into an RxDB `WithDeleted<SyncedDoc>`, and applies the empty-page
|
|
9
|
+
* `checkpoint: null` rule.
|
|
10
|
+
*/
|
|
11
|
+
import type { WithDeleted } from 'rxdb/plugins/core';
|
|
12
|
+
import type { SyncCheckpoint, SyncedDoc, WasSyncPort, WireDoc } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Maps one `changes`-feed wire document into an RxDB document. The envelope
|
|
15
|
+
* fields map straight across; the content body stays under `data` (omitted for
|
|
16
|
+
* tombstones, which carry no `data`) and the metadata body under `custom`.
|
|
17
|
+
* `metaVersion` / `custom` are present only once metadata has been written for
|
|
18
|
+
* the resource, and are simply absent otherwise (forward-compatible with a
|
|
19
|
+
* server that does not yet surface them on the feed). `_deleted` becomes RxDB's
|
|
20
|
+
* native deleted flag.
|
|
21
|
+
*
|
|
22
|
+
* @param doc {WireDoc}
|
|
23
|
+
* @returns {WithDeleted<SyncedDoc>}
|
|
24
|
+
*/
|
|
25
|
+
export declare function wireDocToRxDoc(doc: WireDoc): WithDeleted<SyncedDoc>;
|
|
26
|
+
/**
|
|
27
|
+
* Builds the RxDB pull handler that fetches one `changes` page per call and
|
|
28
|
+
* resumes from the previous checkpoint. RxDB passes the last stored checkpoint
|
|
29
|
+
* (`undefined` on the first pull -- which the port omits from the request) and
|
|
30
|
+
* the batch size.
|
|
31
|
+
*
|
|
32
|
+
* The empty-page rule: when the server returns `checkpoint: null` (no change),
|
|
33
|
+
* keep the checkpoint RxDB gave us rather than persisting `null`, so the next
|
|
34
|
+
* pull resumes from the same position instead of restarting the feed.
|
|
35
|
+
*
|
|
36
|
+
* @param port {WasSyncPort}
|
|
37
|
+
* @returns {(lastCheckpoint: SyncCheckpoint | undefined, batchSize: number) =>
|
|
38
|
+
* Promise<{ documents: WithDeleted<SyncedDoc>[], checkpoint: SyncCheckpoint | undefined }>}
|
|
39
|
+
*/
|
|
40
|
+
export declare function createPullHandler(port: WasSyncPort): (lastCheckpoint: SyncCheckpoint | undefined, batchSize: number) => Promise<{
|
|
41
|
+
documents: WithDeleted<SyncedDoc>[];
|
|
42
|
+
checkpoint: SyncCheckpoint | undefined;
|
|
43
|
+
}>;
|
|
44
|
+
//# sourceMappingURL=changesQuery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changesQuery.d.ts","sourceRoot":"","sources":["../../src/sync/changesQuery.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,KAAK,EACV,cAAc,EACd,SAAS,EACT,WAAW,EACX,OAAO,EACR,MAAM,YAAY,CAAA;AAEnB;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,CAAC,SAAS,CAAC,CAiBnE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,IAE/C,gBAAgB,cAAc,GAAG,SAAS,EAC1C,WAAW,MAAM,KAChB,OAAO,CAAC;IACT,SAAS,EAAE,WAAW,CAAC,SAAS,CAAC,EAAE,CAAA;IACnC,UAAU,EAAE,cAAc,GAAG,SAAS,CAAA;CACvC,CAAC,CAcH"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps one `changes`-feed wire document into an RxDB document. The envelope
|
|
3
|
+
* fields map straight across; the content body stays under `data` (omitted for
|
|
4
|
+
* tombstones, which carry no `data`) and the metadata body under `custom`.
|
|
5
|
+
* `metaVersion` / `custom` are present only once metadata has been written for
|
|
6
|
+
* the resource, and are simply absent otherwise (forward-compatible with a
|
|
7
|
+
* server that does not yet surface them on the feed). `_deleted` becomes RxDB's
|
|
8
|
+
* native deleted flag.
|
|
9
|
+
*
|
|
10
|
+
* @param doc {WireDoc}
|
|
11
|
+
* @returns {WithDeleted<SyncedDoc>}
|
|
12
|
+
*/
|
|
13
|
+
export function wireDocToRxDoc(doc) {
|
|
14
|
+
const rxDoc = {
|
|
15
|
+
id: doc.id,
|
|
16
|
+
updatedAt: doc.updatedAt,
|
|
17
|
+
version: doc.version,
|
|
18
|
+
_deleted: doc._deleted
|
|
19
|
+
};
|
|
20
|
+
if (doc.data !== undefined) {
|
|
21
|
+
rxDoc.data = doc.data;
|
|
22
|
+
}
|
|
23
|
+
if (doc.metaVersion !== undefined) {
|
|
24
|
+
rxDoc.metaVersion = doc.metaVersion;
|
|
25
|
+
}
|
|
26
|
+
if (doc.custom !== undefined) {
|
|
27
|
+
rxDoc.custom = doc.custom;
|
|
28
|
+
}
|
|
29
|
+
return rxDoc;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Builds the RxDB pull handler that fetches one `changes` page per call and
|
|
33
|
+
* resumes from the previous checkpoint. RxDB passes the last stored checkpoint
|
|
34
|
+
* (`undefined` on the first pull -- which the port omits from the request) and
|
|
35
|
+
* the batch size.
|
|
36
|
+
*
|
|
37
|
+
* The empty-page rule: when the server returns `checkpoint: null` (no change),
|
|
38
|
+
* keep the checkpoint RxDB gave us rather than persisting `null`, so the next
|
|
39
|
+
* pull resumes from the same position instead of restarting the feed.
|
|
40
|
+
*
|
|
41
|
+
* @param port {WasSyncPort}
|
|
42
|
+
* @returns {(lastCheckpoint: SyncCheckpoint | undefined, batchSize: number) =>
|
|
43
|
+
* Promise<{ documents: WithDeleted<SyncedDoc>[], checkpoint: SyncCheckpoint | undefined }>}
|
|
44
|
+
*/
|
|
45
|
+
export function createPullHandler(port) {
|
|
46
|
+
return async function pull(lastCheckpoint, batchSize) {
|
|
47
|
+
const response = await port.query({
|
|
48
|
+
// Omit `checkpoint` entirely on the first pull (the port sends no
|
|
49
|
+
// checkpoint field, not `null`).
|
|
50
|
+
...(lastCheckpoint !== undefined && { checkpoint: lastCheckpoint }),
|
|
51
|
+
limit: batchSize
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
documents: response.documents.map(wireDocToRxDoc),
|
|
55
|
+
// Empty page (`checkpoint: null`) means "no change": keep the prior
|
|
56
|
+
// checkpoint so the feed does not restart from the beginning.
|
|
57
|
+
checkpoint: response.checkpoint ?? lastCheckpoint
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=changesQuery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changesQuery.js","sourceRoot":"","sources":["../../src/sync/changesQuery.ts"],"names":[],"mappings":"AAkBA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,GAAY;IACzC,MAAM,KAAK,GAA2B;QACpC,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,SAAS,EAAE,GAAG,CAAC,SAAS;QACxB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,QAAQ,EAAE,GAAG,CAAC,QAAQ;KACvB,CAAA;IACD,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAA;IACvB,CAAC;IACD,IAAI,GAAG,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QAClC,KAAK,CAAC,WAAW,GAAG,GAAG,CAAC,WAAW,CAAA;IACrC,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC7B,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAA;IAC3B,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAiB;IACjD,OAAO,KAAK,UAAU,IAAI,CACxB,cAA0C,EAC1C,SAAiB;QAKjB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC;YAChC,kEAAkE;YAClE,iCAAiC;YACjC,GAAG,CAAC,cAAc,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,cAAc,EAAE,CAAC;YACnE,KAAK,EAAE,SAAS;SACjB,CAAC,CAAA;QACF,OAAO;YACL,SAAS,EAAE,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,cAAc,CAAC;YACjD,oEAAoE;YACpE,8DAA8D;YAC9D,UAAU,EAAE,QAAQ,CAAC,UAAU,IAAI,cAAc;SAClD,CAAA;IACH,CAAC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changesQuery.test.d.ts","sourceRoot":"","sources":["../../src/sync/changesQuery.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Unit tests for the pull side of the sync adapter (the `changes`-feed mapping
|
|
6
|
+
* and pull handler), driven by a fake WAS port -- no server, no RxDB engine.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
9
|
+
import { createPullHandler, wireDocToRxDoc } from './changesQuery.js';
|
|
10
|
+
/**
|
|
11
|
+
* A minimal fake port whose `query` replays a scripted list of pages. Only the
|
|
12
|
+
* methods the pull path uses are implemented; the write methods throw.
|
|
13
|
+
*/
|
|
14
|
+
function fakePullPort(pages) {
|
|
15
|
+
const calls = [];
|
|
16
|
+
let index = 0;
|
|
17
|
+
return {
|
|
18
|
+
calls,
|
|
19
|
+
async query(options) {
|
|
20
|
+
calls.push(options);
|
|
21
|
+
const page = pages[index] ?? { documents: [], checkpoint: null };
|
|
22
|
+
index += 1;
|
|
23
|
+
return page;
|
|
24
|
+
},
|
|
25
|
+
putContent: vi.fn(),
|
|
26
|
+
deleteContent: vi.fn(),
|
|
27
|
+
putMeta: vi.fn(),
|
|
28
|
+
get: vi.fn()
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
describe('wireDocToRxDoc', () => {
|
|
32
|
+
it('maps a live content document, nesting the body under data', () => {
|
|
33
|
+
const doc = {
|
|
34
|
+
id: 'abc',
|
|
35
|
+
_deleted: false,
|
|
36
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
37
|
+
version: 3,
|
|
38
|
+
data: { hello: 'world' }
|
|
39
|
+
};
|
|
40
|
+
expect(wireDocToRxDoc(doc)).toEqual({
|
|
41
|
+
id: 'abc',
|
|
42
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
43
|
+
version: 3,
|
|
44
|
+
data: { hello: 'world' },
|
|
45
|
+
_deleted: false
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('carries metaVersion and the custom envelope when present', () => {
|
|
49
|
+
const doc = {
|
|
50
|
+
id: 'abc',
|
|
51
|
+
_deleted: false,
|
|
52
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
53
|
+
version: 3,
|
|
54
|
+
metaVersion: 2,
|
|
55
|
+
data: { hello: 'world' },
|
|
56
|
+
custom: { jwe: { ciphertext: '...' } }
|
|
57
|
+
};
|
|
58
|
+
expect(wireDocToRxDoc(doc)).toEqual({
|
|
59
|
+
id: 'abc',
|
|
60
|
+
updatedAt: '2026-01-01T00:00:00Z',
|
|
61
|
+
version: 3,
|
|
62
|
+
metaVersion: 2,
|
|
63
|
+
data: { hello: 'world' },
|
|
64
|
+
custom: { jwe: { ciphertext: '...' } },
|
|
65
|
+
_deleted: false
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
it('projects a tombstone with no data and no metadata', () => {
|
|
69
|
+
const doc = {
|
|
70
|
+
id: 'gone',
|
|
71
|
+
_deleted: true,
|
|
72
|
+
updatedAt: '2026-01-02T00:00:00Z',
|
|
73
|
+
version: 4
|
|
74
|
+
};
|
|
75
|
+
const rx = wireDocToRxDoc(doc);
|
|
76
|
+
expect(rx).toEqual({
|
|
77
|
+
id: 'gone',
|
|
78
|
+
updatedAt: '2026-01-02T00:00:00Z',
|
|
79
|
+
version: 4,
|
|
80
|
+
_deleted: true
|
|
81
|
+
});
|
|
82
|
+
expect('data' in rx).toBe(false);
|
|
83
|
+
expect('custom' in rx).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('createPullHandler', () => {
|
|
87
|
+
const cpA = { id: 'a', updatedAt: '2026-01-01T00:00:01Z' };
|
|
88
|
+
const cpB = { id: 'b', updatedAt: '2026-01-01T00:00:02Z' };
|
|
89
|
+
it('omits the checkpoint on the first pull and forwards it on resume', async () => {
|
|
90
|
+
const port = fakePullPort([
|
|
91
|
+
{
|
|
92
|
+
documents: [
|
|
93
|
+
{ id: 'a', _deleted: false, updatedAt: cpA.updatedAt, version: 1 }
|
|
94
|
+
],
|
|
95
|
+
checkpoint: cpA
|
|
96
|
+
}
|
|
97
|
+
]);
|
|
98
|
+
const pull = createPullHandler(port);
|
|
99
|
+
const first = await pull(undefined, 100);
|
|
100
|
+
expect(port.calls[0]).toEqual({ limit: 100 });
|
|
101
|
+
expect('checkpoint' in port.calls[0]).toBe(false);
|
|
102
|
+
expect(first.checkpoint).toEqual(cpA);
|
|
103
|
+
expect(first.documents).toHaveLength(1);
|
|
104
|
+
await pull(cpA, 50);
|
|
105
|
+
expect(port.calls[1]).toEqual({ checkpoint: cpA, limit: 50 });
|
|
106
|
+
});
|
|
107
|
+
it('iterates: each page returns its own checkpoint to resume from', async () => {
|
|
108
|
+
const port = fakePullPort([
|
|
109
|
+
{
|
|
110
|
+
documents: [
|
|
111
|
+
{ id: 'a', _deleted: false, updatedAt: cpA.updatedAt, version: 1 }
|
|
112
|
+
],
|
|
113
|
+
checkpoint: cpA
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
documents: [
|
|
117
|
+
{ id: 'b', _deleted: false, updatedAt: cpB.updatedAt, version: 1 }
|
|
118
|
+
],
|
|
119
|
+
checkpoint: cpB
|
|
120
|
+
}
|
|
121
|
+
]);
|
|
122
|
+
const pull = createPullHandler(port);
|
|
123
|
+
const page1 = await pull(undefined, 100);
|
|
124
|
+
const page2 = await pull(page1.checkpoint, 100);
|
|
125
|
+
expect(page1.checkpoint).toEqual(cpA);
|
|
126
|
+
expect(page2.checkpoint).toEqual(cpB);
|
|
127
|
+
expect(page2.documents[0].id).toBe('b');
|
|
128
|
+
});
|
|
129
|
+
it('keeps the prior checkpoint on an empty page (checkpoint: null)', async () => {
|
|
130
|
+
const port = fakePullPort([{ documents: [], checkpoint: null }]);
|
|
131
|
+
const pull = createPullHandler(port);
|
|
132
|
+
const result = await pull(cpA, 100);
|
|
133
|
+
// The empty-page rule: do NOT persist null -- resume from the same position.
|
|
134
|
+
expect(result.documents).toEqual([]);
|
|
135
|
+
expect(result.checkpoint).toEqual(cpA);
|
|
136
|
+
});
|
|
137
|
+
it('returns undefined checkpoint on a first, empty pull', async () => {
|
|
138
|
+
const port = fakePullPort([{ documents: [], checkpoint: null }]);
|
|
139
|
+
const pull = createPullHandler(port);
|
|
140
|
+
const result = await pull(undefined, 100);
|
|
141
|
+
expect(result.documents).toEqual([]);
|
|
142
|
+
expect(result.checkpoint).toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
//# sourceMappingURL=changesQuery.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changesQuery.test.js","sourceRoot":"","sources":["../../src/sync/changesQuery.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The per-collection EDV document cipher: the local encrypt/decrypt seam. It
|
|
6
|
+
* wraps the same `@interop/was-client` EDV codec the remote WAS handles use,
|
|
7
|
+
* pointed at the local replica: `encrypt` turns a JSON document into its stored
|
|
8
|
+
* EDV envelope (`{ id, sequence, jwe }`) minting a stable random resource id
|
|
9
|
+
* (`idDerivation: 'random'`), and `encryptUpdate` re-encrypts a mutable head
|
|
10
|
+
* document under its EXISTING id, advancing the envelope `sequence` from the
|
|
11
|
+
* prior envelope. `decrypt` reverses it. The sync layer moves the envelope
|
|
12
|
+
* verbatim; it never touches these keys.
|
|
13
|
+
*/
|
|
14
|
+
import type { IKeyAgreementKey, IKeyResolver } from '@interop/data-integrity-core';
|
|
15
|
+
import type { Json } from './types.js';
|
|
16
|
+
/**
|
|
17
|
+
* A per-collection document cipher. `encrypt` is the create path (mints a random
|
|
18
|
+
* envelope id); `encryptUpdate` is the in-place update path (re-encrypts under
|
|
19
|
+
* an existing id, advancing `sequence` from the prior envelope); `decrypt`
|
|
20
|
+
* reverses either.
|
|
21
|
+
*/
|
|
22
|
+
export interface DocCipher {
|
|
23
|
+
encrypt(options: {
|
|
24
|
+
data: Json;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
id: string;
|
|
27
|
+
envelope: Json;
|
|
28
|
+
}>;
|
|
29
|
+
encryptUpdate(options: {
|
|
30
|
+
id: string;
|
|
31
|
+
data: Json;
|
|
32
|
+
current: Json;
|
|
33
|
+
}): Promise<{
|
|
34
|
+
id: string;
|
|
35
|
+
envelope: Json;
|
|
36
|
+
}>;
|
|
37
|
+
decrypt(options: {
|
|
38
|
+
envelope: Json;
|
|
39
|
+
}): Promise<Json>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whether a stored body is an EDV encryption envelope (carries an object `jwe`)
|
|
43
|
+
* rather than plaintext. Lets read paths stay tolerant of any legacy plaintext
|
|
44
|
+
* row.
|
|
45
|
+
*
|
|
46
|
+
* @param data {Json | undefined}
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
export declare function isEncryptedEnvelope(data: Json | undefined): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Builds a {@link DocCipher} for one collection from its derived key material
|
|
52
|
+
* (the per-collection X25519 key agreement key). Keys are supplied directly (no
|
|
53
|
+
* keystore lookup). `idDerivation: 'random'` mints a stable random id updated in
|
|
54
|
+
* place via `sequence` -- the mutable head-document model every entity here uses
|
|
55
|
+
* (constant bump / toggle / re-categorize edits).
|
|
56
|
+
*
|
|
57
|
+
* @param options {object}
|
|
58
|
+
* @param options.keyAgreementKey {IKeyAgreementKey}
|
|
59
|
+
* @param options.keyResolver {IKeyResolver}
|
|
60
|
+
* @param options.collectionId {string} labels errors; the codec is agnostic
|
|
61
|
+
* @returns {Promise<DocCipher>}
|
|
62
|
+
*/
|
|
63
|
+
export declare function createDocCipher({ keyAgreementKey, keyResolver, collectionId }: {
|
|
64
|
+
keyAgreementKey: IKeyAgreementKey;
|
|
65
|
+
keyResolver: IKeyResolver;
|
|
66
|
+
collectionId: string;
|
|
67
|
+
}): Promise<DocCipher>;
|
|
68
|
+
//# sourceMappingURL=docCipher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docCipher.d.ts","sourceRoot":"","sources":["../../src/sync/docCipher.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;GASG;AACH,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACb,MAAM,8BAA8B,CAAA;AAErC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAC,CAAA;IACzE,aAAa,CAAC,OAAO,EAAE;QACrB,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,IAAI,CAAA;QACV,OAAO,EAAE,IAAI,CAAA;KACd,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAC,CAAA;IAC3C,OAAO,CAAC,OAAO,EAAE;QAAE,QAAQ,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpD;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,GAAG,OAAO,CAMnE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CAAC,EACpC,eAAe,EACf,WAAW,EACX,YAAY,EACb,EAAE;IACD,eAAe,EAAE,gBAAgB,CAAA;IACjC,WAAW,EAAE,YAAY,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC,SAAS,CAAC,CA+ErB"}
|