was-teaching-server 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/CHANGELOG.md +1432 -0
  2. package/LICENSE +661 -0
  3. package/README.md +305 -0
  4. package/common/css/materialize.1.0.0.min.css +13 -0
  5. package/dist/auth-header-hooks.d.ts +84 -0
  6. package/dist/auth-header-hooks.d.ts.map +1 -0
  7. package/dist/auth-header-hooks.js +136 -0
  8. package/dist/auth-header-hooks.js.map +1 -0
  9. package/dist/authorize.d.ts +45 -0
  10. package/dist/authorize.d.ts.map +1 -0
  11. package/dist/authorize.js +76 -0
  12. package/dist/authorize.js.map +1 -0
  13. package/dist/backends/filesystem.d.ts +1179 -0
  14. package/dist/backends/filesystem.d.ts.map +1 -0
  15. package/dist/backends/filesystem.js +2727 -0
  16. package/dist/backends/filesystem.js.map +1 -0
  17. package/dist/backends/postgres.d.ts +833 -0
  18. package/dist/backends/postgres.d.ts.map +1 -0
  19. package/dist/backends/postgres.js +2164 -0
  20. package/dist/backends/postgres.js.map +1 -0
  21. package/dist/backends/postgresSchema.d.ts +36 -0
  22. package/dist/backends/postgresSchema.d.ts.map +1 -0
  23. package/dist/backends/postgresSchema.js +203 -0
  24. package/dist/backends/postgresSchema.js.map +1 -0
  25. package/dist/config.default.d.ts +360 -0
  26. package/dist/config.default.d.ts.map +1 -0
  27. package/dist/config.default.js +493 -0
  28. package/dist/config.default.js.map +1 -0
  29. package/dist/corsProxy.d.ts +45 -0
  30. package/dist/corsProxy.d.ts.map +1 -0
  31. package/dist/corsProxy.js +278 -0
  32. package/dist/corsProxy.js.map +1 -0
  33. package/dist/digest.d.ts +45 -0
  34. package/dist/digest.d.ts.map +1 -0
  35. package/dist/digest.js +174 -0
  36. package/dist/digest.js.map +1 -0
  37. package/dist/errors.d.ts +654 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +960 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/index.d.ts +22 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +21 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/lib/atomicFile.d.ts +61 -0
  46. package/dist/lib/atomicFile.d.ts.map +1 -0
  47. package/dist/lib/atomicFile.js +150 -0
  48. package/dist/lib/atomicFile.js.map +1 -0
  49. package/dist/lib/backendRegistry.d.ts +59 -0
  50. package/dist/lib/backendRegistry.d.ts.map +1 -0
  51. package/dist/lib/backendRegistry.js +92 -0
  52. package/dist/lib/backendRegistry.js.map +1 -0
  53. package/dist/lib/backendUsage.d.ts +28 -0
  54. package/dist/lib/backendUsage.d.ts.map +1 -0
  55. package/dist/lib/backendUsage.js +55 -0
  56. package/dist/lib/backendUsage.js.map +1 -0
  57. package/dist/lib/backends.d.ts +142 -0
  58. package/dist/lib/backends.d.ts.map +1 -0
  59. package/dist/lib/backends.js +272 -0
  60. package/dist/lib/backends.js.map +1 -0
  61. package/dist/lib/blindedIndex.d.ts +156 -0
  62. package/dist/lib/blindedIndex.d.ts.map +1 -0
  63. package/dist/lib/blindedIndex.js +338 -0
  64. package/dist/lib/blindedIndex.js.map +1 -0
  65. package/dist/lib/collectionDescription.d.ts +33 -0
  66. package/dist/lib/collectionDescription.d.ts.map +1 -0
  67. package/dist/lib/collectionDescription.js +21 -0
  68. package/dist/lib/collectionDescription.js.map +1 -0
  69. package/dist/lib/cursor.d.ts +18 -0
  70. package/dist/lib/cursor.d.ts.map +1 -0
  71. package/dist/lib/cursor.js +50 -0
  72. package/dist/lib/cursor.js.map +1 -0
  73. package/dist/lib/edvEnvelope.d.ts +50 -0
  74. package/dist/lib/edvEnvelope.d.ts.map +1 -0
  75. package/dist/lib/edvEnvelope.js +106 -0
  76. package/dist/lib/edvEnvelope.js.map +1 -0
  77. package/dist/lib/encryption.d.ts +192 -0
  78. package/dist/lib/encryption.d.ts.map +1 -0
  79. package/dist/lib/encryption.js +383 -0
  80. package/dist/lib/encryption.js.map +1 -0
  81. package/dist/lib/etag.d.ts +33 -0
  82. package/dist/lib/etag.d.ts.map +1 -0
  83. package/dist/lib/etag.js +36 -0
  84. package/dist/lib/etag.js.map +1 -0
  85. package/dist/lib/exportManifest.d.ts +29 -0
  86. package/dist/lib/exportManifest.d.ts.map +1 -0
  87. package/dist/lib/exportManifest.js +77 -0
  88. package/dist/lib/exportManifest.js.map +1 -0
  89. package/dist/lib/importTar.d.ts +81 -0
  90. package/dist/lib/importTar.d.ts.map +1 -0
  91. package/dist/lib/importTar.js +301 -0
  92. package/dist/lib/importTar.js.map +1 -0
  93. package/dist/lib/isJson.d.ts +16 -0
  94. package/dist/lib/isJson.d.ts.map +1 -0
  95. package/dist/lib/isJson.js +17 -0
  96. package/dist/lib/isJson.js.map +1 -0
  97. package/dist/lib/keyEpoch.d.ts +38 -0
  98. package/dist/lib/keyEpoch.d.ts.map +1 -0
  99. package/dist/lib/keyEpoch.js +75 -0
  100. package/dist/lib/keyEpoch.js.map +1 -0
  101. package/dist/lib/keyedMutex.d.ts +28 -0
  102. package/dist/lib/keyedMutex.d.ts.map +1 -0
  103. package/dist/lib/keyedMutex.js +45 -0
  104. package/dist/lib/keyedMutex.js.map +1 -0
  105. package/dist/lib/kmsModule.d.ts +62 -0
  106. package/dist/lib/kmsModule.d.ts.map +1 -0
  107. package/dist/lib/kmsModule.js +327 -0
  108. package/dist/lib/kmsModule.js.map +1 -0
  109. package/dist/lib/kmsRecordCipher.d.ts +82 -0
  110. package/dist/lib/kmsRecordCipher.d.ts.map +1 -0
  111. package/dist/lib/kmsRecordCipher.js +226 -0
  112. package/dist/lib/kmsRecordCipher.js.map +1 -0
  113. package/dist/lib/pagination.d.ts +19 -0
  114. package/dist/lib/pagination.d.ts.map +1 -0
  115. package/dist/lib/pagination.js +21 -0
  116. package/dist/lib/pagination.js.map +1 -0
  117. package/dist/lib/paths.d.ts +273 -0
  118. package/dist/lib/paths.d.ts.map +1 -0
  119. package/dist/lib/paths.js +270 -0
  120. package/dist/lib/paths.js.map +1 -0
  121. package/dist/lib/preconditions.d.ts +65 -0
  122. package/dist/lib/preconditions.d.ts.map +1 -0
  123. package/dist/lib/preconditions.js +112 -0
  124. package/dist/lib/preconditions.js.map +1 -0
  125. package/dist/lib/resourceFileName.d.ts +41 -0
  126. package/dist/lib/resourceFileName.d.ts.map +1 -0
  127. package/dist/lib/resourceFileName.js +58 -0
  128. package/dist/lib/resourceFileName.js.map +1 -0
  129. package/dist/lib/revocations.d.ts +49 -0
  130. package/dist/lib/revocations.d.ts.map +1 -0
  131. package/dist/lib/revocations.js +86 -0
  132. package/dist/lib/revocations.js.map +1 -0
  133. package/dist/lib/validateDid.d.ts +19 -0
  134. package/dist/lib/validateDid.d.ts.map +1 -0
  135. package/dist/lib/validateDid.js +35 -0
  136. package/dist/lib/validateDid.js.map +1 -0
  137. package/dist/lib/validateId.d.ts +66 -0
  138. package/dist/lib/validateId.d.ts.map +1 -0
  139. package/dist/lib/validateId.js +126 -0
  140. package/dist/lib/validateId.js.map +1 -0
  141. package/dist/plugin.d.ts +115 -0
  142. package/dist/plugin.d.ts.map +1 -0
  143. package/dist/plugin.js +134 -0
  144. package/dist/plugin.js.map +1 -0
  145. package/dist/policy.d.ts +78 -0
  146. package/dist/policy.d.ts.map +1 -0
  147. package/dist/policy.js +115 -0
  148. package/dist/policy.js.map +1 -0
  149. package/dist/provisioning.d.ts +38 -0
  150. package/dist/provisioning.d.ts.map +1 -0
  151. package/dist/provisioning.js +72 -0
  152. package/dist/provisioning.js.map +1 -0
  153. package/dist/requests/BackendRequest.d.ts +61 -0
  154. package/dist/requests/BackendRequest.d.ts.map +1 -0
  155. package/dist/requests/BackendRequest.js +152 -0
  156. package/dist/requests/BackendRequest.js.map +1 -0
  157. package/dist/requests/CollectionRequest.d.ts +234 -0
  158. package/dist/requests/CollectionRequest.d.ts.map +1 -0
  159. package/dist/requests/CollectionRequest.js +705 -0
  160. package/dist/requests/CollectionRequest.js.map +1 -0
  161. package/dist/requests/KeyRequest.d.ts +115 -0
  162. package/dist/requests/KeyRequest.d.ts.map +1 -0
  163. package/dist/requests/KeyRequest.js +468 -0
  164. package/dist/requests/KeyRequest.js.map +1 -0
  165. package/dist/requests/KeystoreRequest.d.ts +99 -0
  166. package/dist/requests/KeystoreRequest.d.ts.map +1 -0
  167. package/dist/requests/KeystoreRequest.js +247 -0
  168. package/dist/requests/KeystoreRequest.js.map +1 -0
  169. package/dist/requests/PolicyRequest.d.ts +53 -0
  170. package/dist/requests/PolicyRequest.d.ts.map +1 -0
  171. package/dist/requests/PolicyRequest.js +124 -0
  172. package/dist/requests/PolicyRequest.js.map +1 -0
  173. package/dist/requests/ResourceRequest.d.ts +134 -0
  174. package/dist/requests/ResourceRequest.d.ts.map +1 -0
  175. package/dist/requests/ResourceRequest.js +548 -0
  176. package/dist/requests/ResourceRequest.js.map +1 -0
  177. package/dist/requests/RevocationRequest.d.ts +67 -0
  178. package/dist/requests/RevocationRequest.d.ts.map +1 -0
  179. package/dist/requests/RevocationRequest.js +187 -0
  180. package/dist/requests/RevocationRequest.js.map +1 -0
  181. package/dist/requests/SpaceRequest.d.ts +204 -0
  182. package/dist/requests/SpaceRequest.d.ts.map +1 -0
  183. package/dist/requests/SpaceRequest.js +520 -0
  184. package/dist/requests/SpaceRequest.js.map +1 -0
  185. package/dist/requests/SpacesRepositoryRequest.d.ts +50 -0
  186. package/dist/requests/SpacesRepositoryRequest.d.ts.map +1 -0
  187. package/dist/requests/SpacesRepositoryRequest.js +159 -0
  188. package/dist/requests/SpacesRepositoryRequest.js.map +1 -0
  189. package/dist/requests/collectionContext.d.ts +21 -0
  190. package/dist/requests/collectionContext.d.ts.map +1 -0
  191. package/dist/requests/collectionContext.js +30 -0
  192. package/dist/requests/collectionContext.js.map +1 -0
  193. package/dist/requests/controllerConsent.d.ts +58 -0
  194. package/dist/requests/controllerConsent.d.ts.map +1 -0
  195. package/dist/requests/controllerConsent.js +73 -0
  196. package/dist/requests/controllerConsent.js.map +1 -0
  197. package/dist/requests/keystoreContext.d.ts +68 -0
  198. package/dist/requests/keystoreContext.d.ts.map +1 -0
  199. package/dist/requests/keystoreContext.js +87 -0
  200. package/dist/requests/keystoreContext.js.map +1 -0
  201. package/dist/requests/resourceInput.d.ts +19 -0
  202. package/dist/requests/resourceInput.d.ts.map +1 -0
  203. package/dist/requests/resourceInput.js +102 -0
  204. package/dist/requests/resourceInput.js.map +1 -0
  205. package/dist/requests/spaceContext.d.ts +127 -0
  206. package/dist/requests/spaceContext.d.ts.map +1 -0
  207. package/dist/requests/spaceContext.js +201 -0
  208. package/dist/requests/spaceContext.js.map +1 -0
  209. package/dist/routes.d.ts +55 -0
  210. package/dist/routes.d.ts.map +1 -0
  211. package/dist/routes.js +306 -0
  212. package/dist/routes.js.map +1 -0
  213. package/dist/server.d.ts +20 -0
  214. package/dist/server.d.ts.map +1 -0
  215. package/dist/server.js +60 -0
  216. package/dist/server.js.map +1 -0
  217. package/dist/start.d.ts +7 -0
  218. package/dist/start.d.ts.map +1 -0
  219. package/dist/start.js +61 -0
  220. package/dist/start.js.map +1 -0
  221. package/dist/storage.d.ts +29 -0
  222. package/dist/storage.d.ts.map +1 -0
  223. package/dist/storage.js +47 -0
  224. package/dist/storage.js.map +1 -0
  225. package/dist/types.d.ts +915 -0
  226. package/dist/types.d.ts.map +1 -0
  227. package/dist/types.js +2 -0
  228. package/dist/types.js.map +1 -0
  229. package/dist/views/home.hbs +9 -0
  230. package/dist/views/templates/main.hbs +12 -0
  231. package/dist/zcap.d.ts +267 -0
  232. package/dist/zcap.d.ts.map +1 -0
  233. package/dist/zcap.js +478 -0
  234. package/dist/zcap.js.map +1 -0
  235. package/package.json +109 -0
  236. package/src/auth-header-hooks.ts +163 -0
  237. package/src/authorize.ts +117 -0
  238. package/src/backends/filesystem.ts +3626 -0
  239. package/src/backends/postgres.ts +3073 -0
  240. package/src/backends/postgresSchema.ts +231 -0
  241. package/src/config.default.ts +657 -0
  242. package/src/corsProxy.ts +326 -0
  243. package/src/declarations.d.ts +28 -0
  244. package/src/digest.ts +223 -0
  245. package/src/errors.ts +1122 -0
  246. package/src/index.ts +21 -0
  247. package/src/lib/atomicFile.ts +176 -0
  248. package/src/lib/backendRegistry.ts +139 -0
  249. package/src/lib/backendUsage.ts +79 -0
  250. package/src/lib/backends.ts +351 -0
  251. package/src/lib/blindedIndex.ts +466 -0
  252. package/src/lib/collectionDescription.ts +43 -0
  253. package/src/lib/cursor.ts +52 -0
  254. package/src/lib/edvEnvelope.ts +120 -0
  255. package/src/lib/encryption.ts +472 -0
  256. package/src/lib/etag.ts +40 -0
  257. package/src/lib/exportManifest.ts +115 -0
  258. package/src/lib/importTar.ts +381 -0
  259. package/src/lib/isJson.ts +18 -0
  260. package/src/lib/keyEpoch.ts +89 -0
  261. package/src/lib/keyedMutex.ts +45 -0
  262. package/src/lib/kmsModule.ts +451 -0
  263. package/src/lib/kmsRecordCipher.ts +292 -0
  264. package/src/lib/pagination.ts +22 -0
  265. package/src/lib/paths.ts +382 -0
  266. package/src/lib/preconditions.ts +145 -0
  267. package/src/lib/resourceFileName.ts +69 -0
  268. package/src/lib/revocations.ts +116 -0
  269. package/src/lib/validateDid.ts +41 -0
  270. package/src/lib/validateId.ts +148 -0
  271. package/src/plugin.ts +288 -0
  272. package/src/policy.ts +176 -0
  273. package/src/provisioning.ts +98 -0
  274. package/src/requests/BackendRequest.ts +219 -0
  275. package/src/requests/CollectionRequest.ts +899 -0
  276. package/src/requests/KeyRequest.ts +617 -0
  277. package/src/requests/KeystoreRequest.ts +325 -0
  278. package/src/requests/PolicyRequest.ts +169 -0
  279. package/src/requests/ResourceRequest.ts +649 -0
  280. package/src/requests/RevocationRequest.ts +267 -0
  281. package/src/requests/SpaceRequest.ts +686 -0
  282. package/src/requests/SpacesRepositoryRequest.ts +194 -0
  283. package/src/requests/collectionContext.ts +41 -0
  284. package/src/requests/controllerConsent.ts +108 -0
  285. package/src/requests/keystoreContext.ts +124 -0
  286. package/src/requests/resourceInput.ts +118 -0
  287. package/src/requests/spaceContext.ts +311 -0
  288. package/src/routes.ts +420 -0
  289. package/src/server.ts +69 -0
  290. package/src/start.ts +71 -0
  291. package/src/storage.ts +60 -0
  292. package/src/types.ts +1016 -0
  293. package/src/views/home.hbs +9 -0
  294. package/src/views/templates/main.hbs +12 -0
  295. package/src/zcap.ts +657 -0
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Library entry point: the surface a downstream composition consumes when this
3
+ * package is used as a dependency rather than run standalone. Exports the
4
+ * `fastifyWas` protocol plugin, the community-edition `createApp()`
5
+ * composition, the
6
+ * storage pieces a custom backend needs (the `StorageBackend` contract types,
7
+ * the reference `FileSystemBackend`, and `defaultBackend()`), and the typed
8
+ * protocol errors a backend or composition throws and handles.
9
+ *
10
+ * Importing anything from here also loads the Fastify module augmentation in
11
+ * types.ts (`FastifyInstance.serverUrl` / `.storage`, `FastifyRequest.zcap`),
12
+ * so consumers get the decorated instance typed for free.
13
+ */
14
+ export { fastifyWas, type FastifyWasOptions } from './plugin.js'
15
+ export { createApp } from './server.js'
16
+ export { defaultBackend } from './storage.js'
17
+ export { FileSystemBackend } from './backends/filesystem.js'
18
+ export { PostgresBackend } from './backends/postgres.js'
19
+ export { onboardingTokenAuthorizer } from './provisioning.js'
20
+ export type * from './types.js'
21
+ export * from './errors.js'
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Atomic, durable filesystem write helpers, shared by the filesystem backend so
3
+ * every write goes through one audited path. A durable write must survive a
4
+ * crash or power loss without leaving a torn (partially-written) or missing
5
+ * file: bytes land in a temp file in the SAME directory, the file descriptor is
6
+ * fsync'd before close, the temp is `rename`d (or hard-`link`ed) onto the final
7
+ * path -- an atomic metadata operation -- and finally the containing directory
8
+ * is fsync'd so the new directory entry itself is on stable storage.
9
+ *
10
+ * Pure and backend-agnostic (no backend imports). Temp files use a `.tmp-`
11
+ * dot-prefix that no directory enumeration in the tree parses or filters on
12
+ * (those match `r.`, `.meta.`, `.space.`, `.collection.`, `.policy.`,
13
+ * `.backend.`, or a `.json` suffix), so a temp file transiently present during
14
+ * a write is never mistaken for a Resource, sidecar, or config record.
15
+ */
16
+ import path from 'node:path'
17
+ import { randomUUID } from 'node:crypto'
18
+ import fs from 'node:fs'
19
+
20
+ const { open, rename, unlink, link } = fs.promises
21
+
22
+ /**
23
+ * The temp path a write for `filePath` stages into: a `.tmp-<uuid>` dot-file in
24
+ * the SAME directory as the target, so the final `rename` / `link` stays on one
25
+ * filesystem (a cross-device move is not atomic).
26
+ * @param filePath {string} the final destination path
27
+ * @returns {string}
28
+ */
29
+ export function tempPathFor(filePath: string): string {
30
+ return path.join(path.dirname(filePath), `.tmp-${randomUUID()}`)
31
+ }
32
+
33
+ /**
34
+ * fsync a directory so a just-created/renamed entry within it is durable.
35
+ * Swallows the errors platforms raise when a directory handle cannot be fsync'd
36
+ * (e.g. Windows), but lets genuine failures propagate.
37
+ * @param dirPath {string}
38
+ * @returns {Promise<void>}
39
+ */
40
+ export async function fsyncDirectory(dirPath: string): Promise<void> {
41
+ let handle: fs.promises.FileHandle | undefined
42
+ try {
43
+ handle = await open(dirPath, 'r')
44
+ await handle.sync()
45
+ } catch (err) {
46
+ const code = (err as NodeJS.ErrnoException).code
47
+ // Platforms that cannot fsync a directory handle report one of these; the
48
+ // rename/link itself already succeeded, so treat directory fsync as a
49
+ // best-effort durability step rather than a hard failure.
50
+ if (
51
+ code === 'EISDIR' ||
52
+ code === 'EPERM' ||
53
+ code === 'EBADF' ||
54
+ code === 'ENOTSUP' ||
55
+ code === 'EINVAL'
56
+ ) {
57
+ return
58
+ }
59
+ throw err
60
+ } finally {
61
+ await handle?.close()
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Writes `data` to `tempPath` (exclusive create) and fsyncs the file descriptor
67
+ * before closing, so the bytes are on stable storage before any rename/link
68
+ * publishes the temp under a caller-visible name.
69
+ * @param options {object}
70
+ * @param options.tempPath {string}
71
+ * @param options.data {string|Buffer}
72
+ * @returns {Promise<void>}
73
+ */
74
+ async function writeAndSyncTemp({
75
+ tempPath,
76
+ data
77
+ }: {
78
+ tempPath: string
79
+ data: string | Buffer
80
+ }): Promise<void> {
81
+ const handle = await open(tempPath, 'wx')
82
+ try {
83
+ await handle.writeFile(data)
84
+ await handle.sync()
85
+ } finally {
86
+ await handle.close()
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Atomically and durably writes `data` to `filePath` (full replacement):
92
+ * write + fsync a temp file, `rename` it onto `filePath`, then fsync the
93
+ * directory. The final path never observes a partially-written file. On any
94
+ * failure the temp file is cleaned up.
95
+ * @param options {object}
96
+ * @param options.filePath {string}
97
+ * @param options.data {string|Buffer}
98
+ * @returns {Promise<void>}
99
+ */
100
+ export async function atomicWriteFile({
101
+ filePath,
102
+ data
103
+ }: {
104
+ filePath: string
105
+ data: string | Buffer
106
+ }): Promise<void> {
107
+ const tempPath = tempPathFor(filePath)
108
+ try {
109
+ await writeAndSyncTemp({ tempPath, data })
110
+ await rename(tempPath, filePath)
111
+ } catch (err) {
112
+ await unlink(tempPath).catch(() => {})
113
+ throw err
114
+ }
115
+ await fsyncDirectory(path.dirname(filePath))
116
+ }
117
+
118
+ /**
119
+ * Atomically and durably creates `filePath`, failing if it already exists --
120
+ * the `wx`-style create-only semantics, preserved atomically: write + fsync a
121
+ * temp file, then `fs.promises.link` it onto `filePath` (which rejects with
122
+ * `EEXIST` when the target exists), unlink the temp, and fsync the directory.
123
+ * An `EEXIST` propagates so callers can map it to their conflict errors; on any
124
+ * failure the temp file is cleaned up while `filePath` is left untouched.
125
+ * @param options {object}
126
+ * @param options.filePath {string}
127
+ * @param options.data {string|Buffer}
128
+ * @returns {Promise<void>}
129
+ */
130
+ export async function atomicCreateFile({
131
+ filePath,
132
+ data
133
+ }: {
134
+ filePath: string
135
+ data: string | Buffer
136
+ }): Promise<void> {
137
+ const tempPath = tempPathFor(filePath)
138
+ try {
139
+ await writeAndSyncTemp({ tempPath, data })
140
+ await link(tempPath, filePath)
141
+ } catch (err) {
142
+ // Only ever remove the temp file here, never `filePath`: on an EEXIST the
143
+ // pre-existing target must survive intact.
144
+ await unlink(tempPath).catch(() => {})
145
+ throw err
146
+ }
147
+ await unlink(tempPath).catch(() => {})
148
+ await fsyncDirectory(path.dirname(filePath))
149
+ }
150
+
151
+ /**
152
+ * Commits a temp file a caller streamed into (see `tempPathFor`) onto its final
153
+ * path durably: fsync the temp's bytes, `rename` it onto `filePath`, then fsync
154
+ * the directory. The caller owns cleanup of the temp file on a streaming
155
+ * failure (it never reaches here in that case).
156
+ * @param options {object}
157
+ * @param options.tempPath {string}
158
+ * @param options.filePath {string}
159
+ * @returns {Promise<void>}
160
+ */
161
+ export async function commitTempFile({
162
+ tempPath,
163
+ filePath
164
+ }: {
165
+ tempPath: string
166
+ filePath: string
167
+ }): Promise<void> {
168
+ const handle = await open(tempPath, 'r+')
169
+ try {
170
+ await handle.sync()
171
+ } finally {
172
+ await handle.close()
173
+ }
174
+ await rename(tempPath, filePath)
175
+ await fsyncDirectory(path.dirname(filePath))
176
+ }
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Per-Collection backend resolver (spec "Backends"; stage 3 of the BYOS plan).
3
+ * Maps a Collection's selected `backend.id` to the live `StorageBackend` that
4
+ * serves its **data plane** (resource bytes, metadata, listings, change feed),
5
+ * while the server `default` backend (`request.server.storage`) keeps the
6
+ * **control plane** (Space/Collection descriptions, policies, the backend
7
+ * registry records). For the `default` backend the two planes are the same
8
+ * instance, so behavior is byte-for-byte unchanged.
9
+ *
10
+ * `resolveBackend` short-circuits to the default backend for the common
11
+ * `default`/absent selection (one branch, no extra I/O). For a Collection that
12
+ * selects a registered `external` backend it loads the (secret-bearing) backend
13
+ * record from the control plane and builds -- memoized -- the provider adapter
14
+ * from the injected `backendProviders` registry; with no factory for the record's
15
+ * `provider` it fails closed with `unsupported-backend` (409). Memoization
16
+ * mirrors spaceContext.ts: one `LruCache` per registry (a `WeakMap`), so adapter
17
+ * instances are reused across requests and a registry's adapters are GC'd with
18
+ * it.
19
+ */
20
+ import type { FastifyRequest } from 'fastify'
21
+ import { LruCache } from '@interop/lru-memoize'
22
+ import { DEFAULT_BACKEND_ID } from './backends.js'
23
+ import { UnsupportedBackendError } from '../errors.js'
24
+ import { RESOLVED_BACKEND_CACHE_MAX } from '../config.default.js'
25
+ import type {
26
+ BackendProviderRegistry,
27
+ CollectionDescription,
28
+ StorageBackend
29
+ } from '../types.js'
30
+
31
+ /**
32
+ * One adapter cache per provider registry, keyed by `${spaceId}/${backendId}`.
33
+ * Scoped to the registry instance via a `WeakMap` (rather than module-global) so
34
+ * parallel test suites never share adapters, and a cache is discarded with its
35
+ * registry. `memoize` evicts a rejected promise automatically, so a failed
36
+ * resolution (missing record / no factory) is retried on the next request rather
37
+ * than cached.
38
+ */
39
+ const adapterCaches = new WeakMap<BackendProviderRegistry, LruCache>()
40
+
41
+ /**
42
+ * Returns the (lazily created) adapter cache for a provider registry.
43
+ * @param providers {BackendProviderRegistry}
44
+ * @returns {LruCache}
45
+ */
46
+ function adapterCacheFor(providers: BackendProviderRegistry): LruCache {
47
+ let cache = adapterCaches.get(providers)
48
+ if (!cache) {
49
+ cache = new LruCache({ max: RESOLVED_BACKEND_CACHE_MAX })
50
+ adapterCaches.set(providers, cache)
51
+ }
52
+ return cache
53
+ }
54
+
55
+ /**
56
+ * Resolves the data-plane `StorageBackend` for a Collection. Returns the server
57
+ * default backend when the Collection selects `default` (or selects nothing);
58
+ * otherwise builds (memoized) the provider adapter for its registered backend.
59
+ *
60
+ * @param options {object}
61
+ * @param options.request {FastifyRequest} supplies `request.server` (the
62
+ * control-plane `storage`, the `backendProviders` registry, and the
63
+ * non-request instance `log` passed to the memoized adapter factory)
64
+ * @param options.spaceId {string}
65
+ * @param options.collectionId {string}
66
+ * @param [options.collectionDescription] {CollectionDescription} the
67
+ * already-fetched description (most handlers fetch it for auth); read from the
68
+ * control plane when omitted
69
+ * @returns {Promise<StorageBackend>}
70
+ */
71
+ export async function resolveBackend({
72
+ request,
73
+ spaceId,
74
+ collectionId,
75
+ collectionDescription
76
+ }: {
77
+ request: FastifyRequest
78
+ spaceId: string
79
+ collectionId: string
80
+ collectionDescription?: CollectionDescription
81
+ }): Promise<StorageBackend> {
82
+ const { storage, backendProviders } = request.server
83
+ const description =
84
+ collectionDescription ??
85
+ (await storage.getCollectionDescription({ spaceId, collectionId }))
86
+ const backendId = description?.backend?.id
87
+ // Fast path: the default backend is both control and data plane (unchanged).
88
+ if (backendId === undefined || backendId === DEFAULT_BACKEND_ID) {
89
+ return storage
90
+ }
91
+ return adapterCacheFor(backendProviders).memoize<StorageBackend>({
92
+ key: `${spaceId}/${backendId}`,
93
+ fn: async () => {
94
+ // Control-plane read: the secret-bearing record lives on the default backend.
95
+ const record = await storage.getBackend({ spaceId, backendId })
96
+ if (!record) {
97
+ throw new UnsupportedBackendError({ backendId })
98
+ }
99
+ // Status gating is deferred (stage 4): the resolver keys on whether a
100
+ // provider factory is registered, not on `connection.status` (which only
101
+ // becomes `connected` once the OAuth exchange exists).
102
+ const factory = backendProviders.get(record.provider)
103
+ if (!factory) {
104
+ throw new UnsupportedBackendError({
105
+ backendId,
106
+ detail: `Backend '${backendId}' is registered but not connected/operable yet.`
107
+ })
108
+ }
109
+ // Wire the non-request instance logger (as `createApp` does for the
110
+ // primary backend), NOT `request.log`: the adapter is memoized and reused
111
+ // across requests, so capturing the first request's per-request child
112
+ // logger would mis-tag every later log line with that first request's id.
113
+ return factory(record, { logger: request.server.log })
114
+ }
115
+ })
116
+ }
117
+
118
+ /**
119
+ * Drops the memoized adapter for a backend record. Call after a record changes
120
+ * (register-replace / deregister) so the next resolve rebuilds the adapter from
121
+ * the new connection material.
122
+ * @param options {object}
123
+ * @param options.providers {BackendProviderRegistry}
124
+ * @param options.spaceId {string}
125
+ * @param options.backendId {string}
126
+ * @returns {void}
127
+ */
128
+ export function invalidateResolvedBackend({
129
+ providers,
130
+ spaceId,
131
+ backendId
132
+ }: {
133
+ providers: BackendProviderRegistry
134
+ spaceId: string
135
+ backendId: string
136
+ }): void {
137
+ // Only touch a cache that already exists for this registry.
138
+ adapterCaches.get(providers)?.delete(`${spaceId}/${backendId}`)
139
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Backend-agnostic derivation of the quota-report condition fields (spec
3
+ * "Quotas"). Both storage backends build their `BackendUsage` entries through
4
+ * this helper so the `state` / `limit` / `restrictedActions` thresholds cannot
5
+ * drift between them; only the usage *measurement* (a `du` pass vs a
6
+ * transactional counter) is backend-specific.
7
+ */
8
+ import { QUOTA_NEAR_LIMIT_FRACTION } from '../config.default.js'
9
+ import type {
10
+ Action,
11
+ BackendState,
12
+ BackendUsage,
13
+ StorageLimit
14
+ } from '../types.js'
15
+
16
+ /**
17
+ * Builds the backend-identity and condition fields shared by the Space and
18
+ * per-Collection quota reports. `usageBytes` is what the report shows (the
19
+ * Space total or a single Collection's slice); `spaceTotalBytes` drives the
20
+ * `state` / `restrictedActions`, which are backend-wide (the quota is a
21
+ * per-Space limit) and so always measured against the Space total. The
22
+ * `constraints.maxUploadBytes` cap is advertised when configured.
23
+ * @param options {object}
24
+ * @param options.usageBytes {number} the usage figure to report
25
+ * @param options.spaceTotalBytes {number} the Space total, for state
26
+ * @param [options.capacityBytes] {number} the configured per-Space limit
27
+ * @param [options.maxUploadBytes] {number} the per-upload cap to advertise
28
+ * @param options.id {string} the backend's id (from `describe()`)
29
+ * @param [options.name] {string} the backend's display name
30
+ * @param options.managedBy {string} the backend's custody mode
31
+ * @returns {Omit<BackendUsage, 'measuredAt' | 'usageByCollection'>}
32
+ */
33
+ export function backendUsageFields({
34
+ usageBytes,
35
+ spaceTotalBytes,
36
+ capacityBytes,
37
+ maxUploadBytes,
38
+ id,
39
+ name,
40
+ managedBy
41
+ }: {
42
+ usageBytes: number
43
+ spaceTotalBytes: number
44
+ capacityBytes?: number
45
+ maxUploadBytes?: number
46
+ id: string
47
+ name?: string
48
+ managedBy: BackendUsage['managedBy']
49
+ }): Omit<BackendUsage, 'measuredAt' | 'usageByCollection'> {
50
+ const limit: StorageLimit =
51
+ capacityBytes === undefined
52
+ ? { isUnlimited: true }
53
+ : { capacityBytes, isUnlimited: false }
54
+
55
+ let state: BackendState = 'ok'
56
+ let restrictedActions: Action[] = []
57
+ if (capacityBytes !== undefined) {
58
+ if (spaceTotalBytes >= capacityBytes) {
59
+ state = 'over-quota'
60
+ // The backend is full: writes are restricted, reads/deletes still work.
61
+ restrictedActions = ['POST', 'PUT']
62
+ } else if (spaceTotalBytes >= capacityBytes * QUOTA_NEAR_LIMIT_FRACTION) {
63
+ state = 'near-limit'
64
+ }
65
+ }
66
+
67
+ return {
68
+ id,
69
+ ...(name !== undefined && { name }),
70
+ managedBy,
71
+ state,
72
+ usageBytes,
73
+ limit,
74
+ ...(maxUploadBytes !== undefined && {
75
+ constraints: { maxUploadBytes }
76
+ }),
77
+ restrictedActions
78
+ }
79
+ }