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.
Files changed (295) hide show
  1. package/CHANGELOG.md +1432 -0
  2. package/LICENSE +661 -0
  3. package/README.md +305 -0
  4. package/common/css/materialize.1.0.0.min.css +13 -0
  5. package/dist/auth-header-hooks.d.ts +84 -0
  6. package/dist/auth-header-hooks.d.ts.map +1 -0
  7. package/dist/auth-header-hooks.js +136 -0
  8. package/dist/auth-header-hooks.js.map +1 -0
  9. package/dist/authorize.d.ts +45 -0
  10. package/dist/authorize.d.ts.map +1 -0
  11. package/dist/authorize.js +76 -0
  12. package/dist/authorize.js.map +1 -0
  13. package/dist/backends/filesystem.d.ts +1179 -0
  14. package/dist/backends/filesystem.d.ts.map +1 -0
  15. package/dist/backends/filesystem.js +2727 -0
  16. package/dist/backends/filesystem.js.map +1 -0
  17. package/dist/backends/postgres.d.ts +833 -0
  18. package/dist/backends/postgres.d.ts.map +1 -0
  19. package/dist/backends/postgres.js +2164 -0
  20. package/dist/backends/postgres.js.map +1 -0
  21. package/dist/backends/postgresSchema.d.ts +36 -0
  22. package/dist/backends/postgresSchema.d.ts.map +1 -0
  23. package/dist/backends/postgresSchema.js +203 -0
  24. package/dist/backends/postgresSchema.js.map +1 -0
  25. package/dist/config.default.d.ts +360 -0
  26. package/dist/config.default.d.ts.map +1 -0
  27. package/dist/config.default.js +493 -0
  28. package/dist/config.default.js.map +1 -0
  29. package/dist/corsProxy.d.ts +45 -0
  30. package/dist/corsProxy.d.ts.map +1 -0
  31. package/dist/corsProxy.js +278 -0
  32. package/dist/corsProxy.js.map +1 -0
  33. package/dist/digest.d.ts +45 -0
  34. package/dist/digest.d.ts.map +1 -0
  35. package/dist/digest.js +174 -0
  36. package/dist/digest.js.map +1 -0
  37. package/dist/errors.d.ts +654 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +960 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/index.d.ts +22 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +21 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/lib/atomicFile.d.ts +61 -0
  46. package/dist/lib/atomicFile.d.ts.map +1 -0
  47. package/dist/lib/atomicFile.js +150 -0
  48. package/dist/lib/atomicFile.js.map +1 -0
  49. package/dist/lib/backendRegistry.d.ts +59 -0
  50. package/dist/lib/backendRegistry.d.ts.map +1 -0
  51. package/dist/lib/backendRegistry.js +92 -0
  52. package/dist/lib/backendRegistry.js.map +1 -0
  53. package/dist/lib/backendUsage.d.ts +28 -0
  54. package/dist/lib/backendUsage.d.ts.map +1 -0
  55. package/dist/lib/backendUsage.js +55 -0
  56. package/dist/lib/backendUsage.js.map +1 -0
  57. package/dist/lib/backends.d.ts +142 -0
  58. package/dist/lib/backends.d.ts.map +1 -0
  59. package/dist/lib/backends.js +272 -0
  60. package/dist/lib/backends.js.map +1 -0
  61. package/dist/lib/blindedIndex.d.ts +156 -0
  62. package/dist/lib/blindedIndex.d.ts.map +1 -0
  63. package/dist/lib/blindedIndex.js +338 -0
  64. package/dist/lib/blindedIndex.js.map +1 -0
  65. package/dist/lib/collectionDescription.d.ts +33 -0
  66. package/dist/lib/collectionDescription.d.ts.map +1 -0
  67. package/dist/lib/collectionDescription.js +21 -0
  68. package/dist/lib/collectionDescription.js.map +1 -0
  69. package/dist/lib/cursor.d.ts +18 -0
  70. package/dist/lib/cursor.d.ts.map +1 -0
  71. package/dist/lib/cursor.js +50 -0
  72. package/dist/lib/cursor.js.map +1 -0
  73. package/dist/lib/edvEnvelope.d.ts +50 -0
  74. package/dist/lib/edvEnvelope.d.ts.map +1 -0
  75. package/dist/lib/edvEnvelope.js +106 -0
  76. package/dist/lib/edvEnvelope.js.map +1 -0
  77. package/dist/lib/encryption.d.ts +192 -0
  78. package/dist/lib/encryption.d.ts.map +1 -0
  79. package/dist/lib/encryption.js +383 -0
  80. package/dist/lib/encryption.js.map +1 -0
  81. package/dist/lib/etag.d.ts +33 -0
  82. package/dist/lib/etag.d.ts.map +1 -0
  83. package/dist/lib/etag.js +36 -0
  84. package/dist/lib/etag.js.map +1 -0
  85. package/dist/lib/exportManifest.d.ts +29 -0
  86. package/dist/lib/exportManifest.d.ts.map +1 -0
  87. package/dist/lib/exportManifest.js +77 -0
  88. package/dist/lib/exportManifest.js.map +1 -0
  89. package/dist/lib/importTar.d.ts +81 -0
  90. package/dist/lib/importTar.d.ts.map +1 -0
  91. package/dist/lib/importTar.js +301 -0
  92. package/dist/lib/importTar.js.map +1 -0
  93. package/dist/lib/isJson.d.ts +16 -0
  94. package/dist/lib/isJson.d.ts.map +1 -0
  95. package/dist/lib/isJson.js +17 -0
  96. package/dist/lib/isJson.js.map +1 -0
  97. package/dist/lib/keyEpoch.d.ts +38 -0
  98. package/dist/lib/keyEpoch.d.ts.map +1 -0
  99. package/dist/lib/keyEpoch.js +75 -0
  100. package/dist/lib/keyEpoch.js.map +1 -0
  101. package/dist/lib/keyedMutex.d.ts +28 -0
  102. package/dist/lib/keyedMutex.d.ts.map +1 -0
  103. package/dist/lib/keyedMutex.js +45 -0
  104. package/dist/lib/keyedMutex.js.map +1 -0
  105. package/dist/lib/kmsModule.d.ts +62 -0
  106. package/dist/lib/kmsModule.d.ts.map +1 -0
  107. package/dist/lib/kmsModule.js +327 -0
  108. package/dist/lib/kmsModule.js.map +1 -0
  109. package/dist/lib/kmsRecordCipher.d.ts +82 -0
  110. package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
  111. package/dist/lib/kmsRecordCipher.js +226 -0
  112. package/dist/lib/kmsRecordCipher.js.map +1 -0
  113. package/dist/lib/pagination.d.ts +19 -0
  114. package/dist/lib/pagination.d.ts.map +1 -0
  115. package/dist/lib/pagination.js +21 -0
  116. package/dist/lib/pagination.js.map +1 -0
  117. package/dist/lib/paths.d.ts +273 -0
  118. package/dist/lib/paths.d.ts.map +1 -0
  119. package/dist/lib/paths.js +270 -0
  120. package/dist/lib/paths.js.map +1 -0
  121. package/dist/lib/preconditions.d.ts +65 -0
  122. package/dist/lib/preconditions.d.ts.map +1 -0
  123. package/dist/lib/preconditions.js +112 -0
  124. package/dist/lib/preconditions.js.map +1 -0
  125. package/dist/lib/resourceFileName.d.ts +41 -0
  126. package/dist/lib/resourceFileName.d.ts.map +1 -0
  127. package/dist/lib/resourceFileName.js +58 -0
  128. package/dist/lib/resourceFileName.js.map +1 -0
  129. package/dist/lib/revocations.d.ts +49 -0
  130. package/dist/lib/revocations.d.ts.map +1 -0
  131. package/dist/lib/revocations.js +86 -0
  132. package/dist/lib/revocations.js.map +1 -0
  133. package/dist/lib/validateDid.d.ts +19 -0
  134. package/dist/lib/validateDid.d.ts.map +1 -0
  135. package/dist/lib/validateDid.js +35 -0
  136. package/dist/lib/validateDid.js.map +1 -0
  137. package/dist/lib/validateId.d.ts +66 -0
  138. package/dist/lib/validateId.d.ts.map +1 -0
  139. package/dist/lib/validateId.js +126 -0
  140. package/dist/lib/validateId.js.map +1 -0
  141. package/dist/plugin.d.ts +115 -0
  142. package/dist/plugin.d.ts.map +1 -0
  143. package/dist/plugin.js +134 -0
  144. package/dist/plugin.js.map +1 -0
  145. package/dist/policy.d.ts +78 -0
  146. package/dist/policy.d.ts.map +1 -0
  147. package/dist/policy.js +115 -0
  148. package/dist/policy.js.map +1 -0
  149. package/dist/provisioning.d.ts +38 -0
  150. package/dist/provisioning.d.ts.map +1 -0
  151. package/dist/provisioning.js +72 -0
  152. package/dist/provisioning.js.map +1 -0
  153. package/dist/requests/BackendRequest.d.ts +61 -0
  154. package/dist/requests/BackendRequest.d.ts.map +1 -0
  155. package/dist/requests/BackendRequest.js +152 -0
  156. package/dist/requests/BackendRequest.js.map +1 -0
  157. package/dist/requests/CollectionRequest.d.ts +234 -0
  158. package/dist/requests/CollectionRequest.d.ts.map +1 -0
  159. package/dist/requests/CollectionRequest.js +705 -0
  160. package/dist/requests/CollectionRequest.js.map +1 -0
  161. package/dist/requests/KeyRequest.d.ts +115 -0
  162. package/dist/requests/KeyRequest.d.ts.map +1 -0
  163. package/dist/requests/KeyRequest.js +468 -0
  164. package/dist/requests/KeyRequest.js.map +1 -0
  165. package/dist/requests/KeystoreRequest.d.ts +99 -0
  166. package/dist/requests/KeystoreRequest.d.ts.map +1 -0
  167. package/dist/requests/KeystoreRequest.js +247 -0
  168. package/dist/requests/KeystoreRequest.js.map +1 -0
  169. package/dist/requests/PolicyRequest.d.ts +53 -0
  170. package/dist/requests/PolicyRequest.d.ts.map +1 -0
  171. package/dist/requests/PolicyRequest.js +124 -0
  172. package/dist/requests/PolicyRequest.js.map +1 -0
  173. package/dist/requests/ResourceRequest.d.ts +134 -0
  174. package/dist/requests/ResourceRequest.d.ts.map +1 -0
  175. package/dist/requests/ResourceRequest.js +548 -0
  176. package/dist/requests/ResourceRequest.js.map +1 -0
  177. package/dist/requests/RevocationRequest.d.ts +67 -0
  178. package/dist/requests/RevocationRequest.d.ts.map +1 -0
  179. package/dist/requests/RevocationRequest.js +187 -0
  180. package/dist/requests/RevocationRequest.js.map +1 -0
  181. package/dist/requests/SpaceRequest.d.ts +204 -0
  182. package/dist/requests/SpaceRequest.d.ts.map +1 -0
  183. package/dist/requests/SpaceRequest.js +520 -0
  184. package/dist/requests/SpaceRequest.js.map +1 -0
  185. package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
  186. package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
  187. package/dist/requests/SpacesRepositoryRequest.js +159 -0
  188. package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
  189. package/dist/requests/collectionContext.d.ts +21 -0
  190. package/dist/requests/collectionContext.d.ts.map +1 -0
  191. package/dist/requests/collectionContext.js +30 -0
  192. package/dist/requests/collectionContext.js.map +1 -0
  193. package/dist/requests/controllerConsent.d.ts +58 -0
  194. package/dist/requests/controllerConsent.d.ts.map +1 -0
  195. package/dist/requests/controllerConsent.js +73 -0
  196. package/dist/requests/controllerConsent.js.map +1 -0
  197. package/dist/requests/keystoreContext.d.ts +68 -0
  198. package/dist/requests/keystoreContext.d.ts.map +1 -0
  199. package/dist/requests/keystoreContext.js +87 -0
  200. package/dist/requests/keystoreContext.js.map +1 -0
  201. package/dist/requests/resourceInput.d.ts +19 -0
  202. package/dist/requests/resourceInput.d.ts.map +1 -0
  203. package/dist/requests/resourceInput.js +102 -0
  204. package/dist/requests/resourceInput.js.map +1 -0
  205. package/dist/requests/spaceContext.d.ts +127 -0
  206. package/dist/requests/spaceContext.d.ts.map +1 -0
  207. package/dist/requests/spaceContext.js +201 -0
  208. package/dist/requests/spaceContext.js.map +1 -0
  209. package/dist/routes.d.ts +55 -0
  210. package/dist/routes.d.ts.map +1 -0
  211. package/dist/routes.js +306 -0
  212. package/dist/routes.js.map +1 -0
  213. package/dist/server.d.ts +20 -0
  214. package/dist/server.d.ts.map +1 -0
  215. package/dist/server.js +60 -0
  216. package/dist/server.js.map +1 -0
  217. package/dist/start.d.ts +7 -0
  218. package/dist/start.d.ts.map +1 -0
  219. package/dist/start.js +61 -0
  220. package/dist/start.js.map +1 -0
  221. package/dist/storage.d.ts +29 -0
  222. package/dist/storage.d.ts.map +1 -0
  223. package/dist/storage.js +47 -0
  224. package/dist/storage.js.map +1 -0
  225. package/dist/types.d.ts +915 -0
  226. package/dist/types.d.ts.map +1 -0
  227. package/dist/types.js +2 -0
  228. package/dist/types.js.map +1 -0
  229. package/dist/views/home.hbs +9 -0
  230. package/dist/views/templates/main.hbs +12 -0
  231. package/dist/zcap.d.ts +267 -0
  232. package/dist/zcap.d.ts.map +1 -0
  233. package/dist/zcap.js +478 -0
  234. package/dist/zcap.js.map +1 -0
  235. package/package.json +109 -0
  236. package/src/auth-header-hooks.ts +163 -0
  237. package/src/authorize.ts +117 -0
  238. package/src/backends/filesystem.ts +3626 -0
  239. package/src/backends/postgres.ts +3073 -0
  240. package/src/backends/postgresSchema.ts +231 -0
  241. package/src/config.default.ts +657 -0
  242. package/src/corsProxy.ts +326 -0
  243. package/src/declarations.d.ts +28 -0
  244. package/src/digest.ts +223 -0
  245. package/src/errors.ts +1122 -0
  246. package/src/index.ts +21 -0
  247. package/src/lib/atomicFile.ts +176 -0
  248. package/src/lib/backendRegistry.ts +139 -0
  249. package/src/lib/backendUsage.ts +79 -0
  250. package/src/lib/backends.ts +351 -0
  251. package/src/lib/blindedIndex.ts +466 -0
  252. package/src/lib/collectionDescription.ts +43 -0
  253. package/src/lib/cursor.ts +52 -0
  254. package/src/lib/edvEnvelope.ts +120 -0
  255. package/src/lib/encryption.ts +472 -0
  256. package/src/lib/etag.ts +40 -0
  257. package/src/lib/exportManifest.ts +115 -0
  258. package/src/lib/importTar.ts +381 -0
  259. package/src/lib/isJson.ts +18 -0
  260. package/src/lib/keyEpoch.ts +89 -0
  261. package/src/lib/keyedMutex.ts +45 -0
  262. package/src/lib/kmsModule.ts +451 -0
  263. package/src/lib/kmsRecordCipher.ts +292 -0
  264. package/src/lib/pagination.ts +22 -0
  265. package/src/lib/paths.ts +382 -0
  266. package/src/lib/preconditions.ts +145 -0
  267. package/src/lib/resourceFileName.ts +69 -0
  268. package/src/lib/revocations.ts +116 -0
  269. package/src/lib/validateDid.ts +41 -0
  270. package/src/lib/validateId.ts +148 -0
  271. package/src/plugin.ts +288 -0
  272. package/src/policy.ts +176 -0
  273. package/src/provisioning.ts +98 -0
  274. package/src/requests/BackendRequest.ts +219 -0
  275. package/src/requests/CollectionRequest.ts +899 -0
  276. package/src/requests/KeyRequest.ts +617 -0
  277. package/src/requests/KeystoreRequest.ts +325 -0
  278. package/src/requests/PolicyRequest.ts +169 -0
  279. package/src/requests/ResourceRequest.ts +649 -0
  280. package/src/requests/RevocationRequest.ts +267 -0
  281. package/src/requests/SpaceRequest.ts +686 -0
  282. package/src/requests/SpacesRepositoryRequest.ts +194 -0
  283. package/src/requests/collectionContext.ts +41 -0
  284. package/src/requests/controllerConsent.ts +108 -0
  285. package/src/requests/keystoreContext.ts +124 -0
  286. package/src/requests/resourceInput.ts +118 -0
  287. package/src/requests/spaceContext.ts +311 -0
  288. package/src/routes.ts +420 -0
  289. package/src/server.ts +69 -0
  290. package/src/start.ts +71 -0
  291. package/src/storage.ts +60 -0
  292. package/src/types.ts +1016 -0
  293. package/src/views/home.hbs +9 -0
  294. package/src/views/templates/main.hbs +12 -0
  295. 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