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,267 @@
1
+ /**
2
+ * Request handlers for zcap revocation, one per route family:
3
+ * - POST /kms/keystores/:keystoreId/zcaps/revocations/:revocationId
4
+ * - POST /space/:spaceId/zcaps/revocations/:revocationId
5
+ *
6
+ * Both share one submission flow (`submitRevocation`), differing only in the
7
+ * scope the revocation aggregates under -- a keystore or a Space -- the action
8
+ * the invocation must carry (the webkms `write` vs the WAS route families'
9
+ * HTTP verb), and the `/kms` delegation policy the WAS families do not impose.
10
+ *
11
+ * The wire contract is protocol-fixed by ezcap-express's
12
+ * `authorizeZcapRevocation` / `@interop/webkms-client` (the conformance
13
+ * suite): `:revocationId` is the to-be-revoked capability's id, URL-encoded;
14
+ * the body is that capability, verbatim; success is 204 with no body. The
15
+ * submission is authorized under the dual-root rule -- an invocation rooted
16
+ * in the scope, or in the revocation URL itself, whose synthesized root is
17
+ * controlled by every controller in the to-be-revoked capability's (fully
18
+ * verified) chain -- so a delegee can revoke its own zcap without holding a
19
+ * separate capability. Root zcaps cannot be revoked. The stored revocation is
20
+ * consulted by the chain-inspection hook on every subsequent verification
21
+ * rooted in that scope (`lib/revocations.ts`).
22
+ */
23
+ import type { FastifyReply, FastifyRequest } from 'fastify'
24
+ import {
25
+ handleRevocationInvocationVerify,
26
+ verifyRevocationChain
27
+ } from '../zcap.js'
28
+ import { revocationChainInspector } from '../lib/revocations.js'
29
+ import { spacePath } from '../lib/paths.js'
30
+ import {
31
+ KMS_MAX_CHAIN_LENGTH,
32
+ KMS_MAX_DELEGATION_TTL
33
+ } from '../config.default.js'
34
+ import { InvalidRevocationError } from '../errors.js'
35
+ import type { IDID, RevocationRecord, RevocationScope } from '../types.js'
36
+ import { fetchKeystore } from './keystoreContext.js'
37
+ import { fetchSpace } from './spaceContext.js'
38
+
39
+ /** One day in milliseconds -- the revocation record's GC margin. */
40
+ const ONE_DAY = 24 * 60 * 60 * 1000
41
+
42
+ /**
43
+ * The scope-agnostic revocation submission: validate the body capability,
44
+ * verify its delegation chain (it must root in `rootTarget`), which yields
45
+ * the chain's controllers for the dual-root invocation check, then -- only
46
+ * once the invocation is authorized -- reject a chain containing an
47
+ * already-revoked link (resubmissions included) with the 400
48
+ * `InvalidRevocationError`, per ezcap-express, and store the record. The
49
+ * store check runs strictly AFTER the 404-masking authorization so an
50
+ * unauthorized caller cannot probe whether a capability is revoked (a
51
+ * 400-vs-404 oracle otherwise). The record expires one day after the
52
+ * capability itself does (from then on the capability is rejected on expiry
53
+ * alone; the margin covers clock-skew grace periods).
54
+ *
55
+ * @param options {object}
56
+ * @param options.request {FastifyRequest} supplies url, method, headers,
57
+ * logger, and `request.server` for serverUrl + storage
58
+ * @param options.revocationId {string} the URL's revocation id (the
59
+ * to-be-revoked capability's id, already URL-decoded by the router)
60
+ * @param options.scope {RevocationScope} the keystore or Space the record is
61
+ * stored under
62
+ * @param options.rootTarget {string} the scope's full URL (the required root
63
+ * of the revoked capability's chain, and one of the two accepted invocation
64
+ * roots)
65
+ * @param options.rootController {IDID} the scope's controller
66
+ * @param options.expectedAction {string} the action the invocation must
67
+ * carry (`write` on `/kms`, `POST` on the WAS route families)
68
+ * @param options.requestName {string} human-readable request name, used in
69
+ * error titles
70
+ * @param [options.maxChainLength] {number} max chain length, root included
71
+ * @param [options.maxDelegationTtl] {number} max delegated-zcap TTL (ms)
72
+ * @returns {Promise<void>}
73
+ */
74
+ async function submitRevocation({
75
+ request,
76
+ revocationId,
77
+ scope,
78
+ rootTarget,
79
+ rootController,
80
+ expectedAction,
81
+ requestName,
82
+ maxChainLength,
83
+ maxDelegationTtl
84
+ }: {
85
+ request: FastifyRequest
86
+ revocationId: string
87
+ scope: RevocationScope
88
+ rootTarget: string
89
+ rootController: IDID
90
+ expectedAction: string
91
+ requestName: string
92
+ maxChainLength?: number
93
+ maxDelegationTtl?: number
94
+ }): Promise<void> {
95
+ const { url, method, headers, body } = request
96
+ const { serverUrl, storage } = request.server
97
+
98
+ if (
99
+ typeof body !== 'object' ||
100
+ body === null ||
101
+ Array.isArray(body) ||
102
+ typeof (body as Record<string, unknown>).id !== 'string'
103
+ ) {
104
+ throw new InvalidRevocationError({
105
+ detail: 'The revocation body must be a capability with a string "id".'
106
+ })
107
+ }
108
+ const capabilityBody = body as Record<string, unknown> & { id: string }
109
+ if (capabilityBody.id.startsWith('urn:zcap:root:')) {
110
+ throw new InvalidRevocationError({
111
+ detail: 'A root capability cannot be revoked.'
112
+ })
113
+ }
114
+ // The submitted capability must be the one the URL names (the client
115
+ // frames the id with `encodeURIComponent` into the final path segment).
116
+ if (capabilityBody.id !== revocationId) {
117
+ throw new InvalidRevocationError({
118
+ detail: 'The capability "id" does not match the revocation URL.'
119
+ })
120
+ }
121
+
122
+ // Verify the to-be-revoked capability's own delegation chain (400 when
123
+ // invalid); its controllers feed the dual-root invocation check below.
124
+ // Structural only -- the revocation store is not consulted until after
125
+ // authorization (see below).
126
+ const { delegator, chainControllers, capabilities } =
127
+ await verifyRevocationChain({
128
+ capability: capabilityBody,
129
+ rootTarget,
130
+ rootController,
131
+ maxChainLength,
132
+ maxDelegationTtl
133
+ })
134
+
135
+ await handleRevocationInvocationVerify({
136
+ url,
137
+ method,
138
+ headers,
139
+ serverUrl,
140
+ rootTarget,
141
+ rootController,
142
+ chainControllers,
143
+ expectedAction,
144
+ // The *invoking* chain is checked against the store as on every other
145
+ // route: a revoked capability cannot authorize a revocation.
146
+ inspectCapabilityChain: revocationChainInspector({ storage, scope }),
147
+ maxChainLength,
148
+ maxDelegationTtl,
149
+ requestName,
150
+ logger: request.log
151
+ })
152
+
153
+ // The caller is authorized; NOW consult the store for the to-be-revoked
154
+ // chain. A chain containing an already-revoked link (resubmissions
155
+ // included) is the 400, per ezcap-express; the 409 duplicate stays
156
+ // reserved for a write race at the store. Running this after the masked
157
+ // authorization keeps revocation state undisclosed to unauthorized callers.
158
+ if (await storage.isRevoked({ scope, capabilities })) {
159
+ throw new InvalidRevocationError({
160
+ detail:
161
+ 'The capability (or a capability in its chain) is already revoked.'
162
+ })
163
+ }
164
+
165
+ const capability = capabilityBody as RevocationRecord['capability']
166
+ // Compute the record's GC expiry only from a parseable `expires`; an
167
+ // unparseable one yields `NaN`, and `new Date(NaN).toISOString()` would
168
+ // throw a `RangeError` (500). Omitting `expires` here just drops the GC
169
+ // margin -- the capability is still rejected on its own expiry.
170
+ const expiresMs = capability.expires ? Date.parse(capability.expires) : NaN
171
+ const record: RevocationRecord = {
172
+ capability,
173
+ meta: {
174
+ delegator,
175
+ rootTarget,
176
+ created: new Date().toISOString(),
177
+ ...(Number.isFinite(expiresMs) && {
178
+ expires: new Date(expiresMs + ONE_DAY).toISOString()
179
+ })
180
+ }
181
+ }
182
+ await storage.insertRevocation({ scope, record })
183
+ }
184
+
185
+ export class RevocationRequest {
186
+ /**
187
+ * POST /kms/keystores/:keystoreId/zcaps/revocations/:revocationId
188
+ * Revoke a capability delegated from a keystore. Carries the unified `/kms`
189
+ * delegation policy, and the webkms `write` action. Responds 204, no body; a
190
+ * concurrent duplicate insert is the 409 `DuplicateRevocationError`.
191
+ *
192
+ * @param request {import('fastify').FastifyRequest}
193
+ * @param reply {import('fastify').FastifyReply}
194
+ * @returns {Promise<FastifyReply>}
195
+ */
196
+ static async post(
197
+ request: FastifyRequest<{
198
+ Params: { keystoreId: string; revocationId: string }
199
+ Body: Record<string, unknown>
200
+ }>,
201
+ reply: FastifyReply
202
+ ): Promise<FastifyReply> {
203
+ const requestName = 'Revoke Capability'
204
+ const { keystoreId, revocationId } = request.params
205
+
206
+ // 404-masks an unknown keystore before any verification work.
207
+ const config = await fetchKeystore({ request, keystoreId, requestName })
208
+
209
+ await submitRevocation({
210
+ request,
211
+ revocationId,
212
+ scope: { keystoreId },
213
+ rootTarget: config.id,
214
+ rootController: config.controller,
215
+ expectedAction: 'write',
216
+ requestName,
217
+ maxChainLength: KMS_MAX_CHAIN_LENGTH,
218
+ maxDelegationTtl: KMS_MAX_DELEGATION_TTL
219
+ })
220
+
221
+ return reply.status(204).send()
222
+ }
223
+
224
+ /**
225
+ * POST /space/:spaceId/zcaps/revocations/:revocationId
226
+ * Revoke a capability delegated from a Space -- the WAS-route sibling of
227
+ * `post`, scoped to the Space rather than a keystore. The revoked capability
228
+ * is rejected from then on wherever a Space-rooted chain is verified: the
229
+ * write/privileged routes (`fetchSpaceAndVerify`) and the capability leg of
230
+ * the read routes (`authorize`). The invoked action is the HTTP verb, since
231
+ * WAS capabilities are scoped by HTTP method. No delegation-policy caps
232
+ * apply, matching the rest of the WAS route families.
233
+ *
234
+ * Responds 204, no body; a concurrent duplicate insert is the 409
235
+ * `DuplicateRevocationError`.
236
+ *
237
+ * @param request {import('fastify').FastifyRequest}
238
+ * @param reply {import('fastify').FastifyReply}
239
+ * @returns {Promise<FastifyReply>}
240
+ */
241
+ static async postSpace(
242
+ request: FastifyRequest<{
243
+ Params: { spaceId: string; revocationId: string }
244
+ Body: Record<string, unknown>
245
+ }>,
246
+ reply: FastifyReply
247
+ ): Promise<FastifyReply> {
248
+ const requestName = 'Revoke Capability'
249
+ const { spaceId, revocationId } = request.params
250
+ const { serverUrl } = request.server
251
+
252
+ // 404-masks an unknown Space before any verification work.
253
+ const spaceDescription = await fetchSpace({ request, spaceId, requestName })
254
+
255
+ await submitRevocation({
256
+ request,
257
+ revocationId,
258
+ scope: { spaceId },
259
+ rootTarget: new URL(spacePath({ spaceId }), serverUrl).toString(),
260
+ rootController: spaceDescription.controller,
261
+ expectedAction: request.method,
262
+ requestName
263
+ })
264
+
265
+ return reply.status(204).send()
266
+ }
267
+ }