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/plugin.ts ADDED
@@ -0,0 +1,288 @@
1
+ /**
2
+ * The WAS protocol surface as a registerable Fastify plugin (`fastifyWas`):
3
+ * the storage/config decorations, CORS, multipart and content-type parsers,
4
+ * and the route groups (the four WAS groups plus the WebKMS `/kms` facet) --
5
+ * everything from routes.ts down, including the auth/digest hook chains and
6
+ * the error handler those groups install.
7
+ *
8
+ * The community `createApp()` (server.ts) registers this plugin with defaults;
9
+ * a hardened downstream composition registers the same plugin (with its own
10
+ * backend and policy plugins around it) and inherits the identical wire
11
+ * behavior. Wrapped with `fastify-plugin`, so the decorations and parsers land
12
+ * on the root instance -- while each route group still creates its own
13
+ * encapsulated context for its hooks.
14
+ */
15
+ import type { FastifyInstance } from 'fastify'
16
+ import fp from 'fastify-plugin'
17
+ import cors from '@fastify/cors'
18
+ import Multipart from '@fastify/multipart'
19
+
20
+ import {
21
+ initCollectionRoutes,
22
+ initKmsRoutes,
23
+ initResourceRoutes,
24
+ initSpaceRoutes,
25
+ initSpacesRepositoryRoutes
26
+ } from './routes.js'
27
+ import { assertValidServerUrl } from './config.default.js'
28
+ import { defaultBackend } from './storage.js'
29
+ import { onboardingTokenAuthorizer } from './provisioning.js'
30
+ import type {
31
+ StorageBackend,
32
+ BackendProviderRegistry,
33
+ KmsRecordKekRegistry,
34
+ AuthorizeProvisioning
35
+ } from './types.js'
36
+
37
+ export interface FastifyWasOptions {
38
+ /**
39
+ * This server's base URL; used to build and match ZCap invocationTarget URLs
40
+ * (host and port must match exactly). When provided, it must be an absolute
41
+ * `http:`/`https:` URL with no path, query, or fragment (validated at
42
+ * registration -- sub-path deployment is not supported).
43
+ */
44
+ serverUrl?: string
45
+ /**
46
+ * Persistence backend to use; defaults to a filesystem backend rooted at the
47
+ * project `data/` directory. Tests inject their own (e.g. a
48
+ * FileSystemBackend over a temp dir).
49
+ */
50
+ backend?: StorageBackend
51
+ /**
52
+ * Per-Space storage limit in bytes (spec "Quotas"); applied only to the
53
+ * default backend (an injected `backend` carries its own `capacityBytes`).
54
+ * `undefined` means unlimited; `Infinity` (an explicit `unlimited`) is
55
+ * normalized by the backend to the same no-limit behavior.
56
+ */
57
+ storageLimitPerSpace?: number
58
+ /**
59
+ * Per-upload size cap in bytes (spec "Quotas", `maxUploadBytes`); applied
60
+ * only to the default backend (an injected `backend` carries its own).
61
+ * `undefined` applies the backend's default-on cap
62
+ * ({@link DEFAULT_MAX_UPLOAD_BYTES}); `Infinity` (an explicit `unlimited`)
63
+ * disables the cap.
64
+ */
65
+ maxUploadBytes?: number
66
+ /**
67
+ * Max Spaces a single controller may create (spec "Quotas", a default-on
68
+ * count quota); applied only to the default backend (an injected `backend`
69
+ * carries its own). `undefined` applies the backend's default
70
+ * ({@link DEFAULT_MAX_SPACES_PER_CONTROLLER}); `Infinity` (an explicit
71
+ * `unlimited`) disables the cap.
72
+ */
73
+ maxSpacesPerController?: number
74
+ /**
75
+ * Max Collections a single Space may hold (spec "Quotas", a default-on count
76
+ * quota); applied only to the default backend (an injected `backend` carries
77
+ * its own). `undefined` applies the backend's default
78
+ * ({@link DEFAULT_MAX_COLLECTIONS_PER_SPACE}); `Infinity` (an explicit
79
+ * `unlimited`) disables the cap.
80
+ */
81
+ maxCollectionsPerSpace?: number
82
+ /**
83
+ * Max live Resources a single Space may hold across all its Collections (spec
84
+ * "Quotas", a default-on count quota); applied only to the default backend
85
+ * (an injected `backend` carries its own). `undefined` applies the backend's
86
+ * default ({@link DEFAULT_MAX_RESOURCES_PER_SPACE}); `Infinity` (an explicit
87
+ * `unlimited`) disables the cap.
88
+ */
89
+ maxResourcesPerSpace?: number
90
+ /**
91
+ * The provider-adapter registry the resolver uses to build a Collection's
92
+ * selected external backend; defaults to an empty map (no external backend
93
+ * is operable).
94
+ */
95
+ providers?: BackendProviderRegistry
96
+ /**
97
+ * The registration allowlist of backend `provider` names; `undefined` means
98
+ * permissive.
99
+ */
100
+ enabledBackendProviders?: string[]
101
+ /**
102
+ * The at-rest WebKMS key-record encryption registry (config `KMS_RECORD_KEK` /
103
+ * `KMS_RECORD_KEKS` / `KMS_RECORD_CURRENT_KEK`); `undefined` (or
104
+ * `currentKekId: null`) disables encryption -- key records are written
105
+ * plaintext (the teaching default).
106
+ */
107
+ kmsRecordKek?: KmsRecordKekRegistry
108
+ /**
109
+ * Custom provisioning gate for `POST /spaces/` and `POST /kms/keystores`;
110
+ * receives `{ request }` and returns `'verify'` (normal zcap path), `'grant'`
111
+ * (authorized by the callback -- skip zcap verification), or `'deny'` (403).
112
+ * `undefined` means allow (the teaching default). Mutually exclusive with
113
+ * `onboardingToken`.
114
+ */
115
+ authorizeProvisioning?: AuthorizeProvisioning
116
+ /**
117
+ * Shared-secret gate for `POST /spaces/` and `POST /kms/keystores` (config
118
+ * `WAS_ONBOARDING_TOKEN`); when set, those two endpoints require an
119
+ * `Authorization: Bearer <token>` header, which then substitutes for zcap
120
+ * verification on that request. `undefined` means disabled (the teaching
121
+ * default). Mutually exclusive with `authorizeProvisioning`.
122
+ */
123
+ onboardingToken?: string
124
+ }
125
+
126
+ /**
127
+ * Decorates the instance with the WAS storage/config surface, registers the
128
+ * protocol-level plugins and content-type parsers, and mounts the route groups.
129
+ * @param fastify {import('fastify').FastifyInstance}
130
+ * @param options {FastifyWasOptions}
131
+ * @returns {Promise<void>}
132
+ */
133
+ async function wasPlugin(
134
+ fastify: FastifyInstance,
135
+ options: FastifyWasOptions
136
+ ): Promise<void> {
137
+ const {
138
+ serverUrl,
139
+ backend,
140
+ storageLimitPerSpace,
141
+ maxUploadBytes,
142
+ maxSpacesPerController,
143
+ maxCollectionsPerSpace,
144
+ maxResourcesPerSpace,
145
+ providers,
146
+ enabledBackendProviders,
147
+ kmsRecordKek,
148
+ authorizeProvisioning,
149
+ onboardingToken
150
+ } = options
151
+
152
+ // Fail fast on a malformed base URL: a serverUrl carrying a path, query, or
153
+ // fragment silently breaks every ZCap invocationTarget match and Location
154
+ // header (URL-joins drop the base path), so it is rejected at registration.
155
+ if (serverUrl !== undefined) {
156
+ assertValidServerUrl(serverUrl)
157
+ }
158
+
159
+ // The two provisioning gates are alternative ways to configure the same seam.
160
+ if (authorizeProvisioning && onboardingToken) {
161
+ throw new Error(
162
+ 'authorizeProvisioning and onboardingToken are mutually exclusive.'
163
+ )
164
+ }
165
+
166
+ fastify.decorate('serverUrl', serverUrl as string)
167
+ // Route the backend's diagnostics through the Fastify pino logger (the backend
168
+ // defaults to a silent logger until wired here).
169
+ const storage =
170
+ backend ??
171
+ defaultBackend({
172
+ capacityBytes: storageLimitPerSpace,
173
+ maxUploadBytes,
174
+ maxSpacesPerController,
175
+ maxCollectionsPerSpace,
176
+ maxResourcesPerSpace
177
+ })
178
+ storage.logger = fastify.log
179
+ fastify.decorate('storage', storage)
180
+
181
+ // Backend lifecycle: run the optional startup hook (e.g. Postgres connect +
182
+ // migrations) during registration, before the server starts listening, and
183
+ // wire the optional shutdown hook (pool drain) to Fastify's close.
184
+ if (storage.init) {
185
+ await storage.init()
186
+ }
187
+ if (storage.close) {
188
+ fastify.addHook('onClose', async () => {
189
+ await storage.close!()
190
+ })
191
+ }
192
+
193
+ // The provider-adapter registry the resolver (lib/backendRegistry.ts) consults
194
+ // to build a Collection's selected external backend. Injected (rather than a
195
+ // module-global mutable registry) so parallel test suites stay isolated -- the
196
+ // same rationale as the injected `storage`. Empty in production this stage.
197
+ fastify.decorate('backendProviders', providers ?? new Map())
198
+ // The optional registration allowlist (config `WAS_ENABLED_BACKENDS`);
199
+ // `undefined` = permissive (any provider may be registered).
200
+ fastify.decorate('enabledBackendProviders', enabledBackendProviders)
201
+ // The at-rest key-record encryption registry (config `KMS_RECORD_KEK`);
202
+ // `undefined` = disabled (records written plaintext). Read at the KMS
203
+ // orchestration seam (KeyRequest), never inside a backend.
204
+ fastify.decorate('kmsRecordKek', kmsRecordKek)
205
+ // The provisioning gate for `POST /spaces/` and `POST /kms/keystores`: a
206
+ // custom callback, or the stock onboarding-token check when a token is set,
207
+ // or `undefined` = allow (the teaching default). Read by the `provisioningGate`
208
+ // onRequest hook installed by those two route groups.
209
+ fastify.decorate(
210
+ 'authorizeProvisioning',
211
+ authorizeProvisioning ??
212
+ (onboardingToken ? onboardingTokenAuthorizer(onboardingToken) : undefined)
213
+ )
214
+
215
+ // Disable CORS
216
+ fastify.register(cors, {
217
+ origin: '*',
218
+ methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
219
+ })
220
+
221
+ // Multipart file uploading. The cap is `files: 2`, not `1`: a write MUST carry
222
+ // exactly one file part, and `resolveResourceInput` enforces that by iterating
223
+ // the parts and rejecting a second one with `invalid-request-body` (400). With
224
+ // a `files: 1` limit busboy would instead silently drop the second part and
225
+ // raise its own `FST_FILES_LIMIT` (413), so the second part must be allowed
226
+ // through to the iterator to be caught and rejected with the correct error.
227
+ //
228
+ // `fileSize` bounds the in-memory buffer of the single permitted part to the
229
+ // backend's per-upload cap (`throwFileSizeLimit` makes `toBuffer()` throw at
230
+ // the boundary, which the request layer maps to `payload-too-large` (413)) --
231
+ // so an oversize multipart upload is rejected before it is fully buffered.
232
+ // The cap is default-on (the backend applies `DEFAULT_MAX_UPLOAD_BYTES` when
233
+ // none is configured), so `storage.maxUploadBytes` is `undefined` here only
234
+ // when the operator explicitly opted out (`MAX_UPLOAD_BYTES=unlimited`); the
235
+ // conditional spread then leaves multipart uncapped. Large binaries should
236
+ // use the streaming raw-body path, not multipart.
237
+ fastify.register(Multipart, {
238
+ throwFileSizeLimit: true,
239
+ limits: {
240
+ files: 2,
241
+ ...(storage.maxUploadBytes !== undefined && {
242
+ fileSize: storage.maxUploadBytes
243
+ })
244
+ }
245
+ })
246
+
247
+ // Parse `application/<suffix>+json` bodies (e.g. `application/jose+json` for
248
+ // EDV-over-WAS encrypted documents, `application/ld+json`, etc.) as JSON, the
249
+ // same as plain `application/json`. Fastify's built-in JSON parser only
250
+ // matches `application/json` exactly, so structured-suffix JSON media types
251
+ // would otherwise be rejected with a 415. The regex deliberately requires a
252
+ // non-`+` suffix before `+json`, so it never shadows the built-in parser for
253
+ // plain `application/json`. Registered on the root instance so every route
254
+ // group inherits it; `isJson()` already treats `+json` as JSON downstream
255
+ // (digest capture, resource-input resolution).
256
+ fastify.addContentTypeParser(
257
+ /^application\/[^+]+\+json/,
258
+ { parseAs: 'string' },
259
+ fastify.getDefaultJsonParser('error', 'error')
260
+ )
261
+
262
+ // Catch-all parser for arbitrary binary representations. The spec ("Content
263
+ // Types and Representations") lets a Resource be any media type, so a raw
264
+ // (non-multipart) blob PUT/POST -- `application/octet-stream`,
265
+ // `application/jsonl`, images, etc. -- must reach the handler as a byte
266
+ // stream. Fastify only ships parsers for `application/json` and `text/plain`
267
+ // and would otherwise reject every other media type with a 415 before the
268
+ // route runs. This bare pass-through leaves `request.body` as the raw stream,
269
+ // which `resolveResourceInput` normalizes to a `kind: 'binary'` input that the
270
+ // backend streams straight to storage. More specific parsers still win over
271
+ // this fallback: the built-in JSON/text parsers, the `+json` regex above,
272
+ // `@fastify/multipart`'s `multipart/*`, and the `application/x-tar` import
273
+ // parser are all matched ahead of it.
274
+ fastify.addContentTypeParser('*', function (_request, payload, done) {
275
+ done(null, payload)
276
+ })
277
+
278
+ fastify.register(initSpacesRepositoryRoutes)
279
+ fastify.register(initSpaceRoutes)
280
+ fastify.register(initCollectionRoutes)
281
+ fastify.register(initResourceRoutes)
282
+ fastify.register(initKmsRoutes)
283
+ }
284
+
285
+ export const fastifyWas = fp(wasPlugin, {
286
+ fastify: '5.x',
287
+ name: 'fastify-was'
288
+ })
package/src/policy.ts ADDED
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Access-control policy evaluation: resolves the effective policy for a target
3
+ * (most-specific level wins) and decides whether a policy grants an action.
4
+ * Policies are the fallback authorization path -- consulted when a capability
5
+ * invocation is absent or does not grant access (see authorize.ts). v1
6
+ * recognizes only `{ "type": "PublicCanRead" }`; any other `type` grants nothing
7
+ * (fail-closed). Also builds the RFC9264 linkset that advertises a Space's or
8
+ * Collection's policy resource for discovery.
9
+ */
10
+ import type { FastifyBaseLogger } from 'fastify'
11
+ import type { PolicyDocument, StorageBackend } from './types.js'
12
+ import {
13
+ POLICY_LINK_RELATION,
14
+ BACKEND_LINK_RELATION,
15
+ QUOTA_LINK_RELATION,
16
+ BACKENDS_AVAILABLE_LINK_RELATION,
17
+ QUOTAS_LINK_RELATION
18
+ } from './config.default.js'
19
+ import {
20
+ collectionPath,
21
+ spacePath,
22
+ policyPath,
23
+ backendPath,
24
+ quotaPath,
25
+ backendsPath,
26
+ quotasPath
27
+ } from './lib/paths.js'
28
+
29
+ /** The kind of access a request needs; derived from the HTTP method. */
30
+ export type AccessAction = 'read' | 'write'
31
+
32
+ /**
33
+ * Resolves the policy that governs a target, honoring the spec's
34
+ * most-specific-wins inheritance: a Resource policy overrides a Collection
35
+ * policy, which overrides a Space policy. The first level that has a policy
36
+ * document is the effective policy; if none do, resolves undefined.
37
+ *
38
+ * @param options {object}
39
+ * @param options.storage {StorageBackend} the request's storage backend
40
+ * @param options.spaceId {string}
41
+ * @param [options.collectionId] {string}
42
+ * @param [options.resourceId] {string}
43
+ * @returns {Promise<PolicyDocument | undefined>}
44
+ */
45
+ export async function resolveEffectivePolicy({
46
+ storage,
47
+ spaceId,
48
+ collectionId,
49
+ resourceId
50
+ }: {
51
+ storage: StorageBackend
52
+ spaceId: string
53
+ collectionId?: string
54
+ resourceId?: string
55
+ }): Promise<PolicyDocument | undefined> {
56
+ if (collectionId !== undefined && resourceId !== undefined) {
57
+ const resourcePolicy = await storage.getPolicy({
58
+ spaceId,
59
+ collectionId,
60
+ resourceId
61
+ })
62
+ if (resourcePolicy) {
63
+ return resourcePolicy
64
+ }
65
+ }
66
+ if (collectionId !== undefined) {
67
+ const collectionPolicy = await storage.getPolicy({ spaceId, collectionId })
68
+ if (collectionPolicy) {
69
+ return collectionPolicy
70
+ }
71
+ }
72
+ return await storage.getPolicy({ spaceId })
73
+ }
74
+
75
+ /**
76
+ * Decides whether a policy document grants the requested action. Dispatches on
77
+ * the policy `type`; an absent policy or an unrecognized `type` grants nothing
78
+ * (fail-closed).
79
+ *
80
+ * @param options {object}
81
+ * @param [options.policy] {PolicyDocument} the effective policy, if any
82
+ * @param options.action {AccessAction}
83
+ * @param [options.logger] {FastifyBaseLogger} logger for the fail-closed warn
84
+ * on an unrecognized policy type
85
+ * @returns {boolean}
86
+ */
87
+ export function policyGrants({
88
+ policy,
89
+ action,
90
+ logger
91
+ }: {
92
+ policy?: PolicyDocument
93
+ action: AccessAction
94
+ logger?: FastifyBaseLogger
95
+ }): boolean {
96
+ if (!policy) {
97
+ return false
98
+ }
99
+ switch (policy.type) {
100
+ case 'PublicCanRead':
101
+ return action === 'read'
102
+ default:
103
+ // Unknown policy type: grant nothing, fall through to zcap-only decision.
104
+ // Warn so a misconfigured / unsupported policy is diagnosable (the
105
+ // fail-closed behavior itself is correct; this is purely a signal).
106
+ logger?.warn(
107
+ { policyType: policy.type },
108
+ 'Unrecognized access-control policy type; granting nothing (fail-closed).'
109
+ )
110
+ return false
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Builds the RFC9264 linkset for a Space or Collection, advertising its
116
+ * auxiliary resources for discovery. Collection vs Space is selected by whether
117
+ * `collectionId` is present.
118
+ *
119
+ * Relations:
120
+ * - `policy` (`POLICY_LINK_RELATION`) -- the access-control policy resource,
121
+ * advertised only when a policy document is set at that exact level.
122
+ * - On a Collection (`collectionId` present): `backend` (`BACKEND_LINK_RELATION`)
123
+ * and `quota` (`QUOTA_LINK_RELATION`), advertised unconditionally (every
124
+ * Collection has a selected backend and a quota report endpoint).
125
+ * - On a Space (`collectionId` absent): `backends-available`
126
+ * (`BACKENDS_AVAILABLE_LINK_RELATION`) and `quotas` (`QUOTAS_LINK_RELATION`),
127
+ * advertised unconditionally (both endpoints always exist).
128
+ *
129
+ * @param options {object}
130
+ * @param options.storage {StorageBackend} the request's storage backend
131
+ * @param options.spaceId {string}
132
+ * @param [options.collectionId] {string}
133
+ * @returns {Promise<object>} a `{ linkset: [...] }` object
134
+ */
135
+ export async function buildLinkset({
136
+ storage,
137
+ spaceId,
138
+ collectionId
139
+ }: {
140
+ storage: StorageBackend
141
+ spaceId: string
142
+ collectionId?: string
143
+ }): Promise<{ linkset: Array<Record<string, unknown>> }> {
144
+ const anchor =
145
+ collectionId !== undefined
146
+ ? collectionPath({ spaceId, collectionId })
147
+ : spacePath({ spaceId })
148
+ const policyHref = policyPath({ spaceId, collectionId })
149
+ const policy =
150
+ collectionId !== undefined
151
+ ? await storage.getPolicy({ spaceId, collectionId })
152
+ : await storage.getPolicy({ spaceId })
153
+
154
+ const entry: Record<string, unknown> = { anchor }
155
+ if (policy) {
156
+ entry[POLICY_LINK_RELATION] = [
157
+ { href: policyHref, type: 'application/json' }
158
+ ]
159
+ }
160
+ if (collectionId !== undefined) {
161
+ entry[BACKEND_LINK_RELATION] = [
162
+ { href: backendPath({ spaceId, collectionId }), type: 'application/json' }
163
+ ]
164
+ entry[QUOTA_LINK_RELATION] = [
165
+ { href: quotaPath({ spaceId, collectionId }), type: 'application/json' }
166
+ ]
167
+ } else {
168
+ entry[BACKENDS_AVAILABLE_LINK_RELATION] = [
169
+ { href: backendsPath({ spaceId }), type: 'application/json' }
170
+ ]
171
+ entry[QUOTAS_LINK_RELATION] = [
172
+ { href: quotasPath({ spaceId }), type: 'application/json' }
173
+ ]
174
+ }
175
+ return { linkset: [entry] }
176
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Provisioning gate for the two open provisioning endpoints (`POST /spaces/`
3
+ * and `POST /kms/keystores`). By default provisioning is allowed -- anyone may
4
+ * create a Space or keystore by proving control of the controller DID named in
5
+ * the request body (the teaching-server behavior). A deployment that wants to
6
+ * gate provisioning supplies either an `onboardingToken` (a shared secret,
7
+ * checked here by the stock `onboardingTokenAuthorizer`) or a custom
8
+ * `authorizeProvisioning` callback; both flow through the same seam.
9
+ */
10
+ import type { FastifyReply, FastifyRequest } from 'fastify'
11
+ import { createHash, timingSafeEqual } from 'node:crypto'
12
+ import type { AuthorizeProvisioning, ProvisioningDecision } from './types.js'
13
+ import {
14
+ InvalidOnboardingTokenError,
15
+ MissingOnboardingTokenError,
16
+ ProvisioningNotAuthorizedError
17
+ } from './errors.js'
18
+
19
+ /**
20
+ * The exact route URLs the provisioning gate acts on. `/spaces` (no trailing
21
+ * slash) is included so a token-authorized request reaches the canonical-slash
22
+ * 308 redirect instead of failing the auth-header check first.
23
+ */
24
+ const PROVISIONING_ROUTES = new Set(['/spaces', '/spaces/', '/kms/keystores'])
25
+
26
+ /**
27
+ * onRequest hook: the provisioning gate. Runs first in the hook chain of the
28
+ * SpacesRepository and `/kms` route groups. For a `POST` to one of the two
29
+ * provisioning endpoints it consults the configured `authorizeProvisioning`
30
+ * callback: `verify` proceeds to normal zcap verification, `grant` marks the
31
+ * request as provisioning-authorized (skipping zcap verification downstream),
32
+ * and `deny` refuses with a 403. Every other request (and every deployment with
33
+ * no callback configured) passes straight through, so the default zcap path is
34
+ * unchanged.
35
+ * @param request {import('fastify').FastifyRequest}
36
+ * @param reply {import('fastify').FastifyReply}
37
+ * @returns {Promise<void>}
38
+ */
39
+ export async function provisioningGate(
40
+ request: FastifyRequest,
41
+ _reply: FastifyReply
42
+ ): Promise<void> {
43
+ // Only the two provisioning POST endpoints are gated; everything else passes.
44
+ if (
45
+ request.method !== 'POST' ||
46
+ !PROVISIONING_ROUTES.has(request.routeOptions.url ?? '')
47
+ ) {
48
+ return
49
+ }
50
+ const authorize = request.server.authorizeProvisioning
51
+ // No policy configured: default allow -- the zcap path is unchanged.
52
+ if (!authorize) {
53
+ return
54
+ }
55
+ const decision = await authorize({ request })
56
+ if (decision === 'grant') {
57
+ request.provisioningAuthorized = true
58
+ return
59
+ }
60
+ if (decision === 'deny') {
61
+ throw new ProvisioningNotAuthorizedError()
62
+ }
63
+ // 'verify': fall through to the normal zcap capability-invocation path.
64
+ }
65
+
66
+ /**
67
+ * Builds the stock onboarding-token provisioning authorizer: a shared-secret
68
+ * gate that requires an `Authorization: Bearer <token>` header matching the
69
+ * configured token. A request signed with a zcap `Authorization: Signature ...`
70
+ * also lands here -- with a token configured, provisioning requires the token,
71
+ * full stop. Throws `MissingOnboardingTokenError` (401) when no Bearer token is
72
+ * present and `InvalidOnboardingTokenError` (403) when it does not match; a
73
+ * match returns `grant`.
74
+ * @param onboardingToken {string} the configured shared-secret token
75
+ * @returns {AuthorizeProvisioning}
76
+ */
77
+ export function onboardingTokenAuthorizer(
78
+ onboardingToken: string
79
+ ): AuthorizeProvisioning {
80
+ return async function authorizeWithOnboardingToken({
81
+ request
82
+ }): Promise<ProvisioningDecision> {
83
+ const header = request.headers.authorization
84
+ const match = header?.match(/^Bearer\s+(.+)$/i)
85
+ if (!match) {
86
+ throw new MissingOnboardingTokenError()
87
+ }
88
+ const presented = match[1] as string
89
+ // Timing-safe comparison over fixed-length SHA-256 digests, so mismatched
90
+ // token lengths do not leak through `timingSafeEqual`'s length check.
91
+ const presentedDigest = createHash('sha256').update(presented).digest()
92
+ const expectedDigest = createHash('sha256').update(onboardingToken).digest()
93
+ if (!timingSafeEqual(presentedDigest, expectedDigest)) {
94
+ throw new InvalidOnboardingTokenError()
95
+ }
96
+ return 'grant'
97
+ }
98
+ }