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
package/src/errors.ts ADDED
@@ -0,0 +1,1122 @@
1
+ /**
2
+ * Custom error classes (each carries type / title / detail / statusCode) plus
3
+ * handleError, the Fastify error handler installed by each route group.
4
+ */
5
+ import type { FastifyReply, FastifyRequest } from 'fastify'
6
+
7
+ import {
8
+ ProblemTypes,
9
+ type ProblemType,
10
+ type Problem
11
+ } from '@interop/storage-core'
12
+
13
+ // Re-export the wire `Problem` entry shape (one element of the `errors` array)
14
+ // so the rest of the server keeps importing it from this one module.
15
+ export type { Problem }
16
+
17
+ /**
18
+ * Base class for the server's error hierarchy. Carries the four
19
+ * `application/problem+json` fields (`type` / `title` / `detail` /
20
+ * `statusCode`) plus an optional `problems` array of `{ detail, pointer }`
21
+ * entries; `handleError` serializes these to the wire. Subclasses set their
22
+ * distinguishing values via `super({ ... })`.
23
+ * @param options {object}
24
+ * @param options.type {ProblemType} problem-kind URI (see @interop/storage-core)
25
+ * @param options.title {string} short human-readable summary
26
+ * @param options.detail {string} specific explanation of the occurrence
27
+ * @param options.statusCode {number} HTTP status code
28
+ * @param [options.problems] {Problem[]} per-field error entries
29
+ * @param [options.cause] {Error} the underlying error, when wrapping one
30
+ */
31
+ export class ProblemError extends Error {
32
+ type: ProblemType
33
+ title: string
34
+ detail: string
35
+ statusCode: number
36
+ problems?: Problem[]
37
+ constructor({
38
+ type,
39
+ title,
40
+ detail,
41
+ statusCode,
42
+ problems,
43
+ cause
44
+ }: {
45
+ type: ProblemType
46
+ title: string
47
+ detail: string
48
+ statusCode: number
49
+ problems?: Problem[]
50
+ cause?: Error
51
+ }) {
52
+ super(detail, cause ? { cause } : undefined)
53
+ this.type = type
54
+ this.title = title
55
+ this.detail = detail
56
+ this.statusCode = statusCode
57
+ if (problems) {
58
+ this.problems = problems
59
+ }
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 404 — the requested Space does not exist, or the caller is not authorized.
65
+ * @param options {object}
66
+ * @param [options.requestName] {string} request name used in the error title
67
+ */
68
+ export class SpaceNotFoundError extends ProblemError {
69
+ constructor({ requestName }: { requestName?: string } = {}) {
70
+ super({
71
+ type: ProblemTypes.NOT_FOUND,
72
+ title: `Invalid ${requestName || 'Space'} request`,
73
+ detail: 'Space not found or invalid authorization.',
74
+ statusCode: 404
75
+ })
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 400 — the provided space id is not URL-safe / otherwise invalid.
81
+ * @param options {object}
82
+ * @param [options.requestName] {string} request name used in the error title
83
+ */
84
+ export class InvalidSpaceIdError extends ProblemError {
85
+ constructor({ requestName }: { requestName?: string } = {}) {
86
+ super({
87
+ type: ProblemTypes.INVALID_ID,
88
+ title: `Invalid ${requestName || 'Space'} request`,
89
+ detail: 'Invalid space id (make sure it is URL-safe).',
90
+ statusCode: 400
91
+ })
92
+ }
93
+ }
94
+
95
+ /**
96
+ * 400 — the provided collection id is not URL-safe / otherwise invalid.
97
+ * @param options {object}
98
+ * @param [options.requestName] {string} request name used in the error title
99
+ */
100
+ export class InvalidCollectionIdError extends ProblemError {
101
+ constructor({ requestName }: { requestName?: string } = {}) {
102
+ super({
103
+ type: ProblemTypes.INVALID_ID,
104
+ title: `Invalid ${requestName || 'Collection'} request`,
105
+ detail: 'Invalid collection id (make sure it is URL-safe).',
106
+ statusCode: 400
107
+ })
108
+ }
109
+ }
110
+
111
+ /**
112
+ * 400 — the provided resource id is not URL-safe / otherwise invalid.
113
+ * @param options {object}
114
+ * @param [options.requestName] {string} request name used in the error title
115
+ */
116
+ export class InvalidResourceIdError extends ProblemError {
117
+ constructor({ requestName }: { requestName?: string } = {}) {
118
+ super({
119
+ type: ProblemTypes.INVALID_ID,
120
+ title: `Invalid ${requestName || 'Resource'} request`,
121
+ detail: 'Invalid resource id (make sure it is URL-safe).',
122
+ statusCode: 400
123
+ })
124
+ }
125
+ }
126
+
127
+ /**
128
+ * 409 — a `POST` create operation supplied an `id` that already exists.
129
+ * Create-or-replace at a client-chosen id is the idempotent `PUT` path, which
130
+ * does not conflict.
131
+ * @param options {object}
132
+ * @param options.kind {string} what was being created ('Space', 'Collection',
133
+ * 'Resource'), used in the error title and detail
134
+ */
135
+ export class IdConflictError extends ProblemError {
136
+ constructor({ kind }: { kind: string }) {
137
+ const detail = `Use PUT to create-or-replace a ${kind} at a chosen id.`
138
+ super({
139
+ type: ProblemTypes.ID_CONFLICT,
140
+ title: `A ${kind} with this id already exists.`,
141
+ detail,
142
+ statusCode: 409,
143
+ problems: [{ detail, pointer: '#/id' }]
144
+ })
145
+ }
146
+ }
147
+
148
+ /**
149
+ * 409 — a Resource write carries an `indexed` blinded attribute marked
150
+ * `unique: true` whose (HMAC key id, name, value) triple is already claimed by
151
+ * another live Resource in the same Collection (the EDV unique-attribute
152
+ * invariant, enforced by backends carrying the `blinded-index-query` feature).
153
+ * Reuses the `id-conflict` problem type (like the WebKMS conflicts): a unique
154
+ * blinded attribute is a client-chosen identifier-like claim, and the registry
155
+ * defines no more specific kind. Only ever observable by a caller already
156
+ * authorized to write the target.
157
+ */
158
+ export class UniqueAttributeConflictError extends ProblemError {
159
+ constructor() {
160
+ const detail =
161
+ 'Could not write document; a unique blinded index attribute is already' +
162
+ ' in use by another Resource in this Collection.'
163
+ super({
164
+ type: ProblemTypes.ID_CONFLICT,
165
+ title: 'A unique blinded index attribute is already in use.',
166
+ detail,
167
+ statusCode: 409,
168
+ problems: [{ detail, pointer: '#/indexed' }]
169
+ })
170
+ }
171
+ }
172
+
173
+ /**
174
+ * 409 — a client-supplied id collides with a segment from the spec's Reserved
175
+ * Path Segment Registry: the id would shadow the reserved route at that
176
+ * position (e.g. a Collection named `export` would shadow
177
+ * `/space/{id}/export`).
178
+ * @param options {object}
179
+ * @param options.kind {string} which id position collided ('collection',
180
+ * 'resource'), used in the error title and detail
181
+ * @param options.id {string} the offending reserved id
182
+ */
183
+ export class ReservedIdError extends ProblemError {
184
+ constructor({ kind, id }: { kind: string; id: string }) {
185
+ const detail = `'${id}' is a reserved path segment and cannot be used as a ${kind} id.`
186
+ super({
187
+ type: ProblemTypes.RESERVED_ID,
188
+ title: `Invalid ${kind} id (from reserved list).`,
189
+ detail,
190
+ statusCode: 409,
191
+ problems: [{ detail, pointer: '#/id' }]
192
+ })
193
+ }
194
+ }
195
+
196
+ /**
197
+ * 409 — a backend reference cannot be honored. Three occurrences, distinguished
198
+ * by `detail` / `pointer`: a Collection create/update names a `backend` id not in
199
+ * the Space's backends-available list (the default `#/backend` detail); a
200
+ * registration names a `provider` the server does not permit (the allowlist gate,
201
+ * `#/provider`); or a selected backend is registered but has no live adapter yet
202
+ * (the resolver's data-plane backstop). Spec `unsupported-backend`.
203
+ * @param options {object}
204
+ * @param options.backendId {string} the unrecognized backend (or provider) id
205
+ * @param [options.detail] {string} a specific explanation; defaults to the
206
+ * backends-available message
207
+ * @param [options.pointer] {string} RFC 6901 JSON Pointer to the offending
208
+ * field; defaults to `#/backend`
209
+ */
210
+ export class UnsupportedBackendError extends ProblemError {
211
+ constructor({
212
+ backendId,
213
+ detail,
214
+ pointer = '#/backend'
215
+ }: {
216
+ backendId: string
217
+ detail?: string
218
+ pointer?: string
219
+ }) {
220
+ const resolvedDetail =
221
+ detail ??
222
+ `Backend '${backendId}' is not in this Space's backends-available list.`
223
+ super({
224
+ type: ProblemTypes.UNSUPPORTED_BACKEND,
225
+ title:
226
+ "Unsupported backend id, check the space's 'backends available' list.",
227
+ detail: resolvedDetail,
228
+ statusCode: 409,
229
+ problems: [{ detail: resolvedDetail, pointer }]
230
+ })
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 409 — a Collection update tried to change or clear an existing client-side
236
+ * `encryption` marker. The marker is set-once (spec): declaring it on a
237
+ * Collection that lacks one is allowed, but changing its scheme (or clearing
238
+ * it) on a populated Collection would corrupt the stored, client-encrypted
239
+ * Resources. Like the other state-conflict 409s (`id-conflict`,
240
+ * `unsupported-backend`), it is only observable by a caller already authorized
241
+ * to update the Collection -- it is checked after capability verification.
242
+ */
243
+ export class EncryptionImmutableError extends ProblemError {
244
+ constructor() {
245
+ const detail =
246
+ "A Collection's 'encryption' marker is set-once and cannot be changed or cleared."
247
+ super({
248
+ type: ProblemTypes.ENCRYPTION_IMMUTABLE,
249
+ title: 'Collection encryption marker is immutable.',
250
+ detail,
251
+ statusCode: 409,
252
+ problems: [{ detail, pointer: '#/encryption' }]
253
+ })
254
+ }
255
+ }
256
+
257
+ /**
258
+ * 400 — a Collection create/update supplied an `encryption` marker naming a
259
+ * `scheme` this server does not recognize and therefore cannot enforce on write.
260
+ * Taking the spec's fail-closed SHOULD path ("Encryption Scheme Registry"), the
261
+ * reference server rejects such a marker rather than storing it opaquely, so
262
+ * every accepted marker is one whose non-conforming (e.g. plaintext) writes it
263
+ * will reject. A pure body-shape rejection, checked before capability
264
+ * verification like the other `invalid`-shaped 400s.
265
+ * @param options {object}
266
+ * @param options.scheme {string} the unrecognized scheme token
267
+ */
268
+ export class UnsupportedEncryptionSchemeError extends ProblemError {
269
+ constructor({ scheme }: { scheme: string }) {
270
+ const detail = `This server does not support the '${scheme}' encryption scheme.`
271
+ super({
272
+ type: ProblemTypes.UNSUPPORTED_ENCRYPTION_SCHEME,
273
+ title: 'Unsupported encryption scheme.',
274
+ detail,
275
+ statusCode: 400,
276
+ problems: [{ detail, pointer: '#/encryption/scheme' }]
277
+ })
278
+ }
279
+ }
280
+
281
+ /**
282
+ * 422 — a content write into a Collection whose `encryption` marker declares a
283
+ * recognized scheme did not conform to that scheme's envelope profile: the
284
+ * request `Content-Type` was not the scheme's registered media type, or the body
285
+ * was not a structurally valid envelope (spec "Encryption Scheme Registry"). The
286
+ * server validates structure only and never decrypts. Like the write-time 409s
287
+ * (`id-conflict`, `encryption-immutable`), it is only ever observable by a caller
288
+ * already authorized to write the target -- it is checked after capability
289
+ * verification, so an under-authorized caller receives the privacy-merged
290
+ * `not-found` (404) instead.
291
+ * @param options {object}
292
+ * @param [options.detail] {string} which gate failed (media type vs envelope shape)
293
+ */
294
+ export class EncryptionSchemeMismatchError extends ProblemError {
295
+ constructor({ detail }: { detail?: string } = {}) {
296
+ const message =
297
+ detail ??
298
+ "Resource body does not conform to the Collection's declared encryption scheme."
299
+ super({
300
+ type: ProblemTypes.ENCRYPTION_SCHEME_MISMATCH,
301
+ title: 'Encrypted Collection write does not conform to its scheme.',
302
+ detail: message,
303
+ statusCode: 422,
304
+ problems: [{ detail: message }]
305
+ })
306
+ }
307
+ }
308
+
309
+ /**
310
+ * 412 — a conditional write's `If-Match` / `If-None-Match` precondition
311
+ * evaluated false: the Resource's current `ETag` did not match, or a
312
+ * create-if-absent (`If-None-Match: *`) target already exists. Header-driven
313
+ * and deliberately distinct from the 409 conflict kinds (`id-conflict`,
314
+ * `reserved-id`, `unsupported-backend`). A `412` is only ever observable by a
315
+ * caller already authorized to write the target -- the request layer checks
316
+ * authorization before the backend evaluates the precondition.
317
+ * @param options {object}
318
+ * @param [options.detail] {string} a specific explanation of the mismatch
319
+ * @param [options.requestName] {string} request name used in the error title
320
+ */
321
+ export class PreconditionFailedError extends ProblemError {
322
+ constructor({
323
+ detail,
324
+ requestName
325
+ }: { detail?: string; requestName?: string } = {}) {
326
+ const message = detail ?? 'A conditional request precondition was not met.'
327
+ super({
328
+ type: ProblemTypes.PRECONDITION_FAILED,
329
+ title: requestName
330
+ ? `Precondition Failed (${requestName})`
331
+ : 'Precondition Failed',
332
+ detail: message,
333
+ statusCode: 412,
334
+ problems: [{ detail: message }]
335
+ })
336
+ }
337
+ }
338
+
339
+ /**
340
+ * 400 — a pagination `cursor` query parameter is malformed or can no longer be
341
+ * honored (not valid base64url, not JSON, or missing its keyset position; spec
342
+ * `invalid-cursor`). Like `precondition-failed`, it is only ever observable by a
343
+ * caller already authorized to list the target -- the request layer checks
344
+ * authorization before the backend validates the cursor, so an under-authorized
345
+ * caller receives the privacy-merged `not-found` (404) instead.
346
+ * @param options {object}
347
+ * @param [options.detail] {string} a specific explanation of the failure
348
+ * @param [options.cause] {Error} the underlying parse error, when wrapping one
349
+ */
350
+ export class InvalidCursorError extends ProblemError {
351
+ constructor({ detail, cause }: { detail?: string; cause?: Error } = {}) {
352
+ const message =
353
+ detail ??
354
+ 'The pagination cursor is malformed or can no longer be honored.'
355
+ super({
356
+ type: ProblemTypes.INVALID_CURSOR,
357
+ title: 'Invalid pagination cursor',
358
+ detail: message,
359
+ statusCode: 400,
360
+ problems: [{ detail: message }],
361
+ cause
362
+ })
363
+ }
364
+ }
365
+
366
+ /**
367
+ * 400 — the Collection Description request body is missing or invalid.
368
+ */
369
+ export class InvalidCollectionError extends ProblemError {
370
+ constructor() {
371
+ super({
372
+ type: ProblemTypes.INVALID_REQUEST_BODY,
373
+ title: 'Invalid Collection Description body',
374
+ detail: 'Collection Description body is missing or invalid.',
375
+ statusCode: 400
376
+ })
377
+ }
378
+ }
379
+
380
+ /**
381
+ * 404 — the requested Collection does not exist, or the caller is not
382
+ * authorized.
383
+ * @param options {object}
384
+ * @param [options.requestName] {string} request name used in the error title
385
+ */
386
+ export class CollectionNotFoundError extends ProblemError {
387
+ constructor({ requestName }: { requestName?: string } = {}) {
388
+ super({
389
+ type: ProblemTypes.NOT_FOUND,
390
+ title: `Invalid ${requestName || 'Collection'} request`,
391
+ detail: 'Collection not found or invalid authorization.',
392
+ statusCode: 404
393
+ })
394
+ }
395
+ }
396
+
397
+ /**
398
+ * 404 — the requested Resource does not exist, or the caller is not authorized.
399
+ * @param options {object}
400
+ * @param [options.requestName] {string} request name used in the error title
401
+ */
402
+ export class ResourceNotFoundError extends ProblemError {
403
+ constructor({ requestName }: { requestName?: string } = {}) {
404
+ super({
405
+ type: ProblemTypes.NOT_FOUND,
406
+ title: `Invalid ${requestName || 'Resource'} request`,
407
+ detail: 'Resource not found or invalid authorization.',
408
+ statusCode: 404
409
+ })
410
+ }
411
+ }
412
+
413
+ /**
414
+ * 404 — no access-control policy document is set at the requested level
415
+ * (reported as not-found, consistent with the privacy-merged kind).
416
+ * @param options {object}
417
+ * @param [options.requestName] {string} request name used in the error title
418
+ */
419
+ export class PolicyNotFoundError extends ProblemError {
420
+ constructor({ requestName }: { requestName?: string } = {}) {
421
+ super({
422
+ type: ProblemTypes.NOT_FOUND,
423
+ title: `Invalid ${requestName || 'Policy'} request`,
424
+ detail: 'Policy not found or invalid authorization.',
425
+ statusCode: 404
426
+ })
427
+ }
428
+ }
429
+
430
+ /**
431
+ * 400 — the access-control policy document is missing or malformed (it must be
432
+ * a JSON object carrying a non-empty string `type`).
433
+ * @param options {object}
434
+ * @param [options.requestName] {string} request name used in the error title
435
+ */
436
+ export class InvalidPolicyError extends ProblemError {
437
+ constructor({ requestName }: { requestName?: string } = {}) {
438
+ super({
439
+ type: ProblemTypes.INVALID_REQUEST_BODY,
440
+ title: `Invalid ${requestName || 'Policy'} body`,
441
+ detail:
442
+ 'Policy document must be a JSON object with a non-empty string "type".',
443
+ statusCode: 400
444
+ })
445
+ }
446
+ }
447
+
448
+ /**
449
+ * 404 — capability invocation did not verify (reported as not-found so as not
450
+ * to leak resource existence).
451
+ * @param options {object}
452
+ * @param options.requestName {string} request name used in the error title
453
+ */
454
+ export class UnauthorizedError extends ProblemError {
455
+ constructor({ requestName }: { requestName?: string }) {
456
+ super({
457
+ type: ProblemTypes.NOT_FOUND,
458
+ title: `Invalid ${requestName} request.`,
459
+ detail: 'URL not found or invalid authorization.',
460
+ statusCode: 404
461
+ })
462
+ }
463
+ }
464
+
465
+ /**
466
+ * 401 — required `Authorization` / `Capability-Invocation` headers are missing.
467
+ */
468
+ export class MissingAuthError extends ProblemError {
469
+ constructor() {
470
+ super({
471
+ type: ProblemTypes.MISSING_AUTHORIZATION,
472
+ title: 'Invalid request',
473
+ detail: 'Authorization and Capability-Invocation headers are required.',
474
+ statusCode: 401
475
+ })
476
+ }
477
+ }
478
+
479
+ /**
480
+ * 403 — provisioning (creating Spaces or keystores) is refused by the server's
481
+ * configured provisioning policy (`authorizeProvisioning` returned `deny`).
482
+ * There is no dedicated `forbidden` problem type in `@interop/storage-core`, so
483
+ * this deliberately reuses `MISSING_AUTHORIZATION`.
484
+ */
485
+ export class ProvisioningNotAuthorizedError extends ProblemError {
486
+ constructor() {
487
+ super({
488
+ type: ProblemTypes.MISSING_AUTHORIZATION,
489
+ title: 'Provisioning not authorized',
490
+ detail: 'Creating Spaces and keystores is not authorized on this server.',
491
+ statusCode: 403
492
+ })
493
+ }
494
+ }
495
+
496
+ /**
497
+ * 401 — the server requires an onboarding token to provision, but the request
498
+ * carried no `Authorization: Bearer <token>` header.
499
+ */
500
+ export class MissingOnboardingTokenError extends ProblemError {
501
+ constructor() {
502
+ super({
503
+ type: ProblemTypes.MISSING_AUTHORIZATION,
504
+ title: 'Invalid request',
505
+ detail:
506
+ 'An onboarding token (Authorization: Bearer) is required to create ' +
507
+ 'Spaces and keystores on this server.',
508
+ statusCode: 401
509
+ })
510
+ }
511
+ }
512
+
513
+ /**
514
+ * 403 — the onboarding token presented in the `Authorization: Bearer` header
515
+ * did not match the configured token.
516
+ */
517
+ export class InvalidOnboardingTokenError extends ProblemError {
518
+ constructor() {
519
+ super({
520
+ type: ProblemTypes.INVALID_AUTHORIZATION_HEADER,
521
+ title: 'Invalid request',
522
+ detail: 'The provided onboarding token is not valid.',
523
+ statusCode: 403
524
+ })
525
+ }
526
+ }
527
+
528
+ /**
529
+ * 400 — the `Authorization` header did not include a `keyId` parameter.
530
+ */
531
+ export class MissingKeyIdError extends ProblemError {
532
+ constructor() {
533
+ super({
534
+ type: ProblemTypes.INVALID_AUTHORIZATION_HEADER,
535
+ title: 'Invalid Authorization header',
536
+ detail: 'Authorization header is missing the keyId parameter.',
537
+ statusCode: 400
538
+ })
539
+ }
540
+ }
541
+
542
+ /**
543
+ * 400 — failed to parse the `Authorization`, `Capability-Invocation`, or
544
+ * `Digest` headers.
545
+ * @param options {object}
546
+ * @param options.cause {Error} the underlying parse error
547
+ */
548
+ export class AuthHeaderParseError extends ProblemError {
549
+ constructor({ cause }: { cause: Error }) {
550
+ super({
551
+ type: ProblemTypes.INVALID_AUTHORIZATION_HEADER,
552
+ title: 'Invalid authorization headers',
553
+ detail:
554
+ 'Error parsing Authorization, Capability-Invocation, or Digest headers.',
555
+ statusCode: 400,
556
+ cause
557
+ })
558
+ }
559
+ }
560
+
561
+ /**
562
+ * 400 — the `Digest` header is missing, malformed, not covered by the request
563
+ * signature, or does not match the received body (spec "Request Body
564
+ * Integrity"). All of these are reported as `invalid-authorization-header`.
565
+ * @param options {object}
566
+ * @param options.detail {string} which of the digest conditions failed
567
+ * @param [options.cause] {Error} the underlying error, when wrapping one
568
+ */
569
+ export class InvalidDigestError extends ProblemError {
570
+ constructor({ detail, cause }: { detail: string; cause?: Error }) {
571
+ super({
572
+ type: ProblemTypes.INVALID_AUTHORIZATION_HEADER,
573
+ title: 'Invalid Digest header',
574
+ detail,
575
+ statusCode: 400,
576
+ cause
577
+ })
578
+ }
579
+ }
580
+
581
+ /**
582
+ * 400 — an error was thrown while verifying the authorization headers.
583
+ * @param options {object}
584
+ * @param options.requestName {string} request name used in the error title
585
+ * @param options.cause {Error} the underlying verification error
586
+ */
587
+ export class AuthVerificationError extends ProblemError {
588
+ constructor({ requestName, cause }: { requestName: string; cause: Error }) {
589
+ super({
590
+ type: ProblemTypes.INVALID_AUTHORIZATION_HEADER,
591
+ title: `Invalid ${requestName} request.`,
592
+ detail: 'Error verifying authorization headers.',
593
+ statusCode: 400,
594
+ cause
595
+ })
596
+ }
597
+ }
598
+
599
+ /**
600
+ * 400 — the `controller` field is present but is not a syntactically valid
601
+ * `did:key` DID (the only DID method this server accepts). Caught at the request
602
+ * layer so a malformed controller is rejected on the way in rather than failing
603
+ * later, at capability-verification time.
604
+ * @param options {object}
605
+ * @param [options.requestName] {string} request name used in the error title
606
+ */
607
+ export class InvalidControllerError extends ProblemError {
608
+ constructor({ requestName }: { requestName?: string } = {}) {
609
+ const detail = 'The "controller" property must be a valid did:key DID.'
610
+ super({
611
+ type: ProblemTypes.INVALID_REQUEST_BODY,
612
+ title: `Invalid ${requestName || 'request'} body`,
613
+ detail,
614
+ statusCode: 400,
615
+ problems: [{ detail, pointer: '#/controller' }]
616
+ })
617
+ }
618
+ }
619
+
620
+ /**
621
+ * 400 — the capability invocation on a Create Space request (via POST or
622
+ * create-via-PUT) is not *authorized by* the `controller` in the request body:
623
+ * it is neither signed directly by that DID nor accompanied by a delegation
624
+ * chain rooted in it.
625
+ * @param options {object}
626
+ * @param options.zcapSigningDid {string} DID that signed the invocation
627
+ * @param options.controller {string} controller DID supplied in the body
628
+ * @param [options.cause] {Error} the underlying chain-verification failure,
629
+ * for a delegated invocation rejected at verification time
630
+ */
631
+ export class SpaceControllerMismatchError extends ProblemError {
632
+ constructor({
633
+ zcapSigningDid,
634
+ controller,
635
+ cause
636
+ }: {
637
+ zcapSigningDid: string
638
+ controller: string
639
+ cause?: Error
640
+ }) {
641
+ const detail =
642
+ `The invocation must be authorized by the 'controller' DID in the` +
643
+ ` request body ("${controller}"): signed by it, or via a delegation` +
644
+ ` chain rooted in it (invocation signed by "${zcapSigningDid}").`
645
+ super({
646
+ type: ProblemTypes.CONTROLLER_MISMATCH,
647
+ title: 'Invalid Create Space request',
648
+ detail,
649
+ statusCode: 400,
650
+ problems: [{ detail, pointer: '#/controller' }],
651
+ cause
652
+ })
653
+ }
654
+ }
655
+
656
+ /**
657
+ * 500 — an underlying storage operation failed. The client-visible `title` /
658
+ * `detail` are deliberately generic: the underlying `cause` can carry
659
+ * server-internal specifics (filesystem paths, errnos, SQL) and is logged
660
+ * server-side by `handleError`, never serialized to the wire.
661
+ * @param options {object}
662
+ * @param options.cause {Error} the underlying storage error
663
+ * @param [options.requestName] {string} request name used in the error title
664
+ */
665
+ export class StorageError extends ProblemError {
666
+ constructor({ cause, requestName }: { cause: Error; requestName?: string }) {
667
+ super({
668
+ type: ProblemTypes.STORAGE_ERROR,
669
+ title: requestName ? `Storage Error (${requestName})` : 'Storage Error',
670
+ detail: 'An internal storage error occurred.',
671
+ statusCode: 500,
672
+ cause
673
+ })
674
+ }
675
+ }
676
+
677
+ /**
678
+ * 500 — an at-rest WebKMS key record could not be decrypted (its KEK is not
679
+ * registered, or the ciphertext failed to authenticate). A server-side
680
+ * misconfiguration of `KMS_RECORD_KEK`, never a client error; `handleError`
681
+ * logs the underlying `cause`.
682
+ * @param options {object}
683
+ * @param options.cause {Error} the underlying cipher error
684
+ * @param [options.requestName] {string} request name used in the error title
685
+ */
686
+ export class KmsRecordCipherError extends ProblemError {
687
+ constructor({ cause, requestName }: { cause: Error; requestName?: string }) {
688
+ super({
689
+ type: ProblemTypes.INTERNAL_ERROR,
690
+ title: requestName
691
+ ? `KMS Record Cipher Error (${requestName})`
692
+ : 'KMS Record Cipher Error',
693
+ detail:
694
+ 'A stored key record could not be decrypted. This is a server ' +
695
+ 'configuration error (KMS_RECORD_KEK).',
696
+ statusCode: 500,
697
+ cause
698
+ })
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Rethrows a structured failure from a storage call unchanged (any
704
+ * `ProblemError`, e.g. a 507 quota-exceeded raised by the backend), preserving
705
+ * its status code; wraps anything else -- a genuinely unexpected fault -- as a
706
+ * 500 `StorageError`. For use in request handlers' catch blocks around storage
707
+ * operations.
708
+ * @param options {object}
709
+ * @param options.err {unknown} the caught error
710
+ * @param [options.requestName] {string} request name used in the error title
711
+ * @returns {never}
712
+ */
713
+ export function rethrowOrWrapStorageError({
714
+ err,
715
+ requestName
716
+ }: {
717
+ err: unknown
718
+ requestName?: string
719
+ }): never {
720
+ if (err instanceof ProblemError) {
721
+ throw err
722
+ }
723
+ throw new StorageError({ cause: err as Error, requestName })
724
+ }
725
+
726
+ /**
727
+ * 507 — a write was rejected because the target backend's per-Space storage
728
+ * quota is exhausted (spec "Quotas"). Distinct from `payload-too-large` (413),
729
+ * which is a per-request upload-size limit rather than a cumulative one.
730
+ * @param options {object}
731
+ * @param options.spaceId {string} the Space whose quota is exhausted
732
+ * @param options.capacityBytes {number} the configured per-Space limit
733
+ * @param [options.requestName] {string} request name used in the error title
734
+ */
735
+ export class QuotaExceededError extends ProblemError {
736
+ constructor({
737
+ spaceId,
738
+ capacityBytes,
739
+ requestName
740
+ }: {
741
+ spaceId: string
742
+ capacityBytes: number
743
+ requestName?: string
744
+ }) {
745
+ super({
746
+ type: ProblemTypes.QUOTA_EXCEEDED,
747
+ title: `Insufficient Storage${requestName ? ` (${requestName})` : ''}`,
748
+ detail: `Space '${spaceId}' storage quota of ${capacityBytes} bytes is exhausted.`,
749
+ statusCode: 507
750
+ })
751
+ }
752
+ }
753
+
754
+ /**
755
+ * 507 — a create was rejected because a default-on count quota is exhausted:
756
+ * too many Spaces for one controller, too many Collections in a Space, or too
757
+ * many live Resources in a Space. Distinct from `QuotaExceededError` (also 507),
758
+ * which caps cumulative bytes rather than item counts; both reuse the spec's
759
+ * `quota-exceeded` problem type (there is no count-specific slug). Only the
760
+ * create path trips this -- overwriting an existing item never does.
761
+ * @param options {object}
762
+ * @param options.scope {string} short phrase naming the exhausted count (e.g.
763
+ * `'Spaces per controller'`, `'Collections per Space'`,
764
+ * `'Resources per Space'`), used in the detail message
765
+ * @param options.limit {number} the configured maximum count
766
+ */
767
+ export class CountQuotaExceededError extends ProblemError {
768
+ constructor({ scope, limit }: { scope: string; limit: number }) {
769
+ super({
770
+ type: ProblemTypes.QUOTA_EXCEEDED,
771
+ title: 'Insufficient Storage',
772
+ detail: `Count quota exceeded: ${scope} limit is ${limit}.`,
773
+ statusCode: 507
774
+ })
775
+ }
776
+ }
777
+
778
+ /**
779
+ * 413 — an upload exceeds the target backend's per-request `maxUploadBytes`
780
+ * constraint. Distinct from `quota-exceeded` (507): this limit is per-request,
781
+ * not cumulative, so smaller uploads may still succeed.
782
+ * @param options {object}
783
+ * @param options.maxUploadBytes {number} the backend's per-upload limit
784
+ * @param options.backendId {string} the backend enforcing the limit
785
+ * @param [options.uploadBytes] {number} the upload's size, when known up
786
+ * front (a streamed upload without a Content-Length only reveals the
787
+ * overflow, not the total)
788
+ */
789
+ export class PayloadTooLargeError extends ProblemError {
790
+ constructor({
791
+ maxUploadBytes,
792
+ backendId,
793
+ uploadBytes
794
+ }: {
795
+ maxUploadBytes: number
796
+ backendId: string
797
+ uploadBytes?: number
798
+ }) {
799
+ const detail =
800
+ uploadBytes === undefined
801
+ ? `Upload exceeds 'maxUploadBytes' of ${maxUploadBytes} for backend '${backendId}'.`
802
+ : `Upload size ${uploadBytes} exceeds 'maxUploadBytes' of ${maxUploadBytes} for backend '${backendId}'.`
803
+ super({
804
+ type: ProblemTypes.PAYLOAD_TOO_LARGE,
805
+ title: "Upload exceeds the backend's maximum upload size.",
806
+ detail,
807
+ statusCode: 413
808
+ })
809
+ }
810
+ }
811
+
812
+ /**
813
+ * 400 — a required field of the request body is missing or invalid.
814
+ * @param options {object}
815
+ * @param [options.requestName] {string} request name used in the error title
816
+ * @param [options.detail] {string} specific detail describing the problem
817
+ * @param [options.pointer] {string} RFC 6901 JSON Pointer (`#/field` form)
818
+ * to the offending body field
819
+ */
820
+ export class InvalidRequestBodyError extends ProblemError {
821
+ constructor({
822
+ requestName,
823
+ detail,
824
+ pointer
825
+ }: { requestName?: string; detail?: string; pointer?: string } = {}) {
826
+ const resolvedDetail =
827
+ detail || 'Request body is missing one or more required fields.'
828
+ super({
829
+ type: ProblemTypes.INVALID_REQUEST_BODY,
830
+ title: `Invalid ${requestName || 'request'} body`,
831
+ detail: resolvedDetail,
832
+ statusCode: 400,
833
+ ...(pointer ? { problems: [{ detail: resolvedDetail, pointer }] } : {})
834
+ })
835
+ }
836
+ }
837
+
838
+ /**
839
+ * 400 — the request is missing a required `Content-Type` header.
840
+ * @param options {object}
841
+ * @param [options.requestName] {string} request name used in the error title
842
+ */
843
+ export class MissingContentTypeError extends ProblemError {
844
+ constructor({ requestName }: { requestName?: string } = {}) {
845
+ super({
846
+ type: ProblemTypes.MISSING_CONTENT_TYPE,
847
+ title: `Invalid ${requestName || 'request'}`,
848
+ detail: 'A Content-Type header is required for this request.',
849
+ statusCode: 400
850
+ })
851
+ }
852
+ }
853
+
854
+ /**
855
+ * 501 — the server does not implement this OPTIONAL operation (e.g. updating
856
+ * Resource Metadata, which is not yet supported).
857
+ * @param options {object}
858
+ * @param [options.requestName] {string} request name used in the error title
859
+ */
860
+ export class UnsupportedOperationError extends ProblemError {
861
+ constructor({ requestName }: { requestName?: string } = {}) {
862
+ super({
863
+ type: ProblemTypes.UNSUPPORTED_OPERATION,
864
+ title: `Unsupported ${requestName || 'operation'}`,
865
+ detail: 'This server does not implement this optional operation.',
866
+ statusCode: 501
867
+ })
868
+ }
869
+ }
870
+
871
+ /**
872
+ * 400 — the uploaded archive is not a valid WAS space export.
873
+ * @param options {object}
874
+ * @param [options.message] {string} detail message describing the problem
875
+ * @param [options.cause] {Error} the underlying error, when wrapping one
876
+ */
877
+ export class InvalidImportError extends ProblemError {
878
+ constructor({ message, cause }: { message?: string; cause?: Error } = {}) {
879
+ super({
880
+ type: ProblemTypes.INVALID_IMPORT,
881
+ title: 'Invalid space import',
882
+ detail:
883
+ message || 'The uploaded archive is not a valid WAS space export.',
884
+ statusCode: 400,
885
+ cause
886
+ })
887
+ }
888
+ }
889
+
890
+ // WebKMS facet errors (`/kms`). The webkms
891
+ // protocol is status-code driven -- `@interop/webkms-client` distinguishes
892
+ // only 404 and 409 -- so these reuse the nearest WAS problem-kind URIs rather
893
+ // than minting a parallel registry for a non-WAS route family.
894
+
895
+ /**
896
+ * 404 — the requested keystore does not exist, or the caller is not
897
+ * authorized (the WAS existence-masking convention, kept for consistency
898
+ * within this server).
899
+ * @param options {object}
900
+ * @param [options.requestName] {string} request name used in the error title
901
+ */
902
+ export class KeystoreNotFoundError extends ProblemError {
903
+ constructor({ requestName }: { requestName?: string } = {}) {
904
+ super({
905
+ type: ProblemTypes.NOT_FOUND,
906
+ title: `Invalid ${requestName || 'Keystore'} request`,
907
+ detail: 'Keystore not found or invalid authorization.',
908
+ statusCode: 404
909
+ })
910
+ }
911
+ }
912
+
913
+ /**
914
+ * 400 — the capability invocation on a Create Keystore request is not
915
+ * *authorized by* the `controller` in the request body: it is neither signed
916
+ * directly by that DID nor accompanied by a delegation chain rooted in it.
917
+ * The keystore-creation bootstrap rule, mirroring Create Space.
918
+ * @param options {object}
919
+ * @param options.zcapSigningDid {string} DID that signed the invocation
920
+ * @param options.controller {string} controller DID supplied in the body
921
+ * @param [options.cause] {Error} the underlying chain-verification failure,
922
+ * for a delegated invocation rejected at verification time
923
+ */
924
+ export class KeystoreControllerMismatchError extends ProblemError {
925
+ constructor({
926
+ zcapSigningDid,
927
+ controller,
928
+ cause
929
+ }: {
930
+ zcapSigningDid: string
931
+ controller: string
932
+ cause?: Error
933
+ }) {
934
+ const detail =
935
+ `The invocation must be authorized by the 'controller' DID in the` +
936
+ ` request body ("${controller}"): signed by it, or via a delegation` +
937
+ ` chain rooted in it (invocation signed by "${zcapSigningDid}").`
938
+ super({
939
+ type: ProblemTypes.CONTROLLER_MISMATCH,
940
+ title: 'Invalid Create Keystore request',
941
+ detail,
942
+ statusCode: 400,
943
+ problems: [{ detail, pointer: '#/controller' }],
944
+ cause
945
+ })
946
+ }
947
+ }
948
+
949
+ /**
950
+ * 409 — a keystore config update could not be applied: the supplied `sequence`
951
+ * is not exactly the stored sequence + 1, or the immutable `kmsModule` does
952
+ * not match. One merged conflict kind; the client distinguishes only the 409
953
+ * status.
954
+ * `ID_CONFLICT` is the nearest WAS problem kind (a 409 state conflict on an
955
+ * identified record) -- the webkms protocol has no type registry of its own.
956
+ */
957
+ export class KeystoreStateConflictError extends ProblemError {
958
+ constructor() {
959
+ const detail =
960
+ 'Could not update keystore configuration: sequence must be exactly the' +
961
+ ' stored sequence + 1 and "kmsModule" cannot change.'
962
+ super({
963
+ type: ProblemTypes.ID_CONFLICT,
964
+ title: 'Keystore configuration state conflict.',
965
+ detail,
966
+ statusCode: 409,
967
+ problems: [{ detail, pointer: '#/sequence' }]
968
+ })
969
+ }
970
+ }
971
+
972
+ /**
973
+ * 404 — the requested KMS-held key does not exist, or the caller is not
974
+ * authorized (the same existence-masking as `KeystoreNotFoundError`).
975
+ * @param options {object}
976
+ * @param [options.requestName] {string} request name used in the error title
977
+ */
978
+ export class KeyNotFoundError extends ProblemError {
979
+ constructor({ requestName }: { requestName?: string } = {}) {
980
+ super({
981
+ type: ProblemTypes.NOT_FOUND,
982
+ title: `Invalid ${requestName || 'Key'} request`,
983
+ detail: 'Key not found or invalid authorization.',
984
+ statusCode: 404
985
+ })
986
+ }
987
+ }
988
+
989
+ /**
990
+ * 409 — a key record already exists at `(keystoreId, localId)`. Key local ids
991
+ * are server-generated 128-bit random values, so this is effectively
992
+ * unreachable through the API; the storage layer still enforces insert-once
993
+ * semantics (a unique-index insert), and
994
+ * `@interop/webkms-client` maps a 409 on generate to its `DuplicateError`.
995
+ */
996
+ export class KeyIdConflictError extends ProblemError {
997
+ constructor() {
998
+ const detail = 'Duplicate key identifier.'
999
+ super({
1000
+ type: ProblemTypes.ID_CONFLICT,
1001
+ title: 'A key with this id already exists.',
1002
+ detail,
1003
+ statusCode: 409,
1004
+ problems: [{ detail, pointer: '#/id' }]
1005
+ })
1006
+ }
1007
+ }
1008
+
1009
+ /**
1010
+ * 400 — a well-formed key operation names an operation this KMS does not serve
1011
+ * for the key's type (e.g. `VerifyOperation` on an asymmetric key -- custody
1012
+ * is the criterion: asymmetric verify needs only the public key and is
1013
+ * client-local), or an operation type it does not recognize at all. A clean
1014
+ * 400.
1015
+ * @param options {object}
1016
+ * @param options.operationType {string} the operation envelope's `type`
1017
+ * @param [options.keyType] {string} the target key's type, when known
1018
+ */
1019
+ export class UnsupportedKeyOperationError extends ProblemError {
1020
+ constructor({
1021
+ operationType,
1022
+ keyType
1023
+ }: {
1024
+ operationType: string
1025
+ keyType?: string
1026
+ }) {
1027
+ const detail = keyType
1028
+ ? `Unsupported operation "${operationType}" for key type "${keyType}".`
1029
+ : `Unknown operation type "${operationType}".`
1030
+ super({
1031
+ type: ProblemTypes.INVALID_REQUEST_BODY,
1032
+ title: 'Unsupported key operation.',
1033
+ detail,
1034
+ statusCode: 400,
1035
+ problems: [{ detail, pointer: '#/type' }]
1036
+ })
1037
+ }
1038
+ }
1039
+
1040
+ /**
1041
+ * 400 — a submitted zcap revocation cannot be accepted: the body is not a
1042
+ * revocable delegated capability (root zcaps cannot be revoked), its id does
1043
+ * not match the revocation URL, its chain does not root in the keystore being
1044
+ * posted to, or its delegation chain fails verification (a chain containing
1045
+ * an already-revoked capability included -- resubmitting a stored revocation
1046
+ * therefore lands here, per ezcap-express's `authorizeZcapRevocation`; the
1047
+ * 409 duplicate is reserved for a write race at the store).
1048
+ * @param options {object}
1049
+ * @param options.detail {string} which of the revocation conditions failed
1050
+ * @param [options.cause] {Error} the underlying chain-verification failure
1051
+ */
1052
+ export class InvalidRevocationError extends ProblemError {
1053
+ constructor({ detail, cause }: { detail: string; cause?: Error }) {
1054
+ super({
1055
+ type: ProblemTypes.INVALID_REQUEST_BODY,
1056
+ title: 'Invalid Revoke Capability request',
1057
+ detail,
1058
+ statusCode: 400,
1059
+ cause
1060
+ })
1061
+ }
1062
+ }
1063
+
1064
+ /**
1065
+ * 409 — a revocation record already exists at `(delegator, capabilityId)`
1066
+ * (`@interop/webkms-client` maps a 409 to its `DuplicateError`). Reached only
1067
+ * on a concurrent-submission race:
1068
+ * a sequential resubmission fails the chain verification first (the chain now
1069
+ * contains a revoked capability) and is the 400 `InvalidRevocationError`.
1070
+ */
1071
+ export class DuplicateRevocationError extends ProblemError {
1072
+ constructor() {
1073
+ const detail = 'Duplicate revocation.'
1074
+ super({
1075
+ type: ProblemTypes.ID_CONFLICT,
1076
+ title: 'This capability is already revoked.',
1077
+ detail,
1078
+ statusCode: 409,
1079
+ problems: [{ detail, pointer: '#/id' }]
1080
+ })
1081
+ }
1082
+ }
1083
+
1084
+ /**
1085
+ * Fastify error handler installed by each route group. Serializes the error to
1086
+ * an `application/problem+json` response using its `type` / `title` / `detail`
1087
+ * (or `problems`), defaulting to a 500 internal error when no statusCode is
1088
+ * present. The spec requires `type` and `title`, so both always fall back to a
1089
+ * sensible value.
1090
+ * @param error {Error & { statusCode?: number, type?: string, title?: string, detail?: string, problems?: Problem[] }}
1091
+ * @param request {import('fastify').FastifyRequest}
1092
+ * @param reply {import('fastify').FastifyReply}
1093
+ * @returns {Promise<FastifyReply>}
1094
+ */
1095
+ export async function handleError(
1096
+ error: Error & {
1097
+ statusCode?: number
1098
+ type?: string
1099
+ title?: string
1100
+ detail?: string
1101
+ problems?: Problem[]
1102
+ },
1103
+ request: FastifyRequest,
1104
+ reply: FastifyReply
1105
+ ): Promise<FastifyReply> {
1106
+ const statusCode = error.statusCode || 500
1107
+ // Log server-side faults (5xx, e.g. a StorageError and its underlying
1108
+ // `cause`) here through the request logger -- rather than in the error
1109
+ // constructors -- so logging lives in one place. Client errors (4xx) are
1110
+ // expected and not logged.
1111
+ if (statusCode >= 500) {
1112
+ request.log.error({ err: error }, error.title || 'Request error')
1113
+ }
1114
+ return reply
1115
+ .status(statusCode)
1116
+ .type('application/problem+json')
1117
+ .send({
1118
+ type: error.type || ProblemTypes.INTERNAL_ERROR,
1119
+ title: error.title || 'Request error',
1120
+ errors: error.problems ?? [{ detail: error.detail }]
1121
+ })
1122
+ }