@interop/was-client 0.17.0 → 0.19.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 +88 -0
- package/dist/Collection.d.ts +1 -46
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +77 -79
- package/dist/Collection.js.map +1 -1
- package/dist/Resource.d.ts +1 -51
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +61 -58
- package/dist/Resource.js.map +1 -1
- package/dist/Space.d.ts +1 -27
- package/dist/Space.d.ts.map +1 -1
- package/dist/Space.js +65 -65
- package/dist/Space.js.map +1 -1
- package/dist/WasClient.d.ts +1 -12
- package/dist/WasClient.d.ts.map +1 -1
- package/dist/WasClient.js +15 -15
- package/dist/WasClient.js.map +1 -1
- package/dist/codec.d.ts +15 -2
- package/dist/codec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.d.ts +13 -95
- package/dist/edv/EdvCodec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.js +221 -52
- package/dist/edv/EdvCodec.js.map +1 -1
- package/dist/edv/WasTransport.d.ts +1 -53
- package/dist/edv/WasTransport.d.ts.map +1 -1
- package/dist/edv/WasTransport.js +29 -29
- package/dist/edv/WasTransport.js.map +1 -1
- package/dist/edv/docCipher.d.ts +111 -0
- package/dist/edv/docCipher.d.ts.map +1 -0
- package/dist/edv/docCipher.js +236 -0
- package/dist/edv/docCipher.js.map +1 -0
- package/dist/edv/epochKeys.d.ts.map +1 -1
- package/dist/edv/epochKeys.js +46 -16
- package/dist/edv/epochKeys.js.map +1 -1
- package/dist/edv/epochMac.d.ts +32 -0
- package/dist/edv/epochMac.d.ts.map +1 -0
- package/dist/edv/epochMac.js +117 -0
- package/dist/edv/epochMac.js.map +1 -0
- package/dist/edv/index.d.ts +2 -0
- package/dist/edv/index.d.ts.map +1 -1
- package/dist/edv/index.js +1 -0
- package/dist/edv/index.js.map +1 -1
- package/dist/edv/recipients.d.ts.map +1 -1
- package/dist/edv/recipients.js +26 -3
- package/dist/edv/recipients.js.map +1 -1
- package/dist/errors.d.ts +22 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/codec.d.ts +1 -2
- package/dist/internal/codec.d.ts.map +1 -1
- package/dist/internal/codec.js +10 -10
- package/dist/internal/codec.js.map +1 -1
- package/dist/internal/features.d.ts +1 -12
- package/dist/internal/features.d.ts.map +1 -1
- package/dist/internal/features.js +8 -8
- package/dist/internal/features.js.map +1 -1
- package/dist/internal/pagination.d.ts +1 -1
- package/dist/internal/pagination.js +1 -1
- package/dist/sync/cid.d.ts +31 -0
- package/dist/sync/cid.d.ts.map +1 -0
- package/dist/sync/cid.js +55 -0
- package/dist/sync/cid.js.map +1 -0
- package/dist/sync/envelope.d.ts +20 -0
- package/dist/sync/envelope.d.ts.map +1 -0
- package/dist/sync/envelope.js +17 -0
- package/dist/sync/envelope.js.map +1 -0
- package/dist/sync/index.d.ts +29 -0
- package/dist/sync/index.d.ts.map +1 -0
- package/dist/sync/index.js +28 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/plaintextCipher.d.ts +16 -0
- package/dist/sync/plaintextCipher.d.ts.map +1 -0
- package/dist/sync/plaintextCipher.js +36 -0
- package/dist/sync/plaintextCipher.js.map +1 -0
- package/dist/sync/port.d.ts +78 -0
- package/dist/sync/port.d.ts.map +1 -0
- package/dist/sync/port.js +204 -0
- package/dist/sync/port.js.map +1 -0
- package/dist/sync/provisioning.d.ts +41 -0
- package/dist/sync/provisioning.d.ts.map +1 -0
- package/dist/sync/provisioning.js +44 -0
- package/dist/sync/provisioning.js.map +1 -0
- package/dist/sync/types.d.ts +166 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +2 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -5
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The `@interop/was-client/sync` subpath entry: cross-replica synchronization
|
|
6
|
+
* support over WAS. Kept off the core entry so a consumer opts in.
|
|
7
|
+
*
|
|
8
|
+
* - `createWasSyncPort` -- the `WasSyncPort` over `was.request()` + the
|
|
9
|
+
* `changes` feed, moving stored bodies verbatim (no codec) for one Space +
|
|
10
|
+
* Collection.
|
|
11
|
+
* - `contentCid` / `cidFrom` / `deriveSpaceId` -- content-addressed ids and
|
|
12
|
+
* Space-id derivation, byte-identical across replicas.
|
|
13
|
+
* - `createPlaintextDocCipher` / `isEncryptedEnvelope` -- the identity cipher
|
|
14
|
+
* for a plaintext content-addressed collection, and the envelope predicate,
|
|
15
|
+
* both free of the `@interop/was-client/edv` crypto graph.
|
|
16
|
+
* - `ensureSpaceAndCollection` -- idempotent Space + Collection provisioning.
|
|
17
|
+
*
|
|
18
|
+
* The 412 conflict / 404 not-found port signals (`WasSyncConflictError` /
|
|
19
|
+
* `WasSyncNotFoundError`) live in the client's typed error hierarchy and are
|
|
20
|
+
* re-exported here for convenience.
|
|
21
|
+
*/
|
|
22
|
+
export { createWasSyncPort, WAS_KEY_EPOCH_HEADER, formatEtag, parseEtag, errorStatus } from './port.js';
|
|
23
|
+
export { contentCid, cidFrom, deriveSpaceId } from './cid.js';
|
|
24
|
+
export { isEncryptedEnvelope } from './envelope.js';
|
|
25
|
+
export { createPlaintextDocCipher } from './plaintextCipher.js';
|
|
26
|
+
export { ensureSpaceAndCollection } from './provisioning.js';
|
|
27
|
+
export { WasSyncConflictError, WasSyncNotFoundError } from '../errors.js';
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/sync/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,UAAU,EACV,SAAS,EACT,WAAW,EACZ,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AAC5D,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DocCipher } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the identity {@link DocCipher} for a plaintext, content-addressed,
|
|
4
|
+
* insert-only collection. `encrypt` is the identity transform with a content-id
|
|
5
|
+
* key; `decrypt` returns the stored body unchanged; `encryptUpdate` throws -- a
|
|
6
|
+
* content-addressed document is never updated in place (a changed document is a
|
|
7
|
+
* different id).
|
|
8
|
+
*
|
|
9
|
+
* @param options {object}
|
|
10
|
+
* @param options.collectionId {string} labels the `encryptUpdate` error
|
|
11
|
+
* @returns {DocCipher}
|
|
12
|
+
*/
|
|
13
|
+
export declare function createPlaintextDocCipher({ collectionId }: {
|
|
14
|
+
collectionId: string;
|
|
15
|
+
}): DocCipher;
|
|
16
|
+
//# sourceMappingURL=plaintextCipher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plaintextCipher.d.ts","sourceRoot":"","sources":["../../src/sync/plaintextCipher.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAQ,MAAM,YAAY,CAAA;AAEjD;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,EACvC,YAAY,EACb,EAAE;IACD,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,SAAS,CAiBZ"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The no-encryption (identity) {@link DocCipher} for a plaintext, content-
|
|
6
|
+
* addressed collection: the stored "envelope" IS the document, and the resource
|
|
7
|
+
* id is the document's content id. Kept free of the `@interop/was-client/edv`
|
|
8
|
+
* crypto graph so a plaintext-only consumer imports nothing it does not need.
|
|
9
|
+
*/
|
|
10
|
+
import { contentCid } from './cid.js';
|
|
11
|
+
/**
|
|
12
|
+
* Builds the identity {@link DocCipher} for a plaintext, content-addressed,
|
|
13
|
+
* insert-only collection. `encrypt` is the identity transform with a content-id
|
|
14
|
+
* key; `decrypt` returns the stored body unchanged; `encryptUpdate` throws -- a
|
|
15
|
+
* content-addressed document is never updated in place (a changed document is a
|
|
16
|
+
* different id).
|
|
17
|
+
*
|
|
18
|
+
* @param options {object}
|
|
19
|
+
* @param options.collectionId {string} labels the `encryptUpdate` error
|
|
20
|
+
* @returns {DocCipher}
|
|
21
|
+
*/
|
|
22
|
+
export function createPlaintextDocCipher({ collectionId }) {
|
|
23
|
+
return {
|
|
24
|
+
async encrypt({ data }) {
|
|
25
|
+
return { id: contentCid(data), envelope: data };
|
|
26
|
+
},
|
|
27
|
+
async encryptUpdate() {
|
|
28
|
+
throw new Error(`Collection "${collectionId}" is content-addressed plaintext; ` +
|
|
29
|
+
'documents are never updated in place.');
|
|
30
|
+
},
|
|
31
|
+
async decrypt({ envelope }) {
|
|
32
|
+
return envelope;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=plaintextCipher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plaintextCipher.js","sourceRoot":"","sources":["../../src/sync/plaintextCipher.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAGrC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAC,EACvC,YAAY,EAGb;IACC,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,EAAE,IAAI,EAAkB;YACpC,OAAO,EAAE,EAAE,EAAE,UAAU,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAA;QACjD,CAAC;QAED,KAAK,CAAC,aAAa;YACjB,MAAM,IAAI,KAAK,CACb,eAAe,YAAY,oCAAoC;gBAC7D,uCAAuC,CAC1C,CAAA;QACH,CAAC;QAED,KAAK,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAsB;YAC5C,OAAO,QAAQ,CAAA;QACjB,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* `createWasSyncPort`: the {@link WasSyncPort} implementation over a signed
|
|
6
|
+
* {@link WasClient}, bound to one Space + Collection.
|
|
7
|
+
*
|
|
8
|
+
* Writes and single-resource reads ride the raw, signed `was.request()` escape
|
|
9
|
+
* hatch, which moves the stored body VERBATIM (bypassing the encryption codec).
|
|
10
|
+
* The change feed already ships opaque stored bodies -- plaintext for a
|
|
11
|
+
* plaintext collection, the EDV envelope for an encrypted one -- and push must
|
|
12
|
+
* write those same bytes back unchanged; running them through `resource.put()`
|
|
13
|
+
* would re-encrypt an already-encrypted envelope. Encrypt/decrypt therefore
|
|
14
|
+
* stays a read/write-time concern above the port, and the port itself is
|
|
15
|
+
* collection-agnostic and never touches keys.
|
|
16
|
+
*
|
|
17
|
+
* The pull path rides the client's `Collection.changes()` feed, bound to the
|
|
18
|
+
* same Space + Collection, which produces the byte-identical signed
|
|
19
|
+
* `POST /space/:s/:c/query` (profile `changes`) as a root invocation and, like
|
|
20
|
+
* the raw writes, ships the stored bodies verbatim without decrypting.
|
|
21
|
+
*
|
|
22
|
+
* Conditional writes ride the server's monotonic content `version` (`ETag`),
|
|
23
|
+
* enforced uniformly for plaintext and encrypted resources, so there is no
|
|
24
|
+
* plaintext-vs-encrypted fork. `putContent`/`deleteContent` return the server-
|
|
25
|
+
* acked `version` (parsed from the write's `ETag`, re-read only if the backend
|
|
26
|
+
* sent none), so a caller can record acked revisions immediately.
|
|
27
|
+
*/
|
|
28
|
+
import type { WasClient } from '../WasClient.js';
|
|
29
|
+
import type { WasSyncPort } from './types.js';
|
|
30
|
+
/**
|
|
31
|
+
* The request header the server reads a content write's key-epoch id from,
|
|
32
|
+
* stamping it onto the Resource's metadata (an absent header clears any prior
|
|
33
|
+
* stamp). HTTP header names are case-insensitive; the wire form is `WAS-Key-Epoch`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const WAS_KEY_EPOCH_HEADER = "WAS-Key-Epoch";
|
|
36
|
+
/**
|
|
37
|
+
* Extracts an HTTP status from a raw ky/ezcap error. `was.request()` rejects on
|
|
38
|
+
* any non-2xx with `err.status` set; this reads it defensively from either the
|
|
39
|
+
* flat `status` or the nested `response.status` shape.
|
|
40
|
+
*
|
|
41
|
+
* @param err {unknown} the caught error
|
|
42
|
+
* @returns {number | undefined}
|
|
43
|
+
*/
|
|
44
|
+
export declare function errorStatus(err: unknown): number | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Formats a numeric content `version` as the quoted strong `ETag` an
|
|
47
|
+
* update-if-unchanged write passes as its `ifMatch` precondition (e.g. `3` to
|
|
48
|
+
* `"3"`). Inverse of {@link parseEtag}.
|
|
49
|
+
*
|
|
50
|
+
* @param version {number}
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export declare function formatEtag(version: number): string;
|
|
54
|
+
/**
|
|
55
|
+
* Parses a quoted strong `ETag` (`"3"`) into its numeric revision, or
|
|
56
|
+
* `undefined` when the header is absent or non-numeric (no such revision yet).
|
|
57
|
+
*
|
|
58
|
+
* @param etag {string | null}
|
|
59
|
+
* @returns {number | undefined}
|
|
60
|
+
*/
|
|
61
|
+
export declare function parseEtag(etag: string | null): number | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Builds a {@link WasSyncPort} bound to one Space + Collection, backed by the
|
|
64
|
+
* caller's signed {@link WasClient}. Requests invoke the client's own root
|
|
65
|
+
* capability (no delegated `capability` is attached).
|
|
66
|
+
*
|
|
67
|
+
* @param options {object}
|
|
68
|
+
* @param options.was {WasClient} the session client (holds the signer)
|
|
69
|
+
* @param options.spaceId {string} the WAS Space id
|
|
70
|
+
* @param options.collectionId {string} the WAS collection id
|
|
71
|
+
* @returns {WasSyncPort}
|
|
72
|
+
*/
|
|
73
|
+
export declare function createWasSyncPort({ was, spaceId, collectionId }: {
|
|
74
|
+
was: WasClient;
|
|
75
|
+
spaceId: string;
|
|
76
|
+
collectionId: string;
|
|
77
|
+
}): WasSyncPort;
|
|
78
|
+
//# sourceMappingURL=port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port.d.ts","sourceRoot":"","sources":["../../src/sync/port.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAIhD,OAAO,KAAK,EAIV,WAAW,EAEZ,MAAM,YAAY,CAAA;AAEnB;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,kBAAkB,CAAA;AAWnD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAK5D;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,GAAG,EACH,OAAO,EACP,YAAY,EACb,EAAE;IACD,GAAG,EAAE,SAAS,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,WAAW,CA0Jd"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { writeHeaders } from '../internal/conditional.js';
|
|
2
|
+
import { WasSyncConflictError, WasSyncNotFoundError } from '../errors.js';
|
|
3
|
+
/**
|
|
4
|
+
* The request header the server reads a content write's key-epoch id from,
|
|
5
|
+
* stamping it onto the Resource's metadata (an absent header clears any prior
|
|
6
|
+
* stamp). HTTP header names are case-insensitive; the wire form is `WAS-Key-Epoch`.
|
|
7
|
+
*/
|
|
8
|
+
export const WAS_KEY_EPOCH_HEADER = 'WAS-Key-Epoch';
|
|
9
|
+
/**
|
|
10
|
+
* The placeholder `updatedAt` for a 412-conflict re-read whose resource has no
|
|
11
|
+
* `/meta` document yet (its server-managed timestamp is unknown). An epoch-zero
|
|
12
|
+
* ISO string is a valid, sortable timestamp that sorts before every real one --
|
|
13
|
+
* unlike an empty string, which is not a parseable date. The change feed remains
|
|
14
|
+
* the authority on ordering, so this only feeds the one-off conflict entry.
|
|
15
|
+
*/
|
|
16
|
+
const UNKNOWN_UPDATED_AT = new Date(0).toISOString();
|
|
17
|
+
/**
|
|
18
|
+
* Extracts an HTTP status from a raw ky/ezcap error. `was.request()` rejects on
|
|
19
|
+
* any non-2xx with `err.status` set; this reads it defensively from either the
|
|
20
|
+
* flat `status` or the nested `response.status` shape.
|
|
21
|
+
*
|
|
22
|
+
* @param err {unknown} the caught error
|
|
23
|
+
* @returns {number | undefined}
|
|
24
|
+
*/
|
|
25
|
+
export function errorStatus(err) {
|
|
26
|
+
return (err.status ??
|
|
27
|
+
err.response?.status);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Formats a numeric content `version` as the quoted strong `ETag` an
|
|
31
|
+
* update-if-unchanged write passes as its `ifMatch` precondition (e.g. `3` to
|
|
32
|
+
* `"3"`). Inverse of {@link parseEtag}.
|
|
33
|
+
*
|
|
34
|
+
* @param version {number}
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function formatEtag(version) {
|
|
38
|
+
return `"${version}"`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Parses a quoted strong `ETag` (`"3"`) into its numeric revision, or
|
|
42
|
+
* `undefined` when the header is absent or non-numeric (no such revision yet).
|
|
43
|
+
*
|
|
44
|
+
* @param etag {string | null}
|
|
45
|
+
* @returns {number | undefined}
|
|
46
|
+
*/
|
|
47
|
+
export function parseEtag(etag) {
|
|
48
|
+
if (!etag) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const revision = Number(etag.replace(/"/g, ''));
|
|
52
|
+
return Number.isFinite(revision) ? revision : undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds a {@link WasSyncPort} bound to one Space + Collection, backed by the
|
|
56
|
+
* caller's signed {@link WasClient}. Requests invoke the client's own root
|
|
57
|
+
* capability (no delegated `capability` is attached).
|
|
58
|
+
*
|
|
59
|
+
* @param options {object}
|
|
60
|
+
* @param options.was {WasClient} the session client (holds the signer)
|
|
61
|
+
* @param options.spaceId {string} the WAS Space id
|
|
62
|
+
* @param options.collectionId {string} the WAS collection id
|
|
63
|
+
* @returns {WasSyncPort}
|
|
64
|
+
*/
|
|
65
|
+
export function createWasSyncPort({ was, spaceId, collectionId }) {
|
|
66
|
+
const collectionPath = `/space/${spaceId}/${collectionId}`;
|
|
67
|
+
const resourcePath = (id) => `${collectionPath}/${encodeURIComponent(id)}`;
|
|
68
|
+
// Construction is I/O-free (the codec/feature probes are lazy thunks) and
|
|
69
|
+
// `changes()` never resolves the codec, so it ships the stored bodies
|
|
70
|
+
// verbatim -- what this codec-bypassing port requires.
|
|
71
|
+
const changesCollection = was.space(spaceId).collection(collectionId);
|
|
72
|
+
/** Re-reads a resource's raw content body + version (no decrypt, no `/meta`). */
|
|
73
|
+
const readContent = async (id) => {
|
|
74
|
+
let response;
|
|
75
|
+
try {
|
|
76
|
+
response = await was.request({ path: resourcePath(id), method: 'GET' });
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (errorStatus(err) === 404) {
|
|
80
|
+
return null; // absent or tombstoned -- caller treats as deletion-wins
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
version: parseEtag(response.headers.get('etag')) ?? 0,
|
|
86
|
+
updatedAt: UNKNOWN_UPDATED_AT,
|
|
87
|
+
deleted: false,
|
|
88
|
+
data: response.data
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
/** Resolves the acked version from a write response, or via a content re-read. */
|
|
92
|
+
const ackedVersion = async (response, id) => {
|
|
93
|
+
const version = parseEtag(response.headers.get('etag'));
|
|
94
|
+
if (version !== undefined) {
|
|
95
|
+
return version;
|
|
96
|
+
}
|
|
97
|
+
return (await readContent(id))?.version ?? 0;
|
|
98
|
+
};
|
|
99
|
+
return {
|
|
100
|
+
async query({ checkpoint, limit }) {
|
|
101
|
+
const page = await changesCollection.changes({ checkpoint, limit });
|
|
102
|
+
return {
|
|
103
|
+
documents: page.documents,
|
|
104
|
+
checkpoint: page.checkpoint
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
async putContent({ id, data, ifMatch, ifNoneMatch, epoch }) {
|
|
108
|
+
try {
|
|
109
|
+
const response = await was.request({
|
|
110
|
+
path: resourcePath(id),
|
|
111
|
+
method: 'PUT',
|
|
112
|
+
json: data,
|
|
113
|
+
headers: writeHeaders({
|
|
114
|
+
precondition: { ifMatch, ifNoneMatch },
|
|
115
|
+
epoch
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
return await ackedVersion(response, id);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
if (errorStatus(err) === 412) {
|
|
122
|
+
throw new WasSyncConflictError();
|
|
123
|
+
}
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
async deleteContent({ id, ifMatch }) {
|
|
128
|
+
try {
|
|
129
|
+
const response = await was.request({
|
|
130
|
+
path: resourcePath(id),
|
|
131
|
+
method: 'DELETE',
|
|
132
|
+
headers: writeHeaders({ precondition: { ifMatch } })
|
|
133
|
+
});
|
|
134
|
+
return await ackedVersion(response, id);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
const status = errorStatus(err);
|
|
138
|
+
if (status === 404) {
|
|
139
|
+
throw new WasSyncNotFoundError();
|
|
140
|
+
}
|
|
141
|
+
if (status === 412) {
|
|
142
|
+
throw new WasSyncConflictError();
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
async putMeta({ id, custom, ifMatch, ifNoneMatch }) {
|
|
148
|
+
try {
|
|
149
|
+
await was.request({
|
|
150
|
+
path: `${resourcePath(id)}/meta`,
|
|
151
|
+
method: 'PUT',
|
|
152
|
+
json: { custom },
|
|
153
|
+
headers: writeHeaders({ precondition: { ifMatch, ifNoneMatch } })
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
if (errorStatus(err) === 412) {
|
|
158
|
+
throw new WasSyncConflictError();
|
|
159
|
+
}
|
|
160
|
+
throw err;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
async get({ id }) {
|
|
164
|
+
const master = await readContent(id);
|
|
165
|
+
if (master === null) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
// Metadata re-read (best-effort): the `/meta` body carries the server-
|
|
169
|
+
// managed `updatedAt`, the creator DID, the key-epoch id, and the user-
|
|
170
|
+
// writable `custom`, plus its own `metaVersion` ETag. A resource with no
|
|
171
|
+
// metadata yet 404s here; only a hard error propagates.
|
|
172
|
+
try {
|
|
173
|
+
const metaResponse = await was.request({
|
|
174
|
+
path: `${resourcePath(id)}/meta`,
|
|
175
|
+
method: 'GET'
|
|
176
|
+
});
|
|
177
|
+
const metaBody = metaResponse.data;
|
|
178
|
+
if (metaBody?.updatedAt) {
|
|
179
|
+
master.updatedAt = metaBody.updatedAt;
|
|
180
|
+
}
|
|
181
|
+
if (metaBody?.createdBy !== undefined) {
|
|
182
|
+
master.createdBy = metaBody.createdBy;
|
|
183
|
+
}
|
|
184
|
+
if (metaBody?.epoch !== undefined) {
|
|
185
|
+
master.epoch = metaBody.epoch;
|
|
186
|
+
}
|
|
187
|
+
if (metaBody?.custom !== undefined) {
|
|
188
|
+
master.custom = metaBody.custom;
|
|
189
|
+
}
|
|
190
|
+
const metaVersion = parseEtag(metaResponse.headers.get('etag'));
|
|
191
|
+
if (metaVersion !== undefined) {
|
|
192
|
+
master.metaVersion = metaVersion;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
if (errorStatus(err) !== 404) {
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return master;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=port.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/sync/port.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Idempotent Space + Collection provisioning for a synced collection. Re-running
|
|
6
|
+
* is safe: the server upserts the Space and allows a late encryption-marker
|
|
7
|
+
* declaration, so reconnecting an existing account is a no-op upgrade.
|
|
8
|
+
*/
|
|
9
|
+
import type { WasClient } from '../WasClient.js';
|
|
10
|
+
/**
|
|
11
|
+
* Ensures the controller's Space exists and one synced collection is configured.
|
|
12
|
+
* An `'edv'` collection declares the encryption marker `{ scheme: 'edv' }` (so
|
|
13
|
+
* the server stores only ciphertext it can never decrypt); a `'plaintext'`
|
|
14
|
+
* collection is configured without one, with `force` so the marker-less upsert
|
|
15
|
+
* can create a fresh collection (running with the root capability, a 404 from
|
|
16
|
+
* the pre-merge describe really means absent). A public collection additionally
|
|
17
|
+
* gets a collection-level world-read grant (`setPublic`), which is what makes a
|
|
18
|
+
* resource URL in it resolve for anyone. Runs full-tier -- the client invokes
|
|
19
|
+
* its own root capability.
|
|
20
|
+
*
|
|
21
|
+
* @param options {object}
|
|
22
|
+
* @param options.was {WasClient}
|
|
23
|
+
* @param options.spaceId {string}
|
|
24
|
+
* @param options.controllerDid {string} the Space controller (e.g. `did:key`)
|
|
25
|
+
* @param options.collectionId {string} the WAS collection id
|
|
26
|
+
* @param [options.encryption] {'edv' | 'plaintext'} defaults to `'edv'`
|
|
27
|
+
* @param [options.isPublic] {boolean} grant collection-level world read
|
|
28
|
+
* @param [options.spaceName] {string} the Space display name; defaults to
|
|
29
|
+
* `'WAS Space'`
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
export declare function ensureSpaceAndCollection({ was, spaceId, controllerDid, collectionId, encryption, isPublic, spaceName }: {
|
|
33
|
+
was: WasClient;
|
|
34
|
+
spaceId: string;
|
|
35
|
+
controllerDid: string;
|
|
36
|
+
collectionId: string;
|
|
37
|
+
encryption?: 'edv' | 'plaintext';
|
|
38
|
+
isPublic?: boolean;
|
|
39
|
+
spaceName?: string;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
//# sourceMappingURL=provisioning.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provisioning.d.ts","sourceRoot":"","sources":["../../src/sync/provisioning.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;GAIG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,wBAAwB,CAAC,EAC7C,GAAG,EACH,OAAO,EACP,aAAa,EACb,YAAY,EACZ,UAAkB,EAClB,QAAgB,EAChB,SAAuB,EACxB,EAAE;IACD,GAAG,EAAE,SAAS,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,KAAK,GAAG,WAAW,CAAA;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4BhB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ensures the controller's Space exists and one synced collection is configured.
|
|
3
|
+
* An `'edv'` collection declares the encryption marker `{ scheme: 'edv' }` (so
|
|
4
|
+
* the server stores only ciphertext it can never decrypt); a `'plaintext'`
|
|
5
|
+
* collection is configured without one, with `force` so the marker-less upsert
|
|
6
|
+
* can create a fresh collection (running with the root capability, a 404 from
|
|
7
|
+
* the pre-merge describe really means absent). A public collection additionally
|
|
8
|
+
* gets a collection-level world-read grant (`setPublic`), which is what makes a
|
|
9
|
+
* resource URL in it resolve for anyone. Runs full-tier -- the client invokes
|
|
10
|
+
* its own root capability.
|
|
11
|
+
*
|
|
12
|
+
* @param options {object}
|
|
13
|
+
* @param options.was {WasClient}
|
|
14
|
+
* @param options.spaceId {string}
|
|
15
|
+
* @param options.controllerDid {string} the Space controller (e.g. `did:key`)
|
|
16
|
+
* @param options.collectionId {string} the WAS collection id
|
|
17
|
+
* @param [options.encryption] {'edv' | 'plaintext'} defaults to `'edv'`
|
|
18
|
+
* @param [options.isPublic] {boolean} grant collection-level world read
|
|
19
|
+
* @param [options.spaceName] {string} the Space display name; defaults to
|
|
20
|
+
* `'WAS Space'`
|
|
21
|
+
* @returns {Promise<void>}
|
|
22
|
+
*/
|
|
23
|
+
export async function ensureSpaceAndCollection({ was, spaceId, controllerDid, collectionId, encryption = 'edv', isPublic = false, spaceName = 'WAS Space' }) {
|
|
24
|
+
const space = was.space(spaceId);
|
|
25
|
+
try {
|
|
26
|
+
await space.configure({ name: spaceName, controller: controllerDid });
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
throw new Error(`Failed to configure WAS space "${spaceId}" for "${controllerDid}".`, { cause: err });
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const collection = space.collection(collectionId);
|
|
33
|
+
await collection.configure(encryption === 'edv'
|
|
34
|
+
? { name: collectionId, encryption: { scheme: 'edv' } }
|
|
35
|
+
: { name: collectionId, force: true });
|
|
36
|
+
if (isPublic) {
|
|
37
|
+
await collection.setPublic();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new Error(`Failed to configure collection "${collectionId}" in space "${spaceId}".`, { cause: err });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=provisioning.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provisioning.js","sourceRoot":"","sources":["../../src/sync/provisioning.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,EAC7C,GAAG,EACH,OAAO,EACP,aAAa,EACb,YAAY,EACZ,UAAU,GAAG,KAAK,EAClB,QAAQ,GAAG,KAAK,EAChB,SAAS,GAAG,WAAW,EASxB;IACC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAEhC,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,CAAA;IACvE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,kCAAkC,OAAO,UAAU,aAAa,IAAI,EACpE,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAA;IACH,CAAC;IAED,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,YAAY,CAAC,CAAA;QACjD,MAAM,UAAU,CAAC,SAAS,CACxB,UAAU,KAAK,KAAK;YAClB,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE;YACvD,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,CACxC,CAAA;QACD,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,UAAU,CAAC,SAAS,EAAE,CAAA;QAC9B,CAAC;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACb,mCAAmC,YAAY,eAAe,OAAO,IAAI,EACzE,EAAE,KAAK,EAAE,GAAG,EAAE,CACf,CAAA;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Wire and port types for cross-replica WAS synchronization.
|
|
6
|
+
*
|
|
7
|
+
* The change-feed wire model (`SyncCheckpoint`, `WireDoc`, and one page of the
|
|
8
|
+
* feed) is the shared WAS data model from `@interop/storage-core`, re-exported
|
|
9
|
+
* here under replication-facing names so a sync consumer imports one module.
|
|
10
|
+
* `WasSyncPort` and `MasterState` are the injectable access seam the change
|
|
11
|
+
* engine depends on, and `DocCipher` is the encrypt/decrypt seam a per-
|
|
12
|
+
* collection cipher implements.
|
|
13
|
+
*/
|
|
14
|
+
import type { ChangeDocument, ChangesCheckpoint, ChangesPage } from '@interop/storage-core';
|
|
15
|
+
import type { Json } from '../types.js';
|
|
16
|
+
export type { Json };
|
|
17
|
+
/**
|
|
18
|
+
* The keyset position in the change feed: the `{ id, updatedAt }` of the last
|
|
19
|
+
* document a pull returned, passed back verbatim to resume strictly after it.
|
|
20
|
+
* `id` is the total-order tiebreaker within a single `updatedAt`. This is the
|
|
21
|
+
* shared `ChangesCheckpoint` from `@interop/storage-core` -- server time only,
|
|
22
|
+
* an opaque position token, never compared against a device clock.
|
|
23
|
+
*/
|
|
24
|
+
export type SyncCheckpoint = ChangesCheckpoint;
|
|
25
|
+
/**
|
|
26
|
+
* One document as it travels on the `changes` feed wire: `id` is the WAS
|
|
27
|
+
* resource id, `version` the content revision (the push `If-Match` ETag), and
|
|
28
|
+
* the stored body is under `data`. A tombstone carries `_deleted: true` with no
|
|
29
|
+
* `data`. This is the shared `ChangeDocument` from `@interop/storage-core`; on
|
|
30
|
+
* an encrypted collection `data`/`custom` are the opaque stored envelope, moved
|
|
31
|
+
* verbatim (decrypt is a projection-time concern the engine's `DocCipher`
|
|
32
|
+
* handles, never the port).
|
|
33
|
+
*/
|
|
34
|
+
export type WireDoc = ChangeDocument;
|
|
35
|
+
/**
|
|
36
|
+
* One page of the `changes` feed -- the return shape of {@link WasSyncPort.query}
|
|
37
|
+
* (the shared `ChangesPage`): the page's `documents` and its resume
|
|
38
|
+
* `checkpoint`, or `checkpoint: null` for an empty (no-change) page.
|
|
39
|
+
*/
|
|
40
|
+
export type SyncPage = ChangesPage;
|
|
41
|
+
/**
|
|
42
|
+
* The current master state of a single resource, read back for the 412-conflict
|
|
43
|
+
* path ({@link WasSyncPort.get}). `deleted` distinguishes a tombstone from a
|
|
44
|
+
* live resource. `updatedAt`, `metaVersion`, `custom`, `createdBy`, and `epoch`
|
|
45
|
+
* are populated from the resource's `/meta` document when it exists; a resource
|
|
46
|
+
* with no metadata yet reports an epoch-zero `updatedAt` placeholder (a valid,
|
|
47
|
+
* sortable timestamp -- the change feed remains the authority on ordering).
|
|
48
|
+
*/
|
|
49
|
+
export interface MasterState {
|
|
50
|
+
version: number;
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
deleted: boolean;
|
|
53
|
+
metaVersion?: number;
|
|
54
|
+
data?: Json;
|
|
55
|
+
custom?: Json;
|
|
56
|
+
createdBy?: string;
|
|
57
|
+
epoch?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The injected WAS-access seam. `createWasSyncPort` implements this over
|
|
61
|
+
* `@interop/was-client`'s `was.request()` and the `Collection.changes()` feed;
|
|
62
|
+
* a change engine depends only on this interface. Every method moves the stored
|
|
63
|
+
* body verbatim -- no codec, no key handling.
|
|
64
|
+
*
|
|
65
|
+
* `putMeta` is optional: a replica that syncs only content (never the user-
|
|
66
|
+
* writable `/meta` `custom`) may omit it. All other methods are required.
|
|
67
|
+
*/
|
|
68
|
+
export interface WasSyncPort {
|
|
69
|
+
/**
|
|
70
|
+
* Pulls one page of the `changes` feed. Omit `checkpoint` for the first page.
|
|
71
|
+
* Returns the page's `documents` and its resume `checkpoint`, or
|
|
72
|
+
* `checkpoint: null` for an empty (no-change) page.
|
|
73
|
+
*/
|
|
74
|
+
query(options: {
|
|
75
|
+
checkpoint?: SyncCheckpoint;
|
|
76
|
+
limit: number;
|
|
77
|
+
}): Promise<{
|
|
78
|
+
documents: WireDoc[];
|
|
79
|
+
checkpoint: SyncCheckpoint | null;
|
|
80
|
+
}>;
|
|
81
|
+
/**
|
|
82
|
+
* Conditionally writes the content body verbatim (`PUT /:id`). Pass
|
|
83
|
+
* `ifNoneMatch: true` for create-if-absent, or `ifMatch` (a quoted ETag over
|
|
84
|
+
* the content `version`) for update-if-unchanged. `epoch` stamps the opaque
|
|
85
|
+
* key-epoch id the body was encrypted under (absent clears any prior stamp).
|
|
86
|
+
* Returns the new server `version` (parsed from the write's `ETag`). Throws
|
|
87
|
+
* {@link WasSyncConflictError} on `412`.
|
|
88
|
+
*/
|
|
89
|
+
putContent(options: {
|
|
90
|
+
id: string;
|
|
91
|
+
data: Json;
|
|
92
|
+
ifMatch?: string;
|
|
93
|
+
ifNoneMatch?: boolean;
|
|
94
|
+
epoch?: string;
|
|
95
|
+
}): Promise<number>;
|
|
96
|
+
/**
|
|
97
|
+
* Conditionally deletes a resource (writes a tombstone; `DELETE /:id`). Pass
|
|
98
|
+
* `ifMatch` (a quoted ETag over the content `version`) to delete only if
|
|
99
|
+
* unchanged. Returns the tombstone's new server `version`. Throws
|
|
100
|
+
* {@link WasSyncConflictError} on `412`, {@link WasSyncNotFoundError} on `404`
|
|
101
|
+
* (already gone -- a settled outcome for a delete).
|
|
102
|
+
*/
|
|
103
|
+
deleteContent(options: {
|
|
104
|
+
id: string;
|
|
105
|
+
ifMatch?: string;
|
|
106
|
+
}): Promise<number>;
|
|
107
|
+
/**
|
|
108
|
+
* Conditionally writes the user-writable metadata `custom` (`PUT /:id/meta`).
|
|
109
|
+
* Optional -- present only on a port that syncs metadata. Throws
|
|
110
|
+
* {@link WasSyncConflictError} on `412`.
|
|
111
|
+
*/
|
|
112
|
+
putMeta?(options: {
|
|
113
|
+
id: string;
|
|
114
|
+
custom: Json;
|
|
115
|
+
ifMatch?: string;
|
|
116
|
+
ifNoneMatch?: boolean;
|
|
117
|
+
}): Promise<void>;
|
|
118
|
+
/**
|
|
119
|
+
* Re-reads a single resource's current master state for the 412-conflict
|
|
120
|
+
* assembler. Returns `null` when the resource is genuinely absent OR a
|
|
121
|
+
* tombstone (the server's `GET` returns `404` for both -- indistinguishable,
|
|
122
|
+
* mapped to deletion-wins by the callers).
|
|
123
|
+
*/
|
|
124
|
+
get(options: {
|
|
125
|
+
id: string;
|
|
126
|
+
}): Promise<MasterState | null>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A per-collection document cipher: encrypts a JSON document into its stored
|
|
130
|
+
* body (minting the resource id) and decrypts a stored body back. Minting the
|
|
131
|
+
* id once, at write time, is what makes the same document converge on the same
|
|
132
|
+
* bytes -- and the same content-derived id -- on every replica.
|
|
133
|
+
*
|
|
134
|
+
* The members reconcile a plaintext (identity) cipher, a single-recipient EDV
|
|
135
|
+
* cipher, and a multi-recipient (key-epoch) EDV cipher:
|
|
136
|
+
*
|
|
137
|
+
* - `encrypt` may surface the `epoch` id a multi-recipient write encrypted
|
|
138
|
+
* under (the marker's `currentEpoch`); absent on a single-key or plaintext
|
|
139
|
+
* cipher.
|
|
140
|
+
* - `encryptUpdate` is optional -- present only for a mutable, random-id
|
|
141
|
+
* collection that re-encrypts a head document in place under its existing id
|
|
142
|
+
* (advancing the envelope `sequence`). A content-addressed cipher (plaintext
|
|
143
|
+
* or `idDerivation: 'content'`) either omits it or throws: a changed document
|
|
144
|
+
* is a different id, never an in-place update.
|
|
145
|
+
*/
|
|
146
|
+
export interface DocCipher {
|
|
147
|
+
encrypt(options: {
|
|
148
|
+
data: Json;
|
|
149
|
+
}): Promise<{
|
|
150
|
+
id: string;
|
|
151
|
+
envelope: Json;
|
|
152
|
+
epoch?: string;
|
|
153
|
+
}>;
|
|
154
|
+
encryptUpdate?(options: {
|
|
155
|
+
id: string;
|
|
156
|
+
data: Json;
|
|
157
|
+
current: Json;
|
|
158
|
+
}): Promise<{
|
|
159
|
+
id: string;
|
|
160
|
+
envelope: Json;
|
|
161
|
+
}>;
|
|
162
|
+
decrypt(options: {
|
|
163
|
+
envelope: Json;
|
|
164
|
+
}): Promise<Json>;
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/sync/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;GASG;AACH,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,WAAW,EACZ,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC,YAAY,EAAE,IAAI,EAAE,CAAA;AAEpB;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,iBAAiB,CAAA;AAE9C;;;;;;;;GAQG;AACH,MAAM,MAAM,OAAO,GAAG,cAAc,CAAA;AAEpC;;;;GAIG;AACH,MAAM,MAAM,QAAQ,GAAG,WAAW,CAAA;AAElC;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,MAAM,CAAC,EAAE,IAAI,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,KAAK,CAAC,OAAO,EAAE;QAAE,UAAU,CAAC,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QACtE,SAAS,EAAE,OAAO,EAAE,CAAA;QACpB,UAAU,EAAE,cAAc,GAAG,IAAI,CAAA;KAClC,CAAC,CAAA;IAEF;;;;;;;OAOG;IACH,UAAU,CAAC,OAAO,EAAE;QAClB,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,IAAI,CAAA;QACV,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,OAAO,CAAA;QACrB,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEnB;;;;;;OAMG;IACH,aAAa,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEzE;;;;OAIG;IACH,OAAO,CAAC,CAAC,OAAO,EAAE;QAChB,EAAE,EAAE,MAAM,CAAA;QACV,MAAM,EAAE,IAAI,CAAA;QACZ,OAAO,CAAC,EAAE,MAAM,CAAA;QAChB,WAAW,CAAC,EAAE,OAAO,CAAA;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEjB;;;;;OAKG;IACH,GAAG,CAAC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;CAC1D;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,OAAO,EAAE;QACf,IAAI,EAAE,IAAI,CAAA;KACX,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,IAAI,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC3D,aAAa,CAAC,CAAC,OAAO,EAAE;QACtB,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"}
|