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,351 @@
1
+ /**
2
+ * Collection backend selection helpers (spec "Backends" / "Collection Backend
3
+ * Selected"). A Collection carries an optional `backend` object whose `id` MUST
4
+ * name one of the Space's backends-available; this module centralizes the three
5
+ * operations that need that list: enumerating the available backends, validating
6
+ * (and default-filling) a client-supplied `backend` on create/update, and
7
+ * resolving a stored `{ id }` to its full descriptor for `GET .../backend`.
8
+ *
9
+ * This reference server ships a single server-configured backend (the active
10
+ * `StorageBackend`, registered as `default`), so the available list has one
11
+ * entry; the indirection leaves room for multiple registered backends later.
12
+ */
13
+ import type {
14
+ BackendDescriptor,
15
+ BackendConnectionPublic,
16
+ BackendRegistration,
17
+ CollectionDescription,
18
+ StorageBackend,
19
+ StoredBackendRecord
20
+ } from '../types.js'
21
+ import { InvalidRequestBodyError, UnsupportedBackendError } from '../errors.js'
22
+ import { isUrlSafeSegment } from './validateId.js'
23
+
24
+ /** The conventional id of the server-assigned default backend (spec). */
25
+ export const DEFAULT_BACKEND_ID = 'default'
26
+
27
+ // `listRegisteredBackends` (defined below, with the registration helpers) is the
28
+ // single source of a Space's selectable backends -- the server `default` plus
29
+ // its registered `external` records -- used by both the selection-validation
30
+ // (`assertSupportedBackend`) and descriptor-resolution (`resolveBackendDescriptor`)
31
+ // paths below.
32
+
33
+ /** The public (secret-free) subset of a connection a sanitized read may carry. */
34
+ const PUBLIC_CONNECTION_FIELDS = [
35
+ 'account',
36
+ 'scope',
37
+ 'connectedAt',
38
+ 'rootFolderName'
39
+ ] as const
40
+
41
+ /**
42
+ * Validates a client-supplied Collection `backend` value and returns the
43
+ * normalized `{ id }` to persist. An absent value defaults to the server's
44
+ * default backend. A present-but-malformed value (not an object, or no string
45
+ * `id`) is `invalid-request-body` (400); a well-formed value whose `id` is not
46
+ * in the Space's backends-available (the server `default` plus its registered
47
+ * `external` backends) is `unsupported-backend` (409). A registered `external`
48
+ * backend is now selectable here -- routing its data plane to the resolved
49
+ * adapter is the resolver's job (lib/backendRegistry.ts).
50
+ *
51
+ * @param options {object}
52
+ * @param options.storage {StorageBackend} the request's storage backend
53
+ * @param options.spaceId {string} the Space whose backends-available is checked
54
+ * @param [options.backend] {unknown} the request body's `backend` value
55
+ * @param [options.requestName] {string} request name for the 400 error title
56
+ * @returns {Promise<{ id: string }>} the normalized backend reference to store
57
+ */
58
+ export async function assertSupportedBackend({
59
+ storage,
60
+ spaceId,
61
+ backend,
62
+ requestName
63
+ }: {
64
+ storage: StorageBackend
65
+ spaceId: string
66
+ backend?: unknown
67
+ requestName?: string
68
+ }): Promise<{ id: string }> {
69
+ if (backend === undefined) {
70
+ return { id: DEFAULT_BACKEND_ID }
71
+ }
72
+ if (
73
+ typeof backend !== 'object' ||
74
+ backend === null ||
75
+ typeof (backend as { id?: unknown }).id !== 'string'
76
+ ) {
77
+ throw new InvalidRequestBodyError({
78
+ requestName,
79
+ detail: 'Collection "backend" must be an object with a string "id".',
80
+ pointer: '#/backend'
81
+ })
82
+ }
83
+ const { id } = backend as { id: string }
84
+ const available = await listRegisteredBackends({ storage, spaceId })
85
+ if (!available.some(entry => entry.id === id)) {
86
+ throw new UnsupportedBackendError({ backendId: id })
87
+ }
88
+ return { id }
89
+ }
90
+
91
+ /**
92
+ * Resolves a Collection's selected backend to its full descriptor, for the
93
+ * `GET .../backend` ("Collection Backend Selected") response, consulting the
94
+ * Space's backends-available (server `default` plus registered `external`
95
+ * records). A stored description without a `backend` (created before the property
96
+ * existed) defaults to the server's default backend.
97
+ *
98
+ * @param options {object}
99
+ * @param options.storage {StorageBackend} the request's storage backend
100
+ * @param options.spaceId {string} the Space whose backends-available is checked
101
+ * @param options.collectionDescription {CollectionDescription}
102
+ * @returns {Promise<BackendDescriptor>}
103
+ */
104
+ export async function resolveBackendDescriptor({
105
+ storage,
106
+ spaceId,
107
+ collectionDescription
108
+ }: {
109
+ storage: StorageBackend
110
+ spaceId: string
111
+ collectionDescription: CollectionDescription
112
+ }): Promise<BackendDescriptor> {
113
+ const id = collectionDescription.backend?.id ?? DEFAULT_BACKEND_ID
114
+ const available = await listRegisteredBackends({ storage, spaceId })
115
+ const descriptor = available.find(entry => entry.id === id)
116
+ // A stored backend id should always resolve (it was validated on write); fall
117
+ // back to the default descriptor defensively rather than returning undefined.
118
+ return descriptor ?? storage.describe()
119
+ }
120
+
121
+ // --- External backend registration (spec "Backends") ---
122
+ //
123
+ // A registered `external` backend is both listed (below) and now *selectable* as
124
+ // a Collection's `backend` (`assertSupportedBackend` / `resolveBackendDescriptor`
125
+ // above consult `listRegisteredBackends`). Routing the selected Collection's data
126
+ // plane to the live provider adapter is the resolver's job
127
+ // (lib/backendRegistry.ts); a selected backend with no registered provider
128
+ // adapter fails closed there.
129
+
130
+ /**
131
+ * Projects a full (secret-bearing) `StoredBackendRecord` down to the sanitized
132
+ * `BackendDescriptor` that every client-facing read path returns: the public
133
+ * descriptor fields plus a `connection` reduced to its public subset (no
134
+ * `authorizationCode` / `refreshToken` / etc.). The single projection shared by
135
+ * `FileSystemBackend.listBackends` and the register (`POST`/`PUT`) responses, so
136
+ * the secret material has exactly one place it could leak -- and does not.
137
+ * @param record {StoredBackendRecord}
138
+ * @returns {BackendDescriptor}
139
+ */
140
+ export function sanitizeBackendRecord(
141
+ record: StoredBackendRecord
142
+ ): BackendDescriptor {
143
+ const { connection } = record
144
+ const publicConnection: BackendConnectionPublic = {
145
+ kind: connection.kind,
146
+ status:
147
+ (connection.status as BackendConnectionPublic['status']) ?? 'registered'
148
+ }
149
+ for (const field of PUBLIC_CONNECTION_FIELDS) {
150
+ const value = connection[field]
151
+ if (typeof value === 'string') {
152
+ publicConnection[field] = value
153
+ }
154
+ }
155
+ return {
156
+ id: record.id,
157
+ ...(record.name !== undefined && { name: record.name }),
158
+ managedBy: record.managedBy,
159
+ ...(record.storageMode !== undefined && {
160
+ storageMode: record.storageMode
161
+ }),
162
+ ...(record.persistence !== undefined && {
163
+ persistence: record.persistence
164
+ }),
165
+ ...(record.features !== undefined && { features: record.features }),
166
+ provider: record.provider,
167
+ connection: publicConnection
168
+ }
169
+ }
170
+
171
+ /**
172
+ * The backends advertised at `GET /space/:spaceId/backends`: the server's
173
+ * `default` backend first, followed by the Space's sanitized registered
174
+ * `external` backends. Also the single source of a Space's selectable backends
175
+ * for `assertSupportedBackend` / `resolveBackendDescriptor`.
176
+ * @param options {object}
177
+ * @param options.storage {StorageBackend}
178
+ * @param options.spaceId {string}
179
+ * @returns {Promise<BackendDescriptor[]>}
180
+ */
181
+ export async function listRegisteredBackends({
182
+ storage,
183
+ spaceId
184
+ }: {
185
+ storage: StorageBackend
186
+ spaceId: string
187
+ }): Promise<BackendDescriptor[]> {
188
+ return [storage.describe(), ...(await storage.listBackends({ spaceId }))]
189
+ }
190
+
191
+ /**
192
+ * Validates a backend-registration request body and returns the typed
193
+ * `BackendRegistration`. Enforces: an object body; a string `id`; a non-empty
194
+ * string `provider`; `managedBy` absent or `'external'` (a client may not
195
+ * register a `server` backend); and a `connection` object with a string `kind`.
196
+ * Throws `InvalidRequestBodyError` (400) with the offending `#/...` pointer.
197
+ * Does not check `id` URL-safety -- callers run `assertValidBackendId`.
198
+ * @param body {unknown} the parsed request body
199
+ * @param options {object}
200
+ * @param [options.requestName] {string} request name for the 400 error title
201
+ * @returns {BackendRegistration}
202
+ */
203
+ export function parseBackendRegistration(
204
+ body: unknown,
205
+ { requestName }: { requestName?: string } = {}
206
+ ): BackendRegistration {
207
+ if (typeof body !== 'object' || body === null) {
208
+ throw new InvalidRequestBodyError({
209
+ requestName,
210
+ detail: 'Backend registration body must be a JSON object.',
211
+ pointer: '#'
212
+ })
213
+ }
214
+ const candidate = body as Record<string, unknown>
215
+ if (typeof candidate.id !== 'string') {
216
+ throw new InvalidRequestBodyError({
217
+ requestName,
218
+ detail: 'Backend registration requires a string "id".',
219
+ pointer: '#/id'
220
+ })
221
+ }
222
+ if (
223
+ typeof candidate.provider !== 'string' ||
224
+ candidate.provider.length === 0
225
+ ) {
226
+ throw new InvalidRequestBodyError({
227
+ requestName,
228
+ detail: 'Backend registration requires a non-empty string "provider".',
229
+ pointer: '#/provider'
230
+ })
231
+ }
232
+ if (candidate.managedBy !== undefined && candidate.managedBy !== 'external') {
233
+ throw new InvalidRequestBodyError({
234
+ requestName,
235
+ detail:
236
+ 'Backend "managedBy" must be "external"; server backends are not client-registered.',
237
+ pointer: '#/managedBy'
238
+ })
239
+ }
240
+ const connection = candidate.connection
241
+ if (
242
+ typeof connection !== 'object' ||
243
+ connection === null ||
244
+ typeof (connection as Record<string, unknown>).kind !== 'string'
245
+ ) {
246
+ throw new InvalidRequestBodyError({
247
+ requestName,
248
+ detail: 'Backend "connection" must be an object with a string "kind".',
249
+ pointer: '#/connection'
250
+ })
251
+ }
252
+ return {
253
+ id: candidate.id,
254
+ ...(typeof candidate.name === 'string' && { name: candidate.name }),
255
+ managedBy: 'external',
256
+ provider: candidate.provider,
257
+ ...(Array.isArray(candidate.storageMode) && {
258
+ storageMode: candidate.storageMode as Array<'document' | 'blob'>
259
+ }),
260
+ ...(Array.isArray(candidate.features) && {
261
+ features: candidate.features as string[]
262
+ }),
263
+ connection: connection as BackendRegistration['connection']
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Enforces the optional server-wide registration allowlist (config
269
+ * `WAS_ENABLED_BACKENDS`): the set of backend `provider` names a client may
270
+ * register. An `undefined` allowlist is **permissive** -- any provider passes,
271
+ * preserving the prior behavior where any provider could be registered. When the
272
+ * allowlist is configured, a `provider` outside it is rejected with
273
+ * `unsupported-backend` (409) pointing at `#/provider`. This is the fail-fast
274
+ * registration gate; the resolver's data-plane factory check
275
+ * (lib/backendRegistry.ts) remains the backstop for a provider with no live
276
+ * adapter.
277
+ * @param options {object}
278
+ * @param options.provider {string} the registration's `provider`
279
+ * @param [options.enabledProviders] {string[]} the configured allowlist, or
280
+ * `undefined` for permissive
281
+ * @returns {void}
282
+ */
283
+ export function assertProviderAllowed({
284
+ provider,
285
+ enabledProviders
286
+ }: {
287
+ provider: string
288
+ enabledProviders?: string[]
289
+ }): void {
290
+ if (enabledProviders === undefined) {
291
+ return
292
+ }
293
+ if (!enabledProviders.includes(provider)) {
294
+ throw new UnsupportedBackendError({
295
+ backendId: provider,
296
+ detail: `Backend provider '${provider}' is not enabled on this server.`,
297
+ pointer: '#/provider'
298
+ })
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Asserts a registered-backend id is a single URL-safe path segment (it becomes
304
+ * both a URL segment and a `.backend.<id>.json` filename). A backend id is a
305
+ * body field, so a failure is `invalid-request-body` (400) pointing at `#/id`
306
+ * rather than a URL-param id error.
307
+ * @param id {string}
308
+ * @param options {object}
309
+ * @param [options.requestName] {string} request name for the 400 error title
310
+ * @returns {void}
311
+ */
312
+ export function assertValidBackendId(
313
+ id: string,
314
+ { requestName }: { requestName?: string } = {}
315
+ ): void {
316
+ if (!isUrlSafeSegment(id)) {
317
+ throw new InvalidRequestBodyError({
318
+ requestName,
319
+ detail: 'Backend "id" must be a single, URL-safe path segment.',
320
+ pointer: '#/id'
321
+ })
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Assembles the full `StoredBackendRecord` to persist from a validated
327
+ * `BackendRegistration`: the descriptor fields, `managedBy: 'external'`, default
328
+ * `storageMode` / `features`, and the full (secret-bearing) connection stamped
329
+ * with `status: 'registered'` and the registration timestamp. The record is
330
+ * inert until the live provider adapter (future work) connects it.
331
+ * @param registration {BackendRegistration}
332
+ * @returns {StoredBackendRecord}
333
+ */
334
+ export function buildBackendRecord(
335
+ registration: BackendRegistration
336
+ ): StoredBackendRecord {
337
+ const { id, name, provider, storageMode, features, connection } = registration
338
+ return {
339
+ id,
340
+ ...(name !== undefined && { name }),
341
+ managedBy: 'external',
342
+ provider,
343
+ storageMode: storageMode ?? ['document', 'blob'],
344
+ features: features ?? [],
345
+ connection: {
346
+ ...connection,
347
+ status: 'registered',
348
+ connectedAt: new Date().toISOString()
349
+ }
350
+ }
351
+ }