was-teaching-server 0.9.1
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/CHANGELOG.md +1432 -0
- package/LICENSE +661 -0
- package/README.md +305 -0
- package/common/css/materialize.1.0.0.min.css +13 -0
- package/dist/auth-header-hooks.d.ts +84 -0
- package/dist/auth-header-hooks.d.ts.map +1 -0
- package/dist/auth-header-hooks.js +136 -0
- package/dist/auth-header-hooks.js.map +1 -0
- package/dist/authorize.d.ts +45 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +76 -0
- package/dist/authorize.js.map +1 -0
- package/dist/backends/filesystem.d.ts +1179 -0
- package/dist/backends/filesystem.d.ts.map +1 -0
- package/dist/backends/filesystem.js +2727 -0
- package/dist/backends/filesystem.js.map +1 -0
- package/dist/backends/postgres.d.ts +833 -0
- package/dist/backends/postgres.d.ts.map +1 -0
- package/dist/backends/postgres.js +2164 -0
- package/dist/backends/postgres.js.map +1 -0
- package/dist/backends/postgresSchema.d.ts +36 -0
- package/dist/backends/postgresSchema.d.ts.map +1 -0
- package/dist/backends/postgresSchema.js +203 -0
- package/dist/backends/postgresSchema.js.map +1 -0
- package/dist/config.default.d.ts +360 -0
- package/dist/config.default.d.ts.map +1 -0
- package/dist/config.default.js +493 -0
- package/dist/config.default.js.map +1 -0
- package/dist/corsProxy.d.ts +45 -0
- package/dist/corsProxy.d.ts.map +1 -0
- package/dist/corsProxy.js +278 -0
- package/dist/corsProxy.js.map +1 -0
- package/dist/digest.d.ts +45 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +174 -0
- package/dist/digest.js.map +1 -0
- package/dist/errors.d.ts +654 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +960 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/atomicFile.d.ts +61 -0
- package/dist/lib/atomicFile.d.ts.map +1 -0
- package/dist/lib/atomicFile.js +150 -0
- package/dist/lib/atomicFile.js.map +1 -0
- package/dist/lib/backendRegistry.d.ts +59 -0
- package/dist/lib/backendRegistry.d.ts.map +1 -0
- package/dist/lib/backendRegistry.js +92 -0
- package/dist/lib/backendRegistry.js.map +1 -0
- package/dist/lib/backendUsage.d.ts +28 -0
- package/dist/lib/backendUsage.d.ts.map +1 -0
- package/dist/lib/backendUsage.js +55 -0
- package/dist/lib/backendUsage.js.map +1 -0
- package/dist/lib/backends.d.ts +142 -0
- package/dist/lib/backends.d.ts.map +1 -0
- package/dist/lib/backends.js +272 -0
- package/dist/lib/backends.js.map +1 -0
- package/dist/lib/blindedIndex.d.ts +156 -0
- package/dist/lib/blindedIndex.d.ts.map +1 -0
- package/dist/lib/blindedIndex.js +338 -0
- package/dist/lib/blindedIndex.js.map +1 -0
- package/dist/lib/collectionDescription.d.ts +33 -0
- package/dist/lib/collectionDescription.d.ts.map +1 -0
- package/dist/lib/collectionDescription.js +21 -0
- package/dist/lib/collectionDescription.js.map +1 -0
- package/dist/lib/cursor.d.ts +18 -0
- package/dist/lib/cursor.d.ts.map +1 -0
- package/dist/lib/cursor.js +50 -0
- package/dist/lib/cursor.js.map +1 -0
- package/dist/lib/edvEnvelope.d.ts +50 -0
- package/dist/lib/edvEnvelope.d.ts.map +1 -0
- package/dist/lib/edvEnvelope.js +106 -0
- package/dist/lib/edvEnvelope.js.map +1 -0
- package/dist/lib/encryption.d.ts +192 -0
- package/dist/lib/encryption.d.ts.map +1 -0
- package/dist/lib/encryption.js +383 -0
- package/dist/lib/encryption.js.map +1 -0
- package/dist/lib/etag.d.ts +33 -0
- package/dist/lib/etag.d.ts.map +1 -0
- package/dist/lib/etag.js +36 -0
- package/dist/lib/etag.js.map +1 -0
- package/dist/lib/exportManifest.d.ts +29 -0
- package/dist/lib/exportManifest.d.ts.map +1 -0
- package/dist/lib/exportManifest.js +77 -0
- package/dist/lib/exportManifest.js.map +1 -0
- package/dist/lib/importTar.d.ts +81 -0
- package/dist/lib/importTar.d.ts.map +1 -0
- package/dist/lib/importTar.js +301 -0
- package/dist/lib/importTar.js.map +1 -0
- package/dist/lib/isJson.d.ts +16 -0
- package/dist/lib/isJson.d.ts.map +1 -0
- package/dist/lib/isJson.js +17 -0
- package/dist/lib/isJson.js.map +1 -0
- package/dist/lib/keyEpoch.d.ts +38 -0
- package/dist/lib/keyEpoch.d.ts.map +1 -0
- package/dist/lib/keyEpoch.js +75 -0
- package/dist/lib/keyEpoch.js.map +1 -0
- package/dist/lib/keyedMutex.d.ts +28 -0
- package/dist/lib/keyedMutex.d.ts.map +1 -0
- package/dist/lib/keyedMutex.js +45 -0
- package/dist/lib/keyedMutex.js.map +1 -0
- package/dist/lib/kmsModule.d.ts +62 -0
- package/dist/lib/kmsModule.d.ts.map +1 -0
- package/dist/lib/kmsModule.js +327 -0
- package/dist/lib/kmsModule.js.map +1 -0
- package/dist/lib/kmsRecordCipher.d.ts +82 -0
- package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
- package/dist/lib/kmsRecordCipher.js +226 -0
- package/dist/lib/kmsRecordCipher.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.d.ts.map +1 -0
- package/dist/lib/pagination.js +21 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/paths.d.ts +273 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +270 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/preconditions.d.ts +65 -0
- package/dist/lib/preconditions.d.ts.map +1 -0
- package/dist/lib/preconditions.js +112 -0
- package/dist/lib/preconditions.js.map +1 -0
- package/dist/lib/resourceFileName.d.ts +41 -0
- package/dist/lib/resourceFileName.d.ts.map +1 -0
- package/dist/lib/resourceFileName.js +58 -0
- package/dist/lib/resourceFileName.js.map +1 -0
- package/dist/lib/revocations.d.ts +49 -0
- package/dist/lib/revocations.d.ts.map +1 -0
- package/dist/lib/revocations.js +86 -0
- package/dist/lib/revocations.js.map +1 -0
- package/dist/lib/validateDid.d.ts +19 -0
- package/dist/lib/validateDid.d.ts.map +1 -0
- package/dist/lib/validateDid.js +35 -0
- package/dist/lib/validateDid.js.map +1 -0
- package/dist/lib/validateId.d.ts +66 -0
- package/dist/lib/validateId.d.ts.map +1 -0
- package/dist/lib/validateId.js +126 -0
- package/dist/lib/validateId.js.map +1 -0
- package/dist/plugin.d.ts +115 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +134 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +78 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/policy.js.map +1 -0
- package/dist/provisioning.d.ts +38 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +72 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/requests/BackendRequest.d.ts +61 -0
- package/dist/requests/BackendRequest.d.ts.map +1 -0
- package/dist/requests/BackendRequest.js +152 -0
- package/dist/requests/BackendRequest.js.map +1 -0
- package/dist/requests/CollectionRequest.d.ts +234 -0
- package/dist/requests/CollectionRequest.d.ts.map +1 -0
- package/dist/requests/CollectionRequest.js +705 -0
- package/dist/requests/CollectionRequest.js.map +1 -0
- package/dist/requests/KeyRequest.d.ts +115 -0
- package/dist/requests/KeyRequest.d.ts.map +1 -0
- package/dist/requests/KeyRequest.js +468 -0
- package/dist/requests/KeyRequest.js.map +1 -0
- package/dist/requests/KeystoreRequest.d.ts +99 -0
- package/dist/requests/KeystoreRequest.d.ts.map +1 -0
- package/dist/requests/KeystoreRequest.js +247 -0
- package/dist/requests/KeystoreRequest.js.map +1 -0
- package/dist/requests/PolicyRequest.d.ts +53 -0
- package/dist/requests/PolicyRequest.d.ts.map +1 -0
- package/dist/requests/PolicyRequest.js +124 -0
- package/dist/requests/PolicyRequest.js.map +1 -0
- package/dist/requests/ResourceRequest.d.ts +134 -0
- package/dist/requests/ResourceRequest.d.ts.map +1 -0
- package/dist/requests/ResourceRequest.js +548 -0
- package/dist/requests/ResourceRequest.js.map +1 -0
- package/dist/requests/RevocationRequest.d.ts +67 -0
- package/dist/requests/RevocationRequest.d.ts.map +1 -0
- package/dist/requests/RevocationRequest.js +187 -0
- package/dist/requests/RevocationRequest.js.map +1 -0
- package/dist/requests/SpaceRequest.d.ts +204 -0
- package/dist/requests/SpaceRequest.d.ts.map +1 -0
- package/dist/requests/SpaceRequest.js +520 -0
- package/dist/requests/SpaceRequest.js.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.js +159 -0
- package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
- package/dist/requests/collectionContext.d.ts +21 -0
- package/dist/requests/collectionContext.d.ts.map +1 -0
- package/dist/requests/collectionContext.js +30 -0
- package/dist/requests/collectionContext.js.map +1 -0
- package/dist/requests/controllerConsent.d.ts +58 -0
- package/dist/requests/controllerConsent.d.ts.map +1 -0
- package/dist/requests/controllerConsent.js +73 -0
- package/dist/requests/controllerConsent.js.map +1 -0
- package/dist/requests/keystoreContext.d.ts +68 -0
- package/dist/requests/keystoreContext.d.ts.map +1 -0
- package/dist/requests/keystoreContext.js +87 -0
- package/dist/requests/keystoreContext.js.map +1 -0
- package/dist/requests/resourceInput.d.ts +19 -0
- package/dist/requests/resourceInput.d.ts.map +1 -0
- package/dist/requests/resourceInput.js +102 -0
- package/dist/requests/resourceInput.js.map +1 -0
- package/dist/requests/spaceContext.d.ts +127 -0
- package/dist/requests/spaceContext.d.ts.map +1 -0
- package/dist/requests/spaceContext.js +201 -0
- package/dist/requests/spaceContext.js.map +1 -0
- package/dist/routes.d.ts +55 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +306 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +60 -0
- package/dist/server.js.map +1 -0
- package/dist/start.d.ts +7 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +61 -0
- package/dist/start.js.map +1 -0
- package/dist/storage.d.ts +29 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +47 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +915 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/views/home.hbs +9 -0
- package/dist/views/templates/main.hbs +12 -0
- package/dist/zcap.d.ts +267 -0
- package/dist/zcap.d.ts.map +1 -0
- package/dist/zcap.js +478 -0
- package/dist/zcap.js.map +1 -0
- package/package.json +109 -0
- package/src/auth-header-hooks.ts +163 -0
- package/src/authorize.ts +117 -0
- package/src/backends/filesystem.ts +3626 -0
- package/src/backends/postgres.ts +3073 -0
- package/src/backends/postgresSchema.ts +231 -0
- package/src/config.default.ts +657 -0
- package/src/corsProxy.ts +326 -0
- package/src/declarations.d.ts +28 -0
- package/src/digest.ts +223 -0
- package/src/errors.ts +1122 -0
- package/src/index.ts +21 -0
- package/src/lib/atomicFile.ts +176 -0
- package/src/lib/backendRegistry.ts +139 -0
- package/src/lib/backendUsage.ts +79 -0
- package/src/lib/backends.ts +351 -0
- package/src/lib/blindedIndex.ts +466 -0
- package/src/lib/collectionDescription.ts +43 -0
- package/src/lib/cursor.ts +52 -0
- package/src/lib/edvEnvelope.ts +120 -0
- package/src/lib/encryption.ts +472 -0
- package/src/lib/etag.ts +40 -0
- package/src/lib/exportManifest.ts +115 -0
- package/src/lib/importTar.ts +381 -0
- package/src/lib/isJson.ts +18 -0
- package/src/lib/keyEpoch.ts +89 -0
- package/src/lib/keyedMutex.ts +45 -0
- package/src/lib/kmsModule.ts +451 -0
- package/src/lib/kmsRecordCipher.ts +292 -0
- package/src/lib/pagination.ts +22 -0
- package/src/lib/paths.ts +382 -0
- package/src/lib/preconditions.ts +145 -0
- package/src/lib/resourceFileName.ts +69 -0
- package/src/lib/revocations.ts +116 -0
- package/src/lib/validateDid.ts +41 -0
- package/src/lib/validateId.ts +148 -0
- package/src/plugin.ts +288 -0
- package/src/policy.ts +176 -0
- package/src/provisioning.ts +98 -0
- package/src/requests/BackendRequest.ts +219 -0
- package/src/requests/CollectionRequest.ts +899 -0
- package/src/requests/KeyRequest.ts +617 -0
- package/src/requests/KeystoreRequest.ts +325 -0
- package/src/requests/PolicyRequest.ts +169 -0
- package/src/requests/ResourceRequest.ts +649 -0
- package/src/requests/RevocationRequest.ts +267 -0
- package/src/requests/SpaceRequest.ts +686 -0
- package/src/requests/SpacesRepositoryRequest.ts +194 -0
- package/src/requests/collectionContext.ts +41 -0
- package/src/requests/controllerConsent.ts +108 -0
- package/src/requests/keystoreContext.ts +124 -0
- package/src/requests/resourceInput.ts +118 -0
- package/src/requests/spaceContext.ts +311 -0
- package/src/routes.ts +420 -0
- package/src/server.ts +69 -0
- package/src/start.ts +71 -0
- package/src/storage.ts +60 -0
- package/src/types.ts +1016 -0
- package/src/views/home.hbs +9 -0
- package/src/views/templates/main.hbs +12 -0
- package/src/zcap.ts +657 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blinded-index query evaluation -- the `blinded-index` profile of the reserved
|
|
3
|
+
* Collection `POST .../query` endpoint (the `blinded-index-query` backend
|
|
4
|
+
* feature). An EDV-encrypted Resource stored in a Collection carries an
|
|
5
|
+
* `indexed` array of HMAC-blinded attribute entries (produced client-side by
|
|
6
|
+
* `@interop/edv-client`'s IndexHelper); this module evaluates EDV queries
|
|
7
|
+
* (`equals` / `has`) against those entries, and enforces the write-time EDV
|
|
8
|
+
* unique-attribute invariant (`unique: true` claims;
|
|
9
|
+
* `assertNoUniqueBlindedConflict`). Everything here is opaque
|
|
10
|
+
* base64url string matching: the server never sees plaintext attribute names
|
|
11
|
+
* or values and performs no cryptography, so it is agnostic to the client's
|
|
12
|
+
* attribute-blinding version.
|
|
13
|
+
*
|
|
14
|
+
* Both storage backends answer the profile through `runBlindedIndexQuery` so
|
|
15
|
+
* their matching, ordering, and pagination semantics cannot drift (the same
|
|
16
|
+
* pattern as `preconditions.ts` for conditional writes). Matching semantics
|
|
17
|
+
* follow the EDV reference servers (`@interop/edv-server`,
|
|
18
|
+
* `bedrock-edv-storage`): `equals` is an OR across array elements of an AND
|
|
19
|
+
* within each element's `{name: value}` pairs, scoped to the queried index's
|
|
20
|
+
* entries; `has` requires every named attribute be present (value
|
|
21
|
+
* irrelevant). Pagination replaces EDV's `limit`+`hasMore`-only story with
|
|
22
|
+
* WAS's opaque cursor convention (`lib/cursor.ts`), closing the known EDV
|
|
23
|
+
* protocol gap: `cursor` names the keyset position (ascending `resourceId`)
|
|
24
|
+
* to resume from.
|
|
25
|
+
*/
|
|
26
|
+
import { decodeCursor, encodeCursor } from './cursor.js';
|
|
27
|
+
import { clampPageSize, DEFAULT_PAGE_SIZE } from './pagination.js';
|
|
28
|
+
import { InvalidRequestBodyError, UniqueAttributeConflictError } from '../errors.js';
|
|
29
|
+
/**
|
|
30
|
+
* Validates and normalizes the `blinded-index` profile's query body fields
|
|
31
|
+
* (everything besides `profile`), throwing `invalid-request-body` (400) on a
|
|
32
|
+
* malformed query. Mirrors the EDV query schema (`{index, equals | has,
|
|
33
|
+
* count, limit}`, with `equals` and `has` mutually exclusive and exactly one
|
|
34
|
+
* required) plus the WAS `cursor`. `limit` is coerced leniently like the
|
|
35
|
+
* `changes` profile (a non-numeric or `< 1` value falls back to the default;
|
|
36
|
+
* the backend clamps an oversized one), and the opaque `cursor` is validated
|
|
37
|
+
* by the backend's cursor decode (`invalid-cursor` 400).
|
|
38
|
+
*
|
|
39
|
+
* @param options {object}
|
|
40
|
+
* @param options.body {object} the parsed query POST body
|
|
41
|
+
* @param [options.requestName] {string}
|
|
42
|
+
* @returns {{ query: BlindedIndexQuery, count: boolean, limit?: number, cursor?: string }}
|
|
43
|
+
*/
|
|
44
|
+
export function parseBlindedIndexQueryBody({ body, requestName }) {
|
|
45
|
+
const { index, equals, has, count, limit, cursor } = body;
|
|
46
|
+
if (typeof index !== 'string' || index.length === 0) {
|
|
47
|
+
throw new InvalidRequestBodyError({
|
|
48
|
+
requestName,
|
|
49
|
+
detail: 'A blinded-index query requires a non-empty string "index".',
|
|
50
|
+
pointer: '#/index'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
// Exactly one of `equals` / `has` (the EDV client never sends both; the
|
|
54
|
+
// reference server schema requires at least one).
|
|
55
|
+
if ((equals === undefined) === (has === undefined)) {
|
|
56
|
+
throw new InvalidRequestBodyError({
|
|
57
|
+
requestName,
|
|
58
|
+
detail: 'A blinded-index query requires exactly one of "equals" or "has".'
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const query = { index };
|
|
62
|
+
if (equals !== undefined) {
|
|
63
|
+
if (!Array.isArray(equals) || equals.length === 0) {
|
|
64
|
+
throw new InvalidRequestBodyError({
|
|
65
|
+
requestName,
|
|
66
|
+
detail: '"equals" must be a non-empty array of objects.',
|
|
67
|
+
pointer: '#/equals'
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
for (const element of equals) {
|
|
71
|
+
if (typeof element !== 'object' ||
|
|
72
|
+
element === null ||
|
|
73
|
+
Array.isArray(element) ||
|
|
74
|
+
Object.values(element).some(value => typeof value !== 'string')) {
|
|
75
|
+
throw new InvalidRequestBodyError({
|
|
76
|
+
requestName,
|
|
77
|
+
detail: '"equals" elements must be objects mapping blinded attribute names to blinded string values.',
|
|
78
|
+
pointer: '#/equals'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
query.equals = equals;
|
|
83
|
+
}
|
|
84
|
+
if (has !== undefined) {
|
|
85
|
+
if (!Array.isArray(has) ||
|
|
86
|
+
has.length === 0 ||
|
|
87
|
+
has.some(name => typeof name !== 'string')) {
|
|
88
|
+
throw new InvalidRequestBodyError({
|
|
89
|
+
requestName,
|
|
90
|
+
detail: '"has" must be a non-empty array of blinded attribute names.',
|
|
91
|
+
pointer: '#/has'
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
query.has = has;
|
|
95
|
+
}
|
|
96
|
+
if (count !== undefined && typeof count !== 'boolean') {
|
|
97
|
+
throw new InvalidRequestBodyError({
|
|
98
|
+
requestName,
|
|
99
|
+
detail: '"count" must be a boolean.',
|
|
100
|
+
pointer: '#/count'
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (cursor !== undefined && typeof cursor !== 'string') {
|
|
104
|
+
throw new InvalidRequestBodyError({
|
|
105
|
+
requestName,
|
|
106
|
+
detail: '"cursor" must be a string.',
|
|
107
|
+
pointer: '#/cursor'
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Lenient `limit` coercion, same as the `changes` profile: a non-numeric or
|
|
111
|
+
// `< 1` value is ignored so the backend applies its own default/clamp.
|
|
112
|
+
const parsedLimit = Number(limit);
|
|
113
|
+
const resolvedLimit = Number.isFinite(parsedLimit) && parsedLimit >= 1 ? parsedLimit : undefined;
|
|
114
|
+
return {
|
|
115
|
+
query,
|
|
116
|
+
count: count === true,
|
|
117
|
+
...(resolvedLimit !== undefined && { limit: resolvedLimit }),
|
|
118
|
+
...(cursor !== undefined && { cursor })
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Collects the `{name: value}` attribute pairs of a document's `indexed`
|
|
123
|
+
* entries into flat `name:value` terms. `scopedTo` limits collection to the
|
|
124
|
+
* entries of one HMAC key id (the `equals` scope); without it, attributes of
|
|
125
|
+
* every entry are collected (the `has` scope, matching the reference servers'
|
|
126
|
+
* Mongo-parity semantics where `has` names may come from any entry).
|
|
127
|
+
* Malformed entries and attributes are skipped, never thrown on.
|
|
128
|
+
*
|
|
129
|
+
* @param options {object}
|
|
130
|
+
* @param options.indexed {unknown[]} the document's `indexed` array
|
|
131
|
+
* @param [options.scopedTo] {string} collect only entries with this hmac id
|
|
132
|
+
* @returns {{ names: Set<string>, terms: Set<string> }}
|
|
133
|
+
*/
|
|
134
|
+
function collectIndexedAttributes({ indexed, scopedTo }) {
|
|
135
|
+
const names = new Set();
|
|
136
|
+
const terms = new Set();
|
|
137
|
+
for (const entry of indexed) {
|
|
138
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const { hmac, attributes } = entry;
|
|
142
|
+
if (scopedTo !== undefined && hmac?.id !== scopedTo) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!Array.isArray(attributes)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
for (const attribute of attributes) {
|
|
149
|
+
if (typeof attribute?.name !== 'string') {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
names.add(attribute.name);
|
|
153
|
+
if (typeof attribute.value === 'string') {
|
|
154
|
+
terms.add(`${attribute.name}:${attribute.value}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { names, terms };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* True when a stored JSON document matches a blinded-index query. The document
|
|
162
|
+
* must carry an `indexed` entry for the queried `index` (HMAC key id); then
|
|
163
|
+
* `equals` matches when SOME array element has ALL its `{name: value}` pairs
|
|
164
|
+
* present among that index's blinded attributes (an empty element matches
|
|
165
|
+
* nothing -- the reference servers' Mongo `$all: []` parity, which the EDV
|
|
166
|
+
* client emits when querying a never-indexed attribute), and `has` matches
|
|
167
|
+
* when ALL named attributes are present (from any indexed entry). Purely
|
|
168
|
+
* structural string matching -- no decoding, no crypto.
|
|
169
|
+
*
|
|
170
|
+
* @param options {object}
|
|
171
|
+
* @param options.document {unknown} the stored (parsed) resource JSON
|
|
172
|
+
* @param options.query {BlindedIndexQuery}
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
export function matchesBlindedIndexQuery({ document, query }) {
|
|
176
|
+
if (typeof document !== 'object' || document === null) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const { indexed } = document;
|
|
180
|
+
if (!Array.isArray(indexed)) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
// The document must be indexed under the queried HMAC key at all.
|
|
184
|
+
const scoped = collectIndexedAttributes({ indexed, scopedTo: query.index });
|
|
185
|
+
if (scoped.names.size === 0) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (query.equals !== undefined) {
|
|
189
|
+
return query.equals.some(element => {
|
|
190
|
+
const pairs = Object.entries(element);
|
|
191
|
+
return (pairs.length > 0 &&
|
|
192
|
+
pairs.every(([name, value]) => scoped.terms.has(`${name}:${value}`)));
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// `has`: every named attribute present, from any indexed entry.
|
|
196
|
+
const all = collectIndexedAttributes({ indexed });
|
|
197
|
+
return (query.has ?? []).every(name => all.names.has(name));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Evaluates a blinded-index query over a Collection's candidate documents and
|
|
201
|
+
* shapes the result: a bare `{count}` for a count query, else a page of
|
|
202
|
+
* matching documents in ascending `resourceId` (code-unit) order with the
|
|
203
|
+
* standard opaque-cursor pagination (`limit` clamped to `[1, MAX_PAGE_SIZE]`,
|
|
204
|
+
* default `DEFAULT_PAGE_SIZE`; `cursor` resumes strictly after its anchor id,
|
|
205
|
+
* so paging stays correct if the anchor was deleted between pages). A
|
|
206
|
+
* malformed `cursor` rejects with `invalid-cursor` (400).
|
|
207
|
+
*
|
|
208
|
+
* Backends call this with every live JSON document of the Collection -- an
|
|
209
|
+
* O(n) full scan, deliberate for these teaching backends (an indexed backend
|
|
210
|
+
* would answer from flattened `hmacId:name:value` attribute tokens, the
|
|
211
|
+
* bedrock-edv-storage strategy).
|
|
212
|
+
*
|
|
213
|
+
* @param options {object}
|
|
214
|
+
* @param options.candidates {Array<{resourceId: string, document: unknown}>}
|
|
215
|
+
* @param options.query {BlindedIndexQuery}
|
|
216
|
+
* @param [options.count] {boolean} return only the match count
|
|
217
|
+
* @param [options.limit] {number} requested page size
|
|
218
|
+
* @param [options.cursor] {string} opaque cursor from a prior page
|
|
219
|
+
* @returns {{ count: number } | BlindedIndexQueryPage}
|
|
220
|
+
*/
|
|
221
|
+
export function runBlindedIndexQuery({ candidates, query, count, limit, cursor }) {
|
|
222
|
+
const matches = candidates
|
|
223
|
+
.filter(candidate => matchesBlindedIndexQuery({ document: candidate.document, query }))
|
|
224
|
+
// Ascending `resourceId` in code-unit order -- the SAME ordering the
|
|
225
|
+
// cursor seek (`resourceId > after`) uses, so the keyset is stable.
|
|
226
|
+
.sort((left, right) => left.resourceId < right.resourceId
|
|
227
|
+
? -1
|
|
228
|
+
: left.resourceId > right.resourceId
|
|
229
|
+
? 1
|
|
230
|
+
: 0);
|
|
231
|
+
if (count === true) {
|
|
232
|
+
return { count: matches.length };
|
|
233
|
+
}
|
|
234
|
+
// Seek strictly past the cursor's anchor id.
|
|
235
|
+
let startIndex = 0;
|
|
236
|
+
if (cursor !== undefined) {
|
|
237
|
+
const { after } = decodeCursor(cursor);
|
|
238
|
+
const found = matches.findIndex(({ resourceId }) => resourceId > after);
|
|
239
|
+
startIndex = found === -1 ? matches.length : found;
|
|
240
|
+
}
|
|
241
|
+
const pageSize = limit === undefined ? DEFAULT_PAGE_SIZE : clampPageSize(limit);
|
|
242
|
+
// Take `pageSize + 1` to detect a further page without a second pass.
|
|
243
|
+
const window = matches.slice(startIndex, startIndex + pageSize + 1);
|
|
244
|
+
const hasMore = window.length > pageSize;
|
|
245
|
+
const page = hasMore ? window.slice(0, pageSize) : window;
|
|
246
|
+
return {
|
|
247
|
+
documents: page.map(({ document }) => document),
|
|
248
|
+
hasMore,
|
|
249
|
+
...(hasMore && {
|
|
250
|
+
cursor: encodeCursor(page[page.length - 1].resourceId)
|
|
251
|
+
})
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Collects a document's `unique: true` blinded attributes as
|
|
256
|
+
* `(hmac id, name, value)` triples -- the claims the EDV unique-attribute
|
|
257
|
+
* invariant protects. Malformed entries and attributes are skipped, never
|
|
258
|
+
* thrown on; an attribute counts only when `unique` is exactly `true` and
|
|
259
|
+
* both `name` and `value` are strings.
|
|
260
|
+
*
|
|
261
|
+
* @param options {object}
|
|
262
|
+
* @param options.document {unknown} the (parsed) document JSON
|
|
263
|
+
* @returns {Array<{ hmacId: string, name: string, value: string }>}
|
|
264
|
+
*/
|
|
265
|
+
export function collectUniqueBlindedTerms({ document }) {
|
|
266
|
+
if (typeof document !== 'object' || document === null) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
const { indexed } = document;
|
|
270
|
+
if (!Array.isArray(indexed)) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
const terms = [];
|
|
274
|
+
for (const entry of indexed) {
|
|
275
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const { hmac, attributes } = entry;
|
|
279
|
+
if (typeof hmac?.id !== 'string' || !Array.isArray(attributes)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
for (const attribute of attributes) {
|
|
283
|
+
if (attribute?.unique === true &&
|
|
284
|
+
typeof attribute.name === 'string' &&
|
|
285
|
+
typeof attribute.value === 'string') {
|
|
286
|
+
terms.push({
|
|
287
|
+
hmacId: hmac.id,
|
|
288
|
+
name: attribute.name,
|
|
289
|
+
value: attribute.value
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return terms;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Enforces the EDV unique-attribute invariant on a write: a `unique: true`
|
|
298
|
+
* blinded `(hmac id, name, value)` triple may be claimed by at most one live
|
|
299
|
+
* document per Collection. Following the reference servers
|
|
300
|
+
* (`@interop/edv-server`, bedrock-edv-storage's unique index over
|
|
301
|
+
* `uniqueAttributes`), a conflict requires the attribute be marked
|
|
302
|
+
* `unique: true` on BOTH sides -- an existing document carrying the same
|
|
303
|
+
* triple without `unique` does not conflict -- and the triple is keyed on the
|
|
304
|
+
* full `(hmac id, name, value)`, so the same blinded pair under a different
|
|
305
|
+
* HMAC key is no conflict. `candidates` MUST already exclude the document
|
|
306
|
+
* being written (its own resource id), so an update keeping its own unique
|
|
307
|
+
* attribute never self-conflicts. Throws `UniqueAttributeConflictError`
|
|
308
|
+
* (409) on a conflict; a document with no unique attributes never throws.
|
|
309
|
+
*
|
|
310
|
+
* The caller is responsible for making the check-and-write atomic (the
|
|
311
|
+
* filesystem backend serializes unique-carrying writes per Collection; the
|
|
312
|
+
* Postgres backend takes a per-Collection advisory lock in the write
|
|
313
|
+
* transaction).
|
|
314
|
+
*
|
|
315
|
+
* @param options {object}
|
|
316
|
+
* @param options.document {unknown} the incoming (parsed) document JSON
|
|
317
|
+
* @param options.candidates {Array<{resourceId: string, document: unknown}>}
|
|
318
|
+
* the Collection's other live JSON documents
|
|
319
|
+
* @returns {void}
|
|
320
|
+
*/
|
|
321
|
+
export function assertNoUniqueBlindedConflict({ document, candidates }) {
|
|
322
|
+
const incoming = collectUniqueBlindedTerms({ document });
|
|
323
|
+
if (incoming.length === 0) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
// Key the triples unambiguously (hmac ids may themselves contain any
|
|
327
|
+
// delimiter, so a joined string could collide across segment boundaries).
|
|
328
|
+
const claimed = new Set(incoming.map(term => JSON.stringify([term.hmacId, term.name, term.value])));
|
|
329
|
+
for (const candidate of candidates) {
|
|
330
|
+
const held = collectUniqueBlindedTerms({ document: candidate.document });
|
|
331
|
+
for (const term of held) {
|
|
332
|
+
if (claimed.has(JSON.stringify([term.hmacId, term.name, term.value]))) {
|
|
333
|
+
throw new UniqueAttributeConflictError();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=blindedIndex.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blindedIndex.js","sourceRoot":"","sources":["../../src/lib/blindedIndex.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend-agnostic normalization of a Collection Description about to be
|
|
3
|
+
* persisted (the `key-epochs` / conditional-Collection-write feature). Both
|
|
4
|
+
* storage backends run the same rule through {@link normalizeDescriptionWrite}
|
|
5
|
+
* so their stored bodies and version arithmetic cannot drift; only the storage
|
|
6
|
+
* of the resolved version differs downstream (a `_version` member in the
|
|
7
|
+
* filesystem description file vs the Postgres `description_version` column).
|
|
8
|
+
*/
|
|
9
|
+
import type { CollectionDescription } from '../types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Splits an incoming Collection Description into the body to persist and the
|
|
12
|
+
* description version to stamp. The two version-bearing members a wire or
|
|
13
|
+
* archived description may carry are stripped from the body: `_version` (the
|
|
14
|
+
* archive interchange token embedded by `exportSpace`) and
|
|
15
|
+
* `descriptionVersion` (the out-of-band `ETag` validator that
|
|
16
|
+
* `getCollectionDescription` attaches, which a caller may have spread back
|
|
17
|
+
* in). The version to stamp resolves with the precedence: the explicit
|
|
18
|
+
* `descriptionVersion` argument (the write path's monotonic bump), else the
|
|
19
|
+
* archived `_version` (the import path), else 1 (a first write).
|
|
20
|
+
*
|
|
21
|
+
* @param options {object}
|
|
22
|
+
* @param options.collectionDescription {CollectionDescription}
|
|
23
|
+
* @param [options.descriptionVersion] {number} the explicit version to stamp
|
|
24
|
+
* @returns {{ body: CollectionDescription, version: number }}
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeDescriptionWrite({ collectionDescription, descriptionVersion }: {
|
|
27
|
+
collectionDescription: CollectionDescription;
|
|
28
|
+
descriptionVersion?: number;
|
|
29
|
+
}): {
|
|
30
|
+
body: CollectionDescription;
|
|
31
|
+
version: number;
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=collectionDescription.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collectionDescription.d.ts","sourceRoot":"","sources":["../../src/lib/collectionDescription.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAExD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,qBAAqB,EACrB,kBAAkB,EACnB,EAAE;IACD,qBAAqB,EAAE,qBAAqB,CAAA;IAC5C,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,GAAG;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAUnD"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits an incoming Collection Description into the body to persist and the
|
|
3
|
+
* description version to stamp. The two version-bearing members a wire or
|
|
4
|
+
* archived description may carry are stripped from the body: `_version` (the
|
|
5
|
+
* archive interchange token embedded by `exportSpace`) and
|
|
6
|
+
* `descriptionVersion` (the out-of-band `ETag` validator that
|
|
7
|
+
* `getCollectionDescription` attaches, which a caller may have spread back
|
|
8
|
+
* in). The version to stamp resolves with the precedence: the explicit
|
|
9
|
+
* `descriptionVersion` argument (the write path's monotonic bump), else the
|
|
10
|
+
* archived `_version` (the import path), else 1 (a first write).
|
|
11
|
+
*
|
|
12
|
+
* @param options {object}
|
|
13
|
+
* @param options.collectionDescription {CollectionDescription}
|
|
14
|
+
* @param [options.descriptionVersion] {number} the explicit version to stamp
|
|
15
|
+
* @returns {{ body: CollectionDescription, version: number }}
|
|
16
|
+
*/
|
|
17
|
+
export function normalizeDescriptionWrite({ collectionDescription, descriptionVersion }) {
|
|
18
|
+
const { _version: incomingVersion, descriptionVersion: _staleVersion, ...body } = collectionDescription;
|
|
19
|
+
return { body, version: descriptionVersion ?? incomingVersion ?? 1 };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=collectionDescription.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collectionDescription.js","sourceRoot":"","sources":["../../src/lib/collectionDescription.ts"],"names":[],"mappings":"AAUA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,yBAAyB,CAAC,EACxC,qBAAqB,EACrB,kBAAkB,EAInB;IACC,MAAM,EACJ,QAAQ,EAAE,eAAe,EACzB,kBAAkB,EAAE,aAAa,EACjC,GAAG,IAAI,EACR,GAAG,qBAGH,CAAA;IACD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,kBAAkB,IAAI,eAAe,IAAI,CAAC,EAAE,CAAA;AACtE,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encodes a keyset position into an opaque cursor token.
|
|
3
|
+
* @param after {string} - `resourceId` of the last item already returned
|
|
4
|
+
* @returns {string} - base64url-encoded cursor
|
|
5
|
+
*/
|
|
6
|
+
export declare function encodeCursor(after: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Decodes an opaque cursor token back into its keyset position. Throws
|
|
9
|
+
* `InvalidCursorError` (400 `invalid-cursor`) when the token is not valid
|
|
10
|
+
* base64url, is not JSON, or does not carry a string `after` -- i.e. a cursor
|
|
11
|
+
* the server cannot honor (malformed, tampered with, or from another context).
|
|
12
|
+
* @param cursor {string} the cursor query parameter
|
|
13
|
+
* @returns {{ after: string }}
|
|
14
|
+
*/
|
|
15
|
+
export declare function decodeCursor(cursor: string): {
|
|
16
|
+
after: string;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/lib/cursor.ts"],"names":[],"mappings":"AAcA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAoB9D"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opaque pagination cursor codec for the List Collection operation (spec
|
|
3
|
+
* "Pagination"). A cursor names the position to resume an ordered keyset scan
|
|
4
|
+
* from -- here, the `resourceId` of the last item already returned. It is
|
|
5
|
+
* OPAQUE to clients: the server encodes and decodes it, and the client only ever
|
|
6
|
+
* echoes back the `cursor` baked into a prior page's `next` URL.
|
|
7
|
+
*
|
|
8
|
+
* The encoding is `base64url(JSON.stringify({ after }))`. It is deliberately not
|
|
9
|
+
* a signed or encrypted token -- it leaks nothing a caller authorized to list
|
|
10
|
+
* the Collection cannot already see (a `resourceId`), and a tampered cursor is
|
|
11
|
+
* rejected as `invalid-cursor` rather than trusted.
|
|
12
|
+
*/
|
|
13
|
+
import { InvalidCursorError } from '../errors.js';
|
|
14
|
+
/**
|
|
15
|
+
* Encodes a keyset position into an opaque cursor token.
|
|
16
|
+
* @param after {string} - `resourceId` of the last item already returned
|
|
17
|
+
* @returns {string} - base64url-encoded cursor
|
|
18
|
+
*/
|
|
19
|
+
export function encodeCursor(after) {
|
|
20
|
+
return Buffer.from(JSON.stringify({ after }), 'utf8').toString('base64url');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Decodes an opaque cursor token back into its keyset position. Throws
|
|
24
|
+
* `InvalidCursorError` (400 `invalid-cursor`) when the token is not valid
|
|
25
|
+
* base64url, is not JSON, or does not carry a string `after` -- i.e. a cursor
|
|
26
|
+
* the server cannot honor (malformed, tampered with, or from another context).
|
|
27
|
+
* @param cursor {string} the cursor query parameter
|
|
28
|
+
* @returns {{ after: string }}
|
|
29
|
+
*/
|
|
30
|
+
export function decodeCursor(cursor) {
|
|
31
|
+
// base64url charset only (no `+`, `/`, or `=` padding); `Buffer` decoding is
|
|
32
|
+
// lenient and would silently drop stray characters, so reject them up front.
|
|
33
|
+
if (!/^[A-Za-z0-9_-]+$/.test(cursor)) {
|
|
34
|
+
throw new InvalidCursorError();
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new InvalidCursorError({ cause: err });
|
|
42
|
+
}
|
|
43
|
+
if (typeof parsed !== 'object' ||
|
|
44
|
+
parsed === null ||
|
|
45
|
+
typeof parsed.after !== 'string') {
|
|
46
|
+
throw new InvalidCursorError();
|
|
47
|
+
}
|
|
48
|
+
return { after: parsed.after };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/lib/cursor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEjD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AAC7E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc;IACzC,6EAA6E;IAC7E,6EAA6E;IAC7E,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACrC,MAAM,IAAI,kBAAkB,EAAE,CAAA;IAChC,CAAC;IACD,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAA;IACxE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,kBAAkB,CAAC,EAAE,KAAK,EAAE,GAAY,EAAE,CAAC,CAAA;IACvD,CAAC;IACD,IACE,OAAO,MAAM,KAAK,QAAQ;QAC1B,MAAM,KAAK,IAAI;QACf,OAAQ,MAA8B,CAAC,KAAK,KAAK,QAAQ,EACzD,CAAC;QACD,MAAM,IAAI,kBAAkB,EAAE,CAAA;IAChC,CAAC;IACD,OAAO,EAAE,KAAK,EAAG,MAA4B,CAAC,KAAK,EAAE,CAAA;AACvD,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural validator for the `edv` encryption scheme's envelope profile (spec
|
|
3
|
+
* "Encryption Scheme Registry"). An `edv`-encrypted Resource is stored as an EDV
|
|
4
|
+
* **Encrypted Document** -- a JSON object whose `jwe` member is a JWE in JSON
|
|
5
|
+
* serialization (general or flattened, RFC7516). This checks only its *shape* so
|
|
6
|
+
* the server can fail closed on a non-envelope write into an encrypted
|
|
7
|
+
* Collection. The server MUST NOT attempt decryption and MUST NOT interpret the
|
|
8
|
+
* ciphertext or key values -- it validates that the document carries a
|
|
9
|
+
* plausible JWE, not that it decrypts.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* True if `entry` is a structurally valid JWE general-serialization
|
|
13
|
+
* `recipients` array entry: an object whose OPTIONAL `header` is an object and
|
|
14
|
+
* whose OPTIONAL `encrypted_key` is a string. Purely structural (values are
|
|
15
|
+
* never decoded); shared by {@link isValidEdvEnvelope} and the Collection
|
|
16
|
+
* key-epoch marker validator (`lib/encryption.ts`), which reuses the JWE
|
|
17
|
+
* recipients entry shape verbatim -- one wire vocabulary.
|
|
18
|
+
*
|
|
19
|
+
* @param entry {unknown} one element of a `recipients` array
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export declare function isValidJweRecipientEntry(entry: unknown): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* True if `body` is a structurally valid JWE-JSON-serialization object: an
|
|
25
|
+
* object with a required non-empty string `ciphertext`; optional string
|
|
26
|
+
* `protected` / `iv` / `tag` / `encrypted_key`; an optional `recipients` array
|
|
27
|
+
* of `{ header?: object, encrypted_key?: string }`; and at least one key-delivery
|
|
28
|
+
* member present (`recipients` non-empty, top-level `encrypted_key`, or
|
|
29
|
+
* `protected` -- covering general, flattened, and direct JWE serializations).
|
|
30
|
+
* Purely structural: values are never decoded or decrypted.
|
|
31
|
+
*
|
|
32
|
+
* @param body {unknown} the parsed request body to check
|
|
33
|
+
* @returns {boolean}
|
|
34
|
+
*/
|
|
35
|
+
export declare function isValidEdvEnvelope(body: unknown): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* True if `body` is a structurally valid EDV **Encrypted Document**: a JSON
|
|
38
|
+
* object whose `jwe` member is a valid JWE-JSON envelope ({@link
|
|
39
|
+
* isValidEdvEnvelope}). This is the actual stored representation the EDV codec
|
|
40
|
+
* produces -- `documentCipher.encrypt` nests the JWE under `jwe` alongside EDV
|
|
41
|
+
* bookkeeping members (`id`, `sequence`, `indexed`), which are opaque to the
|
|
42
|
+
* server and not checked. A bare JWE (top-level `ciphertext`, no `jwe`) or a
|
|
43
|
+
* plaintext object fails this check, so an enforcing server fails closed on a
|
|
44
|
+
* non-envelope write. Purely structural: values are never decoded or decrypted.
|
|
45
|
+
*
|
|
46
|
+
* @param body {unknown} the parsed request body (or `custom` sub-value) to check
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
export declare function isValidEdvDocument(body: unknown): boolean;
|
|
50
|
+
//# sourceMappingURL=edvEnvelope.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edvEnvelope.d.ts","sourceRoot":"","sources":["../../src/lib/edvEnvelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAoBhE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CA2CzD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAKzD"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural validator for the `edv` encryption scheme's envelope profile (spec
|
|
3
|
+
* "Encryption Scheme Registry"). An `edv`-encrypted Resource is stored as an EDV
|
|
4
|
+
* **Encrypted Document** -- a JSON object whose `jwe` member is a JWE in JSON
|
|
5
|
+
* serialization (general or flattened, RFC7516). This checks only its *shape* so
|
|
6
|
+
* the server can fail closed on a non-envelope write into an encrypted
|
|
7
|
+
* Collection. The server MUST NOT attempt decryption and MUST NOT interpret the
|
|
8
|
+
* ciphertext or key values -- it validates that the document carries a
|
|
9
|
+
* plausible JWE, not that it decrypts.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* True if `entry` is a structurally valid JWE general-serialization
|
|
13
|
+
* `recipients` array entry: an object whose OPTIONAL `header` is an object and
|
|
14
|
+
* whose OPTIONAL `encrypted_key` is a string. Purely structural (values are
|
|
15
|
+
* never decoded); shared by {@link isValidEdvEnvelope} and the Collection
|
|
16
|
+
* key-epoch marker validator (`lib/encryption.ts`), which reuses the JWE
|
|
17
|
+
* recipients entry shape verbatim -- one wire vocabulary.
|
|
18
|
+
*
|
|
19
|
+
* @param entry {unknown} one element of a `recipients` array
|
|
20
|
+
* @returns {boolean}
|
|
21
|
+
*/
|
|
22
|
+
export function isValidJweRecipientEntry(entry) {
|
|
23
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const recipient = entry;
|
|
27
|
+
if (recipient.header !== undefined &&
|
|
28
|
+
(typeof recipient.header !== 'object' ||
|
|
29
|
+
recipient.header === null ||
|
|
30
|
+
Array.isArray(recipient.header))) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (recipient.encrypted_key !== undefined &&
|
|
34
|
+
typeof recipient.encrypted_key !== 'string') {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True if `body` is a structurally valid JWE-JSON-serialization object: an
|
|
41
|
+
* object with a required non-empty string `ciphertext`; optional string
|
|
42
|
+
* `protected` / `iv` / `tag` / `encrypted_key`; an optional `recipients` array
|
|
43
|
+
* of `{ header?: object, encrypted_key?: string }`; and at least one key-delivery
|
|
44
|
+
* member present (`recipients` non-empty, top-level `encrypted_key`, or
|
|
45
|
+
* `protected` -- covering general, flattened, and direct JWE serializations).
|
|
46
|
+
* Purely structural: values are never decoded or decrypted.
|
|
47
|
+
*
|
|
48
|
+
* @param body {unknown} the parsed request body to check
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
export function isValidEdvEnvelope(body) {
|
|
52
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const envelope = body;
|
|
56
|
+
// Required: a non-empty string `ciphertext`.
|
|
57
|
+
if (typeof envelope.ciphertext !== 'string' ||
|
|
58
|
+
envelope.ciphertext.length === 0) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// Optional string members.
|
|
62
|
+
for (const key of ['protected', 'iv', 'tag', 'encrypted_key']) {
|
|
63
|
+
if (envelope[key] !== undefined && typeof envelope[key] !== 'string') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Optional `recipients`: an array of `{ header?: object, encrypted_key?: string }`.
|
|
68
|
+
let hasRecipient = false;
|
|
69
|
+
if (envelope.recipients !== undefined) {
|
|
70
|
+
if (!Array.isArray(envelope.recipients)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
for (const entry of envelope.recipients) {
|
|
74
|
+
if (!isValidJweRecipientEntry(entry)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
hasRecipient = envelope.recipients.length > 0;
|
|
79
|
+
}
|
|
80
|
+
// anyOf: at least one key-delivery member -- a general-serialization
|
|
81
|
+
// `recipients` list, a flattened top-level `encrypted_key`, or a `protected`
|
|
82
|
+
// header (direct encryption, no per-recipient key).
|
|
83
|
+
return (hasRecipient ||
|
|
84
|
+
typeof envelope.encrypted_key === 'string' ||
|
|
85
|
+
typeof envelope.protected === 'string');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* True if `body` is a structurally valid EDV **Encrypted Document**: a JSON
|
|
89
|
+
* object whose `jwe` member is a valid JWE-JSON envelope ({@link
|
|
90
|
+
* isValidEdvEnvelope}). This is the actual stored representation the EDV codec
|
|
91
|
+
* produces -- `documentCipher.encrypt` nests the JWE under `jwe` alongside EDV
|
|
92
|
+
* bookkeeping members (`id`, `sequence`, `indexed`), which are opaque to the
|
|
93
|
+
* server and not checked. A bare JWE (top-level `ciphertext`, no `jwe`) or a
|
|
94
|
+
* plaintext object fails this check, so an enforcing server fails closed on a
|
|
95
|
+
* non-envelope write. Purely structural: values are never decoded or decrypted.
|
|
96
|
+
*
|
|
97
|
+
* @param body {unknown} the parsed request body (or `custom` sub-value) to check
|
|
98
|
+
* @returns {boolean}
|
|
99
|
+
*/
|
|
100
|
+
export function isValidEdvDocument(body) {
|
|
101
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return isValidEdvEnvelope(body.jwe);
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=edvEnvelope.js.map
|