was-teaching-server 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/CHANGELOG.md +1432 -0
  2. package/LICENSE +661 -0
  3. package/README.md +305 -0
  4. package/common/css/materialize.1.0.0.min.css +13 -0
  5. package/dist/auth-header-hooks.d.ts +84 -0
  6. package/dist/auth-header-hooks.d.ts.map +1 -0
  7. package/dist/auth-header-hooks.js +136 -0
  8. package/dist/auth-header-hooks.js.map +1 -0
  9. package/dist/authorize.d.ts +45 -0
  10. package/dist/authorize.d.ts.map +1 -0
  11. package/dist/authorize.js +76 -0
  12. package/dist/authorize.js.map +1 -0
  13. package/dist/backends/filesystem.d.ts +1179 -0
  14. package/dist/backends/filesystem.d.ts.map +1 -0
  15. package/dist/backends/filesystem.js +2727 -0
  16. package/dist/backends/filesystem.js.map +1 -0
  17. package/dist/backends/postgres.d.ts +833 -0
  18. package/dist/backends/postgres.d.ts.map +1 -0
  19. package/dist/backends/postgres.js +2164 -0
  20. package/dist/backends/postgres.js.map +1 -0
  21. package/dist/backends/postgresSchema.d.ts +36 -0
  22. package/dist/backends/postgresSchema.d.ts.map +1 -0
  23. package/dist/backends/postgresSchema.js +203 -0
  24. package/dist/backends/postgresSchema.js.map +1 -0
  25. package/dist/config.default.d.ts +360 -0
  26. package/dist/config.default.d.ts.map +1 -0
  27. package/dist/config.default.js +493 -0
  28. package/dist/config.default.js.map +1 -0
  29. package/dist/corsProxy.d.ts +45 -0
  30. package/dist/corsProxy.d.ts.map +1 -0
  31. package/dist/corsProxy.js +278 -0
  32. package/dist/corsProxy.js.map +1 -0
  33. package/dist/digest.d.ts +45 -0
  34. package/dist/digest.d.ts.map +1 -0
  35. package/dist/digest.js +174 -0
  36. package/dist/digest.js.map +1 -0
  37. package/dist/errors.d.ts +654 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +960 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/index.d.ts +22 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +21 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/lib/atomicFile.d.ts +61 -0
  46. package/dist/lib/atomicFile.d.ts.map +1 -0
  47. package/dist/lib/atomicFile.js +150 -0
  48. package/dist/lib/atomicFile.js.map +1 -0
  49. package/dist/lib/backendRegistry.d.ts +59 -0
  50. package/dist/lib/backendRegistry.d.ts.map +1 -0
  51. package/dist/lib/backendRegistry.js +92 -0
  52. package/dist/lib/backendRegistry.js.map +1 -0
  53. package/dist/lib/backendUsage.d.ts +28 -0
  54. package/dist/lib/backendUsage.d.ts.map +1 -0
  55. package/dist/lib/backendUsage.js +55 -0
  56. package/dist/lib/backendUsage.js.map +1 -0
  57. package/dist/lib/backends.d.ts +142 -0
  58. package/dist/lib/backends.d.ts.map +1 -0
  59. package/dist/lib/backends.js +272 -0
  60. package/dist/lib/backends.js.map +1 -0
  61. package/dist/lib/blindedIndex.d.ts +156 -0
  62. package/dist/lib/blindedIndex.d.ts.map +1 -0
  63. package/dist/lib/blindedIndex.js +338 -0
  64. package/dist/lib/blindedIndex.js.map +1 -0
  65. package/dist/lib/collectionDescription.d.ts +33 -0
  66. package/dist/lib/collectionDescription.d.ts.map +1 -0
  67. package/dist/lib/collectionDescription.js +21 -0
  68. package/dist/lib/collectionDescription.js.map +1 -0
  69. package/dist/lib/cursor.d.ts +18 -0
  70. package/dist/lib/cursor.d.ts.map +1 -0
  71. package/dist/lib/cursor.js +50 -0
  72. package/dist/lib/cursor.js.map +1 -0
  73. package/dist/lib/edvEnvelope.d.ts +50 -0
  74. package/dist/lib/edvEnvelope.d.ts.map +1 -0
  75. package/dist/lib/edvEnvelope.js +106 -0
  76. package/dist/lib/edvEnvelope.js.map +1 -0
  77. package/dist/lib/encryption.d.ts +192 -0
  78. package/dist/lib/encryption.d.ts.map +1 -0
  79. package/dist/lib/encryption.js +383 -0
  80. package/dist/lib/encryption.js.map +1 -0
  81. package/dist/lib/etag.d.ts +33 -0
  82. package/dist/lib/etag.d.ts.map +1 -0
  83. package/dist/lib/etag.js +36 -0
  84. package/dist/lib/etag.js.map +1 -0
  85. package/dist/lib/exportManifest.d.ts +29 -0
  86. package/dist/lib/exportManifest.d.ts.map +1 -0
  87. package/dist/lib/exportManifest.js +77 -0
  88. package/dist/lib/exportManifest.js.map +1 -0
  89. package/dist/lib/importTar.d.ts +81 -0
  90. package/dist/lib/importTar.d.ts.map +1 -0
  91. package/dist/lib/importTar.js +301 -0
  92. package/dist/lib/importTar.js.map +1 -0
  93. package/dist/lib/isJson.d.ts +16 -0
  94. package/dist/lib/isJson.d.ts.map +1 -0
  95. package/dist/lib/isJson.js +17 -0
  96. package/dist/lib/isJson.js.map +1 -0
  97. package/dist/lib/keyEpoch.d.ts +38 -0
  98. package/dist/lib/keyEpoch.d.ts.map +1 -0
  99. package/dist/lib/keyEpoch.js +75 -0
  100. package/dist/lib/keyEpoch.js.map +1 -0
  101. package/dist/lib/keyedMutex.d.ts +28 -0
  102. package/dist/lib/keyedMutex.d.ts.map +1 -0
  103. package/dist/lib/keyedMutex.js +45 -0
  104. package/dist/lib/keyedMutex.js.map +1 -0
  105. package/dist/lib/kmsModule.d.ts +62 -0
  106. package/dist/lib/kmsModule.d.ts.map +1 -0
  107. package/dist/lib/kmsModule.js +327 -0
  108. package/dist/lib/kmsModule.js.map +1 -0
  109. package/dist/lib/kmsRecordCipher.d.ts +82 -0
  110. package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
  111. package/dist/lib/kmsRecordCipher.js +226 -0
  112. package/dist/lib/kmsRecordCipher.js.map +1 -0
  113. package/dist/lib/pagination.d.ts +19 -0
  114. package/dist/lib/pagination.d.ts.map +1 -0
  115. package/dist/lib/pagination.js +21 -0
  116. package/dist/lib/pagination.js.map +1 -0
  117. package/dist/lib/paths.d.ts +273 -0
  118. package/dist/lib/paths.d.ts.map +1 -0
  119. package/dist/lib/paths.js +270 -0
  120. package/dist/lib/paths.js.map +1 -0
  121. package/dist/lib/preconditions.d.ts +65 -0
  122. package/dist/lib/preconditions.d.ts.map +1 -0
  123. package/dist/lib/preconditions.js +112 -0
  124. package/dist/lib/preconditions.js.map +1 -0
  125. package/dist/lib/resourceFileName.d.ts +41 -0
  126. package/dist/lib/resourceFileName.d.ts.map +1 -0
  127. package/dist/lib/resourceFileName.js +58 -0
  128. package/dist/lib/resourceFileName.js.map +1 -0
  129. package/dist/lib/revocations.d.ts +49 -0
  130. package/dist/lib/revocations.d.ts.map +1 -0
  131. package/dist/lib/revocations.js +86 -0
  132. package/dist/lib/revocations.js.map +1 -0
  133. package/dist/lib/validateDid.d.ts +19 -0
  134. package/dist/lib/validateDid.d.ts.map +1 -0
  135. package/dist/lib/validateDid.js +35 -0
  136. package/dist/lib/validateDid.js.map +1 -0
  137. package/dist/lib/validateId.d.ts +66 -0
  138. package/dist/lib/validateId.d.ts.map +1 -0
  139. package/dist/lib/validateId.js +126 -0
  140. package/dist/lib/validateId.js.map +1 -0
  141. package/dist/plugin.d.ts +115 -0
  142. package/dist/plugin.d.ts.map +1 -0
  143. package/dist/plugin.js +134 -0
  144. package/dist/plugin.js.map +1 -0
  145. package/dist/policy.d.ts +78 -0
  146. package/dist/policy.d.ts.map +1 -0
  147. package/dist/policy.js +115 -0
  148. package/dist/policy.js.map +1 -0
  149. package/dist/provisioning.d.ts +38 -0
  150. package/dist/provisioning.d.ts.map +1 -0
  151. package/dist/provisioning.js +72 -0
  152. package/dist/provisioning.js.map +1 -0
  153. package/dist/requests/BackendRequest.d.ts +61 -0
  154. package/dist/requests/BackendRequest.d.ts.map +1 -0
  155. package/dist/requests/BackendRequest.js +152 -0
  156. package/dist/requests/BackendRequest.js.map +1 -0
  157. package/dist/requests/CollectionRequest.d.ts +234 -0
  158. package/dist/requests/CollectionRequest.d.ts.map +1 -0
  159. package/dist/requests/CollectionRequest.js +705 -0
  160. package/dist/requests/CollectionRequest.js.map +1 -0
  161. package/dist/requests/KeyRequest.d.ts +115 -0
  162. package/dist/requests/KeyRequest.d.ts.map +1 -0
  163. package/dist/requests/KeyRequest.js +468 -0
  164. package/dist/requests/KeyRequest.js.map +1 -0
  165. package/dist/requests/KeystoreRequest.d.ts +99 -0
  166. package/dist/requests/KeystoreRequest.d.ts.map +1 -0
  167. package/dist/requests/KeystoreRequest.js +247 -0
  168. package/dist/requests/KeystoreRequest.js.map +1 -0
  169. package/dist/requests/PolicyRequest.d.ts +53 -0
  170. package/dist/requests/PolicyRequest.d.ts.map +1 -0
  171. package/dist/requests/PolicyRequest.js +124 -0
  172. package/dist/requests/PolicyRequest.js.map +1 -0
  173. package/dist/requests/ResourceRequest.d.ts +134 -0
  174. package/dist/requests/ResourceRequest.d.ts.map +1 -0
  175. package/dist/requests/ResourceRequest.js +548 -0
  176. package/dist/requests/ResourceRequest.js.map +1 -0
  177. package/dist/requests/RevocationRequest.d.ts +67 -0
  178. package/dist/requests/RevocationRequest.d.ts.map +1 -0
  179. package/dist/requests/RevocationRequest.js +187 -0
  180. package/dist/requests/RevocationRequest.js.map +1 -0
  181. package/dist/requests/SpaceRequest.d.ts +204 -0
  182. package/dist/requests/SpaceRequest.d.ts.map +1 -0
  183. package/dist/requests/SpaceRequest.js +520 -0
  184. package/dist/requests/SpaceRequest.js.map +1 -0
  185. package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
  186. package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
  187. package/dist/requests/SpacesRepositoryRequest.js +159 -0
  188. package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
  189. package/dist/requests/collectionContext.d.ts +21 -0
  190. package/dist/requests/collectionContext.d.ts.map +1 -0
  191. package/dist/requests/collectionContext.js +30 -0
  192. package/dist/requests/collectionContext.js.map +1 -0
  193. package/dist/requests/controllerConsent.d.ts +58 -0
  194. package/dist/requests/controllerConsent.d.ts.map +1 -0
  195. package/dist/requests/controllerConsent.js +73 -0
  196. package/dist/requests/controllerConsent.js.map +1 -0
  197. package/dist/requests/keystoreContext.d.ts +68 -0
  198. package/dist/requests/keystoreContext.d.ts.map +1 -0
  199. package/dist/requests/keystoreContext.js +87 -0
  200. package/dist/requests/keystoreContext.js.map +1 -0
  201. package/dist/requests/resourceInput.d.ts +19 -0
  202. package/dist/requests/resourceInput.d.ts.map +1 -0
  203. package/dist/requests/resourceInput.js +102 -0
  204. package/dist/requests/resourceInput.js.map +1 -0
  205. package/dist/requests/spaceContext.d.ts +127 -0
  206. package/dist/requests/spaceContext.d.ts.map +1 -0
  207. package/dist/requests/spaceContext.js +201 -0
  208. package/dist/requests/spaceContext.js.map +1 -0
  209. package/dist/routes.d.ts +55 -0
  210. package/dist/routes.d.ts.map +1 -0
  211. package/dist/routes.js +306 -0
  212. package/dist/routes.js.map +1 -0
  213. package/dist/server.d.ts +20 -0
  214. package/dist/server.d.ts.map +1 -0
  215. package/dist/server.js +60 -0
  216. package/dist/server.js.map +1 -0
  217. package/dist/start.d.ts +7 -0
  218. package/dist/start.d.ts.map +1 -0
  219. package/dist/start.js +61 -0
  220. package/dist/start.js.map +1 -0
  221. package/dist/storage.d.ts +29 -0
  222. package/dist/storage.d.ts.map +1 -0
  223. package/dist/storage.js +47 -0
  224. package/dist/storage.js.map +1 -0
  225. package/dist/types.d.ts +915 -0
  226. package/dist/types.d.ts.map +1 -0
  227. package/dist/types.js +2 -0
  228. package/dist/types.js.map +1 -0
  229. package/dist/views/home.hbs +9 -0
  230. package/dist/views/templates/main.hbs +12 -0
  231. package/dist/zcap.d.ts +267 -0
  232. package/dist/zcap.d.ts.map +1 -0
  233. package/dist/zcap.js +478 -0
  234. package/dist/zcap.js.map +1 -0
  235. package/package.json +109 -0
  236. package/src/auth-header-hooks.ts +163 -0
  237. package/src/authorize.ts +117 -0
  238. package/src/backends/filesystem.ts +3626 -0
  239. package/src/backends/postgres.ts +3073 -0
  240. package/src/backends/postgresSchema.ts +231 -0
  241. package/src/config.default.ts +657 -0
  242. package/src/corsProxy.ts +326 -0
  243. package/src/declarations.d.ts +28 -0
  244. package/src/digest.ts +223 -0
  245. package/src/errors.ts +1122 -0
  246. package/src/index.ts +21 -0
  247. package/src/lib/atomicFile.ts +176 -0
  248. package/src/lib/backendRegistry.ts +139 -0
  249. package/src/lib/backendUsage.ts +79 -0
  250. package/src/lib/backends.ts +351 -0
  251. package/src/lib/blindedIndex.ts +466 -0
  252. package/src/lib/collectionDescription.ts +43 -0
  253. package/src/lib/cursor.ts +52 -0
  254. package/src/lib/edvEnvelope.ts +120 -0
  255. package/src/lib/encryption.ts +472 -0
  256. package/src/lib/etag.ts +40 -0
  257. package/src/lib/exportManifest.ts +115 -0
  258. package/src/lib/importTar.ts +381 -0
  259. package/src/lib/isJson.ts +18 -0
  260. package/src/lib/keyEpoch.ts +89 -0
  261. package/src/lib/keyedMutex.ts +45 -0
  262. package/src/lib/kmsModule.ts +451 -0
  263. package/src/lib/kmsRecordCipher.ts +292 -0
  264. package/src/lib/pagination.ts +22 -0
  265. package/src/lib/paths.ts +382 -0
  266. package/src/lib/preconditions.ts +145 -0
  267. package/src/lib/resourceFileName.ts +69 -0
  268. package/src/lib/revocations.ts +116 -0
  269. package/src/lib/validateDid.ts +41 -0
  270. package/src/lib/validateId.ts +148 -0
  271. package/src/plugin.ts +288 -0
  272. package/src/policy.ts +176 -0
  273. package/src/provisioning.ts +98 -0
  274. package/src/requests/BackendRequest.ts +219 -0
  275. package/src/requests/CollectionRequest.ts +899 -0
  276. package/src/requests/KeyRequest.ts +617 -0
  277. package/src/requests/KeystoreRequest.ts +325 -0
  278. package/src/requests/PolicyRequest.ts +169 -0
  279. package/src/requests/ResourceRequest.ts +649 -0
  280. package/src/requests/RevocationRequest.ts +267 -0
  281. package/src/requests/SpaceRequest.ts +686 -0
  282. package/src/requests/SpacesRepositoryRequest.ts +194 -0
  283. package/src/requests/collectionContext.ts +41 -0
  284. package/src/requests/controllerConsent.ts +108 -0
  285. package/src/requests/keystoreContext.ts +124 -0
  286. package/src/requests/resourceInput.ts +118 -0
  287. package/src/requests/spaceContext.ts +311 -0
  288. package/src/routes.ts +420 -0
  289. package/src/server.ts +69 -0
  290. package/src/start.ts +71 -0
  291. package/src/storage.ts +60 -0
  292. package/src/types.ts +1016 -0
  293. package/src/views/home.hbs +9 -0
  294. package/src/views/templates/main.hbs +12 -0
  295. package/src/zcap.ts +657 -0
package/src/routes.ts ADDED
@@ -0,0 +1,420 @@
1
+ /**
2
+ * Route layer: maps URL patterns to *Request handler methods. Each group first
3
+ * installs the `requireAuthHeadersOrPublicRead` then `parseAuthHeaders`
4
+ * onRequest hooks, and redirects slash/no-slash variants to the canonical form.
5
+ * (The WebKMS `/kms` group is the exception on both counts: it installs the
6
+ * strict `requireAuthHeaders` -- the webkms protocol has no public reads --
7
+ * and no slash redirects, since the protocol's URLs are exact.)
8
+ */
9
+ import type {
10
+ FastifyInstance,
11
+ FastifyPluginOptions,
12
+ FastifyReply,
13
+ FastifyRequest
14
+ } from 'fastify'
15
+ import { SpacesRepositoryRequest } from './requests/SpacesRepositoryRequest.js'
16
+ import { SpaceRequest } from './requests/SpaceRequest.js'
17
+ import { handleError } from './errors.js'
18
+ import { ResourceRequest } from './requests/ResourceRequest.js'
19
+ import { CollectionRequest } from './requests/CollectionRequest.js'
20
+ import { PolicyRequest } from './requests/PolicyRequest.js'
21
+ import { BackendRequest } from './requests/BackendRequest.js'
22
+ import { KeystoreRequest } from './requests/KeystoreRequest.js'
23
+ import { KeyRequest } from './requests/KeyRequest.js'
24
+ import { RevocationRequest } from './requests/RevocationRequest.js'
25
+ import {
26
+ parseAuthHeaders,
27
+ requireAuthHeaders,
28
+ requireAuthHeadersOrPublicRead
29
+ } from './auth-header-hooks.js'
30
+ import { captureRawBody, verifyBodyDigest } from './digest.js'
31
+ import { provisioningGate } from './provisioning.js'
32
+
33
+ /**
34
+ * Toggles the trailing slash on the request's actual path (preserving any query
35
+ * string), returning the canonical target for a slash/no-slash redirect. Built
36
+ * from `request.url` rather than the route template so the `Location` carries
37
+ * the concrete ids (`/space/abc123`), not the literal `/space/:spaceId` -- which
38
+ * a client cannot follow.
39
+ * @param url {string} the request URL (path plus optional query string)
40
+ * @param addSlash {boolean} append (true) or strip (false) the trailing slash
41
+ * @returns {string}
42
+ */
43
+ function toggleTrailingSlash(url: string, addSlash: boolean): string {
44
+ const queryIndex = url.indexOf('?')
45
+ const pathPart = queryIndex === -1 ? url : url.slice(0, queryIndex)
46
+ const query = queryIndex === -1 ? '' : url.slice(queryIndex)
47
+ const canonical = addSlash
48
+ ? pathPart.endsWith('/')
49
+ ? pathPart
50
+ : `${pathPart}/`
51
+ : pathPart.endsWith('/')
52
+ ? pathPart.slice(0, -1)
53
+ : pathPart
54
+ return `${canonical}${query}`
55
+ }
56
+
57
+ /**
58
+ * Redirects to the trailing-slash canonical form of the request URL with a
59
+ * `308` (Permanent Redirect), which -- unlike the default `302` -- requires the
60
+ * client to replay the same method and body, so a redirected POST/PUT is not
61
+ * silently downgraded to GET.
62
+ * @param request {import('fastify').FastifyRequest}
63
+ * @param reply {import('fastify').FastifyReply}
64
+ * @returns {FastifyReply}
65
+ */
66
+ function redirectAddSlash(
67
+ request: FastifyRequest,
68
+ reply: FastifyReply
69
+ ): FastifyReply {
70
+ return reply.redirect(toggleTrailingSlash(request.url, true), 308)
71
+ }
72
+
73
+ /**
74
+ * Redirects to the no-trailing-slash canonical form of the request URL (see
75
+ * {@link redirectAddSlash} for the `308` rationale).
76
+ * @param request {import('fastify').FastifyRequest}
77
+ * @param reply {import('fastify').FastifyReply}
78
+ * @returns {FastifyReply}
79
+ */
80
+ function redirectStripSlash(
81
+ request: FastifyRequest,
82
+ reply: FastifyReply
83
+ ): FastifyReply {
84
+ return reply.redirect(toggleTrailingSlash(request.url, false), 308)
85
+ }
86
+
87
+ /**
88
+ * Registers SpacesRepository routes (POST/GET /spaces). Installs the
89
+ * `requireAuthHeadersOrPublicRead` then `parseAuthHeaders` onRequest hooks and
90
+ * the `handleError` error handler.
91
+ * @param app {import('fastify').FastifyInstance}
92
+ * @param options {object} Fastify plugin options
93
+ * @returns {Promise<void>}
94
+ */
95
+ export async function initSpacesRepositoryRoutes(
96
+ app: FastifyInstance,
97
+ _options: FastifyPluginOptions
98
+ ): Promise<void> {
99
+ app.setErrorHandler(handleError)
100
+
101
+ // Gate provisioning (Create Space): the configured policy may grant/deny, or
102
+ // (the default) allow -- in which case the normal zcap path below runs.
103
+ app.addHook('onRequest', provisioningGate)
104
+ // Create Space (POST) requires auth-related headers (401 otherwise); a List
105
+ // Spaces read may proceed unauthenticated -- an anonymous list is the spec's
106
+ // empty-items 200, never an error (the exception to 404 masking).
107
+ app.addHook('onRequest', requireAuthHeadersOrPublicRead)
108
+ // Parse the relevant request headers, set the request.zcap parameter
109
+ app.addHook('onRequest', parseAuthHeaders)
110
+ // Capture raw body bytes (JSON/text) so the digest can be recomputed against
111
+ // exactly what the client signed (spec "Request Body Integrity").
112
+ app.addHook('preParsing', captureRawBody)
113
+ // Enforce the Digest header binding: require it covered by the signature and,
114
+ // when the raw body is available, recompute and compare it.
115
+ app.addHook('preValidation', verifyBodyDigest)
116
+
117
+ // Add a Space to a SpacesRepository (Create Space)
118
+ app.post('/spaces', redirectAddSlash)
119
+ app.post('/spaces/', SpacesRepositoryRequest.post)
120
+
121
+ // List Spaces
122
+ app.get('/spaces', redirectAddSlash)
123
+ app.get('/spaces/', SpacesRepositoryRequest.get)
124
+ }
125
+
126
+ /**
127
+ * Registers Space routes (get/update/delete a Space, add/list Collections,
128
+ * export/import). Installs the auth hooks and the `handleError` error handler.
129
+ * @param app {import('fastify').FastifyInstance}
130
+ * @param options {object} Fastify plugin options
131
+ * @returns {Promise<void>}
132
+ */
133
+ export async function initSpaceRoutes(
134
+ app: FastifyInstance,
135
+ _options: FastifyPluginOptions
136
+ ): Promise<void> {
137
+ app.setErrorHandler(handleError)
138
+
139
+ // Writes require auth; reads (GET/HEAD) may proceed unauthenticated so the
140
+ // handler can fall back to an access-control policy (e.g. a public Space).
141
+ app.addHook('onRequest', requireAuthHeadersOrPublicRead)
142
+ // Parse the relevant request headers, set the request.zcap parameter
143
+ app.addHook('onRequest', parseAuthHeaders)
144
+ // Capture raw body bytes (JSON/text) so the digest can be recomputed against
145
+ // exactly what the client signed (spec "Request Body Integrity").
146
+ app.addHook('preParsing', captureRawBody)
147
+ // Enforce the Digest header binding: require it covered by the signature and,
148
+ // when the raw body is available, recompute and compare it.
149
+ app.addHook('preValidation', verifyBodyDigest)
150
+
151
+ // Get Space description object
152
+ app.get('/space/:spaceId', SpaceRequest.get)
153
+
154
+ // Update or Create Space by Id (only "no trailing slash" is valid)
155
+ app.put('/space/:spaceId/', redirectStripSlash)
156
+ app.put('/space/:spaceId', SpaceRequest.put)
157
+
158
+ // Delete Space
159
+ app.delete('/space/:spaceId', SpaceRequest.delete)
160
+
161
+ // List Collections for a space (the canonical form has a trailing slash;
162
+ // registered as GET so `GET /space/:spaceId/collections` redirects there
163
+ // rather than falling through to the Collection GET route).
164
+ app.get('/space/:spaceId/collections', redirectAddSlash)
165
+ app.get('/space/:spaceId/collections/', SpaceRequest.listCollections)
166
+
167
+ // Space access-control policy (reserved segment; Fastify routes static
168
+ // segments ahead of the `:collectionId` parameter, so this never collides).
169
+ app.get('/space/:spaceId/policy', PolicyRequest.get)
170
+ app.put('/space/:spaceId/policy', PolicyRequest.put)
171
+ app.delete('/space/:spaceId/policy', PolicyRequest.delete)
172
+
173
+ // Space linkset (RFC9264 policy discovery)
174
+ app.get('/space/:spaceId/linkset', SpaceRequest.linkset)
175
+
176
+ // Space Backends Available (reserved segment; static-beats-parametric routing
177
+ // keeps this ahead of the `:collectionId` parameter in the Collection routes).
178
+ app.get('/space/:spaceId/backends', SpaceRequest.listBackends)
179
+
180
+ // Register / replace / deregister an `external` backend record. Static
181
+ // `backends` beats the parametric `:collectionId` / `:resourceId` of the
182
+ // Collection/Resource route groups, so these resolve correctly (the same
183
+ // mechanism that keeps the GET above and the singular `/backend` working).
184
+ app.post('/space/:spaceId/backends', BackendRequest.post)
185
+ app.put('/space/:spaceId/backends/:backendId', BackendRequest.put)
186
+ app.delete('/space/:spaceId/backends/:backendId', BackendRequest.delete)
187
+
188
+ // Space Quota report (reserved segment; static-beats-parametric routing keeps
189
+ // this ahead of the `:collectionId` parameter). The per-Collection breakdown
190
+ // (spec's `?include=collections`) is opt-in via that query string -- see the
191
+ // handler note on the ZCap query-string limitation.
192
+ app.get('/space/:spaceId/quotas', SpaceRequest.quotas)
193
+
194
+ // Revoke a zcap delegated from this Space (`:revocationId` = the URL-encoded
195
+ // id of the capability being revoked, which is also the request body). Four
196
+ // segments deep, so it shadows no Collection or Resource route.
197
+ app.post(
198
+ '/space/:spaceId/zcaps/revocations/:revocationId',
199
+ RevocationRequest.postSpace
200
+ )
201
+
202
+ // Add Collection to a Space
203
+ app.post('/space/:spaceId', redirectAddSlash)
204
+ app.post('/space/:spaceId/', SpaceRequest.post)
205
+
206
+ // POST /space/12345/export
207
+ app.post('/space/:spaceId/export', SpaceRequest.export)
208
+
209
+ // POST /space/12345/import
210
+ app.addContentTypeParser('application/x-tar', (_req, body, done) => {
211
+ done(null, body)
212
+ })
213
+ app.post('/space/:spaceId/import', SpaceRequest.import)
214
+ }
215
+
216
+ /**
217
+ * Registers Collection routes (get/update/delete a Collection, list its items,
218
+ * add a Resource). Installs the auth hooks and the `handleError` error handler.
219
+ * @param app {import('fastify').FastifyInstance}
220
+ * @param options {object} Fastify plugin options
221
+ * @returns {Promise<void>}
222
+ */
223
+ export async function initCollectionRoutes(
224
+ app: FastifyInstance,
225
+ _options: FastifyPluginOptions
226
+ ): Promise<void> {
227
+ app.setErrorHandler(handleError)
228
+
229
+ // Writes require auth; reads (GET/HEAD) may proceed unauthenticated so the
230
+ // handler can fall back to an access-control policy (e.g. a public Collection).
231
+ app.addHook('onRequest', requireAuthHeadersOrPublicRead)
232
+ // Parse the relevant request headers, set the request.zcap parameter
233
+ app.addHook('onRequest', parseAuthHeaders)
234
+ // Capture raw body bytes (JSON/text) so the digest can be recomputed against
235
+ // exactly what the client signed (spec "Request Body Integrity").
236
+ app.addHook('preParsing', captureRawBody)
237
+ // Enforce the Digest header binding: require it covered by the signature and,
238
+ // when the raw body is available, recompute and compare it.
239
+ app.addHook('preValidation', verifyBodyDigest)
240
+
241
+ // Get Collection description
242
+ app.get('/space/:spaceId/:collectionId', CollectionRequest.get)
243
+ // List Collection items
244
+ app.get('/space/:spaceId/:collectionId/', CollectionRequest.list)
245
+
246
+ // Collection access-control policy (reserved segment; static-beats-parametric
247
+ // routing keeps this ahead of the `:resourceId` parameter).
248
+ app.get('/space/:spaceId/:collectionId/policy', PolicyRequest.get)
249
+ app.put('/space/:spaceId/:collectionId/policy', PolicyRequest.put)
250
+ app.delete('/space/:spaceId/:collectionId/policy', PolicyRequest.delete)
251
+
252
+ // Collection linkset (RFC9264 policy discovery)
253
+ app.get('/space/:spaceId/:collectionId/linkset', CollectionRequest.linkset)
254
+
255
+ // Collection Backend Selected (reserved segment; static-beats-parametric
256
+ // routing keeps this ahead of the `:resourceId` parameter in Resource routes).
257
+ app.get('/space/:spaceId/:collectionId/backend', CollectionRequest.getBackend)
258
+
259
+ // Per-Collection Quota report (reserved segment; static-beats-parametric
260
+ // routing keeps this ahead of the `:resourceId` parameter in Resource routes).
261
+ app.get('/space/:spaceId/:collectionId/quota', CollectionRequest.getQuota)
262
+
263
+ // Collection query (reserved segment; spec "Collection-level reserved
264
+ // endpoints"). The WAS server serves the replication change feed as the
265
+ // `changes` profile; params ride the signed
266
+ // POST body. Static-beats-parametric routing keeps this ahead of the
267
+ // `:resourceId` parameter in Resource routes.
268
+ app.post('/space/:spaceId/:collectionId/query', CollectionRequest.query)
269
+
270
+ // Add Resource to a Collection
271
+ app.post('/space/:spaceId/:collectionId', redirectAddSlash)
272
+ app.post('/space/:spaceId/:collectionId/', CollectionRequest.post)
273
+
274
+ // Create a Collection by Id
275
+ app.put(
276
+ '/space/:spaceId/:collectionId/', // no trailing slash allowed
277
+ redirectStripSlash
278
+ )
279
+ app.put('/space/:spaceId/:collectionId', CollectionRequest.put)
280
+
281
+ // Delete Collection by Id
282
+ app.delete(
283
+ '/space/:spaceId/:collectionId/', // no trailing slash allowed
284
+ redirectStripSlash
285
+ )
286
+ app.delete('/space/:spaceId/:collectionId', CollectionRequest.delete)
287
+ }
288
+
289
+ /**
290
+ * Registers Resource routes (create-by-id, get, delete a Resource). Installs the
291
+ * auth hooks and the `handleError` error handler.
292
+ * @param app {import('fastify').FastifyInstance}
293
+ * @param options {object} Fastify plugin options
294
+ * @returns {Promise<void>}
295
+ */
296
+ export async function initResourceRoutes(
297
+ app: FastifyInstance,
298
+ _options: FastifyPluginOptions
299
+ ): Promise<void> {
300
+ app.setErrorHandler(handleError)
301
+
302
+ // Writes require auth; reads (GET/HEAD) may proceed unauthenticated so the
303
+ // handler can fall back to an access-control policy (e.g. a public Resource).
304
+ app.addHook('onRequest', requireAuthHeadersOrPublicRead)
305
+ // Parse the relevant request headers, set the request.zcap parameter
306
+ app.addHook('onRequest', parseAuthHeaders)
307
+ // Capture raw body bytes (JSON/text) so the digest can be recomputed against
308
+ // exactly what the client signed (spec "Request Body Integrity").
309
+ app.addHook('preParsing', captureRawBody)
310
+ // Enforce the Digest header binding: require it covered by the signature and,
311
+ // when the raw body is available, recompute and compare it.
312
+ app.addHook('preValidation', verifyBodyDigest)
313
+
314
+ // Create a Resource by Id
315
+ app.put(
316
+ '/space/:spaceId/:collectionId/:resourceId/', // no trailing slash allowed
317
+ redirectStripSlash
318
+ )
319
+ app.put('/space/:spaceId/:collectionId/:resourceId', ResourceRequest.put)
320
+
321
+ // Head Resource. Declared before the GET route so it overrides Fastify's
322
+ // auto-exposed HEAD (which would share the GET handler and stream the body
323
+ // without a Content-Length); this handler reads only the Metadata and sets
324
+ // Content-Type/Content-Length from it (spec "Content Types and Representations").
325
+ app.head('/space/:spaceId/:collectionId/:resourceId', ResourceRequest.head)
326
+
327
+ // Get Resource
328
+ app.get('/space/:spaceId/:collectionId/:resourceId', ResourceRequest.get)
329
+
330
+ // Delete Resource
331
+ app.delete(
332
+ '/space/:spaceId/:collectionId/:resourceId',
333
+ ResourceRequest.delete
334
+ )
335
+
336
+ // Resource access-control policy (reserved segment)
337
+ app.get('/space/:spaceId/:collectionId/:resourceId/policy', PolicyRequest.get)
338
+ app.put('/space/:spaceId/:collectionId/:resourceId/policy', PolicyRequest.put)
339
+ app.delete(
340
+ '/space/:spaceId/:collectionId/:resourceId/policy',
341
+ PolicyRequest.delete
342
+ )
343
+
344
+ // Resource metadata (reserved segment; spec "Resource Metadata Data Model")
345
+ app.get(
346
+ '/space/:spaceId/:collectionId/:resourceId/meta',
347
+ ResourceRequest.getMeta
348
+ )
349
+ // Update Resource Metadata (full replacement of the user-writable `custom`).
350
+ app.put(
351
+ '/space/:spaceId/:collectionId/:resourceId/meta',
352
+ ResourceRequest.putMeta
353
+ )
354
+ }
355
+
356
+ /**
357
+ * Registers the WebKMS keystore and key routes (the `/kms` facet).
358
+ * Installs the same hook chain as the WAS groups
359
+ * except that the auth requirement is the strict `requireAuthHeaders`: every
360
+ * webkms route, GETs included, is zcap-invoked -- the protocol has no public
361
+ * reads. No slash-redirect variants either; the protocol's URLs are exact --
362
+ * only these shapes are registered.
363
+ * @param app {import('fastify').FastifyInstance}
364
+ * @param options {object} Fastify plugin options
365
+ * @returns {Promise<void>}
366
+ */
367
+ export async function initKmsRoutes(
368
+ app: FastifyInstance,
369
+ _options: FastifyPluginOptions
370
+ ): Promise<void> {
371
+ app.setErrorHandler(handleError)
372
+
373
+ // Gate provisioning (Create Keystore): the configured policy may grant/deny,
374
+ // or (the default) allow -- in which case the normal zcap path below runs.
375
+ app.addHook('onRequest', provisioningGate)
376
+ // Every operation is privileged: 401 when auth headers are absent.
377
+ app.addHook('onRequest', requireAuthHeaders)
378
+ // Parse the relevant request headers, set the request.zcap parameter
379
+ app.addHook('onRequest', parseAuthHeaders)
380
+ // Capture raw body bytes (JSON/text) so the digest can be recomputed against
381
+ // exactly what the client signed (spec "Request Body Integrity").
382
+ app.addHook('preParsing', captureRawBody)
383
+ // Enforce the Digest header binding: require it covered by the signature and,
384
+ // when the raw body is available, recompute and compare it.
385
+ app.addHook('preValidation', verifyBodyDigest)
386
+
387
+ // Create Keystore
388
+ app.post('/kms/keystores', KeystoreRequest.post)
389
+
390
+ // List Keystores by controller (`?controller=<did>`)
391
+ app.get('/kms/keystores', KeystoreRequest.list)
392
+
393
+ // Get Keystore config
394
+ app.get('/kms/keystores/:keystoreId', KeystoreRequest.get)
395
+
396
+ // Update Keystore config
397
+ app.post('/kms/keystores/:keystoreId', KeystoreRequest.update)
398
+
399
+ // Generate Key (GenerateKeyOperation)
400
+ app.post('/kms/keystores/:keystoreId/keys', KeyRequest.generate)
401
+
402
+ // List Keys (fork extension: enumerate the keystore's public key
403
+ // descriptions). Static `/keys` beats the parametric `/keys/:keyId` below,
404
+ // so this never collides with the key-description GET.
405
+ app.get('/kms/keystores/:keystoreId/keys', KeyRequest.list)
406
+
407
+ // Key operation dispatch by envelope type (Sign / Verify / DeriveSecret /
408
+ // WrapKey / UnwrapKey)
409
+ app.post('/kms/keystores/:keystoreId/keys/:keyId', KeyRequest.operation)
410
+
411
+ // Public key description
412
+ app.get('/kms/keystores/:keystoreId/keys/:keyId', KeyRequest.get)
413
+
414
+ // Revoke a delegated zcap (`:revocationId` = the URL-encoded id of the
415
+ // capability being revoked, which is also the request body)
416
+ app.post(
417
+ '/kms/keystores/:keystoreId/zcaps/revocations/:revocationId',
418
+ RevocationRequest.post
419
+ )
420
+ }
package/src/server.ts ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * App factory: createApp() is the community-edition composition. It builds the
3
+ * Fastify instance, registers the `fastifyWas` plugin (the whole WAS protocol
4
+ * surface -- see plugin.ts), and adds the teaching-server extras around it:
5
+ * static assets, the Handlebars-rendered welcome page, the `/health` probe,
6
+ * and the CORS proxy.
7
+ */
8
+ import Fastify, { type FastifyInstance } from 'fastify'
9
+ import fastifyView from '@fastify/view'
10
+ import fastifyStatic from '@fastify/static'
11
+ import handlebars from 'handlebars'
12
+ import path from 'node:path'
13
+
14
+ import { fastifyWas, type FastifyWasOptions } from './plugin.js'
15
+ import { initCorsProxyRoutes as initApiCorsProxyRoutes } from './corsProxy.js'
16
+ import { SPEC_URL, SERVER_VERSION } from './config.default.js'
17
+
18
+ // TODO: https://github.com/fastify/fastify-helmet
19
+
20
+ /**
21
+ * Builds the Fastify instance: registers the `fastifyWas` protocol plugin
22
+ * (which carries the storage/config decorations and all WAS + WebKMS routes;
23
+ * options are passed through -- see {@link FastifyWasOptions}), then the
24
+ * teaching-server extras (static files, welcome page, health probe, CORS
25
+ * proxy).
26
+ * @param options {FastifyWasOptions}
27
+ * @returns {import('fastify').FastifyInstance}
28
+ */
29
+ export function createApp(options: FastifyWasOptions = {}): FastifyInstance {
30
+ // By default uses 'pino' logger
31
+ const fastify = Fastify({ logger: true })
32
+
33
+ // The WAS protocol surface (decorations, parsers, WAS + WebKMS route groups)
34
+ fastify.register(fastifyWas, options)
35
+
36
+ // Serve static files from the /common folder
37
+ fastify.register(fastifyStatic, {
38
+ root: path.join(import.meta.dirname, '..', 'common'),
39
+ prefix: '/common/'
40
+ })
41
+
42
+ // Use Handlebars as the rendering engine
43
+ fastify.register(fastifyView, {
44
+ engine: { handlebars },
45
+ root: path.join(import.meta.dirname, './views'),
46
+ layout: '/templates/main', // ./views/templates/main.hbs
47
+ viewExt: 'hbs'
48
+ })
49
+
50
+ // Add a human-readable 'Welcome' page
51
+ fastify.get('/', async (request, reply) => {
52
+ return reply.view('home', { title: 'Welcome', SPEC_URL, SERVER_VERSION })
53
+ })
54
+
55
+ // Operational liveness probe (not a WAS protocol feature). Public,
56
+ // unauthenticated, and side-effect-free so load balancers, uptime monitors,
57
+ // and orchestrators can poll it cheaply. Body follows the
58
+ // `application/health+json` shape from the IETF `draft-inadarei-api-health-check`
59
+ // draft; Fastify's implicit HEAD route serves bodyless probes for free.
60
+ fastify.get('/health', async (request, reply) => {
61
+ return reply
62
+ .type('application/health+json')
63
+ .send({ status: 'pass', version: SERVER_VERSION })
64
+ })
65
+
66
+ fastify.register(initApiCorsProxyRoutes)
67
+
68
+ return fastify
69
+ }
package/src/start.ts ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Entry point: loads and validates the env config surface via
3
+ * loadConfigFromEnv() (fail-fast on a missing SERVER_URL or any malformed
4
+ * value), builds the app via createApp() and starts listening.
5
+ */
6
+ import type { FastifyInstance } from 'fastify'
7
+ import { createApp } from './server.js'
8
+ import { PostgresBackend } from './backends/postgres.js'
9
+ import { loadConfigFromEnv } from './config.default.js'
10
+
11
+ /**
12
+ * Loads the validated env config, builds the app via createApp(), and starts
13
+ * listening. Exits the process with code 1 on startup failure.
14
+ * @returns {Promise<void>}
15
+ */
16
+ export async function startServer(): Promise<void> {
17
+ let fastify: FastifyInstance
18
+ try {
19
+ const config = loadConfigFromEnv()
20
+ // Backend selection: presence of DATABASE_URL selects the Postgres
21
+ // backend; otherwise createApp falls back to the default filesystem
22
+ // backend (rooted at data/). An injected backend carries its own quota
23
+ // configuration, so the per-Space/per-upload limits are passed to it
24
+ // directly rather than through the createApp options.
25
+ const backend = config.databaseUrl
26
+ ? new PostgresBackend({
27
+ connectionString: config.databaseUrl,
28
+ capacityBytes: config.storageLimitPerSpace,
29
+ maxUploadBytes: config.maxUploadBytes,
30
+ maxSpacesPerController: config.maxSpacesPerController,
31
+ maxCollectionsPerSpace: config.maxCollectionsPerSpace,
32
+ maxResourcesPerSpace: config.maxResourcesPerSpace
33
+ })
34
+ : undefined
35
+ fastify = createApp({
36
+ serverUrl: config.serverUrl,
37
+ ...(backend && { backend }),
38
+ storageLimitPerSpace: config.storageLimitPerSpace,
39
+ maxUploadBytes: config.maxUploadBytes,
40
+ maxSpacesPerController: config.maxSpacesPerController,
41
+ maxCollectionsPerSpace: config.maxCollectionsPerSpace,
42
+ maxResourcesPerSpace: config.maxResourcesPerSpace,
43
+ enabledBackendProviders: config.enabledBackendProviders,
44
+ kmsRecordKek: config.kmsRecordKek,
45
+ onboardingToken: config.onboardingToken
46
+ })
47
+ // Warn (once, at startup, where the Fastify logger now exists) about limits
48
+ // left implicitly unbounded. These warnings live only here so library and
49
+ // test compositions of createApp() stay silent; an explicit `unlimited`
50
+ // (Infinity) or a finite value is a deliberate choice and warns nothing.
51
+ if (config.storageLimitPerSpace === undefined) {
52
+ fastify.log.warn(
53
+ 'No per-Space storage quota configured; Spaces may grow without ' +
54
+ 'bound. Set STORAGE_LIMIT_PER_SPACE (bytes), or ' +
55
+ 'STORAGE_LIMIT_PER_SPACE=unlimited to acknowledge.'
56
+ )
57
+ }
58
+ if (config.maxUploadBytes === Infinity) {
59
+ fastify.log.warn(
60
+ 'Per-upload size cap disabled (MAX_UPLOAD_BYTES=unlimited); a single ' +
61
+ 'upload may consume unbounded memory on buffered write paths.'
62
+ )
63
+ }
64
+ await fastify.listen({ port: config.port, host: '0.0.0.0' })
65
+ } catch (err) {
66
+ console.error('Server startup failed:', err)
67
+ process.exit(1)
68
+ }
69
+ }
70
+
71
+ startServer()
package/src/storage.ts ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Storage composition helper: builds the default StorageBackend used by the
3
+ * server when no backend is injected.
4
+ *
5
+ * This module only supplies the default backend for production / `start.ts`,
6
+ * where `createApp()` is called without an explicit backend.
7
+ * The active backend is injected into the Fastify instance via
8
+ * `createApp({ backend })` (see plugin.ts) and read in handlers as
9
+ * `request.server.storage`.
10
+ *
11
+ * The StorageBackend contract — including its invariants (getters resolve falsy
12
+ * for not-found, writes are upserts, deletes are idempotent) — is documented on
13
+ * the interface in src/types.ts.
14
+ */
15
+ import path from 'node:path'
16
+ import { FileSystemBackend } from './backends/filesystem.js'
17
+ import type { StorageBackend } from './types.js'
18
+
19
+ /**
20
+ * Builds the default filesystem-backed storage, rooted at the project `data/`
21
+ * directory. Used by `createApp()` when no backend is injected (production).
22
+ * @param options {object}
23
+ * @param [options.capacityBytes] {number} per-Space storage limit in bytes
24
+ * (spec "Quotas"); `undefined` (or `Infinity`) means each Space is unlimited.
25
+ * @param [options.maxUploadBytes] {number} per-upload size cap in bytes (spec
26
+ * "Quotas", `maxUploadBytes`); `undefined` applies the backend's default-on
27
+ * cap, `Infinity` means no per-upload cap.
28
+ * @param [options.maxSpacesPerController] {number} max Spaces per controller
29
+ * (spec "Quotas"); `undefined` applies the backend's default-on limit,
30
+ * `Infinity` means no cap.
31
+ * @param [options.maxCollectionsPerSpace] {number} max Collections per Space
32
+ * (spec "Quotas"); `undefined` applies the backend's default-on limit,
33
+ * `Infinity` means no cap.
34
+ * @param [options.maxResourcesPerSpace] {number} max live Resources per Space
35
+ * (spec "Quotas"); `undefined` applies the backend's default-on limit,
36
+ * `Infinity` means no cap.
37
+ * @returns {StorageBackend}
38
+ */
39
+ export function defaultBackend({
40
+ capacityBytes,
41
+ maxUploadBytes,
42
+ maxSpacesPerController,
43
+ maxCollectionsPerSpace,
44
+ maxResourcesPerSpace
45
+ }: {
46
+ capacityBytes?: number
47
+ maxUploadBytes?: number
48
+ maxSpacesPerController?: number
49
+ maxCollectionsPerSpace?: number
50
+ maxResourcesPerSpace?: number
51
+ } = {}): StorageBackend {
52
+ return new FileSystemBackend({
53
+ dataDir: path.join(import.meta.dirname, '..', 'data'),
54
+ capacityBytes,
55
+ maxUploadBytes,
56
+ maxSpacesPerController,
57
+ maxCollectionsPerSpace,
58
+ maxResourcesPerSpace
59
+ })
60
+ }