@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.
Files changed (40) hide show
  1. package/README.md +32 -11
  2. package/dist/edv/acquire.d.ts +98 -0
  3. package/dist/edv/acquire.d.ts.map +1 -0
  4. package/dist/edv/acquire.js +130 -0
  5. package/dist/edv/acquire.js.map +1 -0
  6. package/dist/edv/index.d.ts +35 -2
  7. package/dist/edv/index.d.ts.map +1 -1
  8. package/dist/edv/index.js +34 -2
  9. package/dist/edv/index.js.map +1 -1
  10. package/dist/edv/refresh.d.ts +62 -0
  11. package/dist/edv/refresh.d.ts.map +1 -0
  12. package/dist/edv/refresh.js +69 -0
  13. package/dist/edv/refresh.js.map +1 -0
  14. package/dist/edv/refreshingDocCipher.d.ts +70 -0
  15. package/dist/edv/refreshingDocCipher.d.ts.map +1 -0
  16. package/dist/edv/refreshingDocCipher.js +110 -0
  17. package/dist/edv/refreshingDocCipher.js.map +1 -0
  18. package/dist/errors.d.ts +4 -3
  19. package/dist/errors.d.ts.map +1 -1
  20. package/dist/errors.js +4 -3
  21. package/dist/errors.js.map +1 -1
  22. package/dist/sync/index.d.ts +1 -1
  23. package/dist/sync/index.d.ts.map +1 -1
  24. package/dist/sync/index.js +1 -1
  25. package/dist/sync/index.js.map +1 -1
  26. package/dist/sync/port.d.ts +5 -1
  27. package/dist/sync/port.d.ts.map +1 -1
  28. package/dist/sync/port.js +27 -11
  29. package/dist/sync/port.js.map +1 -1
  30. package/dist/sync/predicates.d.ts +21 -4
  31. package/dist/sync/predicates.d.ts.map +1 -1
  32. package/dist/sync/predicates.js +23 -4
  33. package/dist/sync/predicates.js.map +1 -1
  34. package/dist/sync/provisioning.d.ts +13 -5
  35. package/dist/sync/provisioning.d.ts.map +1 -1
  36. package/dist/sync/provisioning.js +13 -7
  37. package/dist/sync/provisioning.js.map +1 -1
  38. package/dist/sync/types.d.ts +23 -14
  39. package/dist/sync/types.d.ts.map +1 -1
  40. 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 throws
909
- `WasSyncNotFoundError` (404) -- both catchable subtypes of the core
910
- `PreconditionFailedError` / `NotFoundError`. Every other failure arrives as
911
- the typed `WasError` subclass for its status, carrying the server's
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 `err.name`. Use them rather
915
- than `instanceof`: the port and the `DocCipher` are seams your app injects,
916
- and a tree that resolves two copies of this package makes an `instanceof`
917
- check quietly false. Read what you need off the matched value, such as
918
- `err.status` on an auth error to tell a 401 from the masked 404.
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"}
@@ -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 should match on that name rather
54
- * than `instanceof`: the seam may resolve to a second copy of this package.
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;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"}
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 should match on that name rather
54
- * than `instanceof`: the seam may resolve to a second copy of this package.
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/edv/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;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"}
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"}