was-teaching-server 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/CHANGELOG.md +1432 -0
  2. package/LICENSE +661 -0
  3. package/README.md +305 -0
  4. package/common/css/materialize.1.0.0.min.css +13 -0
  5. package/dist/auth-header-hooks.d.ts +84 -0
  6. package/dist/auth-header-hooks.d.ts.map +1 -0
  7. package/dist/auth-header-hooks.js +136 -0
  8. package/dist/auth-header-hooks.js.map +1 -0
  9. package/dist/authorize.d.ts +45 -0
  10. package/dist/authorize.d.ts.map +1 -0
  11. package/dist/authorize.js +76 -0
  12. package/dist/authorize.js.map +1 -0
  13. package/dist/backends/filesystem.d.ts +1179 -0
  14. package/dist/backends/filesystem.d.ts.map +1 -0
  15. package/dist/backends/filesystem.js +2727 -0
  16. package/dist/backends/filesystem.js.map +1 -0
  17. package/dist/backends/postgres.d.ts +833 -0
  18. package/dist/backends/postgres.d.ts.map +1 -0
  19. package/dist/backends/postgres.js +2164 -0
  20. package/dist/backends/postgres.js.map +1 -0
  21. package/dist/backends/postgresSchema.d.ts +36 -0
  22. package/dist/backends/postgresSchema.d.ts.map +1 -0
  23. package/dist/backends/postgresSchema.js +203 -0
  24. package/dist/backends/postgresSchema.js.map +1 -0
  25. package/dist/config.default.d.ts +360 -0
  26. package/dist/config.default.d.ts.map +1 -0
  27. package/dist/config.default.js +493 -0
  28. package/dist/config.default.js.map +1 -0
  29. package/dist/corsProxy.d.ts +45 -0
  30. package/dist/corsProxy.d.ts.map +1 -0
  31. package/dist/corsProxy.js +278 -0
  32. package/dist/corsProxy.js.map +1 -0
  33. package/dist/digest.d.ts +45 -0
  34. package/dist/digest.d.ts.map +1 -0
  35. package/dist/digest.js +174 -0
  36. package/dist/digest.js.map +1 -0
  37. package/dist/errors.d.ts +654 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +960 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/index.d.ts +22 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +21 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/lib/atomicFile.d.ts +61 -0
  46. package/dist/lib/atomicFile.d.ts.map +1 -0
  47. package/dist/lib/atomicFile.js +150 -0
  48. package/dist/lib/atomicFile.js.map +1 -0
  49. package/dist/lib/backendRegistry.d.ts +59 -0
  50. package/dist/lib/backendRegistry.d.ts.map +1 -0
  51. package/dist/lib/backendRegistry.js +92 -0
  52. package/dist/lib/backendRegistry.js.map +1 -0
  53. package/dist/lib/backendUsage.d.ts +28 -0
  54. package/dist/lib/backendUsage.d.ts.map +1 -0
  55. package/dist/lib/backendUsage.js +55 -0
  56. package/dist/lib/backendUsage.js.map +1 -0
  57. package/dist/lib/backends.d.ts +142 -0
  58. package/dist/lib/backends.d.ts.map +1 -0
  59. package/dist/lib/backends.js +272 -0
  60. package/dist/lib/backends.js.map +1 -0
  61. package/dist/lib/blindedIndex.d.ts +156 -0
  62. package/dist/lib/blindedIndex.d.ts.map +1 -0
  63. package/dist/lib/blindedIndex.js +338 -0
  64. package/dist/lib/blindedIndex.js.map +1 -0
  65. package/dist/lib/collectionDescription.d.ts +33 -0
  66. package/dist/lib/collectionDescription.d.ts.map +1 -0
  67. package/dist/lib/collectionDescription.js +21 -0
  68. package/dist/lib/collectionDescription.js.map +1 -0
  69. package/dist/lib/cursor.d.ts +18 -0
  70. package/dist/lib/cursor.d.ts.map +1 -0
  71. package/dist/lib/cursor.js +50 -0
  72. package/dist/lib/cursor.js.map +1 -0
  73. package/dist/lib/edvEnvelope.d.ts +50 -0
  74. package/dist/lib/edvEnvelope.d.ts.map +1 -0
  75. package/dist/lib/edvEnvelope.js +106 -0
  76. package/dist/lib/edvEnvelope.js.map +1 -0
  77. package/dist/lib/encryption.d.ts +192 -0
  78. package/dist/lib/encryption.d.ts.map +1 -0
  79. package/dist/lib/encryption.js +383 -0
  80. package/dist/lib/encryption.js.map +1 -0
  81. package/dist/lib/etag.d.ts +33 -0
  82. package/dist/lib/etag.d.ts.map +1 -0
  83. package/dist/lib/etag.js +36 -0
  84. package/dist/lib/etag.js.map +1 -0
  85. package/dist/lib/exportManifest.d.ts +29 -0
  86. package/dist/lib/exportManifest.d.ts.map +1 -0
  87. package/dist/lib/exportManifest.js +77 -0
  88. package/dist/lib/exportManifest.js.map +1 -0
  89. package/dist/lib/importTar.d.ts +81 -0
  90. package/dist/lib/importTar.d.ts.map +1 -0
  91. package/dist/lib/importTar.js +301 -0
  92. package/dist/lib/importTar.js.map +1 -0
  93. package/dist/lib/isJson.d.ts +16 -0
  94. package/dist/lib/isJson.d.ts.map +1 -0
  95. package/dist/lib/isJson.js +17 -0
  96. package/dist/lib/isJson.js.map +1 -0
  97. package/dist/lib/keyEpoch.d.ts +38 -0
  98. package/dist/lib/keyEpoch.d.ts.map +1 -0
  99. package/dist/lib/keyEpoch.js +75 -0
  100. package/dist/lib/keyEpoch.js.map +1 -0
  101. package/dist/lib/keyedMutex.d.ts +28 -0
  102. package/dist/lib/keyedMutex.d.ts.map +1 -0
  103. package/dist/lib/keyedMutex.js +45 -0
  104. package/dist/lib/keyedMutex.js.map +1 -0
  105. package/dist/lib/kmsModule.d.ts +62 -0
  106. package/dist/lib/kmsModule.d.ts.map +1 -0
  107. package/dist/lib/kmsModule.js +327 -0
  108. package/dist/lib/kmsModule.js.map +1 -0
  109. package/dist/lib/kmsRecordCipher.d.ts +82 -0
  110. package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
  111. package/dist/lib/kmsRecordCipher.js +226 -0
  112. package/dist/lib/kmsRecordCipher.js.map +1 -0
  113. package/dist/lib/pagination.d.ts +19 -0
  114. package/dist/lib/pagination.d.ts.map +1 -0
  115. package/dist/lib/pagination.js +21 -0
  116. package/dist/lib/pagination.js.map +1 -0
  117. package/dist/lib/paths.d.ts +273 -0
  118. package/dist/lib/paths.d.ts.map +1 -0
  119. package/dist/lib/paths.js +270 -0
  120. package/dist/lib/paths.js.map +1 -0
  121. package/dist/lib/preconditions.d.ts +65 -0
  122. package/dist/lib/preconditions.d.ts.map +1 -0
  123. package/dist/lib/preconditions.js +112 -0
  124. package/dist/lib/preconditions.js.map +1 -0
  125. package/dist/lib/resourceFileName.d.ts +41 -0
  126. package/dist/lib/resourceFileName.d.ts.map +1 -0
  127. package/dist/lib/resourceFileName.js +58 -0
  128. package/dist/lib/resourceFileName.js.map +1 -0
  129. package/dist/lib/revocations.d.ts +49 -0
  130. package/dist/lib/revocations.d.ts.map +1 -0
  131. package/dist/lib/revocations.js +86 -0
  132. package/dist/lib/revocations.js.map +1 -0
  133. package/dist/lib/validateDid.d.ts +19 -0
  134. package/dist/lib/validateDid.d.ts.map +1 -0
  135. package/dist/lib/validateDid.js +35 -0
  136. package/dist/lib/validateDid.js.map +1 -0
  137. package/dist/lib/validateId.d.ts +66 -0
  138. package/dist/lib/validateId.d.ts.map +1 -0
  139. package/dist/lib/validateId.js +126 -0
  140. package/dist/lib/validateId.js.map +1 -0
  141. package/dist/plugin.d.ts +115 -0
  142. package/dist/plugin.d.ts.map +1 -0
  143. package/dist/plugin.js +134 -0
  144. package/dist/plugin.js.map +1 -0
  145. package/dist/policy.d.ts +78 -0
  146. package/dist/policy.d.ts.map +1 -0
  147. package/dist/policy.js +115 -0
  148. package/dist/policy.js.map +1 -0
  149. package/dist/provisioning.d.ts +38 -0
  150. package/dist/provisioning.d.ts.map +1 -0
  151. package/dist/provisioning.js +72 -0
  152. package/dist/provisioning.js.map +1 -0
  153. package/dist/requests/BackendRequest.d.ts +61 -0
  154. package/dist/requests/BackendRequest.d.ts.map +1 -0
  155. package/dist/requests/BackendRequest.js +152 -0
  156. package/dist/requests/BackendRequest.js.map +1 -0
  157. package/dist/requests/CollectionRequest.d.ts +234 -0
  158. package/dist/requests/CollectionRequest.d.ts.map +1 -0
  159. package/dist/requests/CollectionRequest.js +705 -0
  160. package/dist/requests/CollectionRequest.js.map +1 -0
  161. package/dist/requests/KeyRequest.d.ts +115 -0
  162. package/dist/requests/KeyRequest.d.ts.map +1 -0
  163. package/dist/requests/KeyRequest.js +468 -0
  164. package/dist/requests/KeyRequest.js.map +1 -0
  165. package/dist/requests/KeystoreRequest.d.ts +99 -0
  166. package/dist/requests/KeystoreRequest.d.ts.map +1 -0
  167. package/dist/requests/KeystoreRequest.js +247 -0
  168. package/dist/requests/KeystoreRequest.js.map +1 -0
  169. package/dist/requests/PolicyRequest.d.ts +53 -0
  170. package/dist/requests/PolicyRequest.d.ts.map +1 -0
  171. package/dist/requests/PolicyRequest.js +124 -0
  172. package/dist/requests/PolicyRequest.js.map +1 -0
  173. package/dist/requests/ResourceRequest.d.ts +134 -0
  174. package/dist/requests/ResourceRequest.d.ts.map +1 -0
  175. package/dist/requests/ResourceRequest.js +548 -0
  176. package/dist/requests/ResourceRequest.js.map +1 -0
  177. package/dist/requests/RevocationRequest.d.ts +67 -0
  178. package/dist/requests/RevocationRequest.d.ts.map +1 -0
  179. package/dist/requests/RevocationRequest.js +187 -0
  180. package/dist/requests/RevocationRequest.js.map +1 -0
  181. package/dist/requests/SpaceRequest.d.ts +204 -0
  182. package/dist/requests/SpaceRequest.d.ts.map +1 -0
  183. package/dist/requests/SpaceRequest.js +520 -0
  184. package/dist/requests/SpaceRequest.js.map +1 -0
  185. package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
  186. package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
  187. package/dist/requests/SpacesRepositoryRequest.js +159 -0
  188. package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
  189. package/dist/requests/collectionContext.d.ts +21 -0
  190. package/dist/requests/collectionContext.d.ts.map +1 -0
  191. package/dist/requests/collectionContext.js +30 -0
  192. package/dist/requests/collectionContext.js.map +1 -0
  193. package/dist/requests/controllerConsent.d.ts +58 -0
  194. package/dist/requests/controllerConsent.d.ts.map +1 -0
  195. package/dist/requests/controllerConsent.js +73 -0
  196. package/dist/requests/controllerConsent.js.map +1 -0
  197. package/dist/requests/keystoreContext.d.ts +68 -0
  198. package/dist/requests/keystoreContext.d.ts.map +1 -0
  199. package/dist/requests/keystoreContext.js +87 -0
  200. package/dist/requests/keystoreContext.js.map +1 -0
  201. package/dist/requests/resourceInput.d.ts +19 -0
  202. package/dist/requests/resourceInput.d.ts.map +1 -0
  203. package/dist/requests/resourceInput.js +102 -0
  204. package/dist/requests/resourceInput.js.map +1 -0
  205. package/dist/requests/spaceContext.d.ts +127 -0
  206. package/dist/requests/spaceContext.d.ts.map +1 -0
  207. package/dist/requests/spaceContext.js +201 -0
  208. package/dist/requests/spaceContext.js.map +1 -0
  209. package/dist/routes.d.ts +55 -0
  210. package/dist/routes.d.ts.map +1 -0
  211. package/dist/routes.js +306 -0
  212. package/dist/routes.js.map +1 -0
  213. package/dist/server.d.ts +20 -0
  214. package/dist/server.d.ts.map +1 -0
  215. package/dist/server.js +60 -0
  216. package/dist/server.js.map +1 -0
  217. package/dist/start.d.ts +7 -0
  218. package/dist/start.d.ts.map +1 -0
  219. package/dist/start.js +61 -0
  220. package/dist/start.js.map +1 -0
  221. package/dist/storage.d.ts +29 -0
  222. package/dist/storage.d.ts.map +1 -0
  223. package/dist/storage.js +47 -0
  224. package/dist/storage.js.map +1 -0
  225. package/dist/types.d.ts +915 -0
  226. package/dist/types.d.ts.map +1 -0
  227. package/dist/types.js +2 -0
  228. package/dist/types.js.map +1 -0
  229. package/dist/views/home.hbs +9 -0
  230. package/dist/views/templates/main.hbs +12 -0
  231. package/dist/zcap.d.ts +267 -0
  232. package/dist/zcap.d.ts.map +1 -0
  233. package/dist/zcap.js +478 -0
  234. package/dist/zcap.js.map +1 -0
  235. package/package.json +109 -0
  236. package/src/auth-header-hooks.ts +163 -0
  237. package/src/authorize.ts +117 -0
  238. package/src/backends/filesystem.ts +3626 -0
  239. package/src/backends/postgres.ts +3073 -0
  240. package/src/backends/postgresSchema.ts +231 -0
  241. package/src/config.default.ts +657 -0
  242. package/src/corsProxy.ts +326 -0
  243. package/src/declarations.d.ts +28 -0
  244. package/src/digest.ts +223 -0
  245. package/src/errors.ts +1122 -0
  246. package/src/index.ts +21 -0
  247. package/src/lib/atomicFile.ts +176 -0
  248. package/src/lib/backendRegistry.ts +139 -0
  249. package/src/lib/backendUsage.ts +79 -0
  250. package/src/lib/backends.ts +351 -0
  251. package/src/lib/blindedIndex.ts +466 -0
  252. package/src/lib/collectionDescription.ts +43 -0
  253. package/src/lib/cursor.ts +52 -0
  254. package/src/lib/edvEnvelope.ts +120 -0
  255. package/src/lib/encryption.ts +472 -0
  256. package/src/lib/etag.ts +40 -0
  257. package/src/lib/exportManifest.ts +115 -0
  258. package/src/lib/importTar.ts +381 -0
  259. package/src/lib/isJson.ts +18 -0
  260. package/src/lib/keyEpoch.ts +89 -0
  261. package/src/lib/keyedMutex.ts +45 -0
  262. package/src/lib/kmsModule.ts +451 -0
  263. package/src/lib/kmsRecordCipher.ts +292 -0
  264. package/src/lib/pagination.ts +22 -0
  265. package/src/lib/paths.ts +382 -0
  266. package/src/lib/preconditions.ts +145 -0
  267. package/src/lib/resourceFileName.ts +69 -0
  268. package/src/lib/revocations.ts +116 -0
  269. package/src/lib/validateDid.ts +41 -0
  270. package/src/lib/validateId.ts +148 -0
  271. package/src/plugin.ts +288 -0
  272. package/src/policy.ts +176 -0
  273. package/src/provisioning.ts +98 -0
  274. package/src/requests/BackendRequest.ts +219 -0
  275. package/src/requests/CollectionRequest.ts +899 -0
  276. package/src/requests/KeyRequest.ts +617 -0
  277. package/src/requests/KeystoreRequest.ts +325 -0
  278. package/src/requests/PolicyRequest.ts +169 -0
  279. package/src/requests/ResourceRequest.ts +649 -0
  280. package/src/requests/RevocationRequest.ts +267 -0
  281. package/src/requests/SpaceRequest.ts +686 -0
  282. package/src/requests/SpacesRepositoryRequest.ts +194 -0
  283. package/src/requests/collectionContext.ts +41 -0
  284. package/src/requests/controllerConsent.ts +108 -0
  285. package/src/requests/keystoreContext.ts +124 -0
  286. package/src/requests/resourceInput.ts +118 -0
  287. package/src/requests/spaceContext.ts +311 -0
  288. package/src/routes.ts +420 -0
  289. package/src/server.ts +69 -0
  290. package/src/start.ts +71 -0
  291. package/src/storage.ts +60 -0
  292. package/src/types.ts +1016 -0
  293. package/src/views/home.hbs +9 -0
  294. package/src/views/templates/main.hbs +12 -0
  295. package/src/zcap.ts +657 -0
@@ -0,0 +1,657 @@
1
+ /**
2
+ * Default configuration: spec and FEP documentation URLs referenced by the
3
+ * home page, error responses, and space-export manifests.
4
+ */
5
+ import fs from 'node:fs'
6
+ import path from 'node:path'
7
+ import { parseKekMultibase } from './lib/kmsRecordCipher.js'
8
+ import type { KmsRecordKekRegistry, RecordKek } from './types.js'
9
+
10
+ // package.json sits a level above both src/ (dev, via tsx) and dist/ (prod),
11
+ // so '../package.json' from import.meta.dirname resolves in either layout.
12
+ const packageJsonPath = path.join(import.meta.dirname, '..', 'package.json')
13
+
14
+ /** Server version, read from package.json at startup. */
15
+ export const SERVER_VERSION = JSON.parse(
16
+ fs.readFileSync(packageJsonPath, 'utf8')
17
+ ).version as string
18
+
19
+ /**
20
+ * Space Description cache (see src/requests/spaceContext.ts). The description is
21
+ * read on every authorized handler, so it is memoized per storage backend.
22
+ * Writes invalidate the entry explicitly; the short TTL is a backstop that also
23
+ * bounds staleness when several server processes share one storage backend (so
24
+ * one process's cache cannot serve another process's write indefinitely).
25
+ */
26
+ export const SPACE_DESCRIPTION_CACHE_TTL = 5_000 // milliseconds
27
+ /** Max number of Space Descriptions held per backend cache (LRU-bounded). */
28
+ export const SPACE_DESCRIPTION_CACHE_MAX = 1_000
29
+
30
+ /**
31
+ * Max number of resolved external-backend adapters held per provider-registry
32
+ * cache (see src/lib/backendRegistry.ts), LRU-bounded. One adapter instance is
33
+ * memoized per selected `{spaceId}/{backendId}` and reused across requests;
34
+ * record changes bust the entry explicitly (no TTL backstop is needed because
35
+ * the provider registry itself is fixed for an instance's lifetime).
36
+ */
37
+ export const RESOLVED_BACKEND_CACHE_MAX = 1_000
38
+
39
+ /**
40
+ * Linkset relation URI for the access-control `policy` auxiliary resource
41
+ * (RFC9264 linkset discovery; see src/policy.ts and the linkset handlers).
42
+ */
43
+ export const POLICY_LINK_RELATION = 'https://wallet.storage/spec#policy'
44
+
45
+ /**
46
+ * Linkset relation URI for a Collection's selected `backend` auxiliary resource
47
+ * (RFC9264 linkset discovery; advertised at `/space/{id}/{cid}/backend`).
48
+ */
49
+ export const BACKEND_LINK_RELATION = 'https://wallet.storage/spec#backend'
50
+
51
+ /**
52
+ * Linkset relation URI for a Collection's `quota` report auxiliary resource
53
+ * (RFC9264 linkset discovery; advertised at `/space/{id}/{cid}/quota`).
54
+ */
55
+ export const QUOTA_LINK_RELATION = 'https://wallet.storage/spec#quota'
56
+
57
+ /**
58
+ * Linkset relation URI for a Space's `backends-available` auxiliary resource
59
+ * (RFC9264 linkset discovery; advertised at `/space/{id}/backends`).
60
+ */
61
+ export const BACKENDS_AVAILABLE_LINK_RELATION =
62
+ 'https://wallet.storage/spec#backends-available'
63
+
64
+ /**
65
+ * Linkset relation URI for a Space's `quotas` report auxiliary resource
66
+ * (RFC9264 linkset discovery; advertised at `/space/{id}/quotas`).
67
+ */
68
+ export const QUOTAS_LINK_RELATION = 'https://wallet.storage/spec#quotas'
69
+
70
+ /**
71
+ * Fraction of a backend's configured capacity at or above which its quota
72
+ * report `state` becomes `near-limit` (spec "Quotas"). Below this it is `ok`;
73
+ * at or above full capacity it is `over-quota`. Only applies when a finite
74
+ * capacity is configured (an unlimited backend is always `ok`).
75
+ */
76
+ export const QUOTA_NEAR_LIMIT_FRACTION = 0.9
77
+
78
+ /**
79
+ * Write-path quota usage cache TTL (see
80
+ * `FileSystemBackend._assertSpaceHeadroom`). The quota pre-flight measures a
81
+ * Space's on-disk usage with `du`, which walks the whole Space tree -- too
82
+ * costly to repeat on every resource write. The measured total is cached per
83
+ * Space for this long, with each accepted write's incoming bytes added to the
84
+ * cached figure; deletes invalidate the entry. The TTL bounds the drift of
85
+ * that optimistic accounting (overwrites that replaced rather than added
86
+ * bytes, streamed bodies of undeclared size) -- the quota is a documented
87
+ * soft limit, and the TTL bounds the re-measurement window.
88
+ */
89
+ export const QUOTA_USAGE_CACHE_TTL = 5_000 // milliseconds
90
+
91
+ /**
92
+ * The single in-process KMS module this server hard-wires.
93
+ * A keystore created without one gets this alias, and it is immutable thereafter.
94
+ */
95
+ export const DEFAULT_KMS_MODULE = 'local-v1'
96
+
97
+ /**
98
+ * Max keystore configs returned by `GET /kms/keystores?controller=...` (the
99
+ * webkms protocol's list cap).
100
+ */
101
+ export const KEYSTORE_LIST_LIMIT = 100
102
+
103
+ /**
104
+ * Max key descriptions returned per page by
105
+ * `GET /kms/keystores/:keystoreId/keys` (the List Keys fork extension). A
106
+ * further page is signalled by the response's `next` cursor URL; realistic
107
+ * keystores hold a handful of keys, so one page is the steady state.
108
+ */
109
+ export const KEY_LIST_LIMIT = 100
110
+
111
+ /**
112
+ * Max capability delegation chain length accepted on `/kms` invocations, the
113
+ * root capability included (webkms-switch's `maxChainLength` default). A
114
+ * per-key `maxCapabilityChainLength` (1-10, set at generate time) may narrow
115
+ * this further for key operations; it can never widen it.
116
+ */
117
+ export const KMS_MAX_CHAIN_LENGTH = 10
118
+
119
+ /**
120
+ * Max time-to-live of a delegated capability accepted on `/kms` invocations,
121
+ * measured `expires` minus the delegation proof's `created`, in milliseconds
122
+ * (90 days -- webkms-switch's `maxDelegationTtl` default). One unified bound
123
+ * for every `/kms` route family:
124
+ * revocation plus the mandatory `expires` is the real control.
125
+ */
126
+ export const KMS_MAX_DELEGATION_TTL = 90 * 24 * 60 * 60 * 1000
127
+
128
+ /** TCP port the server listens on when `PORT` is unset. */
129
+ export const DEFAULT_PORT = 3002
130
+
131
+ /**
132
+ * The default per-upload byte cap applied by BOTH backends when
133
+ * `MAX_UPLOAD_BYTES` is unset (a default-on limit, 64 MiB). Every blob write
134
+ * buffers through process memory on at least one path (the Postgres single
135
+ * `bytea`, multipart's in-memory `toBuffer()`), so "no cap" would be a footgun.
136
+ * Opt out explicitly with `MAX_UPLOAD_BYTES=unlimited`, which the backends
137
+ * accept only where an unbounded upload is actually safe (the filesystem
138
+ * streaming path; the Postgres backend rejects it at construction).
139
+ */
140
+ export const DEFAULT_MAX_UPLOAD_BYTES = 64 * 1024 * 1024
141
+
142
+ /**
143
+ * Default cap on the number of Spaces a single controller may create, applied
144
+ * by BOTH backends when `MAX_SPACES_PER_CONTROLLER` is unset (a default-on
145
+ * count quota). Chosen far above any teaching or conformance workload while
146
+ * still bounding runaway creation by one controller. Opt out with
147
+ * `MAX_SPACES_PER_CONTROLLER=unlimited`.
148
+ */
149
+ export const DEFAULT_MAX_SPACES_PER_CONTROLLER = 100
150
+
151
+ /**
152
+ * Default cap on the number of Collections a single Space may hold, applied by
153
+ * BOTH backends when `MAX_COLLECTIONS_PER_SPACE` is unset (a default-on count
154
+ * quota). Opt out with `MAX_COLLECTIONS_PER_SPACE=unlimited`.
155
+ */
156
+ export const DEFAULT_MAX_COLLECTIONS_PER_SPACE = 100
157
+
158
+ /**
159
+ * Default cap on the number of live Resources a single Space may hold across
160
+ * all its Collections, applied by BOTH backends when `MAX_RESOURCES_PER_SPACE`
161
+ * is unset (a default-on count quota). A tombstone (soft-deleted Resource) does
162
+ * not count against it. Opt out with `MAX_RESOURCES_PER_SPACE=unlimited`.
163
+ */
164
+ export const DEFAULT_MAX_RESOURCES_PER_SPACE = 10_000
165
+
166
+ /**
167
+ * The validated env-derived server configuration returned by
168
+ * {@link loadConfigFromEnv} and consumed by `start.ts`.
169
+ */
170
+ export interface EnvConfig {
171
+ /** The server base URL (`SERVER_URL`); required, validated. */
172
+ serverUrl: string
173
+ /** TCP port to listen on (`PORT`); defaults to {@link DEFAULT_PORT}. */
174
+ port: number
175
+ /** Postgres connection string (`DATABASE_URL`); unset selects the filesystem backend. */
176
+ databaseUrl?: string
177
+ /**
178
+ * Per-Space storage quota in bytes (`STORAGE_LIMIT_PER_SPACE`). `undefined`
179
+ * means unset -- unlimited, but `start.ts` warns to prompt an explicit
180
+ * choice; `Infinity` means `unlimited` was set explicitly (no warning).
181
+ */
182
+ storageLimitPerSpace?: number
183
+ /**
184
+ * Per-upload size cap in bytes (`MAX_UPLOAD_BYTES`). `undefined` means unset
185
+ * -- the backends apply the {@link DEFAULT_MAX_UPLOAD_BYTES} default;
186
+ * `Infinity` means `unlimited` was set explicitly (no per-upload cap).
187
+ */
188
+ maxUploadBytes?: number
189
+ /**
190
+ * Max Spaces a single controller may create (`MAX_SPACES_PER_CONTROLLER`).
191
+ * `undefined` means unset -- the backends apply the
192
+ * {@link DEFAULT_MAX_SPACES_PER_CONTROLLER} default; `Infinity` means
193
+ * `unlimited` was set explicitly (no cap).
194
+ */
195
+ maxSpacesPerController?: number
196
+ /**
197
+ * Max Collections a single Space may hold (`MAX_COLLECTIONS_PER_SPACE`).
198
+ * `undefined` means unset -- the backends apply the
199
+ * {@link DEFAULT_MAX_COLLECTIONS_PER_SPACE} default; `Infinity` means
200
+ * `unlimited` was set explicitly (no cap).
201
+ */
202
+ maxCollectionsPerSpace?: number
203
+ /**
204
+ * Max live Resources a single Space may hold across all its Collections
205
+ * (`MAX_RESOURCES_PER_SPACE`). `undefined` means unset -- the backends apply
206
+ * the {@link DEFAULT_MAX_RESOURCES_PER_SPACE} default; `Infinity` means
207
+ * `unlimited` was set explicitly (no cap).
208
+ */
209
+ maxResourcesPerSpace?: number
210
+ /** Backend registration allowlist (`WAS_ENABLED_BACKENDS`); unset = permissive. */
211
+ enabledBackendProviders?: string[]
212
+ /**
213
+ * At-rest KMS key-record encryption registry (`KMS_RECORD_KEK` /
214
+ * `KMS_RECORD_KEKS` / `KMS_RECORD_CURRENT_KEK`); unset = plaintext.
215
+ */
216
+ kmsRecordKek?: KmsRecordKekRegistry
217
+ /** Shared-secret provisioning gate (`WAS_ONBOARDING_TOKEN`); unset = open provisioning. */
218
+ onboardingToken?: string
219
+ }
220
+
221
+ /**
222
+ * Reads and validates the server's whole env config surface in one place
223
+ * (fail-fast startup): a missing `SERVER_URL` or any malformed value throws
224
+ * with the offending variable named, before the server starts listening --
225
+ * instead of silently breaking ZCap matching at request time.
226
+ * @param [env] {NodeJS.ProcessEnv} defaults to `process.env`
227
+ * @returns {EnvConfig}
228
+ */
229
+ export function loadConfigFromEnv(
230
+ env: NodeJS.ProcessEnv = process.env
231
+ ): EnvConfig {
232
+ return {
233
+ serverUrl: parseServerUrl(env.SERVER_URL),
234
+ port: parsePort(env.PORT),
235
+ databaseUrl: parseDatabaseUrl(env.DATABASE_URL),
236
+ storageLimitPerSpace: parseStorageLimit(env.STORAGE_LIMIT_PER_SPACE),
237
+ maxUploadBytes: parseMaxUploadBytes(env.MAX_UPLOAD_BYTES),
238
+ maxSpacesPerController: parseCountLimit(
239
+ env.MAX_SPACES_PER_CONTROLLER,
240
+ 'MAX_SPACES_PER_CONTROLLER'
241
+ ),
242
+ maxCollectionsPerSpace: parseCountLimit(
243
+ env.MAX_COLLECTIONS_PER_SPACE,
244
+ 'MAX_COLLECTIONS_PER_SPACE'
245
+ ),
246
+ maxResourcesPerSpace: parseCountLimit(
247
+ env.MAX_RESOURCES_PER_SPACE,
248
+ 'MAX_RESOURCES_PER_SPACE'
249
+ ),
250
+ enabledBackendProviders: parseEnabledBackends(env.WAS_ENABLED_BACKENDS),
251
+ kmsRecordKek: parseKmsRecordKekRegistry({
252
+ kek: env.KMS_RECORD_KEK,
253
+ keks: env.KMS_RECORD_KEKS,
254
+ currentKek: env.KMS_RECORD_CURRENT_KEK
255
+ }),
256
+ onboardingToken: parseOnboardingToken(env.WAS_ONBOARDING_TOKEN)
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Validates a server base URL (the `serverUrl` option / `SERVER_URL` env
262
+ * value): it must be an absolute `http:`/`https:` URL with no path, query, or
263
+ * fragment. ZCap `invocationTarget` URLs and `Location` headers are built by
264
+ * resolving absolute paths against this base (`new URL(path, serverUrl)`),
265
+ * which silently drops any base path -- so a sub-path deployment would break
266
+ * every delegated invocation. Rejected at startup instead (fail-fast).
267
+ * @param serverUrl {string} the candidate base URL
268
+ * @returns {void} throws on an invalid value
269
+ */
270
+ export function assertValidServerUrl(serverUrl: string): void {
271
+ let url: URL
272
+ try {
273
+ url = new URL(serverUrl)
274
+ } catch {
275
+ throw new Error(
276
+ `serverUrl (env SERVER_URL) must be an absolute URL; got "${serverUrl}".`
277
+ )
278
+ }
279
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
280
+ throw new Error(
281
+ `serverUrl (env SERVER_URL) must use http: or https:; got "${serverUrl}".`
282
+ )
283
+ }
284
+ if (url.pathname !== '/' || url.search !== '' || url.hash !== '') {
285
+ throw new Error(
286
+ `serverUrl (env SERVER_URL) must not include a path, query, or ` +
287
+ `fragment -- deploying under a sub-path is not supported; ` +
288
+ `got "${serverUrl}".`
289
+ )
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Parses the `SERVER_URL` env value: the server's base URL, used to build and
295
+ * match ZCap `invocationTarget` URLs (host and port must match the client's
296
+ * exactly). Required -- unset would silently break all ZCap matching, so
297
+ * startup fails instead. The value is trimmed but otherwise preserved
298
+ * byte-for-byte (never normalized), since capability targets compare as exact
299
+ * strings. Validated by {@link assertValidServerUrl}.
300
+ * @param raw {string|undefined} the raw env value
301
+ * @returns {string} the trimmed, validated base URL
302
+ */
303
+ export function parseServerUrl(raw: string | undefined): string {
304
+ if (raw === undefined || raw.trim() === '') {
305
+ throw new Error(
306
+ `SERVER_URL is required: the server base URL used to build and match ` +
307
+ `ZCap invocationTarget URLs (e.g. SERVER_URL='http://localhost:3002').`
308
+ )
309
+ }
310
+ const serverUrl = raw.trim()
311
+ assertValidServerUrl(serverUrl)
312
+ return serverUrl
313
+ }
314
+
315
+ /**
316
+ * Parses the `PORT` env value into the TCP port to listen on. An unset or
317
+ * empty value returns {@link DEFAULT_PORT}.
318
+ * @param raw {string|undefined} the raw env value
319
+ * @returns {number} the port
320
+ */
321
+ export function parsePort(raw: string | undefined): number {
322
+ if (raw === undefined || raw.trim() === '') {
323
+ return DEFAULT_PORT
324
+ }
325
+ const value = Number(raw)
326
+ if (!Number.isInteger(value) || value < 1 || value > 65535) {
327
+ throw new Error(
328
+ `PORT must be an integer between 1 and 65535; got "${raw}".`
329
+ )
330
+ }
331
+ return value
332
+ }
333
+
334
+ /**
335
+ * Parses the `DATABASE_URL` env value: a Postgres connection string that,
336
+ * when set, selects the PostgreSQL storage backend (unset keeps the default
337
+ * filesystem backend). The string's shape is left to the `pg` driver, which
338
+ * accepts several connection-string forms; an unset or empty value returns
339
+ * `undefined`.
340
+ * @param raw {string|undefined} the raw env value
341
+ * @returns {string|undefined} the trimmed connection string, or `undefined`
342
+ */
343
+ export function parseDatabaseUrl(raw: string | undefined): string | undefined {
344
+ if (raw === undefined || raw.trim() === '') {
345
+ return undefined
346
+ }
347
+ return raw.trim()
348
+ }
349
+
350
+ /**
351
+ * Parses the at-rest key-record encryption env surface into a
352
+ * {@link KmsRecordKekRegistry} (the optional hardening increment). Each KEK is a
353
+ * single AES-256 key-encryption key in base58btc Multikey form
354
+ * (`secretKeyMultibase`, header `0xa2 0x01`), whose id is derived from its
355
+ * material (`deriveKekId`) and stored per record -- so registering a second KEK
356
+ * for rotation is a config change, not a schema migration. Three env variables
357
+ * feed it:
358
+ *
359
+ * - `KMS_RECORD_KEK` -- a single KEK (the teaching default alias);
360
+ * - `KMS_RECORD_KEKS` -- a comma-separated list of KEKs, each registered by its
361
+ * derived id; the FIRST entry is the current KEK by default, so a rotation is
362
+ * "prepend the new KEK, keep the old one behind it". Surrounding whitespace is
363
+ * trimmed and empty entries ignored; a duplicate entry (same derived id twice)
364
+ * throws;
365
+ * - `KMS_RECORD_CURRENT_KEK` -- optionally overrides which registered KEK wraps
366
+ * NEW records: a `urn:kek:sha256:<hex>` id, a multibase KEK value (its id
367
+ * derived), or the literal `none` (case-insensitive) which sets
368
+ * `currentKekId: null` -- the decrypt-only posture (old records still read, new
369
+ * records are written plaintext). It must name a registered KEK.
370
+ *
371
+ * All three unset/empty returns `undefined`, meaning encryption is disabled --
372
+ * key records are written plaintext (the default, honest about the teaching
373
+ * server's threat model). Setting both `KMS_RECORD_KEK` and `KMS_RECORD_KEKS` is
374
+ * ambiguous and throws; setting `KMS_RECORD_CURRENT_KEK` with no KEK configured
375
+ * throws. A malformed value throws (fails startup), naming the offending
376
+ * variable (and, for a list entry, its 1-based position) but never echoing the
377
+ * secret.
378
+ * @param options {object}
379
+ * @param [options.kek] {string} the `KMS_RECORD_KEK` value
380
+ * @param [options.keks] {string} the `KMS_RECORD_KEKS` value
381
+ * @param [options.currentKek] {string} the `KMS_RECORD_CURRENT_KEK` value
382
+ * @returns {KmsRecordKekRegistry|undefined} the registry, or `undefined` when
383
+ * no KEK is configured
384
+ */
385
+ export function parseKmsRecordKekRegistry({
386
+ kek,
387
+ keks,
388
+ currentKek
389
+ }: {
390
+ kek?: string
391
+ keks?: string
392
+ currentKek?: string
393
+ }): KmsRecordKekRegistry | undefined {
394
+ const kekSet = kek !== undefined && kek.trim() !== ''
395
+ const keksSet = keks !== undefined && keks.trim() !== ''
396
+ const currentSet = currentKek !== undefined && currentKek.trim() !== ''
397
+
398
+ if (kekSet && keksSet) {
399
+ throw new Error(
400
+ 'Set only one of KMS_RECORD_KEK or KMS_RECORD_KEKS, not both (ambiguous).'
401
+ )
402
+ }
403
+
404
+ // Build the ordered registry: each KEK keyed by its derived id, in config
405
+ // order (the first entry is the default current KEK).
406
+ const registry = new Map<string, RecordKek>()
407
+ const order: RecordKek[] = []
408
+
409
+ if (kekSet) {
410
+ const parsed = parseKekMultibase(kek!.trim())
411
+ registry.set(parsed.id, parsed)
412
+ order.push(parsed)
413
+ } else if (keksSet) {
414
+ let position = 0
415
+ for (const entry of keks!.split(',')) {
416
+ const trimmed = entry.trim()
417
+ if (trimmed === '') {
418
+ continue // empty entries are ignored (like WAS_ENABLED_BACKENDS)
419
+ }
420
+ position += 1
421
+ const parsed = parseKekMultibase(
422
+ trimmed,
423
+ `KMS_RECORD_KEKS entry ${position}`
424
+ )
425
+ if (registry.has(parsed.id)) {
426
+ throw new Error(
427
+ `KMS_RECORD_KEKS entry ${position} duplicates an earlier KEK ` +
428
+ `(same derived kekId); register each KEK only once.`
429
+ )
430
+ }
431
+ registry.set(parsed.id, parsed)
432
+ order.push(parsed)
433
+ }
434
+ }
435
+
436
+ // No KEK material at all (all unset, or a list of only empty entries): a
437
+ // dangling KMS_RECORD_CURRENT_KEK is an error; otherwise encryption is off.
438
+ if (registry.size === 0) {
439
+ if (currentSet) {
440
+ throw new Error(
441
+ 'KMS_RECORD_CURRENT_KEK is set but no KEK is configured; ' +
442
+ 'set KMS_RECORD_KEK or KMS_RECORD_KEKS.'
443
+ )
444
+ }
445
+ return undefined
446
+ }
447
+
448
+ const currentKekId = currentSet
449
+ ? resolveCurrentKekId(currentKek!.trim(), registry)
450
+ : order[0]!.id
451
+ return { keks: registry, currentKekId }
452
+ }
453
+
454
+ /**
455
+ * Resolves a `KMS_RECORD_CURRENT_KEK` value to a registered `kekId` (or `null`
456
+ * for the literal `none`, the decrypt-only posture). The value is either a
457
+ * `urn:kek:sha256:<hex>` id or a multibase KEK whose id is derived; either way
458
+ * it must match a KEK already in `registry`. Throws on an unregistered target,
459
+ * naming the variable but never echoing the secret.
460
+ * @param value {string} the trimmed `KMS_RECORD_CURRENT_KEK` value
461
+ * @param registry {Map<string, RecordKek>} the registered KEKs, by id
462
+ * @returns {string|null} the resolved current `kekId`, or `null` for `none`
463
+ */
464
+ function resolveCurrentKekId(
465
+ value: string,
466
+ registry: Map<string, RecordKek>
467
+ ): string | null {
468
+ if (value.toLowerCase() === 'none') {
469
+ return null // decrypt-only: keep KEKs for unwrap, write new records plaintext
470
+ }
471
+ const kekId = value.startsWith('urn:kek:sha256:')
472
+ ? value
473
+ : parseKekMultibase(value, 'KMS_RECORD_CURRENT_KEK').id
474
+ if (!registry.has(kekId)) {
475
+ throw new Error(
476
+ 'KMS_RECORD_CURRENT_KEK names a KEK that is not registered ' +
477
+ '(no matching KMS_RECORD_KEK / KMS_RECORD_KEKS entry).'
478
+ )
479
+ }
480
+ return kekId
481
+ }
482
+
483
+ /**
484
+ * Parses the `STORAGE_LIMIT_PER_SPACE` env value into a per-Space capacity in
485
+ * bytes (spec "Quotas"). Accepts a plain non-negative integer number of bytes,
486
+ * or the literal `unlimited` (case-insensitive, trimmed) which returns
487
+ * `Infinity` -- an explicitly acknowledged unlimited quota (no startup
488
+ * warning). An unset or empty value returns `undefined`, meaning no limit is
489
+ * configured (still unlimited, but `start.ts` warns to prompt an explicit
490
+ * choice). A malformed value throws.
491
+ * @param raw {string|undefined} the raw env value
492
+ * @returns {number|undefined} capacity in bytes, `Infinity` for `unlimited`,
493
+ * or `undefined` when unset
494
+ */
495
+ export function parseStorageLimit(raw: string | undefined): number | undefined {
496
+ if (raw === undefined || raw.trim() === '') {
497
+ return undefined
498
+ }
499
+ if (raw.trim().toLowerCase() === 'unlimited') {
500
+ return Infinity
501
+ }
502
+ const value = Number(raw)
503
+ if (!Number.isInteger(value) || value < 0) {
504
+ throw new Error(
505
+ `STORAGE_LIMIT_PER_SPACE must be a non-negative integer number of ` +
506
+ `bytes, or "unlimited"; got "${raw}".`
507
+ )
508
+ }
509
+ return value
510
+ }
511
+
512
+ /**
513
+ * Parses the `MAX_UPLOAD_BYTES` env value into a per-upload size cap in bytes
514
+ * (spec "Quotas", the backend's `maxUploadBytes` constraint). Accepts a plain
515
+ * non-negative integer number of bytes, or the literal `unlimited`
516
+ * (case-insensitive, trimmed) which returns `Infinity` -- explicitly no cap.
517
+ * An unset or empty value returns `undefined`, meaning not configured: the
518
+ * backends apply the {@link DEFAULT_MAX_UPLOAD_BYTES} default (a default-on
519
+ * limit). A single upload exceeding the cap is rejected with
520
+ * `payload-too-large` (413), while smaller uploads still succeed. A malformed
521
+ * value throws.
522
+ * @param raw {string|undefined} the raw env value
523
+ * @returns {number|undefined} the per-upload cap in bytes, `Infinity` for
524
+ * `unlimited`, or `undefined` when unset
525
+ */
526
+ export function parseMaxUploadBytes(
527
+ raw: string | undefined
528
+ ): number | undefined {
529
+ if (raw === undefined || raw.trim() === '') {
530
+ return undefined
531
+ }
532
+ if (raw.trim().toLowerCase() === 'unlimited') {
533
+ return Infinity
534
+ }
535
+ const value = Number(raw)
536
+ if (!Number.isInteger(value) || value < 0) {
537
+ throw new Error(
538
+ `MAX_UPLOAD_BYTES must be a non-negative integer number of bytes, or ` +
539
+ `"unlimited"; got "${raw}".`
540
+ )
541
+ }
542
+ return value
543
+ }
544
+
545
+ /**
546
+ * Parses one of the count-quota env values (`MAX_SPACES_PER_CONTROLLER`,
547
+ * `MAX_COLLECTIONS_PER_SPACE`, `MAX_RESOURCES_PER_SPACE`) into a maximum count.
548
+ * Accepts a plain non-negative integer, or the literal `unlimited`
549
+ * (case-insensitive, trimmed) which returns `Infinity` -- explicitly no cap. An
550
+ * unset or empty value returns `undefined`, meaning not configured: the backends
551
+ * apply the matching default-on limit ({@link DEFAULT_MAX_SPACES_PER_CONTROLLER}
552
+ * and friends). A malformed value throws, naming the variable and the
553
+ * `unlimited` escape hatch.
554
+ * @param raw {string|undefined} the raw env value
555
+ * @param name {string} the env variable name, for the error message
556
+ * @returns {number|undefined} the max count, `Infinity` for `unlimited`, or
557
+ * `undefined` when unset
558
+ */
559
+ export function parseCountLimit(
560
+ raw: string | undefined,
561
+ name: string
562
+ ): number | undefined {
563
+ if (raw === undefined || raw.trim() === '') {
564
+ return undefined
565
+ }
566
+ if (raw.trim().toLowerCase() === 'unlimited') {
567
+ return Infinity
568
+ }
569
+ const value = Number(raw)
570
+ if (!Number.isInteger(value) || value < 0) {
571
+ throw new Error(
572
+ `${name} must be a non-negative integer, or "unlimited"; got "${raw}".`
573
+ )
574
+ }
575
+ return value
576
+ }
577
+
578
+ /**
579
+ * Normalizes a count-quota constructor option to the internal instance field
580
+ * both backends store: an unset option (`undefined`) applies the default-on
581
+ * `fallback` limit; a non-finite option (`Infinity`, from an explicit
582
+ * `unlimited`) becomes `undefined` (no cap); a finite value passes through.
583
+ * Shared so the two backends cannot drift on the mapping (the same posture as
584
+ * the `maxUploadBytes` normalization each backend already applies), letting each
585
+ * count guard keep its plain `!== undefined` test.
586
+ * @param value {number|undefined} the constructor option
587
+ * @param fallback {number} the default-on limit applied when unset
588
+ * @returns {number|undefined} the internal limit, or `undefined` for no cap
589
+ */
590
+ export function normalizeCountLimit(
591
+ value: number | undefined,
592
+ fallback: number
593
+ ): number | undefined {
594
+ if (value === undefined) {
595
+ return fallback
596
+ }
597
+ return Number.isFinite(value) ? value : undefined
598
+ }
599
+
600
+ /**
601
+ * Parses the `WAS_ENABLED_BACKENDS` env value into the server-wide registration
602
+ * allowlist: the backend `provider` names a client may register (spec
603
+ * "Backends"). A comma-separated list (e.g. `gdrive,s3`); surrounding whitespace
604
+ * and empty entries are ignored. An unset or empty value returns `undefined`,
605
+ * meaning no allowlist is configured -- any provider may be registered (the
606
+ * permissive default, preserving prior behavior). When set, a registration whose
607
+ * `provider` is not listed is rejected with `unsupported-backend` (409).
608
+ * @param raw {string|undefined} the raw env value
609
+ * @returns {string[]|undefined} the allowed provider names, or `undefined`
610
+ */
611
+ export function parseEnabledBackends(
612
+ raw: string | undefined
613
+ ): string[] | undefined {
614
+ if (raw === undefined) {
615
+ return undefined
616
+ }
617
+ const providers = raw
618
+ .split(',')
619
+ .map(entry => entry.trim())
620
+ .filter(entry => entry.length > 0)
621
+ return providers.length > 0 ? providers : undefined
622
+ }
623
+
624
+ /**
625
+ * Parses the `WAS_ONBOARDING_TOKEN` env value into the shared-secret onboarding
626
+ * token gating the two open provisioning endpoints (`POST /spaces/`,
627
+ * `POST /kms/keystores`). An unset or empty/whitespace-only value returns
628
+ * `undefined`, meaning the feature is off -- provisioning is authorized by
629
+ * proving control of the body's controller DID (the teaching default). When
630
+ * set, those two endpoints instead require an `Authorization: Bearer <token>`
631
+ * header matching this value, which then substitutes for zcap verification.
632
+ * @param raw {string|undefined} the raw env value
633
+ * @returns {string|undefined} the trimmed token, or `undefined` when unset
634
+ */
635
+ export function parseOnboardingToken(
636
+ raw: string | undefined
637
+ ): string | undefined {
638
+ if (raw === undefined || raw.trim() === '') {
639
+ return undefined
640
+ }
641
+ return raw.trim()
642
+ }
643
+
644
+ export const SPEC_URL =
645
+ 'https://digitalcredentials.github.io/wallet-attached-storage-spec/'
646
+ export const UBC_MANIFEST_URL =
647
+ 'https://codeberg.org/fediverse/fep/src/branch/main/fep/6fcd/fep-6fcd.md#manifest-file'
648
+ export const SPACE_URL =
649
+ 'https://digitalcredentials.github.io/wallet-attached-storage-spec/#spaces'
650
+ export const COLLECTION_URL =
651
+ 'https://digitalcredentials.github.io/wallet-attached-storage-spec/#collection-data-model'
652
+ export const RESOURCE_URL =
653
+ 'https://digitalcredentials.github.io/wallet-attached-storage-spec/#resource-data-model'
654
+ export const POLICY_URL =
655
+ 'https://digitalcredentials.github.io/wallet-attached-storage-spec/#policy'
656
+ export const META_URL =
657
+ 'https://digitalcredentials.github.io/wallet-attached-storage-spec/#resource-metadata-data-model'