cursedbelt-server 1.0.0

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 (606) hide show
  1. package/LICENSE +15 -0
  2. package/dist/server/activity/index.d.ts +36 -0
  3. package/dist/server/activity/index.js +36 -0
  4. package/dist/server/activity/migrations.d.ts +21 -0
  5. package/dist/server/activity/migrations.js +55 -0
  6. package/dist/server/activity/shareSource.d.ts +11 -0
  7. package/dist/server/activity/shareSource.js +45 -0
  8. package/dist/server/activity/store.d.ts +123 -0
  9. package/dist/server/activity/store.js +178 -0
  10. package/dist/server/app.d.ts +56 -0
  11. package/dist/server/app.js +45 -0
  12. package/dist/server/audit/index.d.ts +2 -0
  13. package/dist/server/audit/index.js +2 -0
  14. package/dist/server/audit/migrations.d.ts +2 -0
  15. package/dist/server/audit/migrations.js +79 -0
  16. package/dist/server/audit/sqliteSink.d.ts +36 -0
  17. package/dist/server/audit/sqliteSink.js +95 -0
  18. package/dist/server/audit/sqliteSink.test.d.ts +1 -0
  19. package/dist/server/audit/sqliteSink.test.js +180 -0
  20. package/dist/server/auth/cookie.d.ts +22 -0
  21. package/dist/server/auth/cookie.js +35 -0
  22. package/dist/server/auth/csrf.d.ts +42 -0
  23. package/dist/server/auth/csrf.js +36 -0
  24. package/dist/server/auth/fileTokenSigner.d.ts +10 -0
  25. package/dist/server/auth/fileTokenSigner.js +22 -0
  26. package/dist/server/auth/getOnly.d.ts +20 -0
  27. package/dist/server/auth/getOnly.js +27 -0
  28. package/dist/server/auth/index.d.ts +12 -0
  29. package/dist/server/auth/index.js +15 -0
  30. package/dist/server/auth/jwt.d.ts +18 -0
  31. package/dist/server/auth/jwt.js +56 -0
  32. package/dist/server/auth/loginThrottle.d.ts +118 -0
  33. package/dist/server/auth/loginThrottle.js +204 -0
  34. package/dist/server/auth/middleware.d.ts +14 -0
  35. package/dist/server/auth/middleware.js +47 -0
  36. package/dist/server/auth/password.d.ts +14 -0
  37. package/dist/server/auth/password.js +65 -0
  38. package/dist/server/auth/sessionStore.d.ts +19 -0
  39. package/dist/server/auth/sessionStore.js +221 -0
  40. package/dist/server/auth/totp.d.ts +34 -0
  41. package/dist/server/auth/totp.js +81 -0
  42. package/dist/server/auth/types.d.ts +133 -0
  43. package/dist/server/auth/types.js +1 -0
  44. package/dist/server/auth/vault.d.ts +27 -0
  45. package/dist/server/auth/vault.js +57 -0
  46. package/dist/server/context.d.ts +35 -0
  47. package/dist/server/context.js +1 -0
  48. package/dist/server/ctgr.d.ts +32 -0
  49. package/dist/server/ctgr.js +69 -0
  50. package/dist/server/db/kysely.d.ts +14 -0
  51. package/dist/server/db/kysely.js +16 -0
  52. package/dist/server/db/plumbingTypes.d.ts +116 -0
  53. package/dist/server/db/plumbingTypes.js +12 -0
  54. package/dist/server/dropzone/duplicates.d.ts +82 -0
  55. package/dist/server/dropzone/duplicates.js +90 -0
  56. package/dist/server/dropzone/index.d.ts +13 -0
  57. package/dist/server/dropzone/index.js +13 -0
  58. package/dist/server/dropzone/scan.d.ts +166 -0
  59. package/dist/server/dropzone/scan.js +272 -0
  60. package/dist/server/errors.d.ts +7 -0
  61. package/dist/server/errors.js +7 -0
  62. package/dist/server/guard/argon2Binding.d.ts +97 -0
  63. package/dist/server/guard/argon2Binding.js +138 -0
  64. package/dist/server/guard/index.d.ts +4 -0
  65. package/dist/server/guard/index.js +6 -0
  66. package/dist/server/guard/kdfWorker.d.ts +101 -0
  67. package/dist/server/guard/kdfWorker.js +301 -0
  68. package/dist/server/guard/kdfWorkerEntry.d.ts +1 -0
  69. package/dist/server/guard/kdfWorkerEntry.js +15 -0
  70. package/dist/server/guard/passwordVerifier.d.ts +65 -0
  71. package/dist/server/guard/passwordVerifier.js +122 -0
  72. package/dist/server/guard/revocationStore.d.ts +132 -0
  73. package/dist/server/guard/revocationStore.js +211 -0
  74. package/dist/server/guard/sessionSigner.d.ts +132 -0
  75. package/dist/server/guard/sessionSigner.js +303 -0
  76. package/dist/server/guard/verifier.d.ts +21 -0
  77. package/dist/server/guard/verifier.js +1 -0
  78. package/dist/server/incidents/channels.d.ts +161 -0
  79. package/dist/server/incidents/channels.js +357 -0
  80. package/dist/server/incidents/checks.d.ts +61 -0
  81. package/dist/server/incidents/checks.js +55 -0
  82. package/dist/server/incidents/index.d.ts +51 -0
  83. package/dist/server/incidents/index.js +51 -0
  84. package/dist/server/incidents/migrations.d.ts +20 -0
  85. package/dist/server/incidents/migrations.js +58 -0
  86. package/dist/server/incidents/notifier.d.ts +79 -0
  87. package/dist/server/incidents/notifier.js +141 -0
  88. package/dist/server/incidents/reporter.d.ts +60 -0
  89. package/dist/server/incidents/reporter.js +49 -0
  90. package/dist/server/incidents/routes.d.ts +91 -0
  91. package/dist/server/incidents/routes.js +202 -0
  92. package/dist/server/incidents/store.d.ts +75 -0
  93. package/dist/server/incidents/store.js +176 -0
  94. package/dist/server/incidents/types.d.ts +107 -0
  95. package/dist/server/incidents/types.js +32 -0
  96. package/dist/server/index.d.ts +25 -0
  97. package/dist/server/index.js +53 -0
  98. package/dist/server/jobs/fireQueue.d.ts +12 -0
  99. package/dist/server/jobs/fireQueue.js +37 -0
  100. package/dist/server/jobs/index.d.ts +6 -0
  101. package/dist/server/jobs/index.js +10 -0
  102. package/dist/server/jobs/logger.d.ts +10 -0
  103. package/dist/server/jobs/logger.js +14 -0
  104. package/dist/server/jobs/plainjobAdapter.d.ts +19 -0
  105. package/dist/server/jobs/plainjobAdapter.js +48 -0
  106. package/dist/server/jobs/polling.d.ts +17 -0
  107. package/dist/server/jobs/polling.js +79 -0
  108. package/dist/server/jobs/runner.d.ts +24 -0
  109. package/dist/server/jobs/runner.js +186 -0
  110. package/dist/server/jobs/timeout.d.ts +28 -0
  111. package/dist/server/jobs/timeout.js +47 -0
  112. package/dist/server/jobs/types.d.ts +167 -0
  113. package/dist/server/jobs/types.js +12 -0
  114. package/dist/server/master-lock/guard.d.ts +53 -0
  115. package/dist/server/master-lock/guard.js +205 -0
  116. package/dist/server/master-lock/index.d.ts +21 -0
  117. package/dist/server/master-lock/index.js +21 -0
  118. package/dist/server/master-lock/lockPage.d.ts +34 -0
  119. package/dist/server/master-lock/lockPage.js +281 -0
  120. package/dist/server/master-lock/masterLock.d.ts +294 -0
  121. package/dist/server/master-lock/masterLock.js +523 -0
  122. package/dist/server/master-lock/principals.d.ts +76 -0
  123. package/dist/server/master-lock/principals.js +157 -0
  124. package/dist/server/master-lock/seed.d.ts +47 -0
  125. package/dist/server/master-lock/seed.js +93 -0
  126. package/dist/server/master-lock/store.d.ts +19 -0
  127. package/dist/server/master-lock/store.js +20 -0
  128. package/dist/server/media-bun/image.d.ts +7 -0
  129. package/dist/server/media-bun/image.js +96 -0
  130. package/dist/server/media-bun/index.d.ts +4 -0
  131. package/dist/server/media-bun/index.js +10 -0
  132. package/dist/server/media-bun/streaming.d.ts +86 -0
  133. package/dist/server/media-bun/streaming.js +237 -0
  134. package/dist/server/media-bun/video.d.ts +35 -0
  135. package/dist/server/media-bun/video.js +181 -0
  136. package/dist/server/metrics/metricsBuffer.d.ts +39 -0
  137. package/dist/server/metrics/metricsBuffer.js +36 -0
  138. package/dist/server/metrics/normalizeRoute.d.ts +5 -0
  139. package/dist/server/metrics/normalizeRoute.js +58 -0
  140. package/dist/server/middleware/correlationId.d.ts +9 -0
  141. package/dist/server/middleware/correlationId.js +20 -0
  142. package/dist/server/middleware/corsAllowlist.d.ts +38 -0
  143. package/dist/server/middleware/corsAllowlist.js +37 -0
  144. package/dist/server/middleware/errorEnvelope.d.ts +17 -0
  145. package/dist/server/middleware/errorEnvelope.js +32 -0
  146. package/dist/server/middleware/index.d.ts +9 -0
  147. package/dist/server/middleware/index.js +10 -0
  148. package/dist/server/middleware/localOrigin.d.ts +71 -0
  149. package/dist/server/middleware/localOrigin.js +119 -0
  150. package/dist/server/middleware/rateLimit.d.ts +56 -0
  151. package/dist/server/middleware/rateLimit.js +86 -0
  152. package/dist/server/middleware/requestLogger.d.ts +29 -0
  153. package/dist/server/middleware/requestLogger.js +47 -0
  154. package/dist/server/middleware/securityHeaders.d.ts +29 -0
  155. package/dist/server/middleware/securityHeaders.js +44 -0
  156. package/dist/server/middleware/zValidator.d.ts +29 -0
  157. package/dist/server/middleware/zValidator.js +57 -0
  158. package/dist/server/migration/runner.d.ts +42 -0
  159. package/dist/server/migration/runner.js +70 -0
  160. package/dist/server/migration/types.d.ts +16 -0
  161. package/dist/server/migration/types.js +1 -0
  162. package/dist/server/net/outboundGuard.d.ts +30 -0
  163. package/dist/server/net/outboundGuard.js +199 -0
  164. package/dist/server/notifications/hub.d.ts +67 -0
  165. package/dist/server/notifications/hub.js +187 -0
  166. package/dist/server/notifications/index.d.ts +47 -0
  167. package/dist/server/notifications/index.js +46 -0
  168. package/dist/server/notifications/migrations.d.ts +13 -0
  169. package/dist/server/notifications/migrations.js +20 -0
  170. package/dist/server/notifications/routes.d.ts +58 -0
  171. package/dist/server/notifications/routes.js +113 -0
  172. package/dist/server/notifications/service.d.ts +58 -0
  173. package/dist/server/notifications/service.js +41 -0
  174. package/dist/server/paths/appDataHome.d.ts +19 -0
  175. package/dist/server/paths/appDataHome.js +68 -0
  176. package/dist/server/private-media/crypto.d.ts +37 -0
  177. package/dist/server/private-media/crypto.js +111 -0
  178. package/dist/server/private-media/index-store.d.ts +31 -0
  179. package/dist/server/private-media/index-store.js +36 -0
  180. package/dist/server/private-media/mountPrivateRoutes.d.ts +26 -0
  181. package/dist/server/private-media/mountPrivateRoutes.js +219 -0
  182. package/dist/server/private-media/pipeline.d.ts +37 -0
  183. package/dist/server/private-media/pipeline.js +97 -0
  184. package/dist/server/private-media/unlock.d.ts +52 -0
  185. package/dist/server/private-media/unlock.js +55 -0
  186. package/dist/server/resume/index.d.ts +2 -0
  187. package/dist/server/resume/index.js +2 -0
  188. package/dist/server/resume/routes.d.ts +41 -0
  189. package/dist/server/resume/routes.js +50 -0
  190. package/dist/server/resume/store.d.ts +37 -0
  191. package/dist/server/resume/store.js +38 -0
  192. package/dist/server/retention/archive.d.ts +78 -0
  193. package/dist/server/retention/archive.js +284 -0
  194. package/dist/server/retention/fleet.d.ts +50 -0
  195. package/dist/server/retention/fleet.js +66 -0
  196. package/dist/server/retention/index.d.ts +38 -0
  197. package/dist/server/retention/index.js +38 -0
  198. package/dist/server/retention/policy.d.ts +263 -0
  199. package/dist/server/retention/policy.js +264 -0
  200. package/dist/server/retention/store.d.ts +101 -0
  201. package/dist/server/retention/store.js +251 -0
  202. package/dist/server/retention/sweeper.d.ts +69 -0
  203. package/dist/server/retention/sweeper.js +289 -0
  204. package/dist/server/retention/target.d.ts +143 -0
  205. package/dist/server/retention/target.js +438 -0
  206. package/dist/server/search/index.d.ts +2 -0
  207. package/dist/server/search/index.js +6 -0
  208. package/dist/server/search/pgSearchProvider.d.ts +91 -0
  209. package/dist/server/search/pgSearchProvider.js +330 -0
  210. package/dist/server/search/pgSearchProvider.pg.test.d.ts +1 -0
  211. package/dist/server/search/pgSearchProvider.pg.test.js +338 -0
  212. package/dist/server/search/pgSearchSchema.d.ts +81 -0
  213. package/dist/server/search/pgSearchSchema.js +89 -0
  214. package/dist/server/sharing/index.d.ts +18 -0
  215. package/dist/server/sharing/index.js +16 -0
  216. package/dist/server/sharing/migrations.d.ts +6 -0
  217. package/dist/server/sharing/migrations.js +79 -0
  218. package/dist/server/sharing/router.d.ts +111 -0
  219. package/dist/server/sharing/router.js +93 -0
  220. package/dist/server/sharing/sql.d.ts +67 -0
  221. package/dist/server/sharing/sql.js +88 -0
  222. package/dist/server/sharing/store.d.ts +173 -0
  223. package/dist/server/sharing/store.js +484 -0
  224. package/dist/server/sqlite/backup.d.ts +48 -0
  225. package/dist/server/sqlite/backup.js +97 -0
  226. package/dist/server/sqlite/browse.d.ts +42 -0
  227. package/dist/server/sqlite/browse.js +84 -0
  228. package/dist/server/sqlite/export.d.ts +32 -0
  229. package/dist/server/sqlite/export.js +42 -0
  230. package/dist/server/sqlite/fts5.d.ts +106 -0
  231. package/dist/server/sqlite/fts5.js +154 -0
  232. package/dist/server/sqlite/health.d.ts +44 -0
  233. package/dist/server/sqlite/health.js +52 -0
  234. package/dist/server/sqlite/import.d.ts +36 -0
  235. package/dist/server/sqlite/import.js +108 -0
  236. package/dist/server/sqlite/index.d.ts +9 -0
  237. package/dist/server/sqlite/index.js +15 -0
  238. package/dist/server/sqlite/readonlySql.d.ts +15 -0
  239. package/dist/server/sqlite/readonlySql.js +65 -0
  240. package/dist/server/sqlite/stats.d.ts +22 -0
  241. package/dist/server/sqlite/stats.js +32 -0
  242. package/dist/server/sqlite/viewer.d.ts +17 -0
  243. package/dist/server/sqlite/viewer.js +56 -0
  244. package/dist/server/storage/binaryServerProcessing.d.ts +23 -0
  245. package/dist/server/storage/binaryServerProcessing.js +81 -0
  246. package/dist/server/storage/binaryServerStore.d.ts +3 -0
  247. package/dist/server/storage/binaryServerStore.js +95 -0
  248. package/dist/server/storage/diskStore.d.ts +2 -0
  249. package/dist/server/storage/diskStore.js +88 -0
  250. package/dist/server/storage/files/adapter.d.ts +33 -0
  251. package/dist/server/storage/files/adapter.js +203 -0
  252. package/dist/server/storage/files/catalogue.d.ts +72 -0
  253. package/dist/server/storage/files/catalogue.js +1033 -0
  254. package/dist/server/storage/files/chunkedUpload.d.ts +56 -0
  255. package/dist/server/storage/files/chunkedUpload.js +70 -0
  256. package/dist/server/storage/files/fileRoutes.d.ts +51 -0
  257. package/dist/server/storage/files/fileRoutes.js +405 -0
  258. package/dist/server/storage/files/index.d.ts +9 -0
  259. package/dist/server/storage/files/index.js +11 -0
  260. package/dist/server/storage/files/migrations.d.ts +2 -0
  261. package/dist/server/storage/files/migrations.js +163 -0
  262. package/dist/server/storage/files/mimeByExt.d.ts +1 -0
  263. package/dist/server/storage/files/mimeByExt.js +13 -0
  264. package/dist/server/storage/files/stack.d.ts +136 -0
  265. package/dist/server/storage/files/stack.js +132 -0
  266. package/dist/server/storage/files/storageOps.d.ts +54 -0
  267. package/dist/server/storage/files/storageOps.js +110 -0
  268. package/dist/server/storage/files/types.d.ts +261 -0
  269. package/dist/server/storage/files/types.js +22 -0
  270. package/dist/server/storage/imageThumbnail.d.ts +3 -0
  271. package/dist/server/storage/imageThumbnail.js +50 -0
  272. package/dist/server/storage/index.d.ts +15 -0
  273. package/dist/server/storage/index.js +27 -0
  274. package/dist/server/storage/mountMediaRoutes.d.ts +28 -0
  275. package/dist/server/storage/mountMediaRoutes.js +141 -0
  276. package/dist/server/storage/objectStoreMediaStore.d.ts +4 -0
  277. package/dist/server/storage/objectStoreMediaStore.js +57 -0
  278. package/dist/server/storage/s3ObjectStore.d.ts +63 -0
  279. package/dist/server/storage/s3ObjectStore.js +187 -0
  280. package/dist/server/storage/sources.d.ts +47 -0
  281. package/dist/server/storage/sources.js +148 -0
  282. package/dist/server/storage/streamRange.d.ts +19 -0
  283. package/dist/server/storage/streamRange.js +69 -0
  284. package/dist/server/storage/streamToken.d.ts +16 -0
  285. package/dist/server/storage/streamToken.js +54 -0
  286. package/dist/server/storage/transcodeJobs.d.ts +65 -0
  287. package/dist/server/storage/transcodeJobs.js +104 -0
  288. package/dist/server/storage/types.d.ts +193 -0
  289. package/dist/server/storage/types.js +41 -0
  290. package/dist/server/storage/videoDerivativeKeys.d.ts +127 -0
  291. package/dist/server/storage/videoDerivativeKeys.js +149 -0
  292. package/dist/server/storage/videoProcessor.d.ts +30 -0
  293. package/dist/server/storage/videoProcessor.js +148 -0
  294. package/dist/server/sync/alarm.d.ts +34 -0
  295. package/dist/server/sync/alarm.js +91 -0
  296. package/dist/server/sync/commands.d.ts +87 -0
  297. package/dist/server/sync/commands.js +241 -0
  298. package/dist/server/sync/engine.d.ts +62 -0
  299. package/dist/server/sync/engine.js +184 -0
  300. package/dist/server/sync/http.d.ts +106 -0
  301. package/dist/server/sync/http.js +243 -0
  302. package/dist/server/sync/index.d.ts +41 -0
  303. package/dist/server/sync/index.js +41 -0
  304. package/dist/server/sync/opLog.d.ts +61 -0
  305. package/dist/server/sync/opLog.js +96 -0
  306. package/dist/server/sync/planner.d.ts +31 -0
  307. package/dist/server/sync/planner.js +30 -0
  308. package/dist/server/sync/status.d.ts +63 -0
  309. package/dist/server/sync/status.js +49 -0
  310. package/dist/server/sync/timer.d.ts +52 -0
  311. package/dist/server/sync/timer.js +170 -0
  312. package/dist/server/sync/tokens.d.ts +25 -0
  313. package/dist/server/sync/tokens.js +51 -0
  314. package/dist/server/sync/types.d.ts +101 -0
  315. package/dist/server/sync/types.js +29 -0
  316. package/dist/server/ws/eventBus.d.ts +14 -0
  317. package/dist/server/ws/eventBus.js +42 -0
  318. package/dist/server/ws/hub.d.ts +20 -0
  319. package/dist/server/ws/hub.js +154 -0
  320. package/dist/server/ws/index.d.ts +5 -0
  321. package/dist/server/ws/index.js +7 -0
  322. package/dist/server/ws/presence.d.ts +11 -0
  323. package/dist/server/ws/presence.js +20 -0
  324. package/dist/server/ws/room.d.ts +17 -0
  325. package/dist/server/ws/room.js +23 -0
  326. package/dist/server/ws/types.d.ts +122 -0
  327. package/dist/server/ws/types.js +1 -0
  328. package/package.json +234 -0
  329. package/src/declaredDepsAreImported.spec.ts +87 -0
  330. package/src/leafSubpathsImportNothing.spec.ts +358 -0
  331. package/src/lockfileIsTracked.spec.ts +50 -0
  332. package/src/namedSubpathsResolve.spec.ts +122 -0
  333. package/src/noPathDeps.spec.ts +24 -0
  334. package/src/publishShape.spec.ts +89 -0
  335. package/src/server/activity/index.ts +59 -0
  336. package/src/server/activity/migrations.ts +67 -0
  337. package/src/server/activity/shareSource.ts +57 -0
  338. package/src/server/activity/store.spec.ts +255 -0
  339. package/src/server/activity/store.ts +368 -0
  340. package/src/server/app.spec.ts +243 -0
  341. package/src/server/app.ts +89 -0
  342. package/src/server/audit/index.ts +2 -0
  343. package/src/server/audit/migrations.ts +88 -0
  344. package/src/server/audit/sqliteSink.test.ts +200 -0
  345. package/src/server/audit/sqliteSink.ts +162 -0
  346. package/src/server/auth/cookie.spec.ts +46 -0
  347. package/src/server/auth/cookie.ts +42 -0
  348. package/src/server/auth/csrf.spec.ts +81 -0
  349. package/src/server/auth/csrf.ts +83 -0
  350. package/src/server/auth/fileTokenSigner.ts +40 -0
  351. package/src/server/auth/getOnly.spec.ts +53 -0
  352. package/src/server/auth/getOnly.ts +32 -0
  353. package/src/server/auth/index.ts +51 -0
  354. package/src/server/auth/jwt.spec.ts +106 -0
  355. package/src/server/auth/jwt.ts +75 -0
  356. package/src/server/auth/loginThrottle.spec.ts +213 -0
  357. package/src/server/auth/loginThrottle.ts +261 -0
  358. package/src/server/auth/middleware.spec.ts +148 -0
  359. package/src/server/auth/middleware.ts +56 -0
  360. package/src/server/auth/password.spec.ts +48 -0
  361. package/src/server/auth/password.ts +71 -0
  362. package/src/server/auth/sessionStore.spec.ts +189 -0
  363. package/src/server/auth/sessionStore.ts +261 -0
  364. package/src/server/auth/totp.spec.ts +73 -0
  365. package/src/server/auth/totp.ts +92 -0
  366. package/src/server/auth/types.ts +146 -0
  367. package/src/server/auth/vault.spec.ts +51 -0
  368. package/src/server/auth/vault.ts +71 -0
  369. package/src/server/context.ts +37 -0
  370. package/src/server/ctgr.spec.ts +138 -0
  371. package/src/server/ctgr.ts +108 -0
  372. package/src/server/db/kysely.spec.ts +62 -0
  373. package/src/server/db/kysely.ts +21 -0
  374. package/src/server/db/plumbingTypes.ts +126 -0
  375. package/src/server/dropzone/duplicates.ts +121 -0
  376. package/src/server/dropzone/index.ts +30 -0
  377. package/src/server/dropzone/scan.spec.ts +255 -0
  378. package/src/server/dropzone/scan.ts +342 -0
  379. package/src/server/errors.spec.ts +76 -0
  380. package/src/server/errors.ts +22 -0
  381. package/src/server/guard/argon2Binding.spec.ts +117 -0
  382. package/src/server/guard/argon2Binding.ts +195 -0
  383. package/src/server/guard/argon2Kat.spec.ts +250 -0
  384. package/src/server/guard/guard.spec.ts +390 -0
  385. package/src/server/guard/index.ts +6 -0
  386. package/src/server/guard/kdfWorker.spec.ts +283 -0
  387. package/src/server/guard/kdfWorker.ts +367 -0
  388. package/src/server/guard/kdfWorkerEntry.ts +16 -0
  389. package/src/server/guard/passwordVerifier.spec.ts +71 -0
  390. package/src/server/guard/passwordVerifier.ts +175 -0
  391. package/src/server/guard/revocationStore.spec.ts +135 -0
  392. package/src/server/guard/revocationStore.ts +291 -0
  393. package/src/server/guard/sessionSigner.ts +425 -0
  394. package/src/server/guard/verifier.ts +27 -0
  395. package/src/server/incidents/channels.ts +487 -0
  396. package/src/server/incidents/checks.ts +120 -0
  397. package/src/server/incidents/incidents.spec.ts +463 -0
  398. package/src/server/incidents/index.ts +96 -0
  399. package/src/server/incidents/migrations.ts +66 -0
  400. package/src/server/incidents/notifier.ts +215 -0
  401. package/src/server/incidents/reporter.ts +110 -0
  402. package/src/server/incidents/routes.ts +314 -0
  403. package/src/server/incidents/store.ts +329 -0
  404. package/src/server/incidents/types.ts +116 -0
  405. package/src/server/index.ts +248 -0
  406. package/src/server/jobs/fireQueue.spec.ts +85 -0
  407. package/src/server/jobs/fireQueue.ts +37 -0
  408. package/src/server/jobs/index.ts +26 -0
  409. package/src/server/jobs/logger.ts +16 -0
  410. package/src/server/jobs/plainjobAdapter.spec.ts +34 -0
  411. package/src/server/jobs/plainjobAdapter.ts +54 -0
  412. package/src/server/jobs/polling.spec.ts +219 -0
  413. package/src/server/jobs/polling.ts +81 -0
  414. package/src/server/jobs/runner.spec.ts +213 -0
  415. package/src/server/jobs/runner.ts +213 -0
  416. package/src/server/jobs/timeout.spec.ts +35 -0
  417. package/src/server/jobs/timeout.ts +53 -0
  418. package/src/server/jobs/types.ts +188 -0
  419. package/src/server/master-lock/guard.spec.ts +347 -0
  420. package/src/server/master-lock/guard.ts +290 -0
  421. package/src/server/master-lock/index.ts +44 -0
  422. package/src/server/master-lock/lockPage.spec.ts +88 -0
  423. package/src/server/master-lock/lockPage.ts +313 -0
  424. package/src/server/master-lock/masterLock.spec.ts +338 -0
  425. package/src/server/master-lock/masterLock.ts +627 -0
  426. package/src/server/master-lock/principals.spec.ts +378 -0
  427. package/src/server/master-lock/principals.ts +175 -0
  428. package/src/server/master-lock/seed.spec.ts +69 -0
  429. package/src/server/master-lock/seed.ts +112 -0
  430. package/src/server/master-lock/store.ts +35 -0
  431. package/src/server/media-bun/image.spec.ts +115 -0
  432. package/src/server/media-bun/image.ts +134 -0
  433. package/src/server/media-bun/index.ts +52 -0
  434. package/src/server/media-bun/streaming.spec.ts +134 -0
  435. package/src/server/media-bun/streaming.ts +347 -0
  436. package/src/server/media-bun/video.spec.ts +115 -0
  437. package/src/server/media-bun/video.ts +232 -0
  438. package/src/server/metrics/metricsBuffer.ts +92 -0
  439. package/src/server/metrics/normalizeRoute.spec.ts +51 -0
  440. package/src/server/metrics/normalizeRoute.ts +63 -0
  441. package/src/server/middleware/correlationId.ts +24 -0
  442. package/src/server/middleware/corsAllowlist.spec.ts +118 -0
  443. package/src/server/middleware/corsAllowlist.ts +69 -0
  444. package/src/server/middleware/errorEnvelope.ts +38 -0
  445. package/src/server/middleware/index.ts +19 -0
  446. package/src/server/middleware/localOrigin.spec.ts +168 -0
  447. package/src/server/middleware/localOrigin.ts +139 -0
  448. package/src/server/middleware/rateLimit.spec.ts +72 -0
  449. package/src/server/middleware/rateLimit.ts +125 -0
  450. package/src/server/middleware/requestLogger.ts +95 -0
  451. package/src/server/middleware/securityHeaders.ts +78 -0
  452. package/src/server/middleware/zValidator.spec.ts +251 -0
  453. package/src/server/middleware/zValidator.ts +114 -0
  454. package/src/server/migration/runner.spec.ts +88 -0
  455. package/src/server/migration/runner.ts +87 -0
  456. package/src/server/migration/types.ts +17 -0
  457. package/src/server/net/outboundGuard.spec.ts +90 -0
  458. package/src/server/net/outboundGuard.ts +205 -0
  459. package/src/server/notifications/hub.ts +245 -0
  460. package/src/server/notifications/index.ts +77 -0
  461. package/src/server/notifications/migrations.ts +39 -0
  462. package/src/server/notifications/routes.ts +177 -0
  463. package/src/server/notifications/service.spec.ts +305 -0
  464. package/src/server/notifications/service.ts +112 -0
  465. package/src/server/paths/appDataHome.spec.ts +87 -0
  466. package/src/server/paths/appDataHome.ts +84 -0
  467. package/src/server/private-media/blobRoute.spec.ts +165 -0
  468. package/src/server/private-media/crypto.spec.ts +81 -0
  469. package/src/server/private-media/crypto.ts +155 -0
  470. package/src/server/private-media/gatePolicy.spec.ts +98 -0
  471. package/src/server/private-media/index-store.ts +83 -0
  472. package/src/server/private-media/mountPrivateRoutes.ts +265 -0
  473. package/src/server/private-media/pipeline.spec.ts +111 -0
  474. package/src/server/private-media/pipeline.ts +170 -0
  475. package/src/server/private-media/routes.spec.ts +265 -0
  476. package/src/server/private-media/unlock.ts +112 -0
  477. package/src/server/resume/index.ts +8 -0
  478. package/src/server/resume/resume.spec.ts +118 -0
  479. package/src/server/resume/routes.ts +92 -0
  480. package/src/server/resume/store.ts +82 -0
  481. package/src/server/retention/archive.spec.ts +181 -0
  482. package/src/server/retention/archive.ts +353 -0
  483. package/src/server/retention/fleet.ts +104 -0
  484. package/src/server/retention/index.ts +101 -0
  485. package/src/server/retention/policy.spec.ts +132 -0
  486. package/src/server/retention/policy.ts +392 -0
  487. package/src/server/retention/store.spec.ts +236 -0
  488. package/src/server/retention/store.ts +338 -0
  489. package/src/server/retention/sweeper.spec.ts +321 -0
  490. package/src/server/retention/sweeper.ts +367 -0
  491. package/src/server/retention/target.spec.ts +592 -0
  492. package/src/server/retention/target.ts +613 -0
  493. package/src/server/search/index.ts +12 -0
  494. package/src/server/search/pgSearchProvider.pg.test.ts +392 -0
  495. package/src/server/search/pgSearchProvider.ts +421 -0
  496. package/src/server/search/pgSearchSchema.ts +147 -0
  497. package/src/server/sharing/index.ts +65 -0
  498. package/src/server/sharing/migrations.ts +128 -0
  499. package/src/server/sharing/router.spec.ts +167 -0
  500. package/src/server/sharing/router.ts +194 -0
  501. package/src/server/sharing/sql.ts +109 -0
  502. package/src/server/sharing/store.spec.ts +408 -0
  503. package/src/server/sharing/store.ts +737 -0
  504. package/src/server/sqlite/backup.spec.ts +114 -0
  505. package/src/server/sqlite/backup.ts +154 -0
  506. package/src/server/sqlite/browse.spec.ts +70 -0
  507. package/src/server/sqlite/browse.ts +120 -0
  508. package/src/server/sqlite/export.spec.ts +43 -0
  509. package/src/server/sqlite/export.ts +74 -0
  510. package/src/server/sqlite/fts5.spec.ts +175 -0
  511. package/src/server/sqlite/fts5.ts +255 -0
  512. package/src/server/sqlite/health.spec.ts +79 -0
  513. package/src/server/sqlite/health.ts +105 -0
  514. package/src/server/sqlite/import.spec.ts +75 -0
  515. package/src/server/sqlite/import.ts +141 -0
  516. package/src/server/sqlite/index.ts +64 -0
  517. package/src/server/sqlite/readonlySql.spec.ts +58 -0
  518. package/src/server/sqlite/readonlySql.ts +67 -0
  519. package/src/server/sqlite/stats.spec.ts +21 -0
  520. package/src/server/sqlite/stats.ts +61 -0
  521. package/src/server/sqlite/viewer.spec.ts +127 -0
  522. package/src/server/sqlite/viewer.ts +72 -0
  523. package/src/server/storage/binaryServerProcessing.integration.spec.ts +222 -0
  524. package/src/server/storage/binaryServerProcessing.spec.ts +150 -0
  525. package/src/server/storage/binaryServerProcessing.ts +127 -0
  526. package/src/server/storage/binaryServerStore.ts +102 -0
  527. package/src/server/storage/chunked.integration.spec.ts +185 -0
  528. package/src/server/storage/diskStore.ts +83 -0
  529. package/src/server/storage/files/adapter.photo.display.spec.ts +169 -0
  530. package/src/server/storage/files/adapter.photo.spec.ts +128 -0
  531. package/src/server/storage/files/adapter.spec.ts +121 -0
  532. package/src/server/storage/files/adapter.ts +255 -0
  533. package/src/server/storage/files/catalogue.media-host.spec.ts +184 -0
  534. package/src/server/storage/files/catalogue.orderings.spec.ts +157 -0
  535. package/src/server/storage/files/catalogue.private.spec.ts +106 -0
  536. package/src/server/storage/files/catalogue.recover.spec.ts +131 -0
  537. package/src/server/storage/files/catalogue.sort.spec.ts +154 -0
  538. package/src/server/storage/files/catalogue.spec.ts +133 -0
  539. package/src/server/storage/files/catalogue.trash-import.spec.ts +125 -0
  540. package/src/server/storage/files/catalogue.ts +1326 -0
  541. package/src/server/storage/files/chunkedUpload.spec.ts +111 -0
  542. package/src/server/storage/files/chunkedUpload.ts +146 -0
  543. package/src/server/storage/files/collections.spec.ts +294 -0
  544. package/src/server/storage/files/fileRoutes.replace.spec.ts +88 -0
  545. package/src/server/storage/files/fileRoutes.spec.ts +327 -0
  546. package/src/server/storage/files/fileRoutes.ts +462 -0
  547. package/src/server/storage/files/index.ts +66 -0
  548. package/src/server/storage/files/migrations.ts +187 -0
  549. package/src/server/storage/files/mimeByExt.ts +14 -0
  550. package/src/server/storage/files/stack.spec.ts +258 -0
  551. package/src/server/storage/files/stack.ts +284 -0
  552. package/src/server/storage/files/storageOps.spec.ts +126 -0
  553. package/src/server/storage/files/storageOps.ts +213 -0
  554. package/src/server/storage/files/types.ts +285 -0
  555. package/src/server/storage/imageThumbnail.spec.ts +81 -0
  556. package/src/server/storage/imageThumbnail.ts +60 -0
  557. package/src/server/storage/index.ts +122 -0
  558. package/src/server/storage/mountMediaRoutes.ts +231 -0
  559. package/src/server/storage/objectStore.spec.ts +272 -0
  560. package/src/server/storage/objectStoreMediaStore.ts +63 -0
  561. package/src/server/storage/s3ObjectStore.ts +239 -0
  562. package/src/server/storage/sources.ts +245 -0
  563. package/src/server/storage/storage.spec.ts +219 -0
  564. package/src/server/storage/streamRange.ts +92 -0
  565. package/src/server/storage/streamToken.ts +76 -0
  566. package/src/server/storage/transcode.integration.spec.ts +134 -0
  567. package/src/server/storage/transcodeJobs.spec.ts +152 -0
  568. package/src/server/storage/transcodeJobs.ts +171 -0
  569. package/src/server/storage/types.ts +230 -0
  570. package/src/server/storage/videoDerivativeKeys.spec.ts +112 -0
  571. package/src/server/storage/videoDerivativeKeys.ts +172 -0
  572. package/src/server/storage/videoProcessor.posterOnly.spec.ts +106 -0
  573. package/src/server/storage/videoProcessor.ts +229 -0
  574. package/src/server/sync/alarm.spec.ts +149 -0
  575. package/src/server/sync/alarm.ts +137 -0
  576. package/src/server/sync/commands.spec.ts +145 -0
  577. package/src/server/sync/commands.ts +361 -0
  578. package/src/server/sync/engine.spec.ts +496 -0
  579. package/src/server/sync/engine.ts +255 -0
  580. package/src/server/sync/http.ts +316 -0
  581. package/src/server/sync/httpRemote.spec.ts +158 -0
  582. package/src/server/sync/index.ts +90 -0
  583. package/src/server/sync/opLog.spec.ts +110 -0
  584. package/src/server/sync/opLog.ts +189 -0
  585. package/src/server/sync/planner.spec.ts +53 -0
  586. package/src/server/sync/planner.ts +62 -0
  587. package/src/server/sync/status.spec.ts +92 -0
  588. package/src/server/sync/status.ts +108 -0
  589. package/src/server/sync/timer.spec.ts +150 -0
  590. package/src/server/sync/timer.ts +207 -0
  591. package/src/server/sync/tokens.spec.ts +38 -0
  592. package/src/server/sync/tokens.ts +94 -0
  593. package/src/server/sync/types.ts +108 -0
  594. package/src/server/ws/eventBus.spec.ts +67 -0
  595. package/src/server/ws/eventBus.ts +46 -0
  596. package/src/server/ws/hub.integration.spec.ts +93 -0
  597. package/src/server/ws/hub.spec.ts +282 -0
  598. package/src/server/ws/hub.ts +168 -0
  599. package/src/server/ws/index.ts +20 -0
  600. package/src/server/ws/presence.spec.ts +27 -0
  601. package/src/server/ws/presence.ts +26 -0
  602. package/src/server/ws/room.spec.ts +36 -0
  603. package/src/server/ws/room.ts +29 -0
  604. package/src/server/ws/types.ts +134 -0
  605. package/src/skippedTestsAreAnswered.spec.ts +215 -0
  606. package/src/testTmpRoot.spec.ts +102 -0
@@ -0,0 +1,111 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
3
+ import { mkdtemp, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { runMigrations } from '../../migration/runner';
7
+ import { createDiskStore } from '../diskStore';
8
+ import type { MediaCtx } from '../types';
9
+ import { catalogueUploadKey, createSqliteFileCatalogue } from './catalogue';
10
+ import { createChunkedUploadService } from './chunkedUpload';
11
+ import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
12
+ import { createStorageOpsLedger } from './storageOps';
13
+
14
+ const CTX: MediaCtx = { userId: 'u1', tenantId: 'u1' };
15
+
16
+ describe('chunked upload', () => {
17
+ let db: Database;
18
+ let root: string;
19
+ let clock = 0;
20
+
21
+ beforeEach(async () => {
22
+ db = new Database(':memory:');
23
+ runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
24
+ root = await mkdtemp(path.join(tmpdir(), 'fc-chunk-'));
25
+ clock = 0;
26
+ });
27
+ afterEach(async () => {
28
+ db.close();
29
+ await rm(root, { recursive: true, force: true });
30
+ });
31
+
32
+ const build = () => {
33
+ const store = createDiskStore(root);
34
+ const catalogue = createSqliteFileCatalogue({
35
+ db,
36
+ store,
37
+ workDir: path.join(root, 'w'),
38
+ owner: (c) => c.userId,
39
+ now: () => ++clock,
40
+ });
41
+ const ledger = createStorageOpsLedger(db, () => ++clock);
42
+ const minted: Array<{ key: string; sid: string; ci: number; tc: number }> = [];
43
+ const svc = createChunkedUploadService({
44
+ catalogue,
45
+ ledger,
46
+ backend: 'binary',
47
+ chunkSize: 8 * 1024 * 1024,
48
+ owner: (c) => c.userId,
49
+ now: () => ++clock,
50
+ mintChunkUpload: async (a) => {
51
+ minted.push(a);
52
+ return `https://bs.example/upload-chunk/app/${a.key}?sid=${a.sid}&ci=${a.ci}&tc=${a.tc}`;
53
+ },
54
+ });
55
+ return { catalogue, ledger, svc, minted };
56
+ };
57
+
58
+ test('init reserves a file, records a pending upload op, and mints one URL per chunk', async () => {
59
+ const { ledger, svc, minted } = build();
60
+ // 20 MiB at an 8 MiB chunk size ⇒ 3 chunks.
61
+ const init = await svc.init(CTX, {
62
+ name: 'big.png',
63
+ mime: 'image/png',
64
+ size: 20 * 1024 * 1024,
65
+ });
66
+ expect(init.chunkCount).toBe(3);
67
+ expect(init.chunkUploadUrls).toHaveLength(3);
68
+ expect(minted.map((m) => m.ci)).toEqual([0, 1, 2]);
69
+ expect(minted.every((m) => m.tc === 3 && m.sid === init.sid)).toBe(true);
70
+ // The mint targets exactly the key the catalogue will look for on complete.
71
+ expect(init.key).toBe(catalogueUploadKey('u1', init.fileId, 'image/png'));
72
+
73
+ // A durable pending 'upload' op keyed by the session id.
74
+ const op = ledger.get(init.sid);
75
+ expect(op?.status).toBe('pending');
76
+ expect(op?.action).toBe('upload');
77
+ expect(op?.key).toBe(init.key);
78
+ expect(svc.pending()).toBe(1);
79
+ });
80
+
81
+ test('complete registers the catalogue row over the assembled bytes + marks the op done', async () => {
82
+ const { catalogue, ledger, svc } = build();
83
+ const init = await svc.init(CTX, {
84
+ name: 'photo.png',
85
+ mime: 'image/png',
86
+ size: 12 * 1024 * 1024,
87
+ tags: ['big'],
88
+ });
89
+
90
+ const rec = await svc.complete(init.sid, { size: 12 * 1024 * 1024 });
91
+ expect(rec?.id).toBe(init.fileId);
92
+ expect(rec?.name).toBe('photo.png');
93
+ expect(rec?.tags).toEqual(['big']);
94
+ // Now discoverable through the normal catalogue (no re-upload happened).
95
+ expect((await catalogue.get(CTX, init.fileId))?.name).toBe('photo.png');
96
+ expect(ledger.get(init.sid)?.status).toBe('done');
97
+ expect(svc.pending()).toBe(0);
98
+
99
+ // A second complete for the same (now-cleared) session is a no-op.
100
+ expect(await svc.complete(init.sid)).toBeNull();
101
+ });
102
+
103
+ test('abort marks the op failed and drops the session (no catalogue row)', async () => {
104
+ const { catalogue, ledger, svc } = build();
105
+ const init = await svc.init(CTX, { name: 'x.png', mime: 'image/png', size: 1 });
106
+ svc.abort(init.sid);
107
+ expect(ledger.get(init.sid)?.status).toBe('failed');
108
+ expect(svc.pending()).toBe(0);
109
+ expect(await catalogue.get(CTX, init.fileId)).toBeNull();
110
+ });
111
+ });
@@ -0,0 +1,146 @@
1
+ import { DEFAULT_UPLOAD_CHUNK_SIZE } from 'cursedbelt-core/media/upload-limits';
2
+ import type { MediaCtx } from '../types';
3
+ import { catalogueUploadKey } from './catalogue';
4
+ import type { StorageOpsLedger } from './storageOps';
5
+ import type { FileCatalogue, FileRecord } from './types';
6
+
7
+ // ── Chunked upload (large files, direct-to-backend) ──────────────────────────────
8
+ // For multi-GB uploads we don't proxy bytes through the app origin. Instead: the client asks the
9
+ // app to START an upload (init) → the app mints per-chunk PUT URLs that go DIRECT to the byte
10
+ // backend (binary-server), records a durable `upload` ledger op, and hands the URLs back. The
11
+ // client PUTs each chunk to the backend, which assembles them at the target key once the last
12
+ // arrives. Then the client (or a backend webhook) calls COMPLETE, and the app registers the
13
+ // catalogue row over the already-stored bytes (no re-upload) + kicks processing.
14
+ //
15
+ // This module is backend-AGNOSTIC: the app injects `mintChunkUpload` (the binary-server chunk URL
16
+ // + signed sid/ci/tc token). On disk-only deploys, an app simply doesn't wire chunked uploads and
17
+ // keeps the streamed `PUT /upload`. Sessions are in-memory (the transient name/mime/size); the
18
+ // ledger row is the DURABLE record of intent, so an abandoned upload is observable/reconcilable.
19
+
20
+ const DEFAULT_CHUNK_SIZE = DEFAULT_UPLOAD_CHUNK_SIZE;
21
+
22
+ export type ChunkedInit = {
23
+ fileId: string;
24
+ /** Chunk session id — also the ledger op id; echoed to the backend as sid so its assemble event
25
+ * matches our storage_operations row. */
26
+ sid: string;
27
+ /** The storage key the assembled bytes land at (fsSourceKey for video, else fileBinaryKey). */
28
+ key: string;
29
+ chunkSize: number;
30
+ chunkCount: number;
31
+ /** One direct-to-backend PUT URL per chunk, in index order. */
32
+ chunkUploadUrls: string[];
33
+ };
34
+
35
+ type Session = {
36
+ fileId: string;
37
+ owner: string;
38
+ ctx: MediaCtx;
39
+ name: string;
40
+ mime: string;
41
+ size: number;
42
+ key: string;
43
+ chunkCount: number;
44
+ tags?: string[];
45
+ createdAt: number;
46
+ };
47
+
48
+ export interface ChunkedUploadService {
49
+ /** Begin a chunked upload: reserve an id, record a pending `upload` op, and mint the per-chunk
50
+ * direct-to-backend PUT URLs. */
51
+ init(
52
+ ctx: MediaCtx,
53
+ req: { name: string; mime: string; size: number; tags?: string[] },
54
+ ): Promise<ChunkedInit>;
55
+ /** Finish it: mark the op done and register the catalogue row over the assembled bytes. Returns
56
+ * the new record, or null if the session is unknown (already completed / server restarted). */
57
+ complete(sid: string, confirmed?: { size?: number }): Promise<FileRecord | null>;
58
+ /** Give up on a session (client cancelled / a chunk failed) — mark the ledger op failed. */
59
+ abort(sid: string): void;
60
+ /** Open session count (observability / tests). */
61
+ pending(): number;
62
+ }
63
+
64
+ /**
65
+ * Mint the direct-to-backend PUT URL for one chunk. The app supplies this so the module stays
66
+ * backend-agnostic. For binary-server: `${base}/upload-chunk/${app}/${key}?token=` with a token
67
+ * carrying { k, uid, sid, ci, tc }.
68
+ */
69
+ export type MintChunkUpload = (args: {
70
+ key: string;
71
+ sid: string;
72
+ ci: number;
73
+ tc: number;
74
+ owner: string;
75
+ }) => Promise<string>;
76
+
77
+ export function createChunkedUploadService(cfg: {
78
+ catalogue: FileCatalogue;
79
+ ledger: StorageOpsLedger;
80
+ /** Byte backend label recorded on the ledger op (e.g. 'binary'). */
81
+ backend: string;
82
+ mintChunkUpload: MintChunkUpload;
83
+ chunkSize?: number;
84
+ owner?: (ctx: MediaCtx) => string;
85
+ now?: () => number;
86
+ }): ChunkedUploadService {
87
+ const ownerOf = cfg.owner ?? ((c: MediaCtx) => c.tenantId);
88
+ const now = cfg.now ?? (() => Date.now());
89
+ const chunkSize = cfg.chunkSize ?? DEFAULT_CHUNK_SIZE;
90
+ const sessions = new Map<string, Session>();
91
+
92
+ return {
93
+ async init(ctx, req) {
94
+ const owner = ownerOf(ctx);
95
+ const fileId = crypto.randomUUID();
96
+ const sid = crypto.randomUUID();
97
+ const key = catalogueUploadKey(owner, fileId, req.mime);
98
+ const chunkCount = Math.max(1, Math.ceil((req.size || 1) / chunkSize));
99
+ // Durable record of intent — its op_id (sid) is what the backend echoes on assemble.
100
+ cfg.ledger.record({ owner, fileId, action: 'upload', key, backend: cfg.backend, opId: sid });
101
+ sessions.set(sid, {
102
+ fileId,
103
+ owner,
104
+ ctx,
105
+ name: req.name,
106
+ mime: req.mime,
107
+ size: req.size,
108
+ key,
109
+ chunkCount,
110
+ tags: req.tags,
111
+ createdAt: now(),
112
+ });
113
+ const chunkUploadUrls = await Promise.all(
114
+ Array.from({ length: chunkCount }, (_, ci) =>
115
+ cfg.mintChunkUpload({ key, sid, ci, tc: chunkCount, owner }),
116
+ ),
117
+ );
118
+ return { fileId, sid, key, chunkSize, chunkCount, chunkUploadUrls };
119
+ },
120
+
121
+ async complete(sid, confirmed) {
122
+ const s = sessions.get(sid);
123
+ if (!s) return null;
124
+ cfg.ledger.markDone(sid);
125
+ const rec = await cfg.catalogue.registerUploaded(s.ctx, {
126
+ id: s.fileId,
127
+ name: s.name,
128
+ mime: s.mime,
129
+ size: confirmed?.size ?? s.size,
130
+ tags: s.tags,
131
+ });
132
+ sessions.delete(sid);
133
+ return rec;
134
+ },
135
+
136
+ abort(sid) {
137
+ if (!sessions.has(sid)) return;
138
+ cfg.ledger.markFailed(sid, 'aborted');
139
+ sessions.delete(sid);
140
+ },
141
+
142
+ pending() {
143
+ return sessions.size;
144
+ },
145
+ };
146
+ }
@@ -0,0 +1,294 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
3
+ import { mkdtemp, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { runMigrations } from '../../migration/runner';
7
+ import { createDiskStore } from '../diskStore';
8
+ import type { MediaCtx } from '../types';
9
+ import { createSqliteFileCatalogue } from './catalogue';
10
+ import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
11
+ import type { FileCatalogue } from './types';
12
+
13
+ const CTX: MediaCtx = { userId: 'u1', tenantId: 'owner1' };
14
+ const CTX2: MediaCtx = { userId: 'u2', tenantId: 'owner2' };
15
+ const stream = (s: string) =>
16
+ new Response(new TextEncoder().encode(s)).body as ReadableStream<Uint8Array>;
17
+
18
+ describe('FileCatalogue — Collections', () => {
19
+ let db: Database;
20
+ let root: string;
21
+ let cat: FileCatalogue;
22
+ let clock = 2000;
23
+
24
+ beforeAll(async () => {
25
+ db = new Database(':memory:');
26
+ runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
27
+ root = await mkdtemp(path.join(tmpdir(), 'fc-coll-'));
28
+ cat = createSqliteFileCatalogue({
29
+ db,
30
+ store: createDiskStore(root),
31
+ workDir: path.join(root, 'work'),
32
+ now: () => ++clock,
33
+ });
34
+ });
35
+ afterAll(async () => {
36
+ db.close();
37
+ await rm(root, { recursive: true, force: true });
38
+ });
39
+
40
+ const ingest = (ctx: MediaCtx, name: string) =>
41
+ cat.ingest(ctx, { name, mime: 'image/png', body: stream(`bytes-${name}`) });
42
+
43
+ // ── CRUD ─────────────────────────────────────────────────────────────────────
44
+
45
+ test('createCollection and listCollections round-trip', async () => {
46
+ const coll = await cat.createCollection(CTX, { name: 'Summer 2025' });
47
+ expect(coll.name).toBe('Summer 2025');
48
+ expect(coll.kind).toBe('custom');
49
+ expect(coll.visibility).toBe('normal');
50
+ expect(coll.fileCount).toBe(0);
51
+
52
+ const list = await cat.listCollections(CTX);
53
+ expect(list.some((c) => c.id === coll.id)).toBe(true);
54
+ });
55
+
56
+ test('createCollection trims name and rejects empty', async () => {
57
+ const c = await cat.createCollection(CTX, { name: ' Trip ' });
58
+ expect(c.name).toBe('Trip');
59
+
60
+ await expect(cat.createCollection(CTX, { name: ' ' })).rejects.toThrow();
61
+ });
62
+
63
+ test('updateCollection renames and changes visibility', async () => {
64
+ const coll = await cat.createCollection(CTX, { name: 'Before' });
65
+ const updated = await cat.updateCollection(CTX, coll.id, {
66
+ name: 'After',
67
+ visibility: 'unlock',
68
+ });
69
+ expect(updated?.name).toBe('After');
70
+ expect(updated?.visibility).toBe('unlock');
71
+ // updateCollection with no such id returns null
72
+ expect(await cat.updateCollection(CTX, 'nonexistent', { name: 'X' })).toBeNull();
73
+ });
74
+
75
+ test('removeCollection deletes collection but not files', async () => {
76
+ const coll = await cat.createCollection(CTX, { name: 'ToDelete' });
77
+ const file = await ingest(CTX, 'willsurvive.png');
78
+ await cat.addToCollection(CTX, coll.id, [file.id]);
79
+
80
+ await cat.removeCollection(CTX, coll.id);
81
+
82
+ // Collection is gone
83
+ const list = await cat.listCollections(CTX);
84
+ expect(list.some((c) => c.id === coll.id)).toBe(false);
85
+ // File still exists
86
+ expect(await cat.get(CTX, file.id)).not.toBeNull();
87
+ });
88
+
89
+ test('removeCollection silently ignores nonexistent id', async () => {
90
+ await expect(cat.removeCollection(CTX, 'ghost-id')).resolves.toBeUndefined();
91
+ });
92
+
93
+ // ── Membership ───────────────────────────────────────────────────────────────
94
+
95
+ test('addToCollection / removeFromCollection are idempotent', async () => {
96
+ const coll = await cat.createCollection(CTX, { name: 'Membership' });
97
+ const f1 = await ingest(CTX, 'f1.png');
98
+ const f2 = await ingest(CTX, 'f2.png');
99
+
100
+ // Add twice — should be idempotent
101
+ await cat.addToCollection(CTX, coll.id, [f1.id, f2.id]);
102
+ await cat.addToCollection(CTX, coll.id, [f1.id]);
103
+
104
+ const page = await cat.list(CTX, { collectionId: coll.id });
105
+ const ids = page.items.map((f) => f.id).sort();
106
+ expect(ids).toEqual([f1.id, f2.id].sort());
107
+
108
+ // Remove f1; f2 remains
109
+ await cat.removeFromCollection(CTX, coll.id, [f1.id]);
110
+ const after = await cat.list(CTX, { collectionId: coll.id });
111
+ expect(after.items.map((f) => f.id)).toEqual([f2.id]);
112
+ });
113
+
114
+ test('addToCollection ignores unknown or other-owner file ids', async () => {
115
+ const coll = await cat.createCollection(CTX, { name: 'OwnerGuard' });
116
+ const other = await ingest(CTX2, 'other-owner.png');
117
+
118
+ // Should not throw and should not add the foreign file
119
+ await cat.addToCollection(CTX, coll.id, [other.id, 'completely-unknown']);
120
+ const page = await cat.list(CTX, { collectionId: coll.id });
121
+ expect(page.items).toHaveLength(0);
122
+ });
123
+
124
+ // ── collectionIds on FileRecord ───────────────────────────────────────────────
125
+
126
+ test('FileRecord.collectionIds reflects current memberships', async () => {
127
+ const c1 = await cat.createCollection(CTX, { name: 'C1' });
128
+ const c2 = await cat.createCollection(CTX, { name: 'C2' });
129
+ const file = await ingest(CTX, 'multi.png');
130
+
131
+ await cat.addToCollection(CTX, c1.id, [file.id]);
132
+ await cat.addToCollection(CTX, c2.id, [file.id]);
133
+
134
+ const rec = await cat.get(CTX, file.id);
135
+ expect(rec?.collectionIds.sort()).toEqual([c1.id, c2.id].sort());
136
+ expect(rec?.favorite).toBe(false);
137
+ });
138
+
139
+ // ── Favorites ────────────────────────────────────────────────────────────────
140
+
141
+ test('setFavorite lazily creates Favorites collection and toggles membership', async () => {
142
+ const file = await ingest(CTX, 'favfile.png');
143
+
144
+ // Not favorited initially
145
+ expect((await cat.get(CTX, file.id))?.favorite).toBe(false);
146
+
147
+ // Favorite it — Favorites collection should be created
148
+ await cat.setFavorite(CTX, file.id, true);
149
+ const rec = await cat.get(CTX, file.id);
150
+ expect(rec?.favorite).toBe(true);
151
+
152
+ const collections = await cat.listCollections(CTX);
153
+ const favColl = collections.find((c) => c.kind === 'favorites');
154
+ expect(favColl).toBeDefined();
155
+ expect(favColl?.name).toBe('Favorites');
156
+ expect(favColl?.fileCount).toBe(1);
157
+
158
+ // Unfavorite
159
+ await cat.setFavorite(CTX, file.id, false);
160
+ expect((await cat.get(CTX, file.id))?.favorite).toBe(false);
161
+ });
162
+
163
+ test('setFavorite is idempotent (toggle twice stays in correct state)', async () => {
164
+ const file = await ingest(CTX, 'idem.png');
165
+ await cat.setFavorite(CTX, file.id, true);
166
+ await cat.setFavorite(CTX, file.id, true); // idempotent
167
+ expect((await cat.get(CTX, file.id))?.favorite).toBe(true);
168
+ await cat.setFavorite(CTX, file.id, false);
169
+ await cat.setFavorite(CTX, file.id, false); // idempotent
170
+ expect((await cat.get(CTX, file.id))?.favorite).toBe(false);
171
+ });
172
+
173
+ test('Favorites collection is a singleton per owner', async () => {
174
+ const f1 = await ingest(CTX, 'sing1.png');
175
+ const f2 = await ingest(CTX, 'sing2.png');
176
+ await cat.setFavorite(CTX, f1.id, true);
177
+ await cat.setFavorite(CTX, f2.id, true);
178
+
179
+ const collections = await cat.listCollections(CTX);
180
+ expect(collections.filter((c) => c.kind === 'favorites')).toHaveLength(1);
181
+ });
182
+
183
+ test('favorite=true filter on list returns only favorited files', async () => {
184
+ // Create a clean ctx3 so we start fresh
185
+ const CTX3: MediaCtx = { userId: 'u3', tenantId: 'owner3' };
186
+ const a = await ingest(CTX3, 'favA.png');
187
+ const b = await ingest(CTX3, 'favB.png');
188
+ await ingest(CTX3, 'notFav.png');
189
+
190
+ await cat.setFavorite(CTX3, a.id, true);
191
+ await cat.setFavorite(CTX3, b.id, true);
192
+
193
+ const page = await cat.list(CTX3, { favorite: true });
194
+ const ids = page.items.map((f) => f.id).sort();
195
+ expect(ids).toEqual([a.id, b.id].sort());
196
+ });
197
+
198
+ test('favorite=true with no Favorites collection returns empty page', async () => {
199
+ const CTX4: MediaCtx = { userId: 'u4', tenantId: 'owner4' };
200
+ await ingest(CTX4, 'nofav.png');
201
+ const page = await cat.list(CTX4, { favorite: true });
202
+ expect(page.items).toHaveLength(0);
203
+ });
204
+
205
+ test('Favorites collection cannot be removed', async () => {
206
+ const f = await ingest(CTX, 'keepfav.png');
207
+ await cat.setFavorite(CTX, f.id, true);
208
+ const colls = await cat.listCollections(CTX);
209
+ const favColl = colls.find((c) => c.kind === 'favorites')!;
210
+ await expect(cat.removeCollection(CTX, favColl.id)).rejects.toThrow('Favorites');
211
+ });
212
+
213
+ // ── Unlock-gated visibility ───────────────────────────────────────────────────
214
+
215
+ test('unlock-gated collection excluded from listCollections by default', async () => {
216
+ const coll = await cat.createCollection(CTX, { name: 'Secret', visibility: 'unlock' });
217
+
218
+ const defaultList = await cat.listCollections(CTX);
219
+ expect(defaultList.some((c) => c.id === coll.id)).toBe(false);
220
+
221
+ const fullList = await cat.listCollections(CTX, { includeUnlockable: true });
222
+ expect(fullList.some((c) => c.id === coll.id)).toBe(true);
223
+ });
224
+
225
+ test('list with unlock-gated collectionId returns empty unless includeUnlockable', async () => {
226
+ const coll = await cat.createCollection(CTX, { name: 'Hidden', visibility: 'unlock' });
227
+ const file = await ingest(CTX, 'hidden.png');
228
+ await cat.addToCollection(CTX, coll.id, [file.id]);
229
+
230
+ const gated = await cat.list(CTX, { collectionId: coll.id });
231
+ expect(gated.items).toHaveLength(0);
232
+
233
+ const open = await cat.list(CTX, { collectionId: coll.id, includeUnlockable: true });
234
+ expect(open.items.some((f) => f.id === file.id)).toBe(true);
235
+ });
236
+
237
+ // ── Owner isolation ───────────────────────────────────────────────────────────
238
+
239
+ test('owner isolation: collections and memberships are tenant-scoped', async () => {
240
+ const collA = await cat.createCollection(CTX, { name: 'AOnly' });
241
+ const collB = await cat.createCollection(CTX2, { name: 'BOnly' });
242
+
243
+ const fileA = await ingest(CTX, 'a-file.png');
244
+ const fileB = await ingest(CTX2, 'b-file.png');
245
+ await cat.addToCollection(CTX, collA.id, [fileA.id]);
246
+ await cat.addToCollection(CTX2, collB.id, [fileB.id]);
247
+
248
+ // CTX can only see its own collections
249
+ const listA = await cat.listCollections(CTX);
250
+ expect(listA.some((c) => c.id === collB.id)).toBe(false);
251
+
252
+ // CTX cannot see files from collB
253
+ const pageA = await cat.list(CTX, { collectionId: collB.id });
254
+ expect(pageA.items).toHaveLength(0);
255
+
256
+ // CTX2 cannot see files from collA
257
+ const pageB = await cat.list(CTX2, { collectionId: collA.id });
258
+ expect(pageB.items).toHaveLength(0);
259
+ });
260
+
261
+ // ── Cascade on file remove ────────────────────────────────────────────────────
262
+
263
+ test('removing a file removes its collection memberships', async () => {
264
+ const coll = await cat.createCollection(CTX, { name: 'CascadeTest' });
265
+ const file = await ingest(CTX, 'cascade.png');
266
+ await cat.addToCollection(CTX, coll.id, [file.id]);
267
+
268
+ // Confirm it's in there
269
+ const before = await cat.list(CTX, { collectionId: coll.id });
270
+ expect(before.items.some((f) => f.id === file.id)).toBe(true);
271
+
272
+ await cat.remove(CTX, file.id);
273
+
274
+ const after = await cat.list(CTX, { collectionId: coll.id });
275
+ expect(after.items.some((f) => f.id === file.id)).toBe(false);
276
+ // Collection itself still exists
277
+ const colls = await cat.listCollections(CTX);
278
+ expect(colls.some((c) => c.id === coll.id)).toBe(true);
279
+ });
280
+
281
+ test('removing a collection removes memberships but not files', async () => {
282
+ const coll = await cat.createCollection(CTX, { name: 'CollCascade' });
283
+ const file = await ingest(CTX, 'survives.png');
284
+ await cat.addToCollection(CTX, coll.id, [file.id]);
285
+
286
+ await cat.removeCollection(CTX, coll.id);
287
+
288
+ // File survives
289
+ expect(await cat.get(CTX, file.id)).not.toBeNull();
290
+ // collectionIds no longer includes the removed collection
291
+ const rec = await cat.get(CTX, file.id);
292
+ expect(rec?.collectionIds.includes(coll.id)).toBe(false);
293
+ });
294
+ });
@@ -0,0 +1,88 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
3
+ import { mkdtemp, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { Hono } from 'hono';
7
+ import { runMigrations } from '../../migration/runner';
8
+ import { createDiskStore } from '../diskStore';
9
+ import { createStreamTokenService } from '../streamToken';
10
+ import type { MediaCtx, MediaStore } from '../types';
11
+ import { createSqliteFileCatalogue, fileBinaryKey } from './catalogue';
12
+ import { mountFileRoutes } from './fileRoutes';
13
+ import { FILE_CATALOGUE_MIGRATIONS } from './migrations';
14
+ import type { FileCatalogue, PrivateFileCatalogue } from './types';
15
+
16
+ // PUT /_files/replace/:id — the editor's "overwrite original" byte edge.
17
+
18
+ const CTX: MediaCtx = { userId: 'u1', tenantId: 't1' };
19
+ const streamToken = createStreamTokenService({ secret: 'stream' });
20
+ const stream = (s: string) =>
21
+ new Response(new TextEncoder().encode(s)).body as ReadableStream<Uint8Array>;
22
+
23
+ describe('fileRoutes — replace', () => {
24
+ let db: Database;
25
+ let root: string;
26
+ let store: MediaStore;
27
+ let cat: FileCatalogue & PrivateFileCatalogue;
28
+ let authed = true;
29
+ const app = new Hono();
30
+ let fileId: string;
31
+
32
+ beforeAll(async () => {
33
+ db = new Database(':memory:');
34
+ runMigrations(db, FILE_CATALOGUE_MIGRATIONS);
35
+ root = await mkdtemp(path.join(tmpdir(), 'fr-replace-'));
36
+ store = createDiskStore(root);
37
+ cat = createSqliteFileCatalogue({
38
+ db,
39
+ store,
40
+ workDir: path.join(root, 'w'),
41
+ owner: (c) => c.userId,
42
+ });
43
+ mountFileRoutes(app, {
44
+ store,
45
+ token: streamToken,
46
+ catalogue: cat,
47
+ authorizeUpload: () => (authed ? CTX : null),
48
+ });
49
+ fileId = (await cat.ingest(CTX, { name: 'doc.txt', mime: 'text/plain', body: stream('old') }))
50
+ .id;
51
+ });
52
+ afterAll(async () => {
53
+ db.close();
54
+ await rm(root, { recursive: true, force: true });
55
+ });
56
+
57
+ test('replaces bytes under the same id', async () => {
58
+ const res = await app.request(`/_files/replace/${fileId}`, {
59
+ method: 'PUT',
60
+ headers: { 'content-type': 'text/plain', 'x-filename': 'doc-v2.txt' },
61
+ body: 'brand new bytes',
62
+ });
63
+ expect(res.status).toBe(200);
64
+ const { file } = (await res.json()) as { file: { id: string; name: string; size: number } };
65
+ expect(file.id).toBe(fileId);
66
+ expect(file.name).toBe('doc-v2.txt');
67
+ const bytes = await store.get(fileBinaryKey(CTX.userId, fileId));
68
+ expect(new TextDecoder().decode(bytes as Uint8Array)).toBe('brand new bytes');
69
+ });
70
+
71
+ test('unknown id → 404; unauthorized → 401', async () => {
72
+ const missing = await app.request('/_files/replace/nope', {
73
+ method: 'PUT',
74
+ headers: { 'content-type': 'text/plain' },
75
+ body: 'x',
76
+ });
77
+ expect(missing.status).toBe(404);
78
+
79
+ authed = false;
80
+ const denied = await app.request(`/_files/replace/${fileId}`, {
81
+ method: 'PUT',
82
+ headers: { 'content-type': 'text/plain' },
83
+ body: 'x',
84
+ });
85
+ expect(denied.status).toBe(401);
86
+ authed = true;
87
+ });
88
+ });