was-teaching-server 0.9.1

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