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,219 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { Hono } from 'hono';
6
+ import { createDiskStore } from './diskStore';
7
+ import { mountMediaRoutes } from './mountMediaRoutes';
8
+ import { createMediaUrls } from './sources';
9
+ import { decodeRange, serveRange } from './streamRange';
10
+ import { createStreamTokenService } from './streamToken';
11
+ import type { MediaStore } from './types';
12
+
13
+ describe('decodeRange', () => {
14
+ test('no header ⇒ full', () => {
15
+ expect(decodeRange(undefined, 100)).toEqual({ kind: 'full' });
16
+ });
17
+ test('inclusive a-b', () => {
18
+ expect(decodeRange('bytes=4-9', 16)).toEqual({ kind: 'partial', start: 4, end: 9 });
19
+ });
20
+ test('open-ended a-', () => {
21
+ expect(decodeRange('bytes=8-', 16)).toEqual({ kind: 'partial', start: 8, end: 15 });
22
+ });
23
+ test('suffix -n', () => {
24
+ expect(decodeRange('bytes=-4', 16)).toEqual({ kind: 'partial', start: 12, end: 15 });
25
+ });
26
+ test('clamps end past EOF', () => {
27
+ expect(decodeRange('bytes=10-999', 16)).toEqual({ kind: 'partial', start: 10, end: 15 });
28
+ });
29
+ test('out of bounds ⇒ unsatisfiable', () => {
30
+ expect(decodeRange('bytes=999-1000', 16)).toEqual({ kind: 'unsatisfiable' });
31
+ });
32
+ test('malformed ⇒ full', () => {
33
+ expect(decodeRange('rows=1-2', 16)).toEqual({ kind: 'full' });
34
+ });
35
+ });
36
+
37
+ describe('streamTokenService', () => {
38
+ const svc = createStreamTokenService({ secret: 'test-secret-abc' });
39
+
40
+ test('round-trips a grant', async () => {
41
+ const token = await svc.sign({ owner: 'tenant1', sourceId: 'src', videoId: 'vid' });
42
+ expect(await svc.verify(token)).toEqual({ owner: 'tenant1', sourceId: 'src', videoId: 'vid' });
43
+ });
44
+ test('rejects a tampered token', async () => {
45
+ const token = await svc.sign({ owner: 't', sourceId: 's', videoId: 'v' });
46
+ expect(await svc.verify(`${token}x`)).toBeNull();
47
+ });
48
+ test('rejects an expired token', async () => {
49
+ const token = await svc.sign({ owner: 't', sourceId: 's', videoId: 'v' }, -10);
50
+ expect(await svc.verify(token)).toBeNull();
51
+ });
52
+ test('rejects a token signed with a different secret', async () => {
53
+ const other = createStreamTokenService({ secret: 'other-secret' });
54
+ const token = await other.sign({ owner: 't', sourceId: 's', videoId: 'v' });
55
+ expect(await svc.verify(token)).toBeNull();
56
+ });
57
+ });
58
+
59
+ describe('diskStore + serveRange', () => {
60
+ let root: string;
61
+ let store: MediaStore;
62
+ const key = 'fs/tenant1/clip.mp4';
63
+ const payload = new TextEncoder().encode('0123456789ABCDEF'); // 16 bytes
64
+
65
+ beforeAll(async () => {
66
+ root = await mkdtemp(path.join(tmpdir(), 'cb-storage-'));
67
+ store = createDiskStore(root);
68
+ await store.put(key, payload, 'video/mp4');
69
+ });
70
+ afterAll(async () => {
71
+ await rm(root, { recursive: true, force: true });
72
+ });
73
+
74
+ test('stat returns size', async () => {
75
+ expect(await store.stat(key)).toEqual({ size: 16 });
76
+ });
77
+ test('stat of a missing key is null', async () => {
78
+ expect(await store.stat('fs/tenant1/nope')).toBeNull();
79
+ });
80
+ test('getRange returns the inclusive slice', async () => {
81
+ const rr = await store.getRange(key, { start: 4, end: 9 });
82
+ const bytes = new Uint8Array(await new Response(rr?.stream).arrayBuffer());
83
+ expect(new TextDecoder().decode(bytes)).toBe('456789');
84
+ });
85
+ test('serveRange answers 206 with Content-Range', async () => {
86
+ const res = await serveRange(store, key, 'video/mp4', 'bytes=4-9');
87
+ expect(res.status).toBe(206);
88
+ expect(res.headers.get('content-range')).toBe('bytes 4-9/16');
89
+ expect(res.headers.get('content-length')).toBe('6');
90
+ expect(await res.text()).toBe('456789');
91
+ });
92
+ test('serveRange answers 200 for the whole object', async () => {
93
+ const res = await serveRange(store, key, 'video/mp4', undefined);
94
+ expect(res.status).toBe(200);
95
+ expect(res.headers.get('content-length')).toBe('16');
96
+ });
97
+ test('serveRange answers 416 for an unsatisfiable range', async () => {
98
+ const res = await serveRange(store, key, 'video/mp4', 'bytes=999-1000');
99
+ expect(res.status).toBe(416);
100
+ expect(res.headers.get('content-range')).toBe('bytes */16');
101
+ });
102
+ test('serveRange answers 404 for a missing key', async () => {
103
+ const res = await serveRange(store, 'fs/tenant1/nope', 'video/mp4', undefined);
104
+ expect(res.status).toBe(404);
105
+ });
106
+ test('removePrefix clears a subtree', async () => {
107
+ await store.put('hls/tenant1/vid/master.m3u8', payload, 'application/vnd.apple.mpegurl');
108
+ await store.removePrefix('hls/tenant1/vid');
109
+ expect(await store.stat('hls/tenant1/vid/master.m3u8')).toBeNull();
110
+ });
111
+
112
+ test('putStream writes a web ReadableStream without hanging or truncating', async () => {
113
+ const bytes = new Uint8Array(64 * 1024).map((_, i) => i % 256); // 64 KiB, multi-chunk
114
+ const stream = new Response(bytes).body as ReadableStream<Uint8Array>;
115
+ await store.putStream('fs/tenant1/streamed.bin', stream, 'application/octet-stream');
116
+ expect(await store.stat('fs/tenant1/streamed.bin')).toEqual({ size: 64 * 1024 });
117
+ const round = await store.get('fs/tenant1/streamed.bin');
118
+ expect(round?.length).toBe(64 * 1024);
119
+ expect(round?.[1000]).toBe(1000 % 256);
120
+ });
121
+
122
+ test('putStream survives a LIVE request-body stream (Bun 1.3.x detach bug)', async () => {
123
+ // Regression: in Bun 1.3.x a request-body stream's internals detach across an
124
+ // I/O await (the mkdir inside putStream); releaseLock() then throws "undefined
125
+ // is not a function" and every upload 500s. Only a REAL served request exhibits
126
+ // it — a Response-body stream (test above) never detaches.
127
+ const app = new Hono();
128
+ app.put('/u', async (c) => {
129
+ const body = c.req.raw.body;
130
+ if (!body) return c.json({ error: 'empty' }, 400);
131
+ await store.putStream('fs/tenant1/live.bin', body, 'application/octet-stream');
132
+ return c.json({ ok: true });
133
+ });
134
+ const server = Bun.serve({ fetch: app.fetch, port: 0 });
135
+ try {
136
+ const res = await fetch(`http://localhost:${server.port}/u`, {
137
+ method: 'PUT',
138
+ body: 'live request bytes',
139
+ });
140
+ expect(res.status).toBe(200);
141
+ expect(await store.stat('fs/tenant1/live.bin')).toEqual({ size: 18 });
142
+ } finally {
143
+ server.stop();
144
+ }
145
+ });
146
+ });
147
+
148
+ // The split-origin case: assets served from one origin (e.g. Cloudflare Pages), media+API from
149
+ // another (e.g. an EC2 subdomain). Media URLs are built server-side with an ABSOLUTE base, and
150
+ // the serve routes answer cross-origin CORS with the range headers a seeking <video> needs.
151
+ describe('split-origin media (absolute URLs + CORS)', () => {
152
+ let root: string;
153
+ let store: MediaStore;
154
+ const token = createStreamTokenService({ secret: 'origin-secret' });
155
+ const ctx = { userId: 'u1', tenantId: 'tenantA' };
156
+ const payload = new TextEncoder().encode('SEGMENTBYTES-0123456789');
157
+ const PAGE_ORIGIN = 'https://app.example.com';
158
+ const API_BASE = 'https://api.example.com/_media';
159
+
160
+ beforeAll(async () => {
161
+ root = await mkdtemp(path.join(tmpdir(), 'cb-cors-'));
162
+ store = createDiskStore(root);
163
+ // Owner keyspace defaults to tenantId → store under fs/tenantA/…
164
+ await store.put('fs/tenantA/clip.mp4', payload, 'video/mp4');
165
+ });
166
+ afterAll(async () => {
167
+ await rm(root, { recursive: true, force: true });
168
+ });
169
+
170
+ test('createMediaUrls emits ABSOLUTE cross-origin URLs (not window-relative)', async () => {
171
+ const urls = createMediaUrls({ baseUrl: API_BASE, token });
172
+ const built = await urls.build(ctx, 'disk', 'clip');
173
+ expect(built.streamUrl.startsWith(`${API_BASE}/s/`)).toBe(true);
174
+ expect(built.hlsUrl.endsWith('/hls/disk/clip/master.m3u8')).toBe(true);
175
+ // The stream token is a single path segment, so hls.js relative segment fetches inherit it.
176
+ expect(built.streamUrl).toContain('/s/');
177
+ });
178
+
179
+ test('a trailing slash on baseUrl is normalized', async () => {
180
+ const urls = createMediaUrls({ baseUrl: `${API_BASE}/`, token });
181
+ const built = await urls.build(ctx, 'disk', 'clip');
182
+ expect(built.streamUrl.includes('//s/')).toBe(false);
183
+ });
184
+
185
+ test('cross-origin GET gets ACAO + exposes range headers', async () => {
186
+ const app = new Hono();
187
+ mountMediaRoutes(app, { store, token, cors: [PAGE_ORIGIN] });
188
+ const t = await token.sign({ owner: 'tenantA', sourceId: 'disk', videoId: 'clip' });
189
+ const res = await app.request(`/_media/s/${t}/media/disk/clip`, {
190
+ headers: { Origin: PAGE_ORIGIN, Range: 'bytes=0-3' },
191
+ });
192
+ expect(res.status).toBe(206);
193
+ expect(res.headers.get('access-control-allow-origin')).toBe(PAGE_ORIGIN);
194
+ expect(res.headers.get('access-control-expose-headers')).toContain('Content-Range');
195
+ expect(res.headers.get('content-range')).toBe('bytes 0-3/23');
196
+ });
197
+
198
+ test('preflight OPTIONS is answered for an allowed origin', async () => {
199
+ const app = new Hono();
200
+ mountMediaRoutes(app, { store, token, cors: [PAGE_ORIGIN] });
201
+ const res = await app.request('/_media/s/x/media/disk/clip', {
202
+ method: 'OPTIONS',
203
+ headers: { Origin: PAGE_ORIGIN },
204
+ });
205
+ expect(res.status).toBe(204);
206
+ expect(res.headers.get('access-control-allow-origin')).toBe(PAGE_ORIGIN);
207
+ expect(res.headers.get('access-control-allow-methods')).toContain('GET');
208
+ });
209
+
210
+ test('a disallowed origin gets no ACAO', async () => {
211
+ const app = new Hono();
212
+ mountMediaRoutes(app, { store, token, cors: [PAGE_ORIGIN] });
213
+ const t = await token.sign({ owner: 'tenantA', sourceId: 'disk', videoId: 'clip' });
214
+ const res = await app.request(`/_media/s/${t}/media/disk/clip`, {
215
+ headers: { Origin: 'https://evil.example.com' },
216
+ });
217
+ expect(res.headers.get('access-control-allow-origin')).toBeNull();
218
+ });
219
+ });
@@ -0,0 +1,92 @@
1
+ import type { MediaStore } from './types';
2
+
3
+ // HTTP Range streaming for large media (video), Hono/Bun-native. Serves bytes straight from
4
+ // the active store as 200 (whole) or 206 Partial Content (a byte range) so the browser's
5
+ // <video> element can seek without downloading the entire file. Ported from ca's streamRange,
6
+ // re-expressed as a web `Response` (no Express shim).
7
+
8
+ export type RangeDecision =
9
+ | { kind: 'full' }
10
+ | { kind: 'partial'; start: number; end: number }
11
+ | { kind: 'unsatisfiable' };
12
+
13
+ // Parse a single-range `Range: bytes=start-end` header against a known total size. Only the
14
+ // common single-range forms are supported (`a-b`, `a-`, `-n`); anything malformed falls back
15
+ // to a full response, and an out-of-bounds range is rejected so the caller can answer 416.
16
+ // End is clamped to the last byte; ranges are inclusive.
17
+ export function decodeRange(header: string | undefined | null, total: number): RangeDecision {
18
+ if (!header) return { kind: 'full' };
19
+ const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
20
+ if (!match) return { kind: 'full' };
21
+ const [, rawStart, rawEnd] = match;
22
+ if (rawStart === '' && rawEnd === '') return { kind: 'full' };
23
+
24
+ let start: number;
25
+ let end: number;
26
+ if (rawStart === '') {
27
+ // Suffix range: the last N bytes.
28
+ const suffix = Number.parseInt(rawEnd, 10);
29
+ if (suffix <= 0) return { kind: 'unsatisfiable' };
30
+ start = Math.max(0, total - suffix);
31
+ end = total - 1;
32
+ } else {
33
+ start = Number.parseInt(rawStart, 10);
34
+ end = rawEnd === '' ? total - 1 : Number.parseInt(rawEnd, 10);
35
+ }
36
+
37
+ if (start > end || start >= total || start < 0) return { kind: 'unsatisfiable' };
38
+ if (end >= total) end = total - 1;
39
+ return { kind: 'partial', start, end };
40
+ }
41
+
42
+ export type ServeRangeOpts = {
43
+ /** Content-Disposition value, e.g. 'inline' or `attachment; filename="clip.mp4"`. */
44
+ contentDisposition?: string;
45
+ /** Overrides the default 'private, max-age=86400' (e.g. 'no-store' for private bytes). */
46
+ cacheControl?: string;
47
+ extraHeaders?: Record<string, string>;
48
+ };
49
+
50
+ // Build a Range-aware Response for a stored object. `rangeHeader` is the request's Range header
51
+ // (Hono: `c.req.header('range')`). 404 if the key is gone, 416 for an unsatisfiable range, 206
52
+ // for a partial hit, else 200.
53
+ export async function serveRange(
54
+ store: MediaStore,
55
+ key: string,
56
+ mime: string,
57
+ rangeHeader: string | undefined | null,
58
+ opts: ServeRangeOpts = {},
59
+ ): Promise<Response> {
60
+ const meta = await store.stat(key);
61
+ if (!meta) return new Response(null, { status: 404 });
62
+ const total = meta.size;
63
+
64
+ const headers = new Headers({
65
+ 'Accept-Ranges': 'bytes',
66
+ 'Content-Type': mime,
67
+ 'Cache-Control': opts.cacheControl ?? 'private, max-age=86400',
68
+ });
69
+ if (opts.contentDisposition) headers.set('Content-Disposition', opts.contentDisposition);
70
+ if (opts.extraHeaders) {
71
+ for (const [k, v] of Object.entries(opts.extraHeaders)) headers.set(k, v);
72
+ }
73
+
74
+ const decision = decodeRange(rangeHeader, total);
75
+
76
+ if (decision.kind === 'unsatisfiable') {
77
+ headers.set('Content-Range', `bytes */${total}`);
78
+ return new Response(null, { status: 416, headers });
79
+ }
80
+
81
+ const range = decision.kind === 'partial' ? decision : undefined;
82
+ const result = await store.getRange(key, range);
83
+ if (!result) return new Response(null, { status: 404 });
84
+
85
+ if (range) {
86
+ headers.set('Content-Range', `bytes ${range.start}-${range.end}/${total}`);
87
+ headers.set('Content-Length', String(range.end - range.start + 1));
88
+ return new Response(result.stream, { status: 206, headers });
89
+ }
90
+ headers.set('Content-Length', String(total));
91
+ return new Response(result.stream, { status: 200, headers });
92
+ }
@@ -0,0 +1,76 @@
1
+ // 🔴 `../auth/jwt`, NOT `../auth`. The jose half is a REAL dependency — stream tokens are
2
+ // signed and verified here, and jose is a plain `dependencies` entry every consumer already
3
+ // installs. The BARREL was the accident: `../auth/index.ts` also reaches `totp.ts` → `otplib`,
4
+ // an OPTIONAL peer, so every app that touched anything in `cursedbelt-server/storage` was made
5
+ // to install a one-time-password library for code it never calls. Import the leaf module that
6
+ // has what you need; never the index that has everything. `src/leafSubpathsImportNothing.spec.ts`
7
+ // fails if this file (or anything else the storage barrel reaches) undoes it.
8
+ import { signToken, verifyPurposeToken } from '../auth/jwt';
9
+
10
+ // ── Stream tokens ───────────────────────────────────────────────────────────────
11
+ // Media bytes are loaded by <video>/hls.js/<img>, which cannot send the platform Bearer
12
+ // header an app's auth uses. So byte-serving routes live OUTSIDE the guarded API and
13
+ // authorize each request with a short-lived, PATH-embedded token instead.
14
+ //
15
+ // Why path-embedded, not ?t=: hls.js resolves segment/playlist URIs RELATIVE to the master
16
+ // playlist and does NOT inherit query strings — a `?t=` token would be dropped on segment
17
+ // fetches. A token carried as a leading path segment (…/s/<token>/hls/…) is inherited by every
18
+ // relative child URL, so one token authorizes the whole HLS tree.
19
+ //
20
+ // Reuses cursedbelt's ONE jose integration (`signToken`/`verifyPurposeToken`, HS256) — the
21
+ // same primitive the vault/files/pm unlock tokens use — under a dedicated 'media-stream'
22
+ // purpose so it can never be replayed as an access token (or vice-versa).
23
+
24
+ const MEDIA_STREAM_PURPOSE = 'media-stream';
25
+
26
+ /** The scope a stream token authorizes: one video's whole derivative tree, under one owner. */
27
+ export type StreamGrant = {
28
+ /** owner keyspace segment (the media lives under fs/<owner>/… , hls/<owner>/… , …). */
29
+ owner: string;
30
+ sourceId: string;
31
+ videoId: string;
32
+ };
33
+
34
+ export interface StreamTokenService {
35
+ sign(grant: StreamGrant, ttlSeconds?: number): Promise<string>;
36
+ verify(token: string): Promise<StreamGrant | null>;
37
+ }
38
+
39
+ export function createStreamTokenService(opts: {
40
+ secret: string;
41
+ /** Default 6h — long enough to survive a paused-and-resumed playback without a 401. */
42
+ defaultTtlSeconds?: number;
43
+ }): StreamTokenService {
44
+ const ttl = opts.defaultTtlSeconds ?? 6 * 60 * 60;
45
+ return {
46
+ async sign(grant, ttlSeconds = ttl) {
47
+ // userId carries the owner; src/vid are extra claims (JWTPayload allows them). sessionId
48
+ // is unused for stream tokens (they're not tied to a revocable session row) — a stable
49
+ // marker keeps the required-field contract without implying a session.
50
+ return signToken(
51
+ {
52
+ userId: grant.owner,
53
+ sessionId: 'media-stream',
54
+ roles: [],
55
+ purpose: MEDIA_STREAM_PURPOSE,
56
+ src: grant.sourceId,
57
+ vid: grant.videoId,
58
+ },
59
+ opts.secret,
60
+ ttlSeconds,
61
+ );
62
+ },
63
+ async verify(token) {
64
+ try {
65
+ const p = await verifyPurposeToken(token, opts.secret, MEDIA_STREAM_PURPOSE);
66
+ const src = (p as { src?: unknown }).src;
67
+ const vid = (p as { vid?: unknown }).vid;
68
+ if (typeof src !== 'string' || typeof vid !== 'string') return null;
69
+ return { owner: p.userId, sourceId: src, videoId: vid };
70
+ } catch {
71
+ // Bad signature / expiry / purpose mismatch — deny.
72
+ return null;
73
+ }
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,134 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { mkdtemp, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { pickFreePort, startTestServer, type TestServer } from 'cwip/testing';
7
+
8
+ // Integration test for the binary-server's transcode API (POST /api/transcode,
9
+ // GET /api/transcode/:jobId). Skips cleanly when the binary-server checkout is
10
+ // absent. Mirrors the pattern from chunked.integration.spec.ts.
11
+ //
12
+ // Transcode API implemented in #tsk_01KWQNXRP25W6BE086K49M9S3W.
13
+ // Contract: POST /api/transcode → { jobId }, GET /api/transcode/:jobId → { status, progress?, result? }.
14
+ // Auth: x-internal-secret header must match process.env.INTERNAL_SECRET.
15
+
16
+ const BS_DIR = process.env.BINARY_SERVER_DIR ?? '';
17
+ const BS_PRESENT = BS_DIR !== '' && existsSync(path.join(BS_DIR, 'src', 'index.ts'));
18
+
19
+ // SKIPPED until a real binary-server checkout is present: point BINARY_SERVER_DIR at one.
20
+ // There is deliberately no default — a published package must not go looking in one machine's
21
+ // directory layout — and `binary-server` has not been re-homed here, so this skips on every host today.
22
+ describe.skipIf(!BS_PRESENT)('transcode API → real binary-server (integration)', () => {
23
+ let port: number;
24
+ let storeDir: string;
25
+ let server: TestServer | null = null;
26
+ let tmpData: string;
27
+ const INTERNAL_SECRET = 'test-transcode-secret';
28
+
29
+ beforeAll(async () => {
30
+ tmpData = await mkdtemp(path.join(tmpdir(), 'bs-transcode-test-'));
31
+ storeDir = path.join(tmpData, 'binaries');
32
+ mkdirSync(storeDir, { recursive: true });
33
+
34
+ port = await pickFreePort();
35
+ server = await startTestServer({
36
+ cwd: BS_DIR,
37
+ cmd: ['bun', path.join(BS_DIR, 'src', 'index.ts')],
38
+ port,
39
+ env: {
40
+ PORT: String(port),
41
+ HOST: '127.0.0.1',
42
+ STORAGE_ROOT: storeDir,
43
+ DB_PATH: path.join(tmpData, 'test.sqlite'),
44
+ INTERNAL_SECRET,
45
+ NODE_ENV: 'test',
46
+ },
47
+ healthPath: '/healthz',
48
+ timeoutMs: 90_000,
49
+ });
50
+ }, 90_000);
51
+
52
+ afterAll(async () => {
53
+ await server?.stop();
54
+ await rm(tmpData, { recursive: true, force: true }).catch(() => void 0);
55
+ });
56
+
57
+ test('POST /api/transcode rejects missing secret with 401', async () => {
58
+ const res = await fetch(`http://localhost:${port}/api/transcode`, {
59
+ method: 'POST',
60
+ headers: { 'content-type': 'application/json' },
61
+ body: JSON.stringify({
62
+ strategy: 'faststart',
63
+ sourceKey: 'demo/fs/u1/node1',
64
+ binaryKey: 'demo/fs/u1/node1.mp4',
65
+ settings: {},
66
+ }),
67
+ });
68
+ expect(res.status).toBe(401);
69
+ });
70
+
71
+ test('POST /api/transcode rejects wrong secret with 401', async () => {
72
+ const res = await fetch(`http://localhost:${port}/api/transcode`, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'content-type': 'application/json',
76
+ 'x-internal-secret': 'wrong-secret',
77
+ },
78
+ body: JSON.stringify({
79
+ strategy: 'faststart',
80
+ sourceKey: 'demo/fs/u1/node1',
81
+ binaryKey: 'demo/fs/u1/node1.mp4',
82
+ settings: {},
83
+ }),
84
+ });
85
+ expect(res.status).toBe(401);
86
+ });
87
+
88
+ test('POST /api/transcode with missing source returns jobId then fails', async () => {
89
+ const res = await fetch(`http://localhost:${port}/api/transcode`, {
90
+ method: 'POST',
91
+ headers: {
92
+ 'content-type': 'application/json',
93
+ 'x-internal-secret': INTERNAL_SECRET,
94
+ },
95
+ body: JSON.stringify({
96
+ strategy: 'faststart',
97
+ sourceKey: 'demo/fs/u1/nonexistent',
98
+ binaryKey: 'demo/fs/u1/nonexistent.mp4',
99
+ settings: {},
100
+ }),
101
+ });
102
+ expect(res.status).toBe(202);
103
+ const { jobId } = (await res.json()) as { jobId?: string };
104
+ expect(typeof jobId).toBe('string');
105
+
106
+ // Poll until failed or timeout.
107
+ const deadline = Date.now() + 5_000;
108
+ let status: string | undefined;
109
+ while (Date.now() < deadline) {
110
+ const poll = await fetch(
111
+ `http://localhost:${port}/api/transcode/${encodeURIComponent(jobId!)}`,
112
+ { headers: { 'x-internal-secret': INTERNAL_SECRET } },
113
+ );
114
+ expect(poll.status).toBe(200);
115
+ const body = (await poll.json()) as { status?: string };
116
+ status = body.status;
117
+ if (status === 'failed' || status === 'ready') break;
118
+ await Bun.sleep(100);
119
+ }
120
+ expect(status).toBe('failed');
121
+ });
122
+
123
+ test('GET /api/transcode/:jobId returns 404 for unknown job', async () => {
124
+ const res = await fetch(`http://localhost:${port}/api/transcode/nonexistent-job-id`, {
125
+ headers: { 'x-internal-secret': INTERNAL_SECRET },
126
+ });
127
+ expect(res.status).toBe(404);
128
+ });
129
+
130
+ test('GET /api/transcode/:jobId rejects missing secret with 401', async () => {
131
+ const res = await fetch(`http://localhost:${port}/api/transcode/any-id`);
132
+ expect(res.status).toBe(401);
133
+ });
134
+ });
@@ -0,0 +1,152 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createTranscodeJobClient } from './transcodeJobs';
3
+
4
+ /**
5
+ * The transport, driven against a scripted binary-server. The two derivative helpers are what
6
+ * apps call, so what matters is the REQUEST they send (a wrong strategy or a missing
7
+ * `posterOnly` is a full transcode nobody asked for) and that a failure is a plain negative
8
+ * answer rather than a throw — every caller's fallback is "render what you rendered before".
9
+ */
10
+
11
+ type Call = { url: string; body?: Record<string, unknown>; headers: Record<string, string> };
12
+
13
+ function scriptedServer(script: {
14
+ submitStatus?: number;
15
+ statuses: { status: string; progress?: number; result?: Record<string, unknown> }[];
16
+ }) {
17
+ const calls: Call[] = [];
18
+ let polls = 0;
19
+ const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => {
20
+ const href = String(url);
21
+ const headers = Object.fromEntries(
22
+ Object.entries((init?.headers ?? {}) as Record<string, string>),
23
+ );
24
+ if (init?.method === 'POST') {
25
+ calls.push({ url: href, body: JSON.parse(String(init.body)), headers });
26
+ const status = script.submitStatus ?? 202;
27
+ return new Response(JSON.stringify({ jobId: 'job-1' }), { status });
28
+ }
29
+ calls.push({ url: href, headers });
30
+ const next = script.statuses[Math.min(polls++, script.statuses.length - 1)];
31
+ return new Response(JSON.stringify(next), { status: 200 });
32
+ }) as unknown as typeof fetch;
33
+ return { calls, fetchImpl, polls: () => polls };
34
+ }
35
+
36
+ const config = (fetchImpl: typeof fetch) => ({
37
+ baseUrl: 'https://bs.test',
38
+ appId: 'notes',
39
+ internalSecret: 's3cret',
40
+ fetchImpl,
41
+ pollMs: 0,
42
+ sleep: async () => {},
43
+ });
44
+
45
+ describe('createTranscodeJobClient', () => {
46
+ test('an image job asks for the image strategy with app-scoped keys and the secret', async () => {
47
+ const server = scriptedServer({ statuses: [{ status: 'ready', result: { hasThumbnail: true } }] });
48
+ const client = createTranscodeJobClient(config(server.fetchImpl));
49
+
50
+ const out = await client.imageThumbnail({
51
+ sourceKey: 'attachments/abc',
52
+ thumbKey: 'derived/abc-thumb.jpg',
53
+ });
54
+
55
+ expect(out).toEqual({ hasThumbnail: true, hasDisplay: false });
56
+ const submit = server.calls[0] as Call;
57
+ expect(submit.url).toBe('https://bs.test/api/transcode');
58
+ expect(submit.headers['x-internal-secret']).toBe('s3cret');
59
+ expect(submit.body).toMatchObject({
60
+ strategy: 'image',
61
+ sourceKey: 'notes/attachments/abc',
62
+ binaryKey: 'notes/attachments/abc',
63
+ posterKey: 'notes/derived/abc-thumb.jpg',
64
+ });
65
+ // No displayKey unless one was asked for — a HEIC display JPEG is a second derivative,
66
+ // not a default.
67
+ expect(submit.body).not.toHaveProperty('displayKey');
68
+ });
69
+
70
+ test('a display JPEG is requested only when a displayKey is given', async () => {
71
+ const server = scriptedServer({
72
+ statuses: [{ status: 'ready', result: { hasThumbnail: true, hasDisplay: true } }],
73
+ });
74
+ const client = createTranscodeJobClient(config(server.fetchImpl));
75
+ const out = await client.imageThumbnail({
76
+ sourceKey: 'attachments/heic',
77
+ thumbKey: 'derived/heic-thumb.jpg',
78
+ displayKey: 'derived/heic-display.jpg',
79
+ });
80
+ expect(out).toEqual({ hasThumbnail: true, hasDisplay: true });
81
+ expect((server.calls[0] as Call).body).toMatchObject({
82
+ displayKey: 'notes/derived/heic-display.jpg',
83
+ });
84
+ });
85
+
86
+ test('a video poster job is posterOnly — a grid tile must not trigger a full transcode', async () => {
87
+ const server = scriptedServer({
88
+ statuses: [{ status: 'ready', result: { hasPoster: true, durationSeconds: 12.5 } }],
89
+ });
90
+ const client = createTranscodeJobClient(config(server.fetchImpl));
91
+
92
+ const out = await client.videoPoster({
93
+ sourceKey: 'attachments/vid',
94
+ posterKey: 'derived/vid-poster.jpg',
95
+ });
96
+
97
+ expect(out).toEqual({ hasPoster: true, durationSeconds: 12.5 });
98
+ expect((server.calls[0] as Call).body).toMatchObject({
99
+ strategy: 'library',
100
+ posterOnly: true,
101
+ posterKey: 'notes/derived/vid-poster.jpg',
102
+ });
103
+ // No ladder, no hlsPrefix, no sprite — the expensive half must not be implied.
104
+ const body = (server.calls[0] as Call).body as Record<string, unknown>;
105
+ expect(body).not.toHaveProperty('hlsPrefix');
106
+ expect(body).not.toHaveProperty('spriteKey');
107
+ });
108
+
109
+ test('it polls until the job settles, not once', async () => {
110
+ const server = scriptedServer({
111
+ statuses: [
112
+ { status: 'processing', progress: 10 },
113
+ { status: 'processing', progress: 80 },
114
+ { status: 'ready', result: { hasThumbnail: true } },
115
+ ],
116
+ });
117
+ const client = createTranscodeJobClient(config(server.fetchImpl));
118
+ await client.imageThumbnail({ sourceKey: 'a', thumbKey: 'b' });
119
+ expect(server.polls()).toBe(3);
120
+ });
121
+
122
+ test('a FAILED job is a negative answer, not a throw — callers fall back to no thumbnail', async () => {
123
+ const server = scriptedServer({ statuses: [{ status: 'failed' }] });
124
+ const client = createTranscodeJobClient(config(server.fetchImpl));
125
+ expect(await client.imageThumbnail({ sourceKey: 'a', thumbKey: 'b' })).toEqual({
126
+ hasThumbnail: false,
127
+ hasDisplay: false,
128
+ });
129
+ expect(await client.videoPoster({ sourceKey: 'a', posterKey: 'b' })).toEqual({
130
+ hasPoster: false,
131
+ durationSeconds: 0,
132
+ });
133
+ });
134
+
135
+ test('a ready job that produced nothing still reports nothing', async () => {
136
+ // binary-server answers `ready` for a source it could probe but not derive from. Reading
137
+ // the STATUS alone would claim a thumbnail that does not exist, and the consumer would
138
+ // then render a broken image instead of the text it had.
139
+ const server = scriptedServer({ statuses: [{ status: 'ready', result: {} }] });
140
+ const client = createTranscodeJobClient(config(server.fetchImpl));
141
+ expect(await client.imageThumbnail({ sourceKey: 'a', thumbKey: 'b' })).toEqual({
142
+ hasThumbnail: false,
143
+ hasDisplay: false,
144
+ });
145
+ });
146
+
147
+ test('a refused submit throws, so a misconfigured secret is loud', async () => {
148
+ const server = scriptedServer({ submitStatus: 401, statuses: [] });
149
+ const client = createTranscodeJobClient(config(server.fetchImpl));
150
+ await expect(client.imageThumbnail({ sourceKey: 'a', thumbKey: 'b' })).rejects.toThrow(/401/);
151
+ });
152
+ });