was-teaching-server 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1432 -0
- package/LICENSE +661 -0
- package/README.md +305 -0
- package/common/css/materialize.1.0.0.min.css +13 -0
- package/dist/auth-header-hooks.d.ts +84 -0
- package/dist/auth-header-hooks.d.ts.map +1 -0
- package/dist/auth-header-hooks.js +136 -0
- package/dist/auth-header-hooks.js.map +1 -0
- package/dist/authorize.d.ts +45 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +76 -0
- package/dist/authorize.js.map +1 -0
- package/dist/backends/filesystem.d.ts +1179 -0
- package/dist/backends/filesystem.d.ts.map +1 -0
- package/dist/backends/filesystem.js +2727 -0
- package/dist/backends/filesystem.js.map +1 -0
- package/dist/backends/postgres.d.ts +833 -0
- package/dist/backends/postgres.d.ts.map +1 -0
- package/dist/backends/postgres.js +2164 -0
- package/dist/backends/postgres.js.map +1 -0
- package/dist/backends/postgresSchema.d.ts +36 -0
- package/dist/backends/postgresSchema.d.ts.map +1 -0
- package/dist/backends/postgresSchema.js +203 -0
- package/dist/backends/postgresSchema.js.map +1 -0
- package/dist/config.default.d.ts +360 -0
- package/dist/config.default.d.ts.map +1 -0
- package/dist/config.default.js +493 -0
- package/dist/config.default.js.map +1 -0
- package/dist/corsProxy.d.ts +45 -0
- package/dist/corsProxy.d.ts.map +1 -0
- package/dist/corsProxy.js +278 -0
- package/dist/corsProxy.js.map +1 -0
- package/dist/digest.d.ts +45 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +174 -0
- package/dist/digest.js.map +1 -0
- package/dist/errors.d.ts +654 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +960 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/atomicFile.d.ts +61 -0
- package/dist/lib/atomicFile.d.ts.map +1 -0
- package/dist/lib/atomicFile.js +150 -0
- package/dist/lib/atomicFile.js.map +1 -0
- package/dist/lib/backendRegistry.d.ts +59 -0
- package/dist/lib/backendRegistry.d.ts.map +1 -0
- package/dist/lib/backendRegistry.js +92 -0
- package/dist/lib/backendRegistry.js.map +1 -0
- package/dist/lib/backendUsage.d.ts +28 -0
- package/dist/lib/backendUsage.d.ts.map +1 -0
- package/dist/lib/backendUsage.js +55 -0
- package/dist/lib/backendUsage.js.map +1 -0
- package/dist/lib/backends.d.ts +142 -0
- package/dist/lib/backends.d.ts.map +1 -0
- package/dist/lib/backends.js +272 -0
- package/dist/lib/backends.js.map +1 -0
- package/dist/lib/blindedIndex.d.ts +156 -0
- package/dist/lib/blindedIndex.d.ts.map +1 -0
- package/dist/lib/blindedIndex.js +338 -0
- package/dist/lib/blindedIndex.js.map +1 -0
- package/dist/lib/collectionDescription.d.ts +33 -0
- package/dist/lib/collectionDescription.d.ts.map +1 -0
- package/dist/lib/collectionDescription.js +21 -0
- package/dist/lib/collectionDescription.js.map +1 -0
- package/dist/lib/cursor.d.ts +18 -0
- package/dist/lib/cursor.d.ts.map +1 -0
- package/dist/lib/cursor.js +50 -0
- package/dist/lib/cursor.js.map +1 -0
- package/dist/lib/edvEnvelope.d.ts +50 -0
- package/dist/lib/edvEnvelope.d.ts.map +1 -0
- package/dist/lib/edvEnvelope.js +106 -0
- package/dist/lib/edvEnvelope.js.map +1 -0
- package/dist/lib/encryption.d.ts +192 -0
- package/dist/lib/encryption.d.ts.map +1 -0
- package/dist/lib/encryption.js +383 -0
- package/dist/lib/encryption.js.map +1 -0
- package/dist/lib/etag.d.ts +33 -0
- package/dist/lib/etag.d.ts.map +1 -0
- package/dist/lib/etag.js +36 -0
- package/dist/lib/etag.js.map +1 -0
- package/dist/lib/exportManifest.d.ts +29 -0
- package/dist/lib/exportManifest.d.ts.map +1 -0
- package/dist/lib/exportManifest.js +77 -0
- package/dist/lib/exportManifest.js.map +1 -0
- package/dist/lib/importTar.d.ts +81 -0
- package/dist/lib/importTar.d.ts.map +1 -0
- package/dist/lib/importTar.js +301 -0
- package/dist/lib/importTar.js.map +1 -0
- package/dist/lib/isJson.d.ts +16 -0
- package/dist/lib/isJson.d.ts.map +1 -0
- package/dist/lib/isJson.js +17 -0
- package/dist/lib/isJson.js.map +1 -0
- package/dist/lib/keyEpoch.d.ts +38 -0
- package/dist/lib/keyEpoch.d.ts.map +1 -0
- package/dist/lib/keyEpoch.js +75 -0
- package/dist/lib/keyEpoch.js.map +1 -0
- package/dist/lib/keyedMutex.d.ts +28 -0
- package/dist/lib/keyedMutex.d.ts.map +1 -0
- package/dist/lib/keyedMutex.js +45 -0
- package/dist/lib/keyedMutex.js.map +1 -0
- package/dist/lib/kmsModule.d.ts +62 -0
- package/dist/lib/kmsModule.d.ts.map +1 -0
- package/dist/lib/kmsModule.js +327 -0
- package/dist/lib/kmsModule.js.map +1 -0
- package/dist/lib/kmsRecordCipher.d.ts +82 -0
- package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
- package/dist/lib/kmsRecordCipher.js +226 -0
- package/dist/lib/kmsRecordCipher.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.d.ts.map +1 -0
- package/dist/lib/pagination.js +21 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/paths.d.ts +273 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +270 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/preconditions.d.ts +65 -0
- package/dist/lib/preconditions.d.ts.map +1 -0
- package/dist/lib/preconditions.js +112 -0
- package/dist/lib/preconditions.js.map +1 -0
- package/dist/lib/resourceFileName.d.ts +41 -0
- package/dist/lib/resourceFileName.d.ts.map +1 -0
- package/dist/lib/resourceFileName.js +58 -0
- package/dist/lib/resourceFileName.js.map +1 -0
- package/dist/lib/revocations.d.ts +49 -0
- package/dist/lib/revocations.d.ts.map +1 -0
- package/dist/lib/revocations.js +86 -0
- package/dist/lib/revocations.js.map +1 -0
- package/dist/lib/validateDid.d.ts +19 -0
- package/dist/lib/validateDid.d.ts.map +1 -0
- package/dist/lib/validateDid.js +35 -0
- package/dist/lib/validateDid.js.map +1 -0
- package/dist/lib/validateId.d.ts +66 -0
- package/dist/lib/validateId.d.ts.map +1 -0
- package/dist/lib/validateId.js +126 -0
- package/dist/lib/validateId.js.map +1 -0
- package/dist/plugin.d.ts +115 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +134 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +78 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/policy.js.map +1 -0
- package/dist/provisioning.d.ts +38 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +72 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/requests/BackendRequest.d.ts +61 -0
- package/dist/requests/BackendRequest.d.ts.map +1 -0
- package/dist/requests/BackendRequest.js +152 -0
- package/dist/requests/BackendRequest.js.map +1 -0
- package/dist/requests/CollectionRequest.d.ts +234 -0
- package/dist/requests/CollectionRequest.d.ts.map +1 -0
- package/dist/requests/CollectionRequest.js +705 -0
- package/dist/requests/CollectionRequest.js.map +1 -0
- package/dist/requests/KeyRequest.d.ts +115 -0
- package/dist/requests/KeyRequest.d.ts.map +1 -0
- package/dist/requests/KeyRequest.js +468 -0
- package/dist/requests/KeyRequest.js.map +1 -0
- package/dist/requests/KeystoreRequest.d.ts +99 -0
- package/dist/requests/KeystoreRequest.d.ts.map +1 -0
- package/dist/requests/KeystoreRequest.js +247 -0
- package/dist/requests/KeystoreRequest.js.map +1 -0
- package/dist/requests/PolicyRequest.d.ts +53 -0
- package/dist/requests/PolicyRequest.d.ts.map +1 -0
- package/dist/requests/PolicyRequest.js +124 -0
- package/dist/requests/PolicyRequest.js.map +1 -0
- package/dist/requests/ResourceRequest.d.ts +134 -0
- package/dist/requests/ResourceRequest.d.ts.map +1 -0
- package/dist/requests/ResourceRequest.js +548 -0
- package/dist/requests/ResourceRequest.js.map +1 -0
- package/dist/requests/RevocationRequest.d.ts +67 -0
- package/dist/requests/RevocationRequest.d.ts.map +1 -0
- package/dist/requests/RevocationRequest.js +187 -0
- package/dist/requests/RevocationRequest.js.map +1 -0
- package/dist/requests/SpaceRequest.d.ts +204 -0
- package/dist/requests/SpaceRequest.d.ts.map +1 -0
- package/dist/requests/SpaceRequest.js +520 -0
- package/dist/requests/SpaceRequest.js.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.js +159 -0
- package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
- package/dist/requests/collectionContext.d.ts +21 -0
- package/dist/requests/collectionContext.d.ts.map +1 -0
- package/dist/requests/collectionContext.js +30 -0
- package/dist/requests/collectionContext.js.map +1 -0
- package/dist/requests/controllerConsent.d.ts +58 -0
- package/dist/requests/controllerConsent.d.ts.map +1 -0
- package/dist/requests/controllerConsent.js +73 -0
- package/dist/requests/controllerConsent.js.map +1 -0
- package/dist/requests/keystoreContext.d.ts +68 -0
- package/dist/requests/keystoreContext.d.ts.map +1 -0
- package/dist/requests/keystoreContext.js +87 -0
- package/dist/requests/keystoreContext.js.map +1 -0
- package/dist/requests/resourceInput.d.ts +19 -0
- package/dist/requests/resourceInput.d.ts.map +1 -0
- package/dist/requests/resourceInput.js +102 -0
- package/dist/requests/resourceInput.js.map +1 -0
- package/dist/requests/spaceContext.d.ts +127 -0
- package/dist/requests/spaceContext.d.ts.map +1 -0
- package/dist/requests/spaceContext.js +201 -0
- package/dist/requests/spaceContext.js.map +1 -0
- package/dist/routes.d.ts +55 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +306 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +60 -0
- package/dist/server.js.map +1 -0
- package/dist/start.d.ts +7 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +61 -0
- package/dist/start.js.map +1 -0
- package/dist/storage.d.ts +29 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +47 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +915 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/views/home.hbs +9 -0
- package/dist/views/templates/main.hbs +12 -0
- package/dist/zcap.d.ts +267 -0
- package/dist/zcap.d.ts.map +1 -0
- package/dist/zcap.js +478 -0
- package/dist/zcap.js.map +1 -0
- package/package.json +109 -0
- package/src/auth-header-hooks.ts +163 -0
- package/src/authorize.ts +117 -0
- package/src/backends/filesystem.ts +3626 -0
- package/src/backends/postgres.ts +3073 -0
- package/src/backends/postgresSchema.ts +231 -0
- package/src/config.default.ts +657 -0
- package/src/corsProxy.ts +326 -0
- package/src/declarations.d.ts +28 -0
- package/src/digest.ts +223 -0
- package/src/errors.ts +1122 -0
- package/src/index.ts +21 -0
- package/src/lib/atomicFile.ts +176 -0
- package/src/lib/backendRegistry.ts +139 -0
- package/src/lib/backendUsage.ts +79 -0
- package/src/lib/backends.ts +351 -0
- package/src/lib/blindedIndex.ts +466 -0
- package/src/lib/collectionDescription.ts +43 -0
- package/src/lib/cursor.ts +52 -0
- package/src/lib/edvEnvelope.ts +120 -0
- package/src/lib/encryption.ts +472 -0
- package/src/lib/etag.ts +40 -0
- package/src/lib/exportManifest.ts +115 -0
- package/src/lib/importTar.ts +381 -0
- package/src/lib/isJson.ts +18 -0
- package/src/lib/keyEpoch.ts +89 -0
- package/src/lib/keyedMutex.ts +45 -0
- package/src/lib/kmsModule.ts +451 -0
- package/src/lib/kmsRecordCipher.ts +292 -0
- package/src/lib/pagination.ts +22 -0
- package/src/lib/paths.ts +382 -0
- package/src/lib/preconditions.ts +145 -0
- package/src/lib/resourceFileName.ts +69 -0
- package/src/lib/revocations.ts +116 -0
- package/src/lib/validateDid.ts +41 -0
- package/src/lib/validateId.ts +148 -0
- package/src/plugin.ts +288 -0
- package/src/policy.ts +176 -0
- package/src/provisioning.ts +98 -0
- package/src/requests/BackendRequest.ts +219 -0
- package/src/requests/CollectionRequest.ts +899 -0
- package/src/requests/KeyRequest.ts +617 -0
- package/src/requests/KeystoreRequest.ts +325 -0
- package/src/requests/PolicyRequest.ts +169 -0
- package/src/requests/ResourceRequest.ts +649 -0
- package/src/requests/RevocationRequest.ts +267 -0
- package/src/requests/SpaceRequest.ts +686 -0
- package/src/requests/SpacesRepositoryRequest.ts +194 -0
- package/src/requests/collectionContext.ts +41 -0
- package/src/requests/controllerConsent.ts +108 -0
- package/src/requests/keystoreContext.ts +124 -0
- package/src/requests/resourceInput.ts +118 -0
- package/src/requests/spaceContext.ts +311 -0
- package/src/routes.ts +420 -0
- package/src/server.ts +69 -0
- package/src/start.ts +71 -0
- package/src/storage.ts +60 -0
- package/src/types.ts +1016 -0
- package/src/views/home.hbs +9 -0
- package/src/views/templates/main.hbs +12 -0
- package/src/zcap.ts +657 -0
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
|
+
}
|