@interop/was-client 0.14.5 → 0.16.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/dist/Collection.d.ts +17 -26
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +60 -59
- package/dist/Collection.js.map +1 -1
- package/dist/Resource.d.ts +27 -3
- package/dist/Resource.d.ts.map +1 -1
- package/dist/Resource.js +60 -22
- package/dist/Resource.js.map +1 -1
- package/dist/Space.d.ts.map +1 -1
- package/dist/Space.js +19 -11
- package/dist/Space.js.map +1 -1
- package/dist/WasClient.d.ts.map +1 -1
- package/dist/WasClient.js +4 -13
- package/dist/WasClient.js.map +1 -1
- package/dist/edv/EdvCodec.d.ts +11 -2
- package/dist/edv/EdvCodec.d.ts.map +1 -1
- package/dist/edv/EdvCodec.js +85 -70
- package/dist/edv/EdvCodec.js.map +1 -1
- package/dist/edv/WasTransport.d.ts +61 -53
- package/dist/edv/WasTransport.d.ts.map +1 -1
- package/dist/edv/WasTransport.js +189 -157
- package/dist/edv/WasTransport.js.map +1 -1
- package/dist/edv/constants.d.ts +0 -14
- package/dist/edv/constants.d.ts.map +1 -1
- package/dist/edv/constants.js +4 -8
- package/dist/edv/constants.js.map +1 -1
- package/dist/edv/epochCrypto.d.ts +1 -1
- package/dist/edv/epochCrypto.d.ts.map +1 -1
- package/dist/edv/epochCrypto.js +33 -4
- package/dist/edv/epochCrypto.js.map +1 -1
- package/dist/edv/epochKeys.js +11 -1
- package/dist/edv/epochKeys.js.map +1 -1
- package/dist/edv/recipients.d.ts +5 -1
- package/dist/edv/recipients.d.ts.map +1 -1
- package/dist/edv/recipients.js +38 -16
- package/dist/edv/recipients.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/internal/codec.d.ts +19 -0
- package/dist/internal/codec.d.ts.map +1 -1
- package/dist/internal/codec.js +26 -8
- package/dist/internal/codec.js.map +1 -1
- package/dist/internal/conditional.d.ts +11 -0
- package/dist/internal/conditional.d.ts.map +1 -1
- package/dist/internal/conditional.js +12 -0
- package/dist/internal/conditional.js.map +1 -1
- package/dist/internal/content.d.ts +7 -0
- package/dist/internal/content.d.ts.map +1 -1
- package/dist/internal/content.js +5 -3
- package/dist/internal/content.js.map +1 -1
- package/dist/internal/describe.d.ts +49 -1
- package/dist/internal/describe.d.ts.map +1 -1
- package/dist/internal/describe.js +47 -6
- package/dist/internal/describe.js.map +1 -1
- package/dist/internal/features.d.ts +73 -0
- package/dist/internal/features.d.ts.map +1 -0
- package/dist/internal/features.js +129 -0
- package/dist/internal/features.js.map +1 -0
- package/dist/internal/grant.d.ts.map +1 -1
- package/dist/internal/grant.js +2 -1
- package/dist/internal/grant.js.map +1 -1
- package/dist/internal/pagination.d.ts +30 -1
- package/dist/internal/pagination.d.ts.map +1 -1
- package/dist/internal/pagination.js +44 -1
- package/dist/internal/pagination.js.map +1 -1
- package/dist/internal/paths.d.ts +25 -0
- package/dist/internal/paths.d.ts.map +1 -1
- package/dist/internal/paths.js +67 -10
- package/dist/internal/paths.js.map +1 -1
- package/dist/internal/policy.d.ts +28 -0
- package/dist/internal/policy.d.ts.map +1 -1
- package/dist/internal/policy.js +31 -0
- package/dist/internal/policy.js.map +1 -1
- package/dist/internal/revoke.d.ts +11 -0
- package/dist/internal/revoke.d.ts.map +1 -1
- package/dist/internal/revoke.js +14 -1
- package/dist/internal/revoke.js.map +1 -1
- package/dist/internal/write.d.ts +10 -1
- package/dist/internal/write.d.ts.map +1 -1
- package/dist/internal/write.js +29 -4
- package/dist/internal/write.js.map +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -9
|
@@ -32,16 +32,20 @@
|
|
|
32
32
|
* feature). `updateIndex` throws: in this profile the `indexed` array rides
|
|
33
33
|
* inside the stored document envelope, so `update()` IS the re-index
|
|
34
34
|
* operation and no separate index endpoint exists. Chunked streams
|
|
35
|
-
* (`storeChunk` / `getChunk`)
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* (`storeChunk` / `getChunk`) map each EDV chunk onto the reserved
|
|
36
|
+
* `/{id}/chunks/{n}` sub-segment (the server's `chunked-streams` affordance),
|
|
37
|
+
* storing the chunk object as an opaque JSON body -- so `EdvClientCore.insert({
|
|
38
|
+
* stream })` / `getStream` drive chunked encrypted blobs over a WAS server
|
|
39
|
+
* unchanged. Both chunk methods are gated on the backend advertising
|
|
40
|
+
* `chunked-streams` (throwing `NotSupportedError` when it is absent), like
|
|
41
|
+
* `find` is on `blinded-index-query`.
|
|
38
42
|
* `insert` uses an atomic `If-None-Match: *` create when the backend
|
|
39
43
|
* advertises the optional `conditional-writes` feature; otherwise (and for
|
|
40
44
|
* `update`) writes are advisory -- the EDV `sequence` is not enforced
|
|
41
45
|
* (last-writer-wins on `update`).
|
|
42
46
|
*/
|
|
43
47
|
import { Transport } from '@interop/edv-client';
|
|
44
|
-
import type { IEDVQuery, IEncryptedDocument } from '@interop/data-integrity-core';
|
|
48
|
+
import type { IEDVChunk, IEDVQuery, IEncryptedDocument } from '@interop/data-integrity-core';
|
|
45
49
|
import type { WasClient } from '../WasClient.js';
|
|
46
50
|
import { JOSE_CONTENT_TYPE } from './constants.js';
|
|
47
51
|
export { JOSE_CONTENT_TYPE };
|
|
@@ -55,7 +59,7 @@ export declare class WasTransport extends Transport {
|
|
|
55
59
|
readonly collectionId: string;
|
|
56
60
|
readonly contentType: string;
|
|
57
61
|
private readonly _was;
|
|
58
|
-
private
|
|
62
|
+
private readonly _features;
|
|
59
63
|
/**
|
|
60
64
|
* @param options {object}
|
|
61
65
|
* @param options.was {WasClient} a WAS client holding the signer
|
|
@@ -94,42 +98,6 @@ export declare class WasTransport extends Transport {
|
|
|
94
98
|
* @returns {Promise<HttpResponse>}
|
|
95
99
|
*/
|
|
96
100
|
private _put;
|
|
97
|
-
/**
|
|
98
|
-
* The feature tokens the collection's backend advertises in its "Collection
|
|
99
|
-
* Backend Selected" descriptor (e.g. `conditional-writes`,
|
|
100
|
-
* `blinded-index-query`). Memoized once it produces a definitive answer: a
|
|
101
|
-
* successful read (including one that lists no features) and a definitive
|
|
102
|
-
* "endpoint absent" (`404` / `405` / `501`) both resolve to a cached feature
|
|
103
|
-
* list, so every affordance gate falls closed against a server that has no
|
|
104
|
-
* backend descriptors.
|
|
105
|
-
*
|
|
106
|
-
* A transient/ambiguous failure (network error, timeout, `401`, `429`, other
|
|
107
|
-
* `5xx`) is NOT cached: the memo is cleared so the next call re-probes, and
|
|
108
|
-
* the error is rethrown so the caller fails loud rather than silently
|
|
109
|
-
* degrading atomicity against a server that may well be capable. (A single
|
|
110
|
-
* transient failure must not poison the transport for its lifetime.)
|
|
111
|
-
*
|
|
112
|
-
* @returns {Promise<string[]>}
|
|
113
|
-
*/
|
|
114
|
-
private _backendFeatures;
|
|
115
|
-
/**
|
|
116
|
-
* Reads and parses the backend descriptor once. On a definitive answer
|
|
117
|
-
* (success, or a `404` / `405` / `501` that means the endpoint is legitimately
|
|
118
|
-
* absent) resolves the feature list, which `_backendFeatures` then caches. On
|
|
119
|
-
* a transient failure, clears the memo (so the next call re-probes) and
|
|
120
|
-
* rethrows.
|
|
121
|
-
*
|
|
122
|
-
* @returns {Promise<string[]>}
|
|
123
|
-
*/
|
|
124
|
-
private _probeBackendFeatures;
|
|
125
|
-
/**
|
|
126
|
-
* Whether the collection's backend advertises the optional
|
|
127
|
-
* `conditional-writes` feature, so `insert` can use an atomic
|
|
128
|
-
* `If-None-Match: *` create instead of the advisory existence-check path.
|
|
129
|
-
*
|
|
130
|
-
* @returns {Promise<boolean>}
|
|
131
|
-
*/
|
|
132
|
-
private _conditionalWrites;
|
|
133
101
|
/**
|
|
134
102
|
* @inheritdoc
|
|
135
103
|
*
|
|
@@ -185,6 +153,16 @@ export declare class WasTransport extends Transport {
|
|
|
185
153
|
get({ id }?: {
|
|
186
154
|
id?: string;
|
|
187
155
|
}): Promise<IEncryptedDocument>;
|
|
156
|
+
/**
|
|
157
|
+
* Throws a `NotSupportedError` (the name `EdvClientCore` dispatches on)
|
|
158
|
+
* unless the collection's backend advertises the given affordance token --
|
|
159
|
+
* the shared gate in front of every optional-feature operation.
|
|
160
|
+
*
|
|
161
|
+
* @param feature {string} the affordance token (e.g. `chunked-streams`)
|
|
162
|
+
* @param what {string} the operation name, for the message
|
|
163
|
+
* @returns {Promise<void>}
|
|
164
|
+
*/
|
|
165
|
+
private _requireFeature;
|
|
188
166
|
/**
|
|
189
167
|
* Resolves to `true` if a resource exists at the document's path, via a
|
|
190
168
|
* bodiless `HEAD` (the stored envelope is not needed, only its existence). A
|
|
@@ -230,26 +208,56 @@ export declare class WasTransport extends Transport {
|
|
|
230
208
|
*/
|
|
231
209
|
updateIndex(): Promise<never>;
|
|
232
210
|
/**
|
|
233
|
-
*
|
|
211
|
+
* The WAS path of one chunk of a document, delegating to the internal
|
|
212
|
+
* `resourceChunkPath` builder (member form, no trailing slash --
|
|
213
|
+
* put/get/delete one chunk by index).
|
|
234
214
|
*
|
|
235
|
-
*
|
|
215
|
+
* @param docId {string} the EDV document id (= WAS resource id)
|
|
216
|
+
* @param chunkIndex {number} the chunk's non-negative ordinal index
|
|
217
|
+
* @returns {string}
|
|
236
218
|
*/
|
|
237
|
-
|
|
219
|
+
private _chunkPath;
|
|
238
220
|
/**
|
|
239
221
|
* @inheritdoc
|
|
240
222
|
*
|
|
241
|
-
*
|
|
223
|
+
* Stores one encrypted chunk of a document's data stream. The EDV chunk
|
|
224
|
+
* object (`{ sequence, index, jwe, offset }`) is serialized to JSON and
|
|
225
|
+
* `PUT` as an opaque binary body ({@link CHUNK_CONTENT_TYPE}) to the chunk's
|
|
226
|
+
* own URL (`.../chunks/{index}`), signed like every other write. The server
|
|
227
|
+
* stores the bytes verbatim -- it never parses the chunk -- so any
|
|
228
|
+
* client-side crypto framing is transparent to it. Requires the backend's
|
|
229
|
+
* `chunked-streams` affordance (throws `NotSupportedError` when it is
|
|
230
|
+
* absent). The parent Resource must already exist
|
|
231
|
+
* (`EdvClientCore.insert`/`update` writes the document envelope before
|
|
232
|
+
* draining the stream), so a 404 here surfaces as a `NotFoundError`.
|
|
233
|
+
*
|
|
234
|
+
* @param options {object}
|
|
235
|
+
* @param options.docId {string} the owning document id (= WAS resource id)
|
|
236
|
+
* @param options.chunk {IEDVChunk} the encrypted chunk to store
|
|
237
|
+
* @returns {Promise<void>}
|
|
242
238
|
*/
|
|
243
|
-
|
|
239
|
+
storeChunk({ docId, chunk }?: {
|
|
240
|
+
docId?: string;
|
|
241
|
+
chunk?: IEDVChunk;
|
|
242
|
+
}): Promise<void>;
|
|
244
243
|
/**
|
|
245
|
-
*
|
|
246
|
-
* stream operations, which depend on a server-side affordance (the reserved
|
|
247
|
-
* `/{id}/chunks/{n}` sub-segment, the `chunked-streams` backend feature)
|
|
248
|
-
* that neither reference backend provides yet.
|
|
244
|
+
* @inheritdoc
|
|
249
245
|
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
246
|
+
* Reads one encrypted chunk back by index, `GET`ting the chunk's own URL and
|
|
247
|
+
* parsing the opaque body (stored as raw bytes, so parsed client-side) back
|
|
248
|
+
* into the EDV chunk object the decrypt stream consumes. Requires the
|
|
249
|
+
* backend's `chunked-streams` affordance (throws `NotSupportedError` when it
|
|
250
|
+
* is absent). A missing chunk (404) surfaces as a `NotFoundError` (the name
|
|
251
|
+
* `EdvClientCore` expects), so a reassembling reader can distinguish it.
|
|
252
|
+
*
|
|
253
|
+
* @param options {object}
|
|
254
|
+
* @param options.docId {string} the owning document id
|
|
255
|
+
* @param options.chunkIndex {number} the chunk's ordinal index
|
|
256
|
+
* @returns {Promise<IEDVChunk>}
|
|
252
257
|
*/
|
|
253
|
-
|
|
258
|
+
getChunk({ docId, chunkIndex }?: {
|
|
259
|
+
docId?: string;
|
|
260
|
+
chunkIndex?: number;
|
|
261
|
+
}): Promise<IEDVChunk>;
|
|
254
262
|
}
|
|
255
263
|
//# sourceMappingURL=WasTransport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WasTransport.d.ts","sourceRoot":"","sources":["../../src/edv/WasTransport.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH
|
|
1
|
+
{"version":3,"file":"WasTransport.d.ts","sourceRoot":"","sources":["../../src/edv/WasTransport.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAE/C,OAAO,KAAK,EACV,SAAS,EACT,SAAS,EACT,kBAAkB,EACnB,MAAM,8BAA8B,CAAA;AACrC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAUhD,OAAO,EAGL,iBAAiB,EAClB,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,iBAAiB,EAAE,CAAA;AAsB5B;;;GAGG;AACH,KAAK,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAqD9C,qBAAa,YAAa,SAAQ,SAAS;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAE5B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAc;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAE3C;;;;;;;;;OASG;gBACS,EACV,GAAG,EACH,OAAO,EACP,YAAY,EACZ,WAAkC,EACnC,EAAE;QACD,GAAG,EAAE,YAAY,CAAA;QACjB,OAAO,EAAE,MAAM,CAAA;QACf,YAAY,EAAE,MAAM,CAAA;QACpB,WAAW,CAAC,EAAE,MAAM,CAAA;KACrB;IAkBD;;;;;;;;OAQG;IACH,OAAO,CAAC,aAAa;IAIrB;;;;;;;;;;OAUG;YACW,IAAI;IAclB;;;;;;;;;;;;;;;;OAgBG;IACY,MAAM,CAAC,EACpB,SAAS,EACV,GAAE;QAAE,SAAS,CAAC,EAAE,kBAAkB,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAoC1D;;;;;;;;;;;;;;;;;;OAkBG;IACY,MAAM,CAAC,EACpB,SAAS,EACV,GAAE;QAAE,SAAS,CAAC,EAAE,kBAAkB,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAmB1D;;;;;;;;;OASG;IACY,GAAG,CAAC,EACjB,EAAE,EACH,GAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAkBrD;;;;;;;;OAQG;YACW,eAAe;IAW7B;;;;;;;OAOG;YACW,OAAO;IAYrB;;;;;;;;;;;;;;;;;;;;OAoBG;IACY,IAAI,CAAC,EAAE,KAAK,EAAE,GAAE;QAAE,KAAK,CAAC,EAAE,SAAS,CAAA;KAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IA6B3E;;;;;;;;;OASG;IACY,WAAW,IAAI,OAAO,CAAC,KAAK,CAAC;IAU5C;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU;IAIlB;;;;;;;;;;;;;;;;;;OAkBG;IACY,UAAU,CAAC,EACxB,KAAK,EACL,KAAK,EACN,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,SAAS,CAAA;KAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B7D;;;;;;;;;;;;;;OAcG;IACY,QAAQ,CAAC,EACtB,KAAK,EACL,UAAU,EACX,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,SAAS,CAAC;CA6BrE"}
|
package/dist/edv/WasTransport.js
CHANGED
|
@@ -32,9 +32,13 @@
|
|
|
32
32
|
* feature). `updateIndex` throws: in this profile the `indexed` array rides
|
|
33
33
|
* inside the stored document envelope, so `update()` IS the re-index
|
|
34
34
|
* operation and no separate index endpoint exists. Chunked streams
|
|
35
|
-
* (`storeChunk` / `getChunk`)
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* (`storeChunk` / `getChunk`) map each EDV chunk onto the reserved
|
|
36
|
+
* `/{id}/chunks/{n}` sub-segment (the server's `chunked-streams` affordance),
|
|
37
|
+
* storing the chunk object as an opaque JSON body -- so `EdvClientCore.insert({
|
|
38
|
+
* stream })` / `getStream` drive chunked encrypted blobs over a WAS server
|
|
39
|
+
* unchanged. Both chunk methods are gated on the backend advertising
|
|
40
|
+
* `chunked-streams` (throwing `NotSupportedError` when it is absent), like
|
|
41
|
+
* `find` is on `blinded-index-query`.
|
|
38
42
|
* `insert` uses an atomic `If-None-Match: *` create when the backend
|
|
39
43
|
* advertises the optional `conditional-writes` feature; otherwise (and for
|
|
40
44
|
* `update`) writes are advisory -- the EDV `sequence` is not enforced
|
|
@@ -42,21 +46,28 @@
|
|
|
42
46
|
*/
|
|
43
47
|
import { Transport } from '@interop/edv-client';
|
|
44
48
|
import { httpStatus } from '../errors.js';
|
|
49
|
+
import { BackendFeatures } from '../internal/features.js';
|
|
45
50
|
import { readJsonData } from '../internal/content.js';
|
|
46
|
-
import { collectionBackend, collectionQuery, resourcePath } from '../internal/paths.js';
|
|
51
|
+
import { collectionBackend, collectionQuery, resourceChunkPath, resourcePath } from '../internal/paths.js';
|
|
47
52
|
import { DEFAULT_CONTENT_TYPE, envelopeBytes, JOSE_CONTENT_TYPE } from './constants.js';
|
|
48
53
|
export { JOSE_CONTENT_TYPE };
|
|
49
54
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* transient/ambiguous and is re-probed instead of cached.
|
|
55
|
+
* The content type a serialized EDV chunk is stored under. Deliberately an
|
|
56
|
+
* opaque binary type (not `application/json`): the chunk is `PUT` as raw bytes
|
|
57
|
+
* so the server routes it through its streaming binary write path (bounded by
|
|
58
|
+
* the backend's `maxUploadBytes`, tens of MiB) rather than the in-memory JSON
|
|
59
|
+
* body parser (a ~1 MiB cap that a full encrypted chunk would exceed). The
|
|
60
|
+
* body is still JSON text -- the server stores it verbatim and never parses it,
|
|
61
|
+
* so `getChunk` decodes and parses it back client-side.
|
|
58
62
|
*/
|
|
59
|
-
const
|
|
63
|
+
const CHUNK_CONTENT_TYPE = 'application/octet-stream';
|
|
64
|
+
/**
|
|
65
|
+
* The message for the EDV unique-attribute collision (HTTP 409): a
|
|
66
|
+
* `unique: true` blinded attribute value already held by another document.
|
|
67
|
+
* Shared by every write method that can trip it.
|
|
68
|
+
*/
|
|
69
|
+
const DUPLICATE_ATTRIBUTE_MESSAGE = 'A unique indexed attribute value is already held by another document in ' +
|
|
70
|
+
'this collection.';
|
|
60
71
|
/**
|
|
61
72
|
* Builds an `Error` carrying the `name` that `EdvClientCore` (and the reference
|
|
62
73
|
* `HttpsTransport`) dispatch on -- `DuplicateError`, `InvalidStateError`,
|
|
@@ -76,12 +87,31 @@ function namedError({ name, message, cause }) {
|
|
|
76
87
|
}
|
|
77
88
|
return err;
|
|
78
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Rethrows a caught transport error as the named error `EdvClientCore`
|
|
92
|
+
* dispatches on, selected by the error's HTTP status from `mapping`; an
|
|
93
|
+
* unmapped status (or a non-HTTP error) is rethrown verbatim. The single
|
|
94
|
+
* status-to-named-error funnel every transport method's catch goes through.
|
|
95
|
+
*
|
|
96
|
+
* @param err {unknown} the caught error
|
|
97
|
+
* @param mapping {object} HTTP status to `{ name, message }` of the named
|
|
98
|
+
* error to throw in its place (the original error becomes its `cause`)
|
|
99
|
+
* @returns {never}
|
|
100
|
+
*/
|
|
101
|
+
function mapTransportError(err, mapping) {
|
|
102
|
+
const status = httpStatus(err);
|
|
103
|
+
const entry = status === undefined ? undefined : mapping[status];
|
|
104
|
+
if (entry) {
|
|
105
|
+
throw namedError({ ...entry, cause: err });
|
|
106
|
+
}
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
79
109
|
export class WasTransport extends Transport {
|
|
80
110
|
spaceId;
|
|
81
111
|
collectionId;
|
|
82
112
|
contentType;
|
|
83
113
|
_was;
|
|
84
|
-
|
|
114
|
+
_features;
|
|
85
115
|
/**
|
|
86
116
|
* @param options {object}
|
|
87
117
|
* @param options.was {WasClient} a WAS client holding the signer
|
|
@@ -98,6 +128,16 @@ export class WasTransport extends Transport {
|
|
|
98
128
|
this.spaceId = spaceId;
|
|
99
129
|
this.collectionId = collectionId;
|
|
100
130
|
this.contentType = contentType;
|
|
131
|
+
// The shared memoizing feature probe (see `BackendFeatures` for the
|
|
132
|
+
// definitive-vs-transient caching rules), reading this collection's
|
|
133
|
+
// "Collection Backend Selected" descriptor with a signed GET.
|
|
134
|
+
this._features = new BackendFeatures(async () => {
|
|
135
|
+
const response = await this._was.request({
|
|
136
|
+
path: collectionBackend(this.spaceId, this.collectionId),
|
|
137
|
+
method: 'GET'
|
|
138
|
+
});
|
|
139
|
+
return readJsonData(response);
|
|
140
|
+
});
|
|
101
141
|
}
|
|
102
142
|
/**
|
|
103
143
|
* The WAS resource path for a document id, delegating to was-client's
|
|
@@ -131,68 +171,6 @@ export class WasTransport extends Transport {
|
|
|
131
171
|
headers: { 'content-type': this.contentType, ...headers }
|
|
132
172
|
});
|
|
133
173
|
}
|
|
134
|
-
/**
|
|
135
|
-
* The feature tokens the collection's backend advertises in its "Collection
|
|
136
|
-
* Backend Selected" descriptor (e.g. `conditional-writes`,
|
|
137
|
-
* `blinded-index-query`). Memoized once it produces a definitive answer: a
|
|
138
|
-
* successful read (including one that lists no features) and a definitive
|
|
139
|
-
* "endpoint absent" (`404` / `405` / `501`) both resolve to a cached feature
|
|
140
|
-
* list, so every affordance gate falls closed against a server that has no
|
|
141
|
-
* backend descriptors.
|
|
142
|
-
*
|
|
143
|
-
* A transient/ambiguous failure (network error, timeout, `401`, `429`, other
|
|
144
|
-
* `5xx`) is NOT cached: the memo is cleared so the next call re-probes, and
|
|
145
|
-
* the error is rethrown so the caller fails loud rather than silently
|
|
146
|
-
* degrading atomicity against a server that may well be capable. (A single
|
|
147
|
-
* transient failure must not poison the transport for its lifetime.)
|
|
148
|
-
*
|
|
149
|
-
* @returns {Promise<string[]>}
|
|
150
|
-
*/
|
|
151
|
-
_backendFeatures() {
|
|
152
|
-
this._backendFeaturesPromise ??= this._probeBackendFeatures();
|
|
153
|
-
return this._backendFeaturesPromise;
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Reads and parses the backend descriptor once. On a definitive answer
|
|
157
|
-
* (success, or a `404` / `405` / `501` that means the endpoint is legitimately
|
|
158
|
-
* absent) resolves the feature list, which `_backendFeatures` then caches. On
|
|
159
|
-
* a transient failure, clears the memo (so the next call re-probes) and
|
|
160
|
-
* rethrows.
|
|
161
|
-
*
|
|
162
|
-
* @returns {Promise<string[]>}
|
|
163
|
-
*/
|
|
164
|
-
async _probeBackendFeatures() {
|
|
165
|
-
try {
|
|
166
|
-
const response = await this._was.request({
|
|
167
|
-
path: collectionBackend(this.spaceId, this.collectionId),
|
|
168
|
-
method: 'GET'
|
|
169
|
-
});
|
|
170
|
-
const descriptor = (await readJsonData(response));
|
|
171
|
-
return Array.isArray(descriptor?.features)
|
|
172
|
-
? descriptor.features.filter((feature) => typeof feature === 'string')
|
|
173
|
-
: [];
|
|
174
|
-
}
|
|
175
|
-
catch (err) {
|
|
176
|
-
const status = httpStatus(err);
|
|
177
|
-
if (status !== undefined && DESCRIPTOR_ABSENT_STATUSES.has(status)) {
|
|
178
|
-
return [];
|
|
179
|
-
}
|
|
180
|
-
// Transient/ambiguous: do not cache this failure -- drop the memo so the
|
|
181
|
-
// next call re-probes -- and rethrow.
|
|
182
|
-
this._backendFeaturesPromise = undefined;
|
|
183
|
-
throw err;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Whether the collection's backend advertises the optional
|
|
188
|
-
* `conditional-writes` feature, so `insert` can use an atomic
|
|
189
|
-
* `If-None-Match: *` create instead of the advisory existence-check path.
|
|
190
|
-
*
|
|
191
|
-
* @returns {Promise<boolean>}
|
|
192
|
-
*/
|
|
193
|
-
async _conditionalWrites() {
|
|
194
|
-
return (await this._backendFeatures()).includes('conditional-writes');
|
|
195
|
-
}
|
|
196
174
|
/**
|
|
197
175
|
* @inheritdoc
|
|
198
176
|
*
|
|
@@ -214,27 +192,21 @@ export class WasTransport extends Transport {
|
|
|
214
192
|
if (!encrypted) {
|
|
215
193
|
throw new TypeError('"encrypted" is required.');
|
|
216
194
|
}
|
|
217
|
-
if (await this.
|
|
195
|
+
if (await this._features.has('conditional-writes')) {
|
|
218
196
|
try {
|
|
219
197
|
await this._put(encrypted.id, encrypted, { 'if-none-match': '*' });
|
|
220
198
|
}
|
|
221
199
|
catch (err) {
|
|
222
|
-
|
|
223
|
-
|
|
200
|
+
mapTransportError(err, {
|
|
201
|
+
412: {
|
|
224
202
|
name: 'DuplicateError',
|
|
225
|
-
message: `A document with id "${encrypted.id}" already exists
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
if (httpStatus(err) === 409) {
|
|
230
|
-
throw namedError({
|
|
203
|
+
message: `A document with id "${encrypted.id}" already exists.`
|
|
204
|
+
},
|
|
205
|
+
409: {
|
|
231
206
|
name: 'DuplicateError',
|
|
232
|
-
message:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
throw err;
|
|
207
|
+
message: DUPLICATE_ATTRIBUTE_MESSAGE
|
|
208
|
+
}
|
|
209
|
+
});
|
|
238
210
|
}
|
|
239
211
|
return;
|
|
240
212
|
}
|
|
@@ -248,15 +220,9 @@ export class WasTransport extends Transport {
|
|
|
248
220
|
await this._put(encrypted.id, encrypted);
|
|
249
221
|
}
|
|
250
222
|
catch (err) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
message: 'A unique indexed attribute value is already held by another ' +
|
|
255
|
-
'document in this collection.',
|
|
256
|
-
cause: err
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
throw err;
|
|
223
|
+
mapTransportError(err, {
|
|
224
|
+
409: { name: 'DuplicateError', message: DUPLICATE_ATTRIBUTE_MESSAGE }
|
|
225
|
+
});
|
|
260
226
|
}
|
|
261
227
|
}
|
|
262
228
|
/**
|
|
@@ -286,23 +252,14 @@ export class WasTransport extends Transport {
|
|
|
286
252
|
await this._put(encrypted.id, encrypted);
|
|
287
253
|
}
|
|
288
254
|
catch (err) {
|
|
289
|
-
|
|
290
|
-
|
|
255
|
+
mapTransportError(err, {
|
|
256
|
+
412: {
|
|
291
257
|
name: 'InvalidStateError',
|
|
292
258
|
message: 'Document update conflict: the stored document changed since it ' +
|
|
293
|
-
'was read. Re-fetch the current document and retry.'
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (httpStatus(err) === 409) {
|
|
298
|
-
throw namedError({
|
|
299
|
-
name: 'DuplicateError',
|
|
300
|
-
message: 'A unique indexed attribute value is already held by another ' +
|
|
301
|
-
'document in this collection.',
|
|
302
|
-
cause: err
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
throw err;
|
|
259
|
+
'was read. Re-fetch the current document and retry.'
|
|
260
|
+
},
|
|
261
|
+
409: { name: 'DuplicateError', message: DUPLICATE_ATTRIBUTE_MESSAGE }
|
|
262
|
+
});
|
|
306
263
|
}
|
|
307
264
|
}
|
|
308
265
|
/**
|
|
@@ -327,17 +284,30 @@ export class WasTransport extends Transport {
|
|
|
327
284
|
});
|
|
328
285
|
}
|
|
329
286
|
catch (err) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
message: 'Document not found.',
|
|
334
|
-
cause: err
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
throw err;
|
|
287
|
+
mapTransportError(err, {
|
|
288
|
+
404: { name: 'NotFoundError', message: 'Document not found.' }
|
|
289
|
+
});
|
|
338
290
|
}
|
|
339
291
|
return (await readJsonData(response));
|
|
340
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* Throws a `NotSupportedError` (the name `EdvClientCore` dispatches on)
|
|
295
|
+
* unless the collection's backend advertises the given affordance token --
|
|
296
|
+
* the shared gate in front of every optional-feature operation.
|
|
297
|
+
*
|
|
298
|
+
* @param feature {string} the affordance token (e.g. `chunked-streams`)
|
|
299
|
+
* @param what {string} the operation name, for the message
|
|
300
|
+
* @returns {Promise<void>}
|
|
301
|
+
*/
|
|
302
|
+
async _requireFeature(feature, what) {
|
|
303
|
+
if (!(await this._features.has(feature))) {
|
|
304
|
+
throw namedError({
|
|
305
|
+
name: 'NotSupportedError',
|
|
306
|
+
message: `${what} is not supported: the collection's backend ` +
|
|
307
|
+
`does not advertise the "${feature}" affordance.`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
341
311
|
/**
|
|
342
312
|
* Resolves to `true` if a resource exists at the document's path, via a
|
|
343
313
|
* bodiless `HEAD` (the stored envelope is not needed, only its existence). A
|
|
@@ -383,13 +353,7 @@ export class WasTransport extends Transport {
|
|
|
383
353
|
if (!query) {
|
|
384
354
|
throw new TypeError('"query" is required.');
|
|
385
355
|
}
|
|
386
|
-
|
|
387
|
-
throw namedError({
|
|
388
|
-
name: 'NotSupportedError',
|
|
389
|
-
message: "Blinded-index query is not supported: the collection's backend " +
|
|
390
|
-
'does not advertise the "blinded-index-query" affordance.'
|
|
391
|
-
});
|
|
392
|
-
}
|
|
356
|
+
await this._requireFeature('blinded-index-query', 'Blinded-index query');
|
|
393
357
|
// `returnDocuments` is a first-class `IEDVQuery` field, but the WAS profile
|
|
394
358
|
// has no ids-only mode, so it is dropped (whatever its value) and full
|
|
395
359
|
// documents come back -- the best-effort degradation `EdvClientCore.find`
|
|
@@ -404,14 +368,9 @@ export class WasTransport extends Transport {
|
|
|
404
368
|
});
|
|
405
369
|
}
|
|
406
370
|
catch (err) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
message: 'Collection not found.',
|
|
411
|
-
cause: err
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
throw err;
|
|
371
|
+
mapTransportError(err, {
|
|
372
|
+
404: { name: 'NotFoundError', message: 'Collection not found.' }
|
|
373
|
+
});
|
|
415
374
|
}
|
|
416
375
|
const result = await readJsonData(response);
|
|
417
376
|
if (result === null || typeof result !== 'object') {
|
|
@@ -438,36 +397,109 @@ export class WasTransport extends Transport {
|
|
|
438
397
|
});
|
|
439
398
|
}
|
|
440
399
|
/**
|
|
441
|
-
*
|
|
400
|
+
* The WAS path of one chunk of a document, delegating to the internal
|
|
401
|
+
* `resourceChunkPath` builder (member form, no trailing slash --
|
|
402
|
+
* put/get/delete one chunk by index).
|
|
442
403
|
*
|
|
443
|
-
*
|
|
404
|
+
* @param docId {string} the EDV document id (= WAS resource id)
|
|
405
|
+
* @param chunkIndex {number} the chunk's non-negative ordinal index
|
|
406
|
+
* @returns {string}
|
|
444
407
|
*/
|
|
445
|
-
|
|
446
|
-
return this.
|
|
408
|
+
_chunkPath(docId, chunkIndex) {
|
|
409
|
+
return resourceChunkPath(this.spaceId, this.collectionId, docId, chunkIndex);
|
|
447
410
|
}
|
|
448
411
|
/**
|
|
449
412
|
* @inheritdoc
|
|
450
413
|
*
|
|
451
|
-
*
|
|
414
|
+
* Stores one encrypted chunk of a document's data stream. The EDV chunk
|
|
415
|
+
* object (`{ sequence, index, jwe, offset }`) is serialized to JSON and
|
|
416
|
+
* `PUT` as an opaque binary body ({@link CHUNK_CONTENT_TYPE}) to the chunk's
|
|
417
|
+
* own URL (`.../chunks/{index}`), signed like every other write. The server
|
|
418
|
+
* stores the bytes verbatim -- it never parses the chunk -- so any
|
|
419
|
+
* client-side crypto framing is transparent to it. Requires the backend's
|
|
420
|
+
* `chunked-streams` affordance (throws `NotSupportedError` when it is
|
|
421
|
+
* absent). The parent Resource must already exist
|
|
422
|
+
* (`EdvClientCore.insert`/`update` writes the document envelope before
|
|
423
|
+
* draining the stream), so a 404 here surfaces as a `NotFoundError`.
|
|
424
|
+
*
|
|
425
|
+
* @param options {object}
|
|
426
|
+
* @param options.docId {string} the owning document id (= WAS resource id)
|
|
427
|
+
* @param options.chunk {IEDVChunk} the encrypted chunk to store
|
|
428
|
+
* @returns {Promise<void>}
|
|
452
429
|
*/
|
|
453
|
-
async
|
|
454
|
-
|
|
430
|
+
async storeChunk({ docId, chunk } = {}) {
|
|
431
|
+
if (!docId) {
|
|
432
|
+
throw new TypeError('"docId" is required.');
|
|
433
|
+
}
|
|
434
|
+
if (!chunk) {
|
|
435
|
+
throw new TypeError('"chunk" is required.');
|
|
436
|
+
}
|
|
437
|
+
// Gate on the affordance before diagnosing a 404: against a server with no
|
|
438
|
+
// `/chunks/{n}` route at all, the 404 would otherwise be misreported as a
|
|
439
|
+
// missing parent document.
|
|
440
|
+
await this._requireFeature('chunked-streams', 'Chunked encrypted storage');
|
|
441
|
+
try {
|
|
442
|
+
await this._was.request({
|
|
443
|
+
path: this._chunkPath(docId, chunk.index),
|
|
444
|
+
method: 'PUT',
|
|
445
|
+
body: envelopeBytes(chunk),
|
|
446
|
+
headers: { 'content-type': CHUNK_CONTENT_TYPE }
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
mapTransportError(err, {
|
|
451
|
+
404: {
|
|
452
|
+
name: 'NotFoundError',
|
|
453
|
+
message: `Cannot store chunk ${chunk.index}: the parent document ` +
|
|
454
|
+
`"${docId}" does not exist. Write the document before its chunks.`
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
455
458
|
}
|
|
456
459
|
/**
|
|
457
|
-
*
|
|
458
|
-
* stream operations, which depend on a server-side affordance (the reserved
|
|
459
|
-
* `/{id}/chunks/{n}` sub-segment, the `chunked-streams` backend feature)
|
|
460
|
-
* that neither reference backend provides yet.
|
|
460
|
+
* @inheritdoc
|
|
461
461
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
462
|
+
* Reads one encrypted chunk back by index, `GET`ting the chunk's own URL and
|
|
463
|
+
* parsing the opaque body (stored as raw bytes, so parsed client-side) back
|
|
464
|
+
* into the EDV chunk object the decrypt stream consumes. Requires the
|
|
465
|
+
* backend's `chunked-streams` affordance (throws `NotSupportedError` when it
|
|
466
|
+
* is absent). A missing chunk (404) surfaces as a `NotFoundError` (the name
|
|
467
|
+
* `EdvClientCore` expects), so a reassembling reader can distinguish it.
|
|
468
|
+
*
|
|
469
|
+
* @param options {object}
|
|
470
|
+
* @param options.docId {string} the owning document id
|
|
471
|
+
* @param options.chunkIndex {number} the chunk's ordinal index
|
|
472
|
+
* @returns {Promise<IEDVChunk>}
|
|
464
473
|
*/
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
474
|
+
async getChunk({ docId, chunkIndex } = {}) {
|
|
475
|
+
if (!docId) {
|
|
476
|
+
throw new TypeError('"docId" is required.');
|
|
477
|
+
}
|
|
478
|
+
if (chunkIndex === undefined) {
|
|
479
|
+
throw new TypeError('"chunkIndex" is required.');
|
|
480
|
+
}
|
|
481
|
+
// Gate on the affordance before diagnosing a 404: against a server with no
|
|
482
|
+
// `/chunks/{n}` route at all, the 404 would otherwise surface as a spurious
|
|
483
|
+
// missing-chunk (data corruption) report.
|
|
484
|
+
await this._requireFeature('chunked-streams', 'Chunked encrypted storage');
|
|
485
|
+
let response;
|
|
486
|
+
try {
|
|
487
|
+
response = await this._was.request({
|
|
488
|
+
path: this._chunkPath(docId, chunkIndex),
|
|
489
|
+
method: 'GET'
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
mapTransportError(err, {
|
|
494
|
+
404: {
|
|
495
|
+
name: 'NotFoundError',
|
|
496
|
+
message: `Chunk ${chunkIndex} of document "${docId}" not found.`
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
// The chunk was stored as opaque bytes, so the http-client did not
|
|
501
|
+
// pre-parse it: decode the body text and parse the EDV chunk object back.
|
|
502
|
+
return JSON.parse(await response.text());
|
|
471
503
|
}
|
|
472
504
|
}
|
|
473
505
|
//# sourceMappingURL=WasTransport.js.map
|