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/README.md ADDED
@@ -0,0 +1,305 @@
1
+ # WAS Teaching Server (_was-teaching-server_)
2
+
3
+ [![Node.js CI](https://github.com/interop-alliance/was-teaching-server/workflows/CI/badge.svg)](https://github.com/interop-alliance/was-teaching-server/actions?query=workflow%3A%22CI%22)
4
+
5
+ > A basic
6
+ > [Wallet Attached Storage](https://digitalcredentials.github.io/wallet-attached-storage-spec/)
7
+ > Server used to demonstrate the specification
8
+
9
+ ## Table of Contents
10
+
11
+ - [Background](#background)
12
+ - [Install](#install)
13
+ - [Usage](#usage)
14
+ - [Security](#security)
15
+ - [License](#license)
16
+
17
+ ## Background
18
+
19
+ See:
20
+
21
+ - [Wallet Attached Storage Specification](https://digitalcredentials.github.io/wallet-attached-storage-spec/)
22
+
23
+ ### Supported Features
24
+
25
+ Authorization: Uses `ZcapClient` from `@interop/ezcap`, see the
26
+ [zCap Developer Guide](https://github.com/interop-alliance/zcap-developer-guide)
27
+ for more details.
28
+
29
+ #### Spaces API
30
+
31
+ - [Create Space](https://digitalcredentials.github.io/wallet-attached-storage-spec/#http-api-post-spaces)
32
+ (`POST /spaces/`)
33
+ - [Get Space by Id](https://digitalcredentials.github.io/wallet-attached-storage-spec/#read-space-operation)
34
+ (`GET /space/:spaceId`)
35
+ - [Update Space (or Create Space by Id)](https://digitalcredentials.github.io/wallet-attached-storage-spec/#update-space-operation)
36
+ (`PUT /space/:spaceId`)
37
+ - [Delete Space by Id](https://digitalcredentials.github.io/wallet-attached-storage-spec/#delete-space-operation)
38
+ (`DELETE /space/:spaceId`)
39
+
40
+ #### Collections API
41
+
42
+ - [Create Collection](https://digitalcredentials.github.io/wallet-attached-storage-spec/#create-collection-add-collection-to-a-space-operation)
43
+ (`POST /space/:spaceId/`)
44
+ - [Update Collection (or Create Collection by Id)](https://digitalcredentials.github.io/wallet-attached-storage-spec/#update-or-create-by-id-collection-operation)
45
+ (`PUT /space/:spaceId/:collectionId`)
46
+ - [Get a Collection Description object](https://digitalcredentials.github.io/wallet-attached-storage-spec/#get-collection-description-operation)
47
+ (`GET /space/:spaceId/:collectionId` - no trailing slash)
48
+ - [List Resources in a Collection](https://digitalcredentials.github.io/wallet-attached-storage-spec/#get-collection-operation)
49
+ (`GET /space/:spaceId/:collectionId/` - with trailing slash)
50
+ - [Delete Collection by Id](https://digitalcredentials.github.io/wallet-attached-storage-spec/#delete-collection-operation)
51
+ (`DELETE /space/:spaceId/:collectionId`)
52
+
53
+ #### Resources API
54
+
55
+ - [Create Resource](https://digitalcredentials.github.io/wallet-attached-storage-spec/#create-resource-add-resource-to-collection-operation)
56
+ (`POST /space/:spaceId/:collectionId/`)
57
+ - both JSON objects and binary blobs
58
+ - [Get Resource by Id](https://digitalcredentials.github.io/wallet-attached-storage-spec/#read-resource-operation)
59
+ (`GET /space/:spaceId/:collectionId/:resourceId`)
60
+ - [Head Resource by Id](https://digitalcredentials.github.io/wallet-attached-storage-spec/#content-types-and-representations)
61
+ (`HEAD /space/:spaceId/:collectionId/:resourceId`)
62
+ - same headers as `GET` with no body; `Content-Type`/`Content-Length` from the
63
+ Resource's Metadata
64
+ - [Update Resource (or Create Resource by Id)](https://digitalcredentials.github.io/wallet-attached-storage-spec/#update-or-create-by-id-resource-operation)
65
+ (`PUT /space/:spaceId/:collectionId/:resourceId`)
66
+ - [Delete Resource by Id](https://digitalcredentials.github.io/wallet-attached-storage-spec/#delete-resource-operation)
67
+ (`DELETE /space/:spaceId/:collectionId/:resourceId`)
68
+
69
+ **Uploading binary resources (and large files).** Send the file as the **raw
70
+ request body** with its own `Content-Type` (e.g. `image/png`) -- not wrapped in
71
+ a form. The server streams a raw-body upload straight to storage (enforcing the
72
+ upload cap as it goes), so this is the path for large files. The write must
73
+ still be zCap-signed, and because the signature covers the `Digest` header, the
74
+ client hashes the whole body before sending (a plain browser `<form>` cannot do
75
+ this -- uploads are made programmatically by a signing client). The OPTIONAL
76
+ `multipart/form-data` upload is supported as a convenience for the HTML-form
77
+ workflow: it MUST carry exactly one file part, and the server buffers that part
78
+ in memory bounded by `MAX_UPLOAD_BYTES`, so it is best suited to smaller
79
+ uploads.
80
+
81
+ #### Access Control Policy (public read)
82
+
83
+ By default every operation requires a capability invocation. A **policy**
84
+ auxiliary resource can override this to make a Space, Collection, or Resource
85
+ world-readable — the common "create public link" use case (e.g. a wallet sharing
86
+ a Verifiable Credential).
87
+
88
+ - CRUD the policy at any level (controller-only):
89
+ - `GET|PUT|DELETE /space/:spaceId/policy`
90
+ - `GET|PUT|DELETE /space/:spaceId/:collectionId/policy`
91
+ - `GET|PUT|DELETE /space/:spaceId/:collectionId/:resourceId/policy`
92
+ - Discover a policy via the `linkset` property on the Space/Collection
93
+ Description, or `GET /space/:spaceId[/:collectionId]/linkset`
94
+ (`application/linkset+json`, RFC9264).
95
+
96
+ How a read is authorized: the server tries the capability invocation first; if
97
+ none is presented, or it does not grant access, it falls back to the **effective
98
+ policy** (resolved most-specific-first: Resource > Collection > Space). Policies
99
+ are **permissive-only** — they can only broaden access beyond capabilities,
100
+ never deny a valid capability holder. A denied read returns `404` (so existence
101
+ is not leaked); writes always require a capability.
102
+
103
+ The policy document is a small, `type`-discriminated, extensible shape. v1
104
+ recognizes one type (any other `type` is treated as "grants nothing", so it
105
+ falls through to the capability-only decision):
106
+
107
+ ```json
108
+ { "type": "PublicCanRead" }
109
+ ```
110
+
111
+ Set it, for example, on a `public-credentials` Collection so anyone can
112
+ `GET /space/:spaceId/public-credentials/:resourceId` without authorization. This
113
+ minimal canned-policy approach (cf. S3 `public-read`, `chmod o+r`) covers the
114
+ dominant public-read case; a richer policy language (e.g. Cedar) is left as a
115
+ future, separate tier.
116
+
117
+ ## Install
118
+
119
+ Requires Node.js 24.x and [`pnpm`](https://pnpm.io/).
120
+
121
+ ```
122
+ pnpm install
123
+ ```
124
+
125
+ This server is written in TypeScript. In development it runs directly via
126
+ [`tsx`](https://github.com/privatenumber/tsx) (no build step); for production it
127
+ is compiled with `tsc` to `dist/`.
128
+
129
+ ## Usage
130
+
131
+ ### Running in Development
132
+
133
+ `tsx` runs the TypeScript sources directly and watches for changes (views are
134
+ served from `src/views`, so no asset copy is needed):
135
+
136
+ ```
137
+ SERVER_URL='http://localhost:3002' PORT=3002 pnpm dev
138
+ ```
139
+
140
+ ### Building and Starting the Server
141
+
142
+ `pnpm build` compiles `src/` to `dist/` and copies `src/views` > `dist/views`,
143
+ then `pnpm start` runs the compiled output:
144
+
145
+ ```
146
+ pnpm build
147
+ SERVER_URL='http://localhost:3002' PORT=3002 pnpm start
148
+ ```
149
+
150
+ ### Using as a Library
151
+
152
+ The WAS protocol surface is also exported as a registerable Fastify plugin,
153
+ `fastifyWas`, so a downstream server can compose it with its own persistence
154
+ backend and security plugins. See
155
+ [Consuming the Server as a Library](docs/consuming-server-as-library.md).
156
+
157
+ ### Environment Variables
158
+
159
+ The whole env surface below is read and validated once at startup
160
+ (`loadConfigFromEnv` in `src/config.default.ts`): a missing `SERVER_URL` or any
161
+ malformed value fails startup immediately with the offending variable named,
162
+ rather than surfacing later as broken ZCap matching.
163
+
164
+ | Variable | Default | Description |
165
+ | --------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
166
+ | `SERVER_URL` | (none) | **Required.** This server's base URL; used to build and match ZCap `invocationTarget` URLs (host and port must match the client's exactly). Must be an absolute `http:`/`https:` URL with no path, query, or fragment -- deploying under a sub-path is not supported. |
167
+ | `PORT` | `3002` | TCP port to listen on (an integer between 1 and 65535). |
168
+ | `DATABASE_URL` | (unset = filesystem) | Selects the **PostgreSQL storage backend**: a `postgres://` connection string (e.g. `postgres://was:was@localhost:5433/was`). When set, all WAS + WebKMS data is stored in Postgres (schema migrations are applied automatically at startup); unset, the server uses the default filesystem backend rooted at `data/`. See [Storage Backends](#storage-backends). |
169
+ | `STORAGE_LIMIT_PER_SPACE` | (unset = unlimited, warned) | Per-Space storage quota in **bytes** (spec "Quotas"). When set, writes that would push a Space over this limit are rejected with `quota-exceeded` (507). Unset means each Space is unlimited, and startup logs a warning; set `unlimited` to acknowledge the choice explicitly and silence it. |
170
+ | `MAX_UPLOAD_BYTES` | `67108864` (64 MiB) | Per-upload size cap in **bytes** (spec "Quotas", the backend's `maxUploadBytes` constraint), default-on. A single upload exceeding it is rejected with `payload-too-large` (413) -- distinct from the cumulative `STORAGE_LIMIT_PER_SPACE`. Set `unlimited` to remove the cap (filesystem backend only: the Postgres backend buffers each upload in memory as a single `bytea`, so it rejects `unlimited` at startup). |
171
+ | `MAX_SPACES_PER_CONTROLLER` | `100` | Max number of Spaces a single controller DID may create (a default-on count quota). A create beyond the limit is rejected with `quota-exceeded` (507); overwriting an existing Space never trips it. Set `unlimited` for no cap. Hard (transactional) on the Postgres backend, soft under concurrency on the filesystem backend -- like the byte quota. |
172
+ | `MAX_COLLECTIONS_PER_SPACE` | `100` | Max number of Collections one Space may hold (a default-on count quota, also enforced during a tar import). A create beyond the limit is rejected with `quota-exceeded` (507); overwriting an existing Collection's description never trips it. Set `unlimited` for no cap. |
173
+ | `MAX_RESOURCES_PER_SPACE` | `10000` | Max number of live Resources one Space may hold across all its Collections (a default-on count quota, also enforced during a tar import; tombstoned Resources do not count). A create beyond the limit is rejected with `quota-exceeded` (507); overwriting an existing Resource never trips it, and a delete frees a slot. Set `unlimited` for no cap. |
174
+ | `KMS_RECORD_KEK` | (unset = disabled) | At-rest encryption key for WebKMS key records: a single AES-256 key-encryption key (KEK) in base58btc Multikey form (`secretKeyMultibase`, header `0xa2 0x01`) -- the single-KEK alias. When set, the secret fields of newly generated `/kms` key records are envelope-encrypted under it before they reach storage; existing plaintext records stay readable. Unset means key records are stored **plaintext** (the teaching default). Mutually exclusive with `KMS_RECORD_KEKS`. |
175
+ | `KMS_RECORD_KEKS` | (unset = disabled) | A comma-separated list of AES-256 KEKs (same Multikey form as `KMS_RECORD_KEK`), for registering more than one KEK so a rotation can add a new KEK while keeping the old one available to decrypt records already written under it. Each KEK is registered by an id derived from its material; surrounding whitespace is trimmed and empty entries ignored; a duplicate entry is a startup error. The **first entry is the current KEK** (the one that wraps new records) by default -- so a rotation is "prepend the new KEK, keep the old behind it". Use `KMS_RECORD_KEK` for the single-KEK case; setting both is a startup error. |
176
+ | `KMS_RECORD_CURRENT_KEK` | (unset = first entry) | Optionally overrides which registered KEK wraps **new** records: a `urn:kek:sha256:<hex>` id, a multibase KEK value (its id is derived), or the literal `none` (case-insensitive). It must name a KEK registered via `KMS_RECORD_KEK`/`KMS_RECORD_KEKS`. `none` is the **decrypt-only** posture -- existing encrypted records still read, but new key records are written **plaintext** (the wind-down path). Setting it with no KEK configured is a startup error. |
177
+ | `WAS_ONBOARDING_TOKEN` | (unset = disabled) | Shared-secret onboarding token gating the two open provisioning endpoints (`POST /spaces/` and `POST /kms/keystores`). When set, those two endpoints require an `Authorization: Bearer <token>` header, which then substitutes for ZCap verification on that request; every other operation still uses the normal capability-invocation path. Unset means provisioning is open -- anyone may create a Space or keystore by proving control of the `controller` DID in the request body (the teaching default). See [Provisioning gate](#provisioning-gate). |
178
+
179
+ ### Provisioning gate
180
+
181
+ By default, anyone may create a Space (`POST /spaces/`) or a WebKMS keystore
182
+ (`POST /kms/keystores`) by proving control of the `controller` DID named in the
183
+ request body -- the open, teaching-server behavior. A deployment can gate those
184
+ two endpoints in one of two mutually-exclusive ways:
185
+
186
+ - **Onboarding token** -- set `WAS_ONBOARDING_TOKEN` (or pass `onboardingToken`
187
+ to the `fastifyWas` plugin / `createApp`). Provisioning then requires an
188
+ `Authorization: Bearer <token>` header matching the configured secret; a valid
189
+ token substitutes for ZCap verification on that request.
190
+ - **Custom policy** -- pass an `authorizeProvisioning` callback to the plugin.
191
+ It receives `{ request }` and returns `'verify'` (run the normal ZCap path),
192
+ `'grant'` (authorize the request itself, skipping ZCap verification), or
193
+ `'deny'` (403); it may also throw a `ProblemError` for a custom response.
194
+
195
+ Both configure the same seam; setting both at once is rejected at startup.
196
+
197
+ ### Storage Backends
198
+
199
+ Two interchangeable first-party backends implement the same `StorageBackend`
200
+ contract (`src/types.ts`):
201
+
202
+ - **Filesystem** (default): stores everything under `data/`. Zero setup.
203
+ - **PostgreSQL**: selected by setting `DATABASE_URL`. Quota accounting is
204
+ transactional (a _hard_ per-Space limit, unlike the filesystem's documented
205
+ soft limit), conditional writes use row locks instead of an in-process mutex
206
+ (so multiple server processes can share one database), and blob uploads are
207
+ buffered `bytea` writes bounded by `MAX_UPLOAD_BYTES` (default 64 MiB;
208
+ `MAX_UPLOAD_BYTES=unlimited` is rejected at startup, since the in-memory
209
+ buffering makes an unbounded upload unsafe).
210
+
211
+ To run a disposable local Postgres with Podman (substitute `docker` if you
212
+ prefer; the commands are identical):
213
+
214
+ ```bash
215
+ # One-time: pull and start Postgres 17 with a named volume for data.
216
+ # Host port 5433 avoids colliding with any system Postgres on 5432.
217
+ podman run -d --name was-postgres \
218
+ -e POSTGRES_USER=was \
219
+ -e POSTGRES_PASSWORD=was \
220
+ -e POSTGRES_DB=was \
221
+ -p 5433:5432 \
222
+ -v was-pgdata:/var/lib/postgresql/data \
223
+ docker.io/library/postgres:17
224
+
225
+ # Verify it is accepting connections:
226
+ podman exec was-postgres pg_isready -U was
227
+
228
+ # Day-to-day lifecycle:
229
+ podman stop was-postgres
230
+ podman start was-postgres
231
+
232
+ # Full teardown (drops all stored data):
233
+ podman rm -f was-postgres && podman volume rm was-pgdata
234
+ ```
235
+
236
+ Then start the server against it:
237
+
238
+ ```bash
239
+ DATABASE_URL=postgres://was:was@localhost:5433/was pnpm dev
240
+ ```
241
+
242
+ Schema migrations are embedded (`src/backends/postgresSchema.ts`) and applied
243
+ idempotently at startup. To migrate existing data between backends, export each
244
+ Space from one server and import it into the other — both backends speak the
245
+ same tar archive dialect (`POST /space/:id/export` / `.../import`).
246
+
247
+ ### Running Tests
248
+
249
+ The full gate (lint + build + Vitest integration tests):
250
+
251
+ ```
252
+ pnpm test
253
+ ```
254
+
255
+ Just the Vitest integration suite under `test/`:
256
+
257
+ ```
258
+ pnpm test-node
259
+ ```
260
+
261
+ The Postgres backend tests are **opt-in**: they are skipped unless
262
+ `WAS_TEST_DATABASE_URL` points at a running (disposable) Postgres. With the
263
+ Podman container above running:
264
+
265
+ ```
266
+ pnpm test:pg
267
+ ```
268
+
269
+ Each suite isolates itself in a throwaway `was_test_<hex>` schema, dropped on
270
+ teardown, so parallel test workers never collide and the container stays
271
+ reusable.
272
+
273
+ ### Conformance Tests
274
+
275
+ The `conformance/` suite runs against any external WAS server. The server's
276
+ `SERVER_URL` and the test's `TEST_SERVER_URL` **must be byte-for-byte
277
+ identical** (including host and port) — ZCap `invocationTarget` URLs embed the
278
+ full host:port, so even `localhost` vs `127.0.0.1` or a port mismatch will make
279
+ delegated-access tests 404.
280
+
281
+ For a local run, `pnpm conformance:local` does the whole dance for you: it spins
282
+ up the server on a fixed local URL, waits until it answers, runs the suite with
283
+ a matching `TEST_SERVER_URL`, and tears the server down afterward (even if the
284
+ suite fails) — so the two URLs can't drift out of sync.
285
+
286
+ ```bash
287
+ # One-shot local run (recommended). Override the port with PORT=... if 3002 is
288
+ # taken; the server and test URLs are both derived from it, so they stay in sync.
289
+ pnpm conformance:local
290
+
291
+ # Against an already-running or external server. Start it with a matching
292
+ # SERVER_URL, then in another shell:
293
+ TEST_SERVER_URL=https://was.example.com pnpm conformance
294
+
295
+ # With an onboarding token, for servers that require one for POST /spaces/:
296
+ TEST_SERVER_URL=https://was.example.com TEST_ONBOARDING_TOKEN=abc123 pnpm conformance
297
+ ```
298
+
299
+ ## Security
300
+
301
+ This is an experimental research server.
302
+
303
+ ## License
304
+
305
+ [GNU AFFERO GENERAL PUBLIC LICENSE v3](LICENSE)