@interop/was-client 0.53.0 → 0.54.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 +32 -11
- package/dist/edv/acquire.d.ts +98 -0
- package/dist/edv/acquire.d.ts.map +1 -0
- package/dist/edv/acquire.js +130 -0
- package/dist/edv/acquire.js.map +1 -0
- package/dist/edv/index.d.ts +35 -2
- package/dist/edv/index.d.ts.map +1 -1
- package/dist/edv/index.js +34 -2
- package/dist/edv/index.js.map +1 -1
- package/dist/edv/refresh.d.ts +62 -0
- package/dist/edv/refresh.d.ts.map +1 -0
- package/dist/edv/refresh.js +69 -0
- package/dist/edv/refresh.js.map +1 -0
- package/dist/edv/refreshingDocCipher.d.ts +70 -0
- package/dist/edv/refreshingDocCipher.d.ts.map +1 -0
- package/dist/edv/refreshingDocCipher.js +110 -0
- package/dist/edv/refreshingDocCipher.js.map +1 -0
- package/dist/errors.d.ts +4 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -3
- package/dist/errors.js.map +1 -1
- package/dist/sync/index.d.ts +1 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/port.d.ts +5 -1
- package/dist/sync/port.d.ts.map +1 -1
- package/dist/sync/port.js +27 -11
- package/dist/sync/port.js.map +1 -1
- package/dist/sync/predicates.d.ts +21 -4
- package/dist/sync/predicates.d.ts.map +1 -1
- package/dist/sync/predicates.js +23 -4
- package/dist/sync/predicates.js.map +1 -1
- package/dist/sync/provisioning.d.ts +13 -5
- package/dist/sync/provisioning.d.ts.map +1 -1
- package/dist/sync/provisioning.js +13 -7
- package/dist/sync/provisioning.js.map +1 -1
- package/dist/sync/types.d.ts +23 -14
- package/dist/sync/types.d.ts.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -905,17 +905,21 @@ plugs into:
|
|
|
905
905
|
`ETag`. The port moves stored bodies **verbatim** -- for an encrypted
|
|
906
906
|
collection that means the opaque EDV envelope, never plaintext, and the port
|
|
907
907
|
itself never touches keys. A rejected precondition throws
|
|
908
|
-
`WasSyncConflictError` (412); a delete of an already-gone resource
|
|
909
|
-
`WasSyncNotFoundError` (404) -- both catchable
|
|
910
|
-
`PreconditionFailedError` / `NotFoundError`. Every other
|
|
911
|
-
the typed `WasError` subclass for its status, carrying the
|
|
912
|
-
`problem+json` fields.
|
|
908
|
+
`WasSyncConflictError` (412); a delete of an already-gone resource or a
|
|
909
|
+
`putMeta` against one throws `WasSyncNotFoundError` (404) -- both catchable
|
|
910
|
+
subtypes of the core `PreconditionFailedError` / `NotFoundError`. Every other
|
|
911
|
+
failure arrives as the typed `WasError` subclass for its status, carrying the
|
|
912
|
+
server's `problem+json` fields.
|
|
913
913
|
- **`isSyncConflictError` / `isSyncNotFoundError` / `isSyncAuthError` /
|
|
914
|
-
`isUnknownEpochError`** classify those signals by
|
|
915
|
-
than `instanceof`: the port and the `DocCipher`
|
|
916
|
-
and a tree that resolves two copies of this
|
|
917
|
-
check quietly false. Read what you need off the
|
|
918
|
-
`err.status` on an auth error to tell a 401 from the
|
|
914
|
+
`isUnknownEpochError` / `isKeyUnwrapError`** classify those signals by
|
|
915
|
+
`err.name`. Use them rather than `instanceof`: the port and the `DocCipher`
|
|
916
|
+
are seams your app injects, and a tree that resolves two copies of this
|
|
917
|
+
package makes an `instanceof` check quietly false. Read what you need off the
|
|
918
|
+
matched value, such as `err.status` on an auth error to tell a 401 from the
|
|
919
|
+
masked 404. The last two tell a decrypt's two no-key outcomes apart: an epoch
|
|
920
|
+
the reader's descriptor does not list (a re-read may fix it) and an epoch it
|
|
921
|
+
lists but this reader holds no key for (real data, permanently unreadable
|
|
922
|
+
here, never garbage).
|
|
919
923
|
- **`SyncStatus`** (`'idle' | 'syncing' | 'synced' | 'error'`) is the closed
|
|
920
924
|
vocabulary a replication driver reports one feed's state through.
|
|
921
925
|
- **`DocCipher`** is the per-collection encrypt/decrypt seam sitting above the
|
|
@@ -932,6 +936,21 @@ plugs into:
|
|
|
932
936
|
counterpart, built from the descriptor alone with no key-agreement secret
|
|
933
937
|
(writes seal to the current epoch's public key, reconstructed from the epoch
|
|
934
938
|
id); `decrypt` on it refuses with the typed `EncryptOnlyCipherError`.
|
|
939
|
+
- **`createRefreshingEdvDocCipher(...)`** (from `@interop/was-client/edv`) is
|
|
940
|
+
`createEdvDocCipher` bound to descriptor acquisition and the unknown-epoch
|
|
941
|
+
refresh rule: it acquires the collection's descriptor through the
|
|
942
|
+
`EncryptionDescriptorSource` / `EncryptionDescriptorCache` seams
|
|
943
|
+
(`wasDescriptorSource` is the source over a `WasClient`; the cache is a
|
|
944
|
+
client-local get/put your app scopes to one Space), refuses fail-closed to
|
|
945
|
+
build without one, and on an `UnknownEpochError` decrypt re-reads the
|
|
946
|
+
descriptor, swaps itself, and retries exactly once per instance. An epoch
|
|
947
|
+
rotation emits no change-feed entry, so this is how a replica meets an
|
|
948
|
+
envelope sealed under an epoch it has never seen without a refetch per
|
|
949
|
+
resource. `acquireDescriptor` / `acquireDescriptors` are the acquisition alone
|
|
950
|
+
(fetch, cache the success, fall back to the cache whenever the fetch yields no
|
|
951
|
+
descriptor), and `DescriptorRefreshPolicy` is the
|
|
952
|
+
once-per-collection-per-session guard for a host whose reads scan rows instead
|
|
953
|
+
of going through a cipher.
|
|
935
954
|
- **`contentCid(doc)`** and **`deriveSpaceId(controllerDid)`** derive
|
|
936
955
|
content-addressed ids -- `base64url(SHA-256(utf8(JCS-canonicalized JSON)))`,
|
|
937
956
|
unpadded -- so the same logical document (and the same controller) lands on
|
|
@@ -940,7 +959,9 @@ plugs into:
|
|
|
940
959
|
Space, configure the collection (`edv`, `plaintext`, or `governed`, optionally
|
|
941
960
|
world-readable). Safe to re-run on every connect. A `governed` collection is
|
|
942
961
|
created with no `encryption` member, the caller declaring governance
|
|
943
|
-
afterwards through the history log's guarded create.
|
|
962
|
+
afterwards through the history log's guarded create. An optional `capability`
|
|
963
|
+
is the invocation capability every request rides, for a client holding a
|
|
964
|
+
delegated zcap rather than the Space's root authority.
|
|
944
965
|
|
|
945
966
|
```ts
|
|
946
967
|
import {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CollectionEncryption } from '../types.js';
|
|
2
|
+
import type { WasClient } from '../WasClient.js';
|
|
3
|
+
/**
|
|
4
|
+
* Where descriptors come from: one signed read of the collection's
|
|
5
|
+
* Description. Resolves `undefined` when the description carries no encryption
|
|
6
|
+
* member -- which a WAS host also serves for a read this client is not
|
|
7
|
+
* authorized to make, so the absence is ambiguous and callers fall back to a
|
|
8
|
+
* cached copy just as they do for a thrown fetch. Network errors throw through
|
|
9
|
+
* (callers treat the fetch as best-effort).
|
|
10
|
+
*/
|
|
11
|
+
export interface EncryptionDescriptorSource {
|
|
12
|
+
collectionEncryption(options: {
|
|
13
|
+
collectionId: string;
|
|
14
|
+
}): Promise<CollectionEncryption | undefined>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Where fetched descriptors survive offline: a client-local get/put
|
|
18
|
+
* pre-scoped by the host to one Space. `readDescriptor` resolves `undefined`
|
|
19
|
+
* when nothing is cached (never throws for absence).
|
|
20
|
+
*/
|
|
21
|
+
export interface EncryptionDescriptorCache {
|
|
22
|
+
readDescriptor(options: {
|
|
23
|
+
collectionId: string;
|
|
24
|
+
}): Promise<CollectionEncryption | undefined>;
|
|
25
|
+
writeDescriptor(options: {
|
|
26
|
+
collectionId: string;
|
|
27
|
+
descriptor: CollectionEncryption;
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The {@link EncryptionDescriptorSource} over a was-client handle: reads the
|
|
32
|
+
* collection's Description in the given Space and returns its `encryption`
|
|
33
|
+
* descriptor.
|
|
34
|
+
*
|
|
35
|
+
* @param options {object}
|
|
36
|
+
* @param options.was {WasClient} a client whose signer can read the Space
|
|
37
|
+
* @param options.spaceId {string}
|
|
38
|
+
* @returns {EncryptionDescriptorSource}
|
|
39
|
+
*/
|
|
40
|
+
export declare function wasDescriptorSource({ was, spaceId }: {
|
|
41
|
+
was: WasClient;
|
|
42
|
+
spaceId: string;
|
|
43
|
+
}): EncryptionDescriptorSource;
|
|
44
|
+
/**
|
|
45
|
+
* Acquires one collection's descriptor: fetches it from the source, caching a
|
|
46
|
+
* success; falls back to the cached copy whenever the fetch yields no
|
|
47
|
+
* descriptor (it threw, or it came back empty -- a masked 404 for an
|
|
48
|
+
* unauthorized read looks exactly like an unencrypted collection); and with no
|
|
49
|
+
* source at all (a purely local code path) reads the cache alone. Any cached
|
|
50
|
+
* copy is deliberately left in place, never cleared by an empty fetch.
|
|
51
|
+
* `undefined` means no descriptor exists anywhere for this collection.
|
|
52
|
+
*
|
|
53
|
+
* @param options {object}
|
|
54
|
+
* @param [options.source] {EncryptionDescriptorSource} omit for cache-only
|
|
55
|
+
* acquisition
|
|
56
|
+
* @param options.cache {EncryptionDescriptorCache}
|
|
57
|
+
* @param options.collectionId {string}
|
|
58
|
+
* @param [options.onFetchError] {function} observes a swallowed fetch
|
|
59
|
+
* failure (the thrown-fetch branch only; an empty description is not an
|
|
60
|
+
* error). Errors from the cache itself throw through, as do a log-governed
|
|
61
|
+
* source's refusal classes (a fabricated or discontinuous log is a security
|
|
62
|
+
* signal, not an outage the cache should paper over) -- except a continuity
|
|
63
|
+
* `rollback`, which falls back to the cache like any transport hiccup
|
|
64
|
+
* @returns {Promise<CollectionEncryption | undefined>}
|
|
65
|
+
*/
|
|
66
|
+
export declare function acquireDescriptor({ source, cache, collectionId, onFetchError }: {
|
|
67
|
+
source?: EncryptionDescriptorSource;
|
|
68
|
+
cache: EncryptionDescriptorCache;
|
|
69
|
+
collectionId: string;
|
|
70
|
+
onFetchError?: (err: unknown, info: {
|
|
71
|
+
collectionId: string;
|
|
72
|
+
}) => void;
|
|
73
|
+
}): Promise<CollectionEncryption | undefined>;
|
|
74
|
+
/**
|
|
75
|
+
* Acquires descriptors for a set of collections concurrently (each fetch is an
|
|
76
|
+
* independent signed round trip, so a session start is not gated on a serial
|
|
77
|
+
* chain of describes). Collections that resolve no descriptor are simply
|
|
78
|
+
* absent from the result.
|
|
79
|
+
*
|
|
80
|
+
* @param options {object}
|
|
81
|
+
* @param [options.source] {EncryptionDescriptorSource} omit for cache-only
|
|
82
|
+
* acquisition
|
|
83
|
+
* @param options.cache {EncryptionDescriptorCache}
|
|
84
|
+
* @param options.collectionIds {string[]}
|
|
85
|
+
* @param [options.onFetchError] {function} observes each swallowed fetch
|
|
86
|
+
* failure
|
|
87
|
+
* @returns {Promise<Record<string, CollectionEncryption>>} keyed by
|
|
88
|
+
* collection id
|
|
89
|
+
*/
|
|
90
|
+
export declare function acquireDescriptors({ source, cache, collectionIds, onFetchError }: {
|
|
91
|
+
source?: EncryptionDescriptorSource;
|
|
92
|
+
cache: EncryptionDescriptorCache;
|
|
93
|
+
collectionIds: string[];
|
|
94
|
+
onFetchError?: (err: unknown, info: {
|
|
95
|
+
collectionId: string;
|
|
96
|
+
}) => void;
|
|
97
|
+
}): Promise<Record<string, CollectionEncryption>>;
|
|
98
|
+
//# sourceMappingURL=acquire.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acquire.d.ts","sourceRoot":"","sources":["../../src/edv/acquire.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD;;;;;;;GAOG;AACH,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,CAAC,OAAO,EAAE;QAC5B,YAAY,EAAE,MAAM,CAAA;KACrB,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAA;CAC9C;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC,cAAc,CAAC,OAAO,EAAE;QACtB,YAAY,EAAE,MAAM,CAAA;KACrB,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAA;IAC7C,eAAe,CAAC,OAAO,EAAE;QACvB,YAAY,EAAE,MAAM,CAAA;QACpB,UAAU,EAAE,oBAAoB,CAAA;KACjC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,GAAG,EACH,OAAO,EACR,EAAE;IACD,GAAG,EAAE,SAAS,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CAChB,GAAG,0BAA0B,CAU7B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,iBAAiB,CAAC,EACtC,MAAM,EACN,KAAK,EACL,YAAY,EACZ,YAAY,EACb,EAAE;IACD,MAAM,CAAC,EAAE,0BAA0B,CAAA;IACnC,KAAK,EAAE,yBAAyB,CAAA;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACtE,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAwB5C;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,kBAAkB,CAAC,EACvC,MAAM,EACN,KAAK,EACL,aAAa,EACb,YAAY,EACb,EAAE;IACD,MAAM,CAAC,EAAE,0BAA0B,CAAA;IACnC,KAAK,EAAE,yBAAyB,CAAA;IAChC,aAAa,EAAE,MAAM,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACtE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAiBhD"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Collection encryption-descriptor acquisition: reading a collection's
|
|
6
|
+
* `CollectionEncryption` descriptor (its key-epoch roster) from the Collection
|
|
7
|
+
* Description, caching each success, and falling back to the cached copy
|
|
8
|
+
* whenever the description yields no descriptor -- whether it could not be
|
|
9
|
+
* fetched at all (offline, a collection must keep encrypting under its current
|
|
10
|
+
* epoch) or came back empty (WAS masks an unauthorized read as an absent one,
|
|
11
|
+
* so an empty description is ambiguous, never an authoritative "no
|
|
12
|
+
* encryption"). `undefined` therefore means only this: nothing, fetched or
|
|
13
|
+
* cached, describes this collection's encryption -- a plaintext collection, or
|
|
14
|
+
* an encrypted one whose epoch[0] install has not landed. A caller that has
|
|
15
|
+
* declared the collection encrypted must refuse fail-closed rather than
|
|
16
|
+
* encrypt without a roster.
|
|
17
|
+
*
|
|
18
|
+
* The two seams are deliberately narrow, so a consumer's own classes satisfy
|
|
19
|
+
* them structurally -- no adapter needed. An {@link EncryptionDescriptorSource}
|
|
20
|
+
* is one signed describe; an {@link EncryptionDescriptorCache} is a
|
|
21
|
+
* client-local get/put the host has already scoped to one Space (a web app: a
|
|
22
|
+
* localStorage pair keyed by Space id; a mobile app: a per-(profile,
|
|
23
|
+
* collection) table column), so no scope key appears in the interface.
|
|
24
|
+
*
|
|
25
|
+
* A source over a log-governed descriptor throws the resource-log refusal
|
|
26
|
+
* classes, and those are security signals rather than outages: the read path
|
|
27
|
+
* rethrows them past a warm cache, matched by `@interop/vh-resource-log`'s
|
|
28
|
+
* `isResourceLogRefusal` (a continuity `rollback` is the one carve-out, and
|
|
29
|
+
* falls back to the cache like a transport failure).
|
|
30
|
+
*/
|
|
31
|
+
import { isResourceLogRefusal } from '@interop/vh-resource-log';
|
|
32
|
+
/**
|
|
33
|
+
* The {@link EncryptionDescriptorSource} over a was-client handle: reads the
|
|
34
|
+
* collection's Description in the given Space and returns its `encryption`
|
|
35
|
+
* descriptor.
|
|
36
|
+
*
|
|
37
|
+
* @param options {object}
|
|
38
|
+
* @param options.was {WasClient} a client whose signer can read the Space
|
|
39
|
+
* @param options.spaceId {string}
|
|
40
|
+
* @returns {EncryptionDescriptorSource}
|
|
41
|
+
*/
|
|
42
|
+
export function wasDescriptorSource({ was, spaceId }) {
|
|
43
|
+
return {
|
|
44
|
+
async collectionEncryption({ collectionId }) {
|
|
45
|
+
const description = await was
|
|
46
|
+
.space(spaceId)
|
|
47
|
+
.collection(collectionId)
|
|
48
|
+
.describe();
|
|
49
|
+
return description?.encryption ?? undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Acquires one collection's descriptor: fetches it from the source, caching a
|
|
55
|
+
* success; falls back to the cached copy whenever the fetch yields no
|
|
56
|
+
* descriptor (it threw, or it came back empty -- a masked 404 for an
|
|
57
|
+
* unauthorized read looks exactly like an unencrypted collection); and with no
|
|
58
|
+
* source at all (a purely local code path) reads the cache alone. Any cached
|
|
59
|
+
* copy is deliberately left in place, never cleared by an empty fetch.
|
|
60
|
+
* `undefined` means no descriptor exists anywhere for this collection.
|
|
61
|
+
*
|
|
62
|
+
* @param options {object}
|
|
63
|
+
* @param [options.source] {EncryptionDescriptorSource} omit for cache-only
|
|
64
|
+
* acquisition
|
|
65
|
+
* @param options.cache {EncryptionDescriptorCache}
|
|
66
|
+
* @param options.collectionId {string}
|
|
67
|
+
* @param [options.onFetchError] {function} observes a swallowed fetch
|
|
68
|
+
* failure (the thrown-fetch branch only; an empty description is not an
|
|
69
|
+
* error). Errors from the cache itself throw through, as do a log-governed
|
|
70
|
+
* source's refusal classes (a fabricated or discontinuous log is a security
|
|
71
|
+
* signal, not an outage the cache should paper over) -- except a continuity
|
|
72
|
+
* `rollback`, which falls back to the cache like any transport hiccup
|
|
73
|
+
* @returns {Promise<CollectionEncryption | undefined>}
|
|
74
|
+
*/
|
|
75
|
+
export async function acquireDescriptor({ source, cache, collectionId, onFetchError }) {
|
|
76
|
+
if (!source) {
|
|
77
|
+
return cache.readDescriptor({ collectionId });
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const fetched = await source.collectionEncryption({ collectionId });
|
|
81
|
+
if (fetched) {
|
|
82
|
+
await cache.writeDescriptor({ collectionId, descriptor: fetched });
|
|
83
|
+
return fetched;
|
|
84
|
+
}
|
|
85
|
+
// Empty description: not authoritative (an unauthorized read is masked as
|
|
86
|
+
// an absent one), so a warm cache still serves the collection.
|
|
87
|
+
return cache.readDescriptor({ collectionId });
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// A resource-log refusal the cache must not paper over -- a fabricated
|
|
91
|
+
// log, or one that is not the continuation of the pinned history. A
|
|
92
|
+
// rollback is not one of them and falls through to the cache, as any
|
|
93
|
+
// transport failure does.
|
|
94
|
+
if (isResourceLogRefusal(err)) {
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
onFetchError?.(err, { collectionId });
|
|
98
|
+
return cache.readDescriptor({ collectionId });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Acquires descriptors for a set of collections concurrently (each fetch is an
|
|
103
|
+
* independent signed round trip, so a session start is not gated on a serial
|
|
104
|
+
* chain of describes). Collections that resolve no descriptor are simply
|
|
105
|
+
* absent from the result.
|
|
106
|
+
*
|
|
107
|
+
* @param options {object}
|
|
108
|
+
* @param [options.source] {EncryptionDescriptorSource} omit for cache-only
|
|
109
|
+
* acquisition
|
|
110
|
+
* @param options.cache {EncryptionDescriptorCache}
|
|
111
|
+
* @param options.collectionIds {string[]}
|
|
112
|
+
* @param [options.onFetchError] {function} observes each swallowed fetch
|
|
113
|
+
* failure
|
|
114
|
+
* @returns {Promise<Record<string, CollectionEncryption>>} keyed by
|
|
115
|
+
* collection id
|
|
116
|
+
*/
|
|
117
|
+
export async function acquireDescriptors({ source, cache, collectionIds, onFetchError }) {
|
|
118
|
+
const resolved = await Promise.all(collectionIds.map(async (collectionId) => [
|
|
119
|
+
collectionId,
|
|
120
|
+
await acquireDescriptor({ source, cache, collectionId, onFetchError })
|
|
121
|
+
]));
|
|
122
|
+
const descriptors = {};
|
|
123
|
+
for (const [collectionId, descriptor] of resolved) {
|
|
124
|
+
if (descriptor) {
|
|
125
|
+
descriptors[collectionId] = descriptor;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return descriptors;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=acquire.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acquire.js","sourceRoot":"","sources":["../../src/edv/acquire.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAA;AAiC/D;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAClC,GAAG,EACH,OAAO,EAIR;IACC,OAAO;QACL,KAAK,CAAC,oBAAoB,CAAC,EAAE,YAAY,EAAE;YACzC,MAAM,WAAW,GAAG,MAAM,GAAG;iBAC1B,KAAK,CAAC,OAAO,CAAC;iBACd,UAAU,CAAC,YAAY,CAAC;iBACxB,QAAQ,EAAE,CAAA;YACb,OAAO,WAAW,EAAE,UAAU,IAAI,SAAS,CAAA;QAC7C,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,EACtC,MAAM,EACN,KAAK,EACL,YAAY,EACZ,YAAY,EAMb;IACC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,KAAK,CAAC,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;IAC/C,CAAC;IACD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;QACnE,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,KAAK,CAAC,eAAe,CAAC,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAA;YAClE,OAAO,OAAO,CAAA;QAChB,CAAC;QACD,0EAA0E;QAC1E,+DAA+D;QAC/D,OAAO,KAAK,CAAC,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;IAC/C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,uEAAuE;QACvE,oEAAoE;QACpE,qEAAqE;QACrE,0BAA0B;QAC1B,IAAI,oBAAoB,CAAC,GAAG,CAAC,EAAE,CAAC;YAC9B,MAAM,GAAG,CAAA;QACX,CAAC;QACD,YAAY,EAAE,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,CAAC,CAAA;QACrC,OAAO,KAAK,CAAC,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;IAC/C,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,EACvC,MAAM,EACN,KAAK,EACL,aAAa,EACb,YAAY,EAMb;IACC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAChC,aAAa,CAAC,GAAG,CACf,KAAK,EAAC,YAAY,EAAC,EAAE,CACnB;QACE,YAAY;QACZ,MAAM,iBAAiB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC;KAC9D,CACb,CACF,CAAA;IACD,MAAM,WAAW,GAAyC,EAAE,CAAA;IAC5D,KAAK,MAAM,CAAC,YAAY,EAAE,UAAU,CAAC,IAAI,QAAQ,EAAE,CAAC;QAClD,IAAI,UAAU,EAAE,CAAC;YACf,WAAW,CAAC,YAAY,CAAC,GAAG,UAAU,CAAA;QACxC,CAAC;IACH,CAAC;IACD,OAAO,WAAW,CAAA;AACpB,CAAC"}
|
package/dist/edv/index.d.ts
CHANGED
|
@@ -50,8 +50,36 @@
|
|
|
50
50
|
* cannot help). Both ship from this subpath beside the cipher that raises
|
|
51
51
|
* them, so a consumer classifying what a cipher threw need not reach for the
|
|
52
52
|
* package root. Both assign their `name` explicitly, and a consumer whose
|
|
53
|
-
* cipher arrives through an injected seam
|
|
54
|
-
*
|
|
53
|
+
* cipher arrives through an injected seam matches on that name rather than
|
|
54
|
+
* `instanceof`, since the seam may resolve to a second copy of this package:
|
|
55
|
+
* `isKeyUnwrapError` here, and `isUnknownEpochError` on `./sync`, are those
|
|
56
|
+
* matchers.
|
|
57
|
+
*
|
|
58
|
+
* Which epoch a collection encrypts under, and when to ask again, is the
|
|
59
|
+
* descriptor acquisition and refresh policy every consumer running an
|
|
60
|
+
* encrypted collection must share (a drift between two replicas does not fail
|
|
61
|
+
* loudly; it fails as a resource one of them cannot decrypt):
|
|
62
|
+
*
|
|
63
|
+
* - `EncryptionDescriptorSource` / `EncryptionDescriptorCache` -- the narrow
|
|
64
|
+
* seams a host implements: one signed Collection Description read, and a
|
|
65
|
+
* client-local get/put pre-scoped to one Space. `wasDescriptorSource` is the
|
|
66
|
+
* source over a `WasClient` handle.
|
|
67
|
+
* - `acquireDescriptor` / `acquireDescriptors` -- fetch + cache with the
|
|
68
|
+
* cached fallback whenever the fetch yields no descriptor, thrown or empty
|
|
69
|
+
* (offline, a collection keeps encrypting under its current epoch; an empty
|
|
70
|
+
* description is ambiguous, since WAS masks an unauthorized read as an
|
|
71
|
+
* absent one). No descriptor anywhere means a plaintext collection, or an
|
|
72
|
+
* encrypted one whose epoch[0] install has not landed -- which a caller that
|
|
73
|
+
* has declared the collection encrypted must refuse fail-closed. A
|
|
74
|
+
* log-governed source's refusal classes rethrow instead of falling back
|
|
75
|
+
* (except a continuity rollback, which is reconcilable divergence).
|
|
76
|
+
* - `DescriptorRefreshPolicy` -- the once-per-collection-per-session
|
|
77
|
+
* unknown-epoch refresh guard, plus the refresh-and-re-read-once wrapper
|
|
78
|
+
* for hosts whose reads scan rows and count unknown-epoch skips.
|
|
79
|
+
* - `createRefreshingEdvDocCipher` -- `createEdvDocCipher` bound to both: a
|
|
80
|
+
* cipher that acquires its own descriptor and, on an unknown-epoch decrypt,
|
|
81
|
+
* re-reads the description, swaps itself, and retries exactly once per
|
|
82
|
+
* instance.
|
|
55
83
|
*/
|
|
56
84
|
export { createEdvEncryption, EdvCodec, wasTransportFactory } from './EdvCodec.js';
|
|
57
85
|
export type { CodecTransportFactory, EdvKeys } from './EdvCodec.js';
|
|
@@ -72,4 +100,9 @@ export { HMAC_KEY_TYPE, mintHmacKey, hmacKeyFromSecret, resolveHmacKey } from '.
|
|
|
72
100
|
export type { BlindingKey } from './hmacKey.js';
|
|
73
101
|
export { createEdvDocCipher, createEdvEncryptOnlyDocCipher, ownerRecipient, EncryptOnlyCipherError, KeyUnwrapError, UnknownEpochError, isEncryptedEnvelope } from './docCipher.js';
|
|
74
102
|
export type { DocCipher, EdvDocCipher } from './docCipher.js';
|
|
103
|
+
export { acquireDescriptor, acquireDescriptors, wasDescriptorSource } from './acquire.js';
|
|
104
|
+
export type { EncryptionDescriptorCache, EncryptionDescriptorSource } from './acquire.js';
|
|
105
|
+
export { DescriptorRefreshPolicy } from './refresh.js';
|
|
106
|
+
export { createRefreshingEdvDocCipher } from './refreshingDocCipher.js';
|
|
107
|
+
export { isKeyUnwrapError } from '../sync/predicates.js';
|
|
75
108
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/edv/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+EG;AACH,OAAO,EACL,mBAAmB,EACnB,QAAQ,EACR,mBAAmB,EACpB,MAAM,eAAe,CAAA;AACtB,YAAY,EAAE,qBAAqB,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AACtE,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,gBAAgB,EACjB,MAAM,iBAAiB,CAAA;AACxB,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EACL,eAAe,EACf,yBAAyB,EAC1B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACxB,MAAM,sBAAsB,CAAA;AAC7B,YAAY,EAAE,yBAAyB,EAAE,MAAM,sBAAsB,CAAA;AACrE,OAAO,EACL,8BAA8B,EAC9B,oCAAoC,EACpC,0BAA0B,EAC1B,8BAA8B,EAC9B,yBAAyB,EAC1B,MAAM,iCAAiC,CAAA;AACxC,YAAY,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAA;AACjF,OAAO,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,eAAe,EAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACjD,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,cAAc,EACf,MAAM,cAAc,CAAA;AACrB,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EACL,kBAAkB,EAClB,6BAA6B,EAC7B,cAAc,EACd,sBAAsB,EACtB,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,gBAAgB,CAAA;AACvB,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,cAAc,CAAA;AACrB,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC3B,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA"}
|
package/dist/edv/index.js
CHANGED
|
@@ -50,8 +50,36 @@
|
|
|
50
50
|
* cannot help). Both ship from this subpath beside the cipher that raises
|
|
51
51
|
* them, so a consumer classifying what a cipher threw need not reach for the
|
|
52
52
|
* package root. Both assign their `name` explicitly, and a consumer whose
|
|
53
|
-
* cipher arrives through an injected seam
|
|
54
|
-
*
|
|
53
|
+
* cipher arrives through an injected seam matches on that name rather than
|
|
54
|
+
* `instanceof`, since the seam may resolve to a second copy of this package:
|
|
55
|
+
* `isKeyUnwrapError` here, and `isUnknownEpochError` on `./sync`, are those
|
|
56
|
+
* matchers.
|
|
57
|
+
*
|
|
58
|
+
* Which epoch a collection encrypts under, and when to ask again, is the
|
|
59
|
+
* descriptor acquisition and refresh policy every consumer running an
|
|
60
|
+
* encrypted collection must share (a drift between two replicas does not fail
|
|
61
|
+
* loudly; it fails as a resource one of them cannot decrypt):
|
|
62
|
+
*
|
|
63
|
+
* - `EncryptionDescriptorSource` / `EncryptionDescriptorCache` -- the narrow
|
|
64
|
+
* seams a host implements: one signed Collection Description read, and a
|
|
65
|
+
* client-local get/put pre-scoped to one Space. `wasDescriptorSource` is the
|
|
66
|
+
* source over a `WasClient` handle.
|
|
67
|
+
* - `acquireDescriptor` / `acquireDescriptors` -- fetch + cache with the
|
|
68
|
+
* cached fallback whenever the fetch yields no descriptor, thrown or empty
|
|
69
|
+
* (offline, a collection keeps encrypting under its current epoch; an empty
|
|
70
|
+
* description is ambiguous, since WAS masks an unauthorized read as an
|
|
71
|
+
* absent one). No descriptor anywhere means a plaintext collection, or an
|
|
72
|
+
* encrypted one whose epoch[0] install has not landed -- which a caller that
|
|
73
|
+
* has declared the collection encrypted must refuse fail-closed. A
|
|
74
|
+
* log-governed source's refusal classes rethrow instead of falling back
|
|
75
|
+
* (except a continuity rollback, which is reconcilable divergence).
|
|
76
|
+
* - `DescriptorRefreshPolicy` -- the once-per-collection-per-session
|
|
77
|
+
* unknown-epoch refresh guard, plus the refresh-and-re-read-once wrapper
|
|
78
|
+
* for hosts whose reads scan rows and count unknown-epoch skips.
|
|
79
|
+
* - `createRefreshingEdvDocCipher` -- `createEdvDocCipher` bound to both: a
|
|
80
|
+
* cipher that acquires its own descriptor and, on an unknown-epoch decrypt,
|
|
81
|
+
* re-reads the description, swaps itself, and retries exactly once per
|
|
82
|
+
* instance.
|
|
55
83
|
*/
|
|
56
84
|
export { createEdvEncryption, EdvCodec, wasTransportFactory } from './EdvCodec.js';
|
|
57
85
|
export { WasTransport } from './WasTransport.js';
|
|
@@ -65,4 +93,8 @@ export { hasKeyEpochs, epochRostersEqual } from './epochRoster.js';
|
|
|
65
93
|
export { resolveEpochKeys } from './epochKeys.js';
|
|
66
94
|
export { HMAC_KEY_TYPE, mintHmacKey, hmacKeyFromSecret, resolveHmacKey } from './hmacKey.js';
|
|
67
95
|
export { createEdvDocCipher, createEdvEncryptOnlyDocCipher, ownerRecipient, EncryptOnlyCipherError, KeyUnwrapError, UnknownEpochError, isEncryptedEnvelope } from './docCipher.js';
|
|
96
|
+
export { acquireDescriptor, acquireDescriptors, wasDescriptorSource } from './acquire.js';
|
|
97
|
+
export { DescriptorRefreshPolicy } from './refresh.js';
|
|
98
|
+
export { createRefreshingEdvDocCipher } from './refreshingDocCipher.js';
|
|
99
|
+
export { isKeyUnwrapError } from '../sync/predicates.js';
|
|
68
100
|
//# sourceMappingURL=index.js.map
|
package/dist/edv/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+EG;AACH,OAAO,EACL,mBAAmB,EACnB,QAAQ,EACR,mBAAmB,EACpB,MAAM,eAAe,CAAA;AAEtB,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AACtE,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,YAAY,EACZ,eAAe,EACf,gBAAgB,EACjB,MAAM,iBAAiB,CAAA;AAExB,OAAO,EACL,eAAe,EACf,yBAAyB,EAC1B,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,yBAAyB,EACzB,uBAAuB,EACxB,MAAM,sBAAsB,CAAA;AAE7B,OAAO,EACL,8BAA8B,EAC9B,oCAAoC,EACpC,0BAA0B,EAC1B,8BAA8B,EAC9B,yBAAyB,EAC1B,MAAM,iCAAiC,CAAA;AAExC,OAAO,EACL,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,eAAe,EAChB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAEjD,OAAO,EACL,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,cAAc,EACf,MAAM,cAAc,CAAA;AAErB,OAAO,EACL,kBAAkB,EAClB,6BAA6B,EAC7B,cAAc,EACd,sBAAsB,EACtB,cAAc,EACd,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACpB,MAAM,cAAc,CAAA;AAKrB,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AACtD,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAA;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The unknown-epoch refresh policy: an epoch rotation emits no change-feed
|
|
6
|
+
* entry, so a cipher built from a cached descriptor can meet envelopes stamped
|
|
7
|
+
* with an epoch it has never seen. The remedy is one re-read of the
|
|
8
|
+
* Collection Description plus a cipher rebuild and a single retry -- and the
|
|
9
|
+
* policy guards that remedy to ONCE per collection per session, so a
|
|
10
|
+
* genuinely foreign envelope (one no descriptor will ever route) cannot drive
|
|
11
|
+
* a refetch loop, let alone a refetch per resource.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Tracks which collections have already spent their one refresh this session,
|
|
15
|
+
* and runs reads under the refresh-and-retry-once rule. One instance per
|
|
16
|
+
* session (it IS the session scope of the guard); the injected `refresh` does
|
|
17
|
+
* the host's whole swap -- re-acquire the descriptor(s), rebuild the
|
|
18
|
+
* cipher(s), and install them wherever the host keeps them.
|
|
19
|
+
*/
|
|
20
|
+
export declare class DescriptorRefreshPolicy {
|
|
21
|
+
#private;
|
|
22
|
+
constructor({ refresh }: {
|
|
23
|
+
refresh: (options: {
|
|
24
|
+
collectionId: string;
|
|
25
|
+
}) => Promise<void>;
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Whether a collection still has its one refresh this session.
|
|
29
|
+
*/
|
|
30
|
+
shouldRefresh({ collectionId }: {
|
|
31
|
+
collectionId: string;
|
|
32
|
+
}): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Runs a read that reports whether it skipped unknown-epoch rows; on the
|
|
35
|
+
* first such report for a collection this session, spends the collection's
|
|
36
|
+
* refresh (descriptor re-read + cipher swap, via the injected `refresh`) and
|
|
37
|
+
* re-reads once. A later unknown-epoch report for the same collection
|
|
38
|
+
* returns the read's value as-is.
|
|
39
|
+
*
|
|
40
|
+
* @param options {object}
|
|
41
|
+
* @param options.collectionId {string}
|
|
42
|
+
* @param options.read {function} the read, reporting `unknownEpoch`
|
|
43
|
+
* @returns {Promise<T>} the (possibly re-read) value
|
|
44
|
+
*/
|
|
45
|
+
readWithRefresh<T>({ collectionId, read }: {
|
|
46
|
+
collectionId: string;
|
|
47
|
+
read: () => Promise<{
|
|
48
|
+
value: T;
|
|
49
|
+
unknownEpoch: boolean;
|
|
50
|
+
}>;
|
|
51
|
+
}): Promise<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Re-arms the guard -- for one collection, or (with no argument) for all.
|
|
54
|
+
* Call when a fresh descriptor is installed by some other path (a share,
|
|
55
|
+
* unshare, or recipient rotation this session performed itself), since the
|
|
56
|
+
* next unknown-epoch read is then evidence of a NEW rotation elsewhere.
|
|
57
|
+
*/
|
|
58
|
+
reset(options?: {
|
|
59
|
+
collectionId?: string;
|
|
60
|
+
}): void;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=refresh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["../../src/edv/refresh.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,qBAAa,uBAAuB;;gBAKtB,EACV,OAAO,EACR,EAAE;QACD,OAAO,EAAE,CAAC,OAAO,EAAE;YAAE,YAAY,EAAE,MAAM,CAAA;SAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;KAC9D;IAID;;OAEG;IACH,aAAa,CAAC,EAAE,YAAY,EAAE,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO;IAIlE;;;;;;;;;;;OAWG;IACG,eAAe,CAAC,CAAC,EAAE,EACvB,YAAY,EACZ,IAAI,EACL,EAAE;QACD,YAAY,EAAE,MAAM,CAAA;QACpB,IAAI,EAAE,MAAM,OAAO,CAAC;YAAE,KAAK,EAAE,CAAC,CAAC;YAAC,YAAY,EAAE,OAAO,CAAA;SAAE,CAAC,CAAA;KACzD,GAAG,OAAO,CAAC,CAAC,CAAC;IAUd;;;;;OAKG;IACH,KAAK,CAAC,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;CAOjD"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* The unknown-epoch refresh policy: an epoch rotation emits no change-feed
|
|
6
|
+
* entry, so a cipher built from a cached descriptor can meet envelopes stamped
|
|
7
|
+
* with an epoch it has never seen. The remedy is one re-read of the
|
|
8
|
+
* Collection Description plus a cipher rebuild and a single retry -- and the
|
|
9
|
+
* policy guards that remedy to ONCE per collection per session, so a
|
|
10
|
+
* genuinely foreign envelope (one no descriptor will ever route) cannot drive
|
|
11
|
+
* a refetch loop, let alone a refetch per resource.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Tracks which collections have already spent their one refresh this session,
|
|
15
|
+
* and runs reads under the refresh-and-retry-once rule. One instance per
|
|
16
|
+
* session (it IS the session scope of the guard); the injected `refresh` does
|
|
17
|
+
* the host's whole swap -- re-acquire the descriptor(s), rebuild the
|
|
18
|
+
* cipher(s), and install them wherever the host keeps them.
|
|
19
|
+
*/
|
|
20
|
+
export class DescriptorRefreshPolicy {
|
|
21
|
+
#refresh;
|
|
22
|
+
// Collection ids whose one refresh this session is already spent.
|
|
23
|
+
#refreshed = new Set();
|
|
24
|
+
constructor({ refresh }) {
|
|
25
|
+
this.#refresh = refresh;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Whether a collection still has its one refresh this session.
|
|
29
|
+
*/
|
|
30
|
+
shouldRefresh({ collectionId }) {
|
|
31
|
+
return !this.#refreshed.has(collectionId);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Runs a read that reports whether it skipped unknown-epoch rows; on the
|
|
35
|
+
* first such report for a collection this session, spends the collection's
|
|
36
|
+
* refresh (descriptor re-read + cipher swap, via the injected `refresh`) and
|
|
37
|
+
* re-reads once. A later unknown-epoch report for the same collection
|
|
38
|
+
* returns the read's value as-is.
|
|
39
|
+
*
|
|
40
|
+
* @param options {object}
|
|
41
|
+
* @param options.collectionId {string}
|
|
42
|
+
* @param options.read {function} the read, reporting `unknownEpoch`
|
|
43
|
+
* @returns {Promise<T>} the (possibly re-read) value
|
|
44
|
+
*/
|
|
45
|
+
async readWithRefresh({ collectionId, read }) {
|
|
46
|
+
const first = await read();
|
|
47
|
+
if (first.unknownEpoch && this.shouldRefresh({ collectionId })) {
|
|
48
|
+
this.#refreshed.add(collectionId);
|
|
49
|
+
await this.#refresh({ collectionId });
|
|
50
|
+
return (await read()).value;
|
|
51
|
+
}
|
|
52
|
+
return first.value;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Re-arms the guard -- for one collection, or (with no argument) for all.
|
|
56
|
+
* Call when a fresh descriptor is installed by some other path (a share,
|
|
57
|
+
* unshare, or recipient rotation this session performed itself), since the
|
|
58
|
+
* next unknown-epoch read is then evidence of a NEW rotation elsewhere.
|
|
59
|
+
*/
|
|
60
|
+
reset(options) {
|
|
61
|
+
if (options?.collectionId === undefined) {
|
|
62
|
+
this.#refreshed.clear();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.#refreshed.delete(options.collectionId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refresh.js","sourceRoot":"","sources":["../../src/edv/refresh.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;GAQG;AAEH;;;;;;GAMG;AACH,MAAM,OAAO,uBAAuB;IACzB,QAAQ,CAAsD;IACvE,kEAAkE;IACzD,UAAU,GAAG,IAAI,GAAG,EAAU,CAAA;IAEvC,YAAY,EACV,OAAO,EAGR;QACC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;IACzB,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,EAAE,YAAY,EAA4B;QACtD,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;IAC3C,CAAC;IAED;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,eAAe,CAAI,EACvB,YAAY,EACZ,IAAI,EAIL;QACC,MAAM,KAAK,GAAG,MAAM,IAAI,EAAE,CAAA;QAC1B,IAAI,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;YAC/D,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;YACjC,MAAM,IAAI,CAAC,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC,CAAA;YACrC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,CAAA;QAC7B,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,CAAA;IACpB,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,OAAmC;QACvC,IAAI,OAAO,EAAE,YAAY,KAAK,SAAS,EAAE,CAAC;YACxC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;QACzB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2026 Interop Alliance. All rights reserved.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* A self-refreshing EDV document cipher: `createEdvDocCipher` bound to
|
|
6
|
+
* descriptor acquisition and the once-per-session refresh rule, for a host
|
|
7
|
+
* whose decrypt seam is the cipher itself (a sync engine's `decryptDoc`, a
|
|
8
|
+
* conflict resolver) rather than a row-scanning store.
|
|
9
|
+
*
|
|
10
|
+
* Built, the cipher acquires the collection's descriptor (fetch, cache the
|
|
11
|
+
* success, cached fallback whenever the fetch yields none -- see
|
|
12
|
+
* `acquire.ts`) and constructs the underlying EDV cipher from it; with no
|
|
13
|
+
* descriptor, or a descriptor with no epochs, the build refuses fail-closed
|
|
14
|
+
* (every encrypted collection's descriptor carries an epoch roster from
|
|
15
|
+
* provisioning, so the absence can only mean the install has not landed or
|
|
16
|
+
* the host is lying). When a decrypt throws
|
|
17
|
+
* `UnknownEpochError`, the cipher re-acquires the descriptor, rebuilds itself,
|
|
18
|
+
* and retries that decrypt exactly once -- and only once per cipher instance,
|
|
19
|
+
* which the host scopes to one `(profile, collection)` session by dropping
|
|
20
|
+
* its cipher cache when the session ends. A second failure (or any unknown
|
|
21
|
+
* epoch after the one refresh is spent) propagates. A refresh that itself
|
|
22
|
+
* fails does not count as spent -- the original `UnknownEpochError` is
|
|
23
|
+
* rethrown and a later decrypt may try again.
|
|
24
|
+
*
|
|
25
|
+
* Only `UnknownEpochError` drives that refresh, and by design. The client
|
|
26
|
+
* splits the two ways a decrypt can find no key: an epoch the descriptor does
|
|
27
|
+
* not list at all raises `UnknownEpochError`, because a fresher descriptor may
|
|
28
|
+
* well list it; an epoch the descriptor does list but this reader holds no key
|
|
29
|
+
* for raises `KeyUnwrapError` (never a recipient, or removed and the epoch
|
|
30
|
+
* rotated). The second is rethrown immediately, with the refresh left
|
|
31
|
+
* untouched, since re-reading the same descriptor cannot produce a key the
|
|
32
|
+
* reader was not given.
|
|
33
|
+
*/
|
|
34
|
+
import type { IKeyAgreementKey, IKeyResolver } from '@interop/data-integrity-core';
|
|
35
|
+
import { type DocCipher } from './docCipher.js';
|
|
36
|
+
import { type EncryptionDescriptorCache, type EncryptionDescriptorSource } from './acquire.js';
|
|
37
|
+
/**
|
|
38
|
+
* Builds a {@link DocCipher} whose descriptor is acquired through the
|
|
39
|
+
* source/cache seams and refreshed (once per instance) on an unknown-epoch
|
|
40
|
+
* decrypt.
|
|
41
|
+
*
|
|
42
|
+
* With no `source` the descriptor is served from the cache alone and the
|
|
43
|
+
* refresh path is inert (an unknown-epoch decrypt propagates immediately) --
|
|
44
|
+
* the shape for a purely local code path that must never touch the network.
|
|
45
|
+
*
|
|
46
|
+
* @param options {object}
|
|
47
|
+
* @param options.keyAgreementKey {IKeyAgreementKey} the vault key pair this
|
|
48
|
+
* collection's envelopes are sealed to
|
|
49
|
+
* @param options.keyResolver {IKeyResolver}
|
|
50
|
+
* @param options.collectionId {string}
|
|
51
|
+
* @param [options.idDerivation] {'content' | 'random'} defaults to
|
|
52
|
+
* `'content'`
|
|
53
|
+
* @param [options.source] {EncryptionDescriptorSource}
|
|
54
|
+
* @param options.cache {EncryptionDescriptorCache}
|
|
55
|
+
* @param [options.onFetchError] {function} observes swallowed
|
|
56
|
+
* descriptor-fetch failures
|
|
57
|
+
* @returns {Promise<DocCipher>}
|
|
58
|
+
*/
|
|
59
|
+
export declare function createRefreshingEdvDocCipher({ keyAgreementKey, keyResolver, collectionId, idDerivation, source, cache, onFetchError }: {
|
|
60
|
+
keyAgreementKey: IKeyAgreementKey;
|
|
61
|
+
keyResolver: IKeyResolver;
|
|
62
|
+
collectionId: string;
|
|
63
|
+
idDerivation?: 'content' | 'random';
|
|
64
|
+
source?: EncryptionDescriptorSource;
|
|
65
|
+
cache: EncryptionDescriptorCache;
|
|
66
|
+
onFetchError?: (err: unknown, info: {
|
|
67
|
+
collectionId: string;
|
|
68
|
+
}) => void;
|
|
69
|
+
}): Promise<DocCipher>;
|
|
70
|
+
//# sourceMappingURL=refreshingDocCipher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"refreshingDocCipher.d.ts","sourceRoot":"","sources":["../../src/edv/refreshingDocCipher.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,OAAO,KAAK,EACV,gBAAgB,EAChB,YAAY,EACb,MAAM,8BAA8B,CAAA;AAErC,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAA;AACnE,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,0BAA0B,EAChC,MAAM,cAAc,CAAA;AAErB;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,4BAA4B,CAAC,EACjD,eAAe,EACf,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,MAAM,EACN,KAAK,EACL,YAAY,EACb,EAAE;IACD,eAAe,EAAE,gBAAgB,CAAA;IACjC,WAAW,EAAE,YAAY,CAAA;IACzB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;IACnC,MAAM,CAAC,EAAE,0BAA0B,CAAA;IACnC,KAAK,EAAE,yBAAyB,CAAA;IAChC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACtE,GAAG,OAAO,CAAC,SAAS,CAAC,CAyFrB"}
|