was-teaching-server 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1432 -0
- package/LICENSE +661 -0
- package/README.md +305 -0
- package/common/css/materialize.1.0.0.min.css +13 -0
- package/dist/auth-header-hooks.d.ts +84 -0
- package/dist/auth-header-hooks.d.ts.map +1 -0
- package/dist/auth-header-hooks.js +136 -0
- package/dist/auth-header-hooks.js.map +1 -0
- package/dist/authorize.d.ts +45 -0
- package/dist/authorize.d.ts.map +1 -0
- package/dist/authorize.js +76 -0
- package/dist/authorize.js.map +1 -0
- package/dist/backends/filesystem.d.ts +1179 -0
- package/dist/backends/filesystem.d.ts.map +1 -0
- package/dist/backends/filesystem.js +2727 -0
- package/dist/backends/filesystem.js.map +1 -0
- package/dist/backends/postgres.d.ts +833 -0
- package/dist/backends/postgres.d.ts.map +1 -0
- package/dist/backends/postgres.js +2164 -0
- package/dist/backends/postgres.js.map +1 -0
- package/dist/backends/postgresSchema.d.ts +36 -0
- package/dist/backends/postgresSchema.d.ts.map +1 -0
- package/dist/backends/postgresSchema.js +203 -0
- package/dist/backends/postgresSchema.js.map +1 -0
- package/dist/config.default.d.ts +360 -0
- package/dist/config.default.d.ts.map +1 -0
- package/dist/config.default.js +493 -0
- package/dist/config.default.js.map +1 -0
- package/dist/corsProxy.d.ts +45 -0
- package/dist/corsProxy.d.ts.map +1 -0
- package/dist/corsProxy.js +278 -0
- package/dist/corsProxy.js.map +1 -0
- package/dist/digest.d.ts +45 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +174 -0
- package/dist/digest.js.map +1 -0
- package/dist/errors.d.ts +654 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +960 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/atomicFile.d.ts +61 -0
- package/dist/lib/atomicFile.d.ts.map +1 -0
- package/dist/lib/atomicFile.js +150 -0
- package/dist/lib/atomicFile.js.map +1 -0
- package/dist/lib/backendRegistry.d.ts +59 -0
- package/dist/lib/backendRegistry.d.ts.map +1 -0
- package/dist/lib/backendRegistry.js +92 -0
- package/dist/lib/backendRegistry.js.map +1 -0
- package/dist/lib/backendUsage.d.ts +28 -0
- package/dist/lib/backendUsage.d.ts.map +1 -0
- package/dist/lib/backendUsage.js +55 -0
- package/dist/lib/backendUsage.js.map +1 -0
- package/dist/lib/backends.d.ts +142 -0
- package/dist/lib/backends.d.ts.map +1 -0
- package/dist/lib/backends.js +272 -0
- package/dist/lib/backends.js.map +1 -0
- package/dist/lib/blindedIndex.d.ts +156 -0
- package/dist/lib/blindedIndex.d.ts.map +1 -0
- package/dist/lib/blindedIndex.js +338 -0
- package/dist/lib/blindedIndex.js.map +1 -0
- package/dist/lib/collectionDescription.d.ts +33 -0
- package/dist/lib/collectionDescription.d.ts.map +1 -0
- package/dist/lib/collectionDescription.js +21 -0
- package/dist/lib/collectionDescription.js.map +1 -0
- package/dist/lib/cursor.d.ts +18 -0
- package/dist/lib/cursor.d.ts.map +1 -0
- package/dist/lib/cursor.js +50 -0
- package/dist/lib/cursor.js.map +1 -0
- package/dist/lib/edvEnvelope.d.ts +50 -0
- package/dist/lib/edvEnvelope.d.ts.map +1 -0
- package/dist/lib/edvEnvelope.js +106 -0
- package/dist/lib/edvEnvelope.js.map +1 -0
- package/dist/lib/encryption.d.ts +192 -0
- package/dist/lib/encryption.d.ts.map +1 -0
- package/dist/lib/encryption.js +383 -0
- package/dist/lib/encryption.js.map +1 -0
- package/dist/lib/etag.d.ts +33 -0
- package/dist/lib/etag.d.ts.map +1 -0
- package/dist/lib/etag.js +36 -0
- package/dist/lib/etag.js.map +1 -0
- package/dist/lib/exportManifest.d.ts +29 -0
- package/dist/lib/exportManifest.d.ts.map +1 -0
- package/dist/lib/exportManifest.js +77 -0
- package/dist/lib/exportManifest.js.map +1 -0
- package/dist/lib/importTar.d.ts +81 -0
- package/dist/lib/importTar.d.ts.map +1 -0
- package/dist/lib/importTar.js +301 -0
- package/dist/lib/importTar.js.map +1 -0
- package/dist/lib/isJson.d.ts +16 -0
- package/dist/lib/isJson.d.ts.map +1 -0
- package/dist/lib/isJson.js +17 -0
- package/dist/lib/isJson.js.map +1 -0
- package/dist/lib/keyEpoch.d.ts +38 -0
- package/dist/lib/keyEpoch.d.ts.map +1 -0
- package/dist/lib/keyEpoch.js +75 -0
- package/dist/lib/keyEpoch.js.map +1 -0
- package/dist/lib/keyedMutex.d.ts +28 -0
- package/dist/lib/keyedMutex.d.ts.map +1 -0
- package/dist/lib/keyedMutex.js +45 -0
- package/dist/lib/keyedMutex.js.map +1 -0
- package/dist/lib/kmsModule.d.ts +62 -0
- package/dist/lib/kmsModule.d.ts.map +1 -0
- package/dist/lib/kmsModule.js +327 -0
- package/dist/lib/kmsModule.js.map +1 -0
- package/dist/lib/kmsRecordCipher.d.ts +82 -0
- package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
- package/dist/lib/kmsRecordCipher.js +226 -0
- package/dist/lib/kmsRecordCipher.js.map +1 -0
- package/dist/lib/pagination.d.ts +19 -0
- package/dist/lib/pagination.d.ts.map +1 -0
- package/dist/lib/pagination.js +21 -0
- package/dist/lib/pagination.js.map +1 -0
- package/dist/lib/paths.d.ts +273 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +270 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/preconditions.d.ts +65 -0
- package/dist/lib/preconditions.d.ts.map +1 -0
- package/dist/lib/preconditions.js +112 -0
- package/dist/lib/preconditions.js.map +1 -0
- package/dist/lib/resourceFileName.d.ts +41 -0
- package/dist/lib/resourceFileName.d.ts.map +1 -0
- package/dist/lib/resourceFileName.js +58 -0
- package/dist/lib/resourceFileName.js.map +1 -0
- package/dist/lib/revocations.d.ts +49 -0
- package/dist/lib/revocations.d.ts.map +1 -0
- package/dist/lib/revocations.js +86 -0
- package/dist/lib/revocations.js.map +1 -0
- package/dist/lib/validateDid.d.ts +19 -0
- package/dist/lib/validateDid.d.ts.map +1 -0
- package/dist/lib/validateDid.js +35 -0
- package/dist/lib/validateDid.js.map +1 -0
- package/dist/lib/validateId.d.ts +66 -0
- package/dist/lib/validateId.d.ts.map +1 -0
- package/dist/lib/validateId.js +126 -0
- package/dist/lib/validateId.js.map +1 -0
- package/dist/plugin.d.ts +115 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +134 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +78 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/policy.js.map +1 -0
- package/dist/provisioning.d.ts +38 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +72 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/requests/BackendRequest.d.ts +61 -0
- package/dist/requests/BackendRequest.d.ts.map +1 -0
- package/dist/requests/BackendRequest.js +152 -0
- package/dist/requests/BackendRequest.js.map +1 -0
- package/dist/requests/CollectionRequest.d.ts +234 -0
- package/dist/requests/CollectionRequest.d.ts.map +1 -0
- package/dist/requests/CollectionRequest.js +705 -0
- package/dist/requests/CollectionRequest.js.map +1 -0
- package/dist/requests/KeyRequest.d.ts +115 -0
- package/dist/requests/KeyRequest.d.ts.map +1 -0
- package/dist/requests/KeyRequest.js +468 -0
- package/dist/requests/KeyRequest.js.map +1 -0
- package/dist/requests/KeystoreRequest.d.ts +99 -0
- package/dist/requests/KeystoreRequest.d.ts.map +1 -0
- package/dist/requests/KeystoreRequest.js +247 -0
- package/dist/requests/KeystoreRequest.js.map +1 -0
- package/dist/requests/PolicyRequest.d.ts +53 -0
- package/dist/requests/PolicyRequest.d.ts.map +1 -0
- package/dist/requests/PolicyRequest.js +124 -0
- package/dist/requests/PolicyRequest.js.map +1 -0
- package/dist/requests/ResourceRequest.d.ts +134 -0
- package/dist/requests/ResourceRequest.d.ts.map +1 -0
- package/dist/requests/ResourceRequest.js +548 -0
- package/dist/requests/ResourceRequest.js.map +1 -0
- package/dist/requests/RevocationRequest.d.ts +67 -0
- package/dist/requests/RevocationRequest.d.ts.map +1 -0
- package/dist/requests/RevocationRequest.js +187 -0
- package/dist/requests/RevocationRequest.js.map +1 -0
- package/dist/requests/SpaceRequest.d.ts +204 -0
- package/dist/requests/SpaceRequest.d.ts.map +1 -0
- package/dist/requests/SpaceRequest.js +520 -0
- package/dist/requests/SpaceRequest.js.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
- package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
- package/dist/requests/SpacesRepositoryRequest.js +159 -0
- package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
- package/dist/requests/collectionContext.d.ts +21 -0
- package/dist/requests/collectionContext.d.ts.map +1 -0
- package/dist/requests/collectionContext.js +30 -0
- package/dist/requests/collectionContext.js.map +1 -0
- package/dist/requests/controllerConsent.d.ts +58 -0
- package/dist/requests/controllerConsent.d.ts.map +1 -0
- package/dist/requests/controllerConsent.js +73 -0
- package/dist/requests/controllerConsent.js.map +1 -0
- package/dist/requests/keystoreContext.d.ts +68 -0
- package/dist/requests/keystoreContext.d.ts.map +1 -0
- package/dist/requests/keystoreContext.js +87 -0
- package/dist/requests/keystoreContext.js.map +1 -0
- package/dist/requests/resourceInput.d.ts +19 -0
- package/dist/requests/resourceInput.d.ts.map +1 -0
- package/dist/requests/resourceInput.js +102 -0
- package/dist/requests/resourceInput.js.map +1 -0
- package/dist/requests/spaceContext.d.ts +127 -0
- package/dist/requests/spaceContext.d.ts.map +1 -0
- package/dist/requests/spaceContext.js +201 -0
- package/dist/requests/spaceContext.js.map +1 -0
- package/dist/routes.d.ts +55 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +306 -0
- package/dist/routes.js.map +1 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +60 -0
- package/dist/server.js.map +1 -0
- package/dist/start.d.ts +7 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.js +61 -0
- package/dist/start.js.map +1 -0
- package/dist/storage.d.ts +29 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +47 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +915 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/views/home.hbs +9 -0
- package/dist/views/templates/main.hbs +12 -0
- package/dist/zcap.d.ts +267 -0
- package/dist/zcap.d.ts.map +1 -0
- package/dist/zcap.js +478 -0
- package/dist/zcap.js.map +1 -0
- package/package.json +109 -0
- package/src/auth-header-hooks.ts +163 -0
- package/src/authorize.ts +117 -0
- package/src/backends/filesystem.ts +3626 -0
- package/src/backends/postgres.ts +3073 -0
- package/src/backends/postgresSchema.ts +231 -0
- package/src/config.default.ts +657 -0
- package/src/corsProxy.ts +326 -0
- package/src/declarations.d.ts +28 -0
- package/src/digest.ts +223 -0
- package/src/errors.ts +1122 -0
- package/src/index.ts +21 -0
- package/src/lib/atomicFile.ts +176 -0
- package/src/lib/backendRegistry.ts +139 -0
- package/src/lib/backendUsage.ts +79 -0
- package/src/lib/backends.ts +351 -0
- package/src/lib/blindedIndex.ts +466 -0
- package/src/lib/collectionDescription.ts +43 -0
- package/src/lib/cursor.ts +52 -0
- package/src/lib/edvEnvelope.ts +120 -0
- package/src/lib/encryption.ts +472 -0
- package/src/lib/etag.ts +40 -0
- package/src/lib/exportManifest.ts +115 -0
- package/src/lib/importTar.ts +381 -0
- package/src/lib/isJson.ts +18 -0
- package/src/lib/keyEpoch.ts +89 -0
- package/src/lib/keyedMutex.ts +45 -0
- package/src/lib/kmsModule.ts +451 -0
- package/src/lib/kmsRecordCipher.ts +292 -0
- package/src/lib/pagination.ts +22 -0
- package/src/lib/paths.ts +382 -0
- package/src/lib/preconditions.ts +145 -0
- package/src/lib/resourceFileName.ts +69 -0
- package/src/lib/revocations.ts +116 -0
- package/src/lib/validateDid.ts +41 -0
- package/src/lib/validateId.ts +148 -0
- package/src/plugin.ts +288 -0
- package/src/policy.ts +176 -0
- package/src/provisioning.ts +98 -0
- package/src/requests/BackendRequest.ts +219 -0
- package/src/requests/CollectionRequest.ts +899 -0
- package/src/requests/KeyRequest.ts +617 -0
- package/src/requests/KeystoreRequest.ts +325 -0
- package/src/requests/PolicyRequest.ts +169 -0
- package/src/requests/ResourceRequest.ts +649 -0
- package/src/requests/RevocationRequest.ts +267 -0
- package/src/requests/SpaceRequest.ts +686 -0
- package/src/requests/SpacesRepositoryRequest.ts +194 -0
- package/src/requests/collectionContext.ts +41 -0
- package/src/requests/controllerConsent.ts +108 -0
- package/src/requests/keystoreContext.ts +124 -0
- package/src/requests/resourceInput.ts +118 -0
- package/src/requests/spaceContext.ts +311 -0
- package/src/routes.ts +420 -0
- package/src/server.ts +69 -0
- package/src/start.ts +71 -0
- package/src/storage.ts +60 -0
- package/src/types.ts +1016 -0
- package/src/views/home.hbs +9 -0
- package/src/views/templates/main.hbs +12 -0
- package/src/zcap.ts +657 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request handlers for the WebKMS keystore lifecycle (the `/kms` facet):
|
|
3
|
+
* - POST /kms/keystores (Create Keystore)
|
|
4
|
+
* - GET /kms/keystores?controller=<did> (List Keystores by controller)
|
|
5
|
+
* - GET /kms/keystores/:keystoreId (Get Keystore config)
|
|
6
|
+
* - POST /kms/keystores/:keystoreId (Update Keystore config)
|
|
7
|
+
*
|
|
8
|
+
* The wire contract is protocol-fixed by `@interop/webkms-client` (which is
|
|
9
|
+
* also the conformance suite for it): all routes are zcap-invoked with
|
|
10
|
+
* `read` / `write` actions (not HTTP verbs), the create response is the bare
|
|
11
|
+
* config (201 + `Location`), list wraps in `{ results }`, and update wraps in
|
|
12
|
+
* `{ config }`. The `meterId` / `ipAllowList` fields are deliberately dropped;
|
|
13
|
+
* the body schemas remain `additionalProperties: false`, so supplying them is
|
|
14
|
+
* rejected.
|
|
15
|
+
*/
|
|
16
|
+
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
17
|
+
import { generateId } from '@digitalcredentials/bnid'
|
|
18
|
+
import { handleZcapVerify } from '../zcap.js'
|
|
19
|
+
import { assertValidController } from '../lib/validateDid.js'
|
|
20
|
+
import { kmsKeystoresPath } from '../lib/paths.js'
|
|
21
|
+
import { fetchKeystoreAndVerify } from './keystoreContext.js'
|
|
22
|
+
import { verifyBodyControllerConsent } from './controllerConsent.js'
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_KMS_MODULE,
|
|
25
|
+
KEYSTORE_LIST_LIMIT,
|
|
26
|
+
KMS_MAX_CHAIN_LENGTH,
|
|
27
|
+
KMS_MAX_DELEGATION_TTL
|
|
28
|
+
} from '../config.default.js'
|
|
29
|
+
import {
|
|
30
|
+
InvalidRequestBodyError,
|
|
31
|
+
KeystoreControllerMismatchError
|
|
32
|
+
} from '../errors.js'
|
|
33
|
+
import type { IDID, KeystoreConfig } from '../types.js'
|
|
34
|
+
|
|
35
|
+
/** The keystore config fields a client may supply (create and update). */
|
|
36
|
+
interface KeystoreConfigBody {
|
|
37
|
+
id?: string
|
|
38
|
+
controller: IDID
|
|
39
|
+
sequence: number
|
|
40
|
+
kmsModule?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Validates a keystore config request body against the webkms schema shape
|
|
45
|
+
* (minus the dropped `meterId` / `ipAllowList`): required fields present, no
|
|
46
|
+
* unknown fields (`additionalProperties: false`), `controller` a valid
|
|
47
|
+
* did:key, `sequence` a non-negative integer -- exactly 0 on create; the
|
|
48
|
+
* update path's previous+1 gate is the storage layer's, where it is atomic.
|
|
49
|
+
* Throws `InvalidRequestBodyError` (400).
|
|
50
|
+
*
|
|
51
|
+
* @param options {object}
|
|
52
|
+
* @param options.body {unknown} the parsed request body
|
|
53
|
+
* @param options.requestName {string} request name used in error titles
|
|
54
|
+
* @param [options.isUpdate] {boolean} validate the update shape (requires
|
|
55
|
+
* `id`, allows any non-negative `sequence`)
|
|
56
|
+
* @returns {KeystoreConfigBody} the body, narrowed
|
|
57
|
+
*/
|
|
58
|
+
function assertKeystoreConfigBody({
|
|
59
|
+
body,
|
|
60
|
+
requestName,
|
|
61
|
+
isUpdate = false
|
|
62
|
+
}: {
|
|
63
|
+
body: unknown
|
|
64
|
+
requestName: string
|
|
65
|
+
isUpdate?: boolean
|
|
66
|
+
}): KeystoreConfigBody {
|
|
67
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body)) {
|
|
68
|
+
throw new InvalidRequestBodyError({
|
|
69
|
+
requestName,
|
|
70
|
+
detail: 'Keystore config body must be a JSON object.'
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
const allowedKeys = isUpdate
|
|
74
|
+
? ['id', 'controller', 'sequence', 'kmsModule']
|
|
75
|
+
: ['controller', 'sequence', 'kmsModule']
|
|
76
|
+
for (const key of Object.keys(body)) {
|
|
77
|
+
if (!allowedKeys.includes(key)) {
|
|
78
|
+
throw new InvalidRequestBodyError({
|
|
79
|
+
requestName,
|
|
80
|
+
detail: `Unexpected keystore config property "${key}".`,
|
|
81
|
+
pointer: `#/${key}`
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const config = body as Partial<KeystoreConfigBody>
|
|
86
|
+
assertValidController(config.controller, { requestName })
|
|
87
|
+
if (
|
|
88
|
+
!Number.isInteger(config.sequence) ||
|
|
89
|
+
(config.sequence as number) < 0 ||
|
|
90
|
+
(config.sequence as number) > Number.MAX_SAFE_INTEGER - 1
|
|
91
|
+
) {
|
|
92
|
+
throw new InvalidRequestBodyError({
|
|
93
|
+
requestName,
|
|
94
|
+
detail: 'Keystore config "sequence" must be a non-negative integer.',
|
|
95
|
+
pointer: '#/sequence'
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
if (!isUpdate && config.sequence !== 0) {
|
|
99
|
+
throw new InvalidRequestBodyError({
|
|
100
|
+
requestName,
|
|
101
|
+
detail: 'Keystore config sequence must be "0" on create.',
|
|
102
|
+
pointer: '#/sequence'
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
if (isUpdate && typeof config.id !== 'string') {
|
|
106
|
+
throw new InvalidRequestBodyError({
|
|
107
|
+
requestName,
|
|
108
|
+
detail: 'Keystore config "id" is required.',
|
|
109
|
+
pointer: '#/id'
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
config.kmsModule !== undefined &&
|
|
114
|
+
(typeof config.kmsModule !== 'string' || config.kmsModule.length === 0)
|
|
115
|
+
) {
|
|
116
|
+
throw new InvalidRequestBodyError({
|
|
117
|
+
requestName,
|
|
118
|
+
detail: 'Keystore config "kmsModule" must be a non-empty string.',
|
|
119
|
+
pointer: '#/kmsModule'
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
return config as KeystoreConfigBody
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class KeystoreRequest {
|
|
126
|
+
/**
|
|
127
|
+
* POST /kms/keystores
|
|
128
|
+
* Create Keystore. Authorization is the keystore-creation bootstrap rule,
|
|
129
|
+
* mirroring Create Space (`SpacesRepositoryRequest.post`): the invocation
|
|
130
|
+
* must be *authorized by* the `controller` DID in the request body -- signed
|
|
131
|
+
* directly by it, or via a delegation chain rooted in it -- so anyone can
|
|
132
|
+
* create their own keystore by proving control of the DID they name.
|
|
133
|
+
* Responds 201 with `Location` and the bare config -- the client asserts a
|
|
134
|
+
* string `id` on it.
|
|
135
|
+
*
|
|
136
|
+
* @param request {import('fastify').FastifyRequest}
|
|
137
|
+
* @param reply {import('fastify').FastifyReply}
|
|
138
|
+
* @returns {Promise<FastifyReply>}
|
|
139
|
+
*/
|
|
140
|
+
static async post(
|
|
141
|
+
request: FastifyRequest<{ Body: KeystoreConfigBody }>,
|
|
142
|
+
reply: FastifyReply
|
|
143
|
+
): Promise<FastifyReply> {
|
|
144
|
+
const requestName = 'Create Keystore'
|
|
145
|
+
const { body } = request
|
|
146
|
+
const { serverUrl, storage } = request.server
|
|
147
|
+
|
|
148
|
+
assertKeystoreConfigBody({ body, requestName })
|
|
149
|
+
|
|
150
|
+
// The invocation must be *authorized by* the body's controller (see
|
|
151
|
+
// `verifyBodyControllerConsent`). Skipped when the provisioning policy
|
|
152
|
+
// already vouched for the request (e.g. a valid onboarding token).
|
|
153
|
+
if (!request.provisioningAuthorized) {
|
|
154
|
+
await verifyBodyControllerConsent({
|
|
155
|
+
request,
|
|
156
|
+
controller: body.controller,
|
|
157
|
+
allowedTarget: new URL(kmsKeystoresPath(), serverUrl).toString(),
|
|
158
|
+
allowedAction: 'write',
|
|
159
|
+
MismatchError: KeystoreControllerMismatchError,
|
|
160
|
+
requestName,
|
|
161
|
+
maxChainLength: KMS_MAX_CHAIN_LENGTH,
|
|
162
|
+
maxDelegationTtl: KMS_MAX_DELEGATION_TTL
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Server-generated local id, per webkms-switch `generateRandom`: multibase
|
|
167
|
+
// base58btc of a multihash-framed (identity, 16-byte) 128-bit random value.
|
|
168
|
+
const keystoreId = await generateId({ multihash: true })
|
|
169
|
+
const keystoreUrl = new URL(
|
|
170
|
+
kmsKeystoresPath({ keystoreId }),
|
|
171
|
+
serverUrl
|
|
172
|
+
).toString()
|
|
173
|
+
const config: KeystoreConfig = {
|
|
174
|
+
id: keystoreUrl,
|
|
175
|
+
controller: body.controller,
|
|
176
|
+
sequence: 0,
|
|
177
|
+
kmsModule: body.kmsModule ?? DEFAULT_KMS_MODULE
|
|
178
|
+
}
|
|
179
|
+
await storage.writeKeystore({ keystoreId, config })
|
|
180
|
+
|
|
181
|
+
reply.header('Location', keystoreUrl)
|
|
182
|
+
return reply.status(201).send(config)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* GET /kms/keystores?controller=<did>
|
|
187
|
+
* List Keystores by controller. The root capability for `/kms/keystores` is
|
|
188
|
+
* synthesized with the `controller` *query parameter* as its controller, so
|
|
189
|
+
* a caller can only list their own keystores (a failed verification is the
|
|
190
|
+
* masked 404, per the server-wide convention). The query string rides the
|
|
191
|
+
* request URL, so verification runs in target-attenuation mode (the same
|
|
192
|
+
* `allowTargetQuery` treatment as List Collection pagination). Responds
|
|
193
|
+
* `{ results }`, capped at `KEYSTORE_LIST_LIMIT`.
|
|
194
|
+
*
|
|
195
|
+
* @param request {import('fastify').FastifyRequest}
|
|
196
|
+
* @param reply {import('fastify').FastifyReply}
|
|
197
|
+
* @returns {Promise<FastifyReply>}
|
|
198
|
+
*/
|
|
199
|
+
static async list(
|
|
200
|
+
request: FastifyRequest<{ Querystring: { controller?: string } }>,
|
|
201
|
+
reply: FastifyReply
|
|
202
|
+
): Promise<FastifyReply> {
|
|
203
|
+
const requestName = 'List Keystores'
|
|
204
|
+
const { url, method, headers, query } = request
|
|
205
|
+
const { serverUrl, storage } = request.server
|
|
206
|
+
|
|
207
|
+
// The list query schema: `controller` required, no other parameters.
|
|
208
|
+
// The controller names whose keystores are being asked for
|
|
209
|
+
// (and thereby the root controller the invocation must verify against).
|
|
210
|
+
for (const key of Object.keys(query)) {
|
|
211
|
+
if (key !== 'controller') {
|
|
212
|
+
throw new InvalidRequestBodyError({
|
|
213
|
+
requestName,
|
|
214
|
+
detail: `Unexpected query parameter "${key}".`
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
assertValidController(query.controller, { requestName })
|
|
219
|
+
const controller = query.controller as IDID
|
|
220
|
+
|
|
221
|
+
const allowedTarget = new URL(kmsKeystoresPath(), serverUrl).toString()
|
|
222
|
+
await handleZcapVerify({
|
|
223
|
+
url,
|
|
224
|
+
allowedTarget,
|
|
225
|
+
allowedAction: 'read',
|
|
226
|
+
method,
|
|
227
|
+
headers,
|
|
228
|
+
serverUrl,
|
|
229
|
+
spaceController: controller,
|
|
230
|
+
requestName,
|
|
231
|
+
logger: request.log,
|
|
232
|
+
allowTargetQuery: true,
|
|
233
|
+
// The chain roots at the keystores-collection URL (keyed by the query
|
|
234
|
+
// `controller`), not at any one keystore, so no RevocationScope exists
|
|
235
|
+
// for it -- collection-level capabilities are outside the per-scope
|
|
236
|
+
// revocation model.
|
|
237
|
+
revocation: 'no-revocation-scope',
|
|
238
|
+
maxChainLength: KMS_MAX_CHAIN_LENGTH,
|
|
239
|
+
maxDelegationTtl: KMS_MAX_DELEGATION_TTL
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const configs = await storage.listKeystoresByController({ controller })
|
|
243
|
+
return reply.send({ results: configs.slice(0, KEYSTORE_LIST_LIMIT) })
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* GET /kms/keystores/:keystoreId
|
|
248
|
+
* Get Keystore config: capability-verified against the stored config's
|
|
249
|
+
* controller (`read`), responding with the bare config.
|
|
250
|
+
*
|
|
251
|
+
* @param request {import('fastify').FastifyRequest}
|
|
252
|
+
* @param reply {import('fastify').FastifyReply}
|
|
253
|
+
* @returns {Promise<FastifyReply>}
|
|
254
|
+
*/
|
|
255
|
+
static async get(
|
|
256
|
+
request: FastifyRequest<{ Params: { keystoreId: string } }>,
|
|
257
|
+
reply: FastifyReply
|
|
258
|
+
): Promise<FastifyReply> {
|
|
259
|
+
const { config } = await fetchKeystoreAndVerify({
|
|
260
|
+
request,
|
|
261
|
+
keystoreId: request.params.keystoreId,
|
|
262
|
+
allowedAction: 'read',
|
|
263
|
+
requestName: 'Get Keystore'
|
|
264
|
+
})
|
|
265
|
+
return reply.send(config)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* POST /kms/keystores/:keystoreId
|
|
270
|
+
* Update Keystore config. Verified against the *stored* config's controller
|
|
271
|
+
* (`write`) -- so a controller change is authorized by the current
|
|
272
|
+
* controller and takes effect immediately. The body's `id` must match the
|
|
273
|
+
* keystore URL (400); the storage layer applies the update atomically iff
|
|
274
|
+
* `sequence` is exactly previous+1 and `kmsModule` is unchanged (409
|
|
275
|
+
* `KeystoreStateConflictError` otherwise; an omitted `kmsModule` defaults to
|
|
276
|
+
* the stored one). Responds `{ config }` -- note the
|
|
277
|
+
* wrapper, unlike get/create.
|
|
278
|
+
*
|
|
279
|
+
* @param request {import('fastify').FastifyRequest}
|
|
280
|
+
* @param reply {import('fastify').FastifyReply}
|
|
281
|
+
* @returns {Promise<FastifyReply>}
|
|
282
|
+
*/
|
|
283
|
+
static async update(
|
|
284
|
+
request: FastifyRequest<{
|
|
285
|
+
Params: { keystoreId: string }
|
|
286
|
+
Body: KeystoreConfigBody
|
|
287
|
+
}>,
|
|
288
|
+
reply: FastifyReply
|
|
289
|
+
): Promise<FastifyReply> {
|
|
290
|
+
const requestName = 'Update Keystore'
|
|
291
|
+
const { body } = request
|
|
292
|
+
const { storage } = request.server
|
|
293
|
+
const { keystoreId } = request.params
|
|
294
|
+
|
|
295
|
+
assertKeystoreConfigBody({ body, requestName, isUpdate: true })
|
|
296
|
+
|
|
297
|
+
const { config: existing } = await fetchKeystoreAndVerify({
|
|
298
|
+
request,
|
|
299
|
+
keystoreId,
|
|
300
|
+
allowedAction: 'write',
|
|
301
|
+
requestName
|
|
302
|
+
})
|
|
303
|
+
// The stored `id` is the keystore's full URL; the submitted config must
|
|
304
|
+
// name the same keystore it is posted to.
|
|
305
|
+
if (body.id !== existing.id) {
|
|
306
|
+
throw new InvalidRequestBodyError({
|
|
307
|
+
requestName,
|
|
308
|
+
detail: 'Configuration "id" does not match request URL.',
|
|
309
|
+
pointer: '#/id'
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const config: KeystoreConfig = {
|
|
314
|
+
id: existing.id,
|
|
315
|
+
controller: body.controller,
|
|
316
|
+
sequence: body.sequence,
|
|
317
|
+
kmsModule: body.kmsModule ?? existing.kmsModule
|
|
318
|
+
}
|
|
319
|
+
// Throws the merged 409 state conflict on a sequence / kmsModule mismatch,
|
|
320
|
+
// atomically with the write.
|
|
321
|
+
await storage.updateKeystore({ keystoreId, config })
|
|
322
|
+
|
|
323
|
+
return reply.send({ config })
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request handlers for access-control policy operations: get/update/delete the
|
|
3
|
+
* `policy` auxiliary resource at the Space, Collection, or Resource level (the
|
|
4
|
+
* level is selected by which path params are present). Reading or modifying a
|
|
5
|
+
* policy is privileged: every operation verifies a capability invocation against
|
|
6
|
+
* the Space controller (the read-method relaxation in auth-header-hooks.ts does
|
|
7
|
+
* not apply here -- a policy is controller-managed metadata, not public data).
|
|
8
|
+
*/
|
|
9
|
+
import type { FastifyReply, FastifyRequest } from 'fastify'
|
|
10
|
+
import { fetchSpaceAndVerify } from './spaceContext.js'
|
|
11
|
+
import { assertValidIds } from '../lib/validateId.js'
|
|
12
|
+
import { policyPath } from '../lib/paths.js'
|
|
13
|
+
import {
|
|
14
|
+
InvalidPolicyError,
|
|
15
|
+
MissingAuthError,
|
|
16
|
+
PolicyNotFoundError
|
|
17
|
+
} from '../errors.js'
|
|
18
|
+
import type { PolicyDocument } from '../types.js'
|
|
19
|
+
|
|
20
|
+
/** Path params shared by the three policy route shapes. */
|
|
21
|
+
interface PolicyParams {
|
|
22
|
+
spaceId: string
|
|
23
|
+
collectionId?: string
|
|
24
|
+
resourceId?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Asserts the request carries auth headers, throwing MissingAuthError (401)
|
|
29
|
+
* otherwise. Needed for the GET handler, which the `requireAuthHeaders` hook
|
|
30
|
+
* lets through unauthenticated (it is a safe method); PUT/DELETE are already
|
|
31
|
+
* gated by the hook.
|
|
32
|
+
* @param request {FastifyRequest}
|
|
33
|
+
* @returns {void}
|
|
34
|
+
*/
|
|
35
|
+
function requireAuth(request: FastifyRequest): void {
|
|
36
|
+
const { headers } = request
|
|
37
|
+
if (!(headers.authorization && headers['capability-invocation'])) {
|
|
38
|
+
throw new MissingAuthError()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class PolicyRequest {
|
|
43
|
+
/**
|
|
44
|
+
* GET /space/:spaceId[/:collectionId[/:resourceId]]/policy
|
|
45
|
+
* Read the access-control policy document set at this level.
|
|
46
|
+
*
|
|
47
|
+
* @param request {import('fastify').FastifyRequest}
|
|
48
|
+
* @param reply {import('fastify').FastifyReply}
|
|
49
|
+
* @returns {Promise<FastifyReply>}
|
|
50
|
+
*/
|
|
51
|
+
static async get(
|
|
52
|
+
request: FastifyRequest<{ Params: PolicyParams }>,
|
|
53
|
+
reply: FastifyReply
|
|
54
|
+
): Promise<FastifyReply> {
|
|
55
|
+
const { spaceId, collectionId, resourceId } = request.params
|
|
56
|
+
const { storage } = request.server
|
|
57
|
+
const requestName = 'Get Policy'
|
|
58
|
+
|
|
59
|
+
assertValidIds({ spaceId, collectionId, resourceId }, { requestName })
|
|
60
|
+
requireAuth(request)
|
|
61
|
+
|
|
62
|
+
// Verify (capability-only): a policy is controller-managed metadata, so
|
|
63
|
+
// reading it requires a valid capability invocation -- no policy fallback.
|
|
64
|
+
await fetchSpaceAndVerify({
|
|
65
|
+
request,
|
|
66
|
+
spaceId,
|
|
67
|
+
targetPath: policyPath({ spaceId, collectionId, resourceId }),
|
|
68
|
+
requestName
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const policy = await storage.getPolicy({
|
|
72
|
+
spaceId,
|
|
73
|
+
collectionId,
|
|
74
|
+
resourceId
|
|
75
|
+
})
|
|
76
|
+
if (!policy) {
|
|
77
|
+
throw new PolicyNotFoundError({ requestName })
|
|
78
|
+
}
|
|
79
|
+
return reply
|
|
80
|
+
.status(200)
|
|
81
|
+
.type('application/json')
|
|
82
|
+
.send(JSON.stringify(policy))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* PUT /space/:spaceId[/:collectionId[/:resourceId]]/policy
|
|
87
|
+
* Create or replace the access-control policy document at this level.
|
|
88
|
+
*
|
|
89
|
+
* @param request {import('fastify').FastifyRequest}
|
|
90
|
+
* @param reply {import('fastify').FastifyReply}
|
|
91
|
+
* @returns {Promise<FastifyReply>}
|
|
92
|
+
*/
|
|
93
|
+
static async put(
|
|
94
|
+
request: FastifyRequest<{ Params: PolicyParams; Body: unknown }>,
|
|
95
|
+
reply: FastifyReply
|
|
96
|
+
): Promise<FastifyReply> {
|
|
97
|
+
const { spaceId, collectionId, resourceId } = request.params
|
|
98
|
+
const { body } = request
|
|
99
|
+
const { storage } = request.server
|
|
100
|
+
const requestName = 'Update Policy'
|
|
101
|
+
|
|
102
|
+
assertValidIds({ spaceId, collectionId, resourceId }, { requestName })
|
|
103
|
+
|
|
104
|
+
// A policy document must be a JSON object carrying a non-empty string
|
|
105
|
+
// `type`. The set of recognized types is intentionally open (unknown types
|
|
106
|
+
// are stored and fail-closed at evaluation time, see policy.ts), so this is
|
|
107
|
+
// a shape check only -- it does not gate on a known-types allowlist.
|
|
108
|
+
const policyType =
|
|
109
|
+
typeof body === 'object' && body !== null
|
|
110
|
+
? (body as Record<string, unknown>).type
|
|
111
|
+
: undefined
|
|
112
|
+
if (typeof policyType !== 'string' || policyType.trim() === '') {
|
|
113
|
+
throw new InvalidPolicyError({ requestName })
|
|
114
|
+
}
|
|
115
|
+
const policy = body as PolicyDocument
|
|
116
|
+
|
|
117
|
+
// Verify (capability-only): a policy is controller-managed metadata, so
|
|
118
|
+
// writing it requires a valid capability invocation -- no policy fallback.
|
|
119
|
+
const { allowedTarget: policyUrl } = await fetchSpaceAndVerify({
|
|
120
|
+
request,
|
|
121
|
+
spaceId,
|
|
122
|
+
targetPath: policyPath({ spaceId, collectionId, resourceId }),
|
|
123
|
+
requestName
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const existing = await storage.getPolicy({
|
|
127
|
+
spaceId,
|
|
128
|
+
collectionId,
|
|
129
|
+
resourceId
|
|
130
|
+
})
|
|
131
|
+
await storage.writePolicy({ spaceId, collectionId, resourceId, policy })
|
|
132
|
+
|
|
133
|
+
reply.header('Location', policyUrl)
|
|
134
|
+
return existing
|
|
135
|
+
? reply.status(204).send() // update
|
|
136
|
+
: reply.status(201).send(policy) // create
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* DELETE /space/:spaceId[/:collectionId[/:resourceId]]/policy
|
|
141
|
+
* Remove the access-control policy document at this level (idempotent).
|
|
142
|
+
*
|
|
143
|
+
* @param request {import('fastify').FastifyRequest}
|
|
144
|
+
* @param reply {import('fastify').FastifyReply}
|
|
145
|
+
* @returns {Promise<FastifyReply>}
|
|
146
|
+
*/
|
|
147
|
+
static async delete(
|
|
148
|
+
request: FastifyRequest<{ Params: PolicyParams }>,
|
|
149
|
+
reply: FastifyReply
|
|
150
|
+
): Promise<FastifyReply> {
|
|
151
|
+
const { spaceId, collectionId, resourceId } = request.params
|
|
152
|
+
const { storage } = request.server
|
|
153
|
+
const requestName = 'Delete Policy'
|
|
154
|
+
|
|
155
|
+
assertValidIds({ spaceId, collectionId, resourceId }, { requestName })
|
|
156
|
+
|
|
157
|
+
// Verify (capability-only): a policy is controller-managed metadata, so
|
|
158
|
+
// deleting it requires a valid capability invocation -- no policy fallback.
|
|
159
|
+
await fetchSpaceAndVerify({
|
|
160
|
+
request,
|
|
161
|
+
spaceId,
|
|
162
|
+
targetPath: policyPath({ spaceId, collectionId, resourceId }),
|
|
163
|
+
requestName
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await storage.deletePolicy({ spaceId, collectionId, resourceId })
|
|
167
|
+
return reply.status(204).send()
|
|
168
|
+
}
|
|
169
|
+
}
|