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,172 @@
1
+ /**
2
+ * ── The NAMES of every video derivative binary-server writes ────────────────────────────
3
+ *
4
+ * A derivative's key is COMPUTED from its source's, never bookkept. A consumer finds one by
5
+ * deriving the name and asking `/meta` whether it exists yet, so there is no webhook, no job
6
+ * id, no schema column and nothing to reconcile: the absence of the object IS the "not ready
7
+ * yet" state, and it answers itself.
8
+ *
9
+ * ── 🔴 Why these live in cursedbelt ─────────────────────────────────────────────────────
10
+ * Owner, 2026-08-14: *"Make it in cb because many apps may move to cloudflare and share the
11
+ * logic."* This contract is the seam between three repos that must never disagree —
12
+ * binary-server WRITES these names, `@satellites/satellite-kit` re-exports them for the
13
+ * satellite apps, and cursedbelt's own `binaryServerStore` mints the tokens that authorize
14
+ * them. A second hand-typed `${key}.web.mp4` anywhere in that triangle is a silent
15
+ * no-preview the day binary-server renames anything, and that has already happened once:
16
+ * `apps/collections` had the contract and `apps/family` shipped video without it, so family
17
+ * had no faststart stream, no scrub previews and no processing state at all.
18
+ *
19
+ * 🔴 What is shared here is the STRATEGY, never the media. Nothing in this module can resolve
20
+ * a stored object — every function is a pure string transform over a key the caller already
21
+ * holds. Each app is its own binary-server tenant with its own signing key, so family cannot
22
+ * reach a collections object through anything here, and that must stay true.
23
+ *
24
+ * ── 🔴 This is NOT the `fs*` keyspace in `./types.ts`, and they must not be merged ───────
25
+ * `fsHlsMasterKey(owner, id)` → `hls/<owner>/<id>/master.m3u8` and its siblings address the
26
+ * FileCatalogue: a caller that holds an owner and an item id, whose derivatives live in their
27
+ * own top-level namespaces. This module addresses a caller that holds a binary-server KEY and
28
+ * derives sibling names from it, which is what makes "ask /meta for a name you computed" work
29
+ * with no catalogue at all.
30
+ *
31
+ * Two consumers, two shapes, both correct:
32
+ * · `fs*` — cursedbelt's own FileCatalogue / the `library` transcode strategy.
33
+ * · this module — binary-server's derivative convention, used by the satellite apps.
34
+ *
35
+ * Collapsing either into the other renames objects that already exist in the store, and a
36
+ * renamed derivative is indistinguishable from an unbuilt one (absence is the "not ready"
37
+ * state) — so the visible result is every video in the fleet losing its previews at once,
38
+ * silently, with the rebuild costing hours of ffmpeg.
39
+ */
40
+
41
+ /**
42
+ * The browser-playable remux: the same pictures with the index at the front.
43
+ *
44
+ * A phone MP4/MOV stores its `moov` index at the END, and a browser cannot decode frame one
45
+ * until it has that index — so it downloads the whole file first. Measured on the owner's
46
+ * 90,339,606-byte upload (2026-08-13): the index began at byte 90,305,514, 99.96% of the way
47
+ * in. That is the "videos stay on a spinner for minutes" report, and no amount of Range
48
+ * support, caching or edge tuning fixes it. A `+faststart` remux is a STREAM COPY for
49
+ * h264+aac sources, which is what phones produce: no re-encode, nothing lost.
50
+ */
51
+ /**
52
+ * ── 🔴 THE ENCODER GENERATION IN THE NAME (2026-09-09) ──────────────────────────────────
53
+ *
54
+ * binary-server rebuilds every video derivative when its encoder improves, and a rebuild
55
+ * used to write **the same filename with different bytes**. That silently cancels out an
56
+ * `immutable` cache lifetime — a browser told `immutable` never re-checks, for a year, and
57
+ * **that cannot be revoked for a copy already on somebody's phone**. So the owner's decision
58
+ * (A, 2026-09-09) is that new bytes get a new address: `clip.web.mp4` → `clip.g2.web.mp4`.
59
+ *
60
+ * 🔴 **Generation 1 is UNTAGGED and always will be.** Every derivative in the store today is
61
+ * at the untagged name; tagging generation 1 would orphan all of them and re-encode the
62
+ * whole library for byte-identical output. Untagged is not a legacy case — it IS generation
63
+ * one.
64
+ *
65
+ * 🔴 **This constant must match binary-server's `VIDEO_ENCODER_GENERATION`**
66
+ * (`src/media/encoderGeneration.ts`). They are in different repos and neither can import the
67
+ * other, so binary-server carries a test that goes RED the moment somebody bumps its number,
68
+ * naming this file. If you are here because of that test: bump this in the same commit, and
69
+ * remember that `notes`, `collections` and `family` all reach these names through
70
+ * `@satellites/satellite-kit/video-derivatives`. Getting it wrong is not an error anywhere —
71
+ * it is a 404 that reads as "no preview available", on every video.
72
+ */
73
+ export const VIDEO_KEY_GENERATION = 1;
74
+
75
+ /** The generation tag a derivative's name carries. Empty for generation 1 — see above. */
76
+ export const videoGenerationTag = (generation: number = VIDEO_KEY_GENERATION): string =>
77
+ generation <= 1 ? "" : `.g${generation}`;
78
+
79
+ export const webPlayableKeyFor = (
80
+ key: string,
81
+ generation: number = VIDEO_KEY_GENERATION,
82
+ ): string => `${key}${videoGenerationTag(generation)}.web.mp4`;
83
+
84
+ /** The scrub-preview sprite sheet — tiled frames from across the clip, as one JPEG. */
85
+ export const spriteKeyFor = (key: string): string => `${key}.sprite.jpg`;
86
+
87
+ /** The WebVTT mapping a playhead position onto a tile of {@link spriteKeyFor}. */
88
+ export const spriteVttKeyFor = (key: string): string => `${key}.sprite.vtt`;
89
+
90
+ /**
91
+ * The single still a gallery draws on a tile.
92
+ *
93
+ * Deliberately the SAME name a still photograph's thumbnail gets, so a consumer has one rule
94
+ * — *the tile for anything is `<key>.thumb.jpg`, and its absence means not ready* — rather
95
+ * than one rule per media kind. The sprite sheet is not a substitute: it is a GRID, so drawing
96
+ * it on a tile shows a contact sheet, and using one cell means every consumer carries the
97
+ * VTT's geometry in order to crop it.
98
+ */
99
+ export const posterKeyFor = (key: string): string => `${key}.thumb.jpg`;
100
+
101
+ /**
102
+ * ── The HLS tree: `<key>.hls/` ──────────────────────────────────────────────────────────
103
+ *
104
+ * 🔴 ONE container, and the trailing name is load-bearing. binary-server's `storage.ts`
105
+ * co-locates a container's whole subtree under a single shard and `removeContainer()` wipes it
106
+ * atomically — so a ladder that is one container is deleted, migrated and reconciled as one
107
+ * thing. Segments scattered under sibling keys would be none of those, and orphaning half a
108
+ * ladder is worse than having none: the master would keep advertising rungs whose segments
109
+ * 404, which a player retries rather than falls back from.
110
+ *
111
+ * 🔴 Why HLS at all, now that Range works. The Range bug (an edge Worker answering 200 + the
112
+ * whole 209 MB to a ranged read) was fixed 2026-08-14 and is NOT what this is for. A single
113
+ * progressive MP4, however well served, cannot ADAPT: a weak cell connection stalls on a
114
+ * 23 Mbps master instead of dropping to a lower rung. Each segment is also a small object that
115
+ * caches WHOLE at the edge, which sidesteps both the 512 MiB single-entry ceiling and the
116
+ * segment-assembly machinery that exists to work around it for big films.
117
+ */
118
+ export const hlsPrefixFor = (
119
+ key: string,
120
+ generation: number = VIDEO_KEY_GENERATION,
121
+ ): string => `${key}${videoGenerationTag(generation)}.hls/`;
122
+
123
+ /**
124
+ * The playlist a player is pointed at — the one object whose presence means "the ladder is
125
+ * ready". It is written LAST, after every rung, so a consumer that finds it can trust that
126
+ * what it names exists. Absence is normal and means "play the faststart remux".
127
+ */
128
+ export const hlsMasterKeyFor = (
129
+ key: string,
130
+ generation: number = VIDEO_KEY_GENERATION,
131
+ ): string => `${hlsPrefixFor(key, generation)}master.m3u8`;
132
+
133
+ /**
134
+ * Every derivative suffix, for the "is this key itself a derivative" question.
135
+ *
136
+ * 🔴 Load-bearing in three directions, each of which has already cost something:
137
+ * 1. A derivative must never become the source of another, or a sweep builds
138
+ * `clip.web.mp4.web.mp4` and then `…web.mp4.web.mp4.web.mp4`, forever.
139
+ * 2. A sweep must not count a derivative as work still owed, or the backlog is permanently
140
+ * non-empty and ffmpeg re-runs on every pass.
141
+ * 3. A duplicate scan must not propose a file against its own derivative. They share a title
142
+ * by construction, so every video would pair with its own remux and a review queue would
143
+ * contain nothing else.
144
+ */
145
+ export const DERIVATIVE_SUFFIXES = [
146
+ ".web.mp4",
147
+ ".sprite.jpg",
148
+ ".sprite.vtt",
149
+ ".thumb.jpg",
150
+ ".web.jpg",
151
+ ] as const;
152
+
153
+ /**
154
+ * Is this key something binary-server DERIVED rather than something a person uploaded?
155
+ *
156
+ * The HLS tree is matched by its container segment rather than a suffix, because every object
157
+ * inside it (`master.m3u8`, `720/index.m3u8`, `720/000.ts`) is a derivative and none of them
158
+ * ends in a name from the list above.
159
+ */
160
+ const DERIVATIVE_RE = new RegExp(
161
+ // 🔴 The optional `.g<N>` is the encoder generation a video derivative's name carries
162
+ // (see VIDEO_KEY_GENERATION above). Without it, `clip.g2.web.mp4` reads as a SOURCE — and
163
+ // then a sweep derives `clip.g2.web.mp4.web.mp4` from it, for ever. Built from the suffix
164
+ // list rather than written out, so the two cannot drift apart.
165
+ `(?:\\.g\\d+)?(?:${DERIVATIVE_SUFFIXES.map((x) => x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})$`,
166
+ );
167
+
168
+ export const isDerivativeKey = (key: string): boolean =>
169
+ DERIVATIVE_RE.test(key) ||
170
+ key.includes(".hls/") ||
171
+ /-thumb\.(jpg|webp|png)$/.test(key) ||
172
+ /\.shot\.\d+\.jpg$/.test(key);
@@ -0,0 +1,106 @@
1
+ import { 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 { isFfmpegAvailable } from '../media-bun';
6
+ import { createDiskStore } from './diskStore';
7
+ import { fsHlsMasterKey, fsPosterKey, fsSourceKey } from './types';
8
+ import { processVideo } from './videoProcessor';
9
+
10
+ const haveFfmpeg = await isFfmpegAvailable();
11
+
12
+ /** Synthesize a short test clip with ffmpeg's lavfi sources. */
13
+ async function makeSource(dest: string, duration = 2): Promise<void> {
14
+ const proc = Bun.spawn(
15
+ [
16
+ 'ffmpeg',
17
+ '-hide_banner',
18
+ '-y',
19
+ '-f',
20
+ 'lavfi',
21
+ '-i',
22
+ `testsrc=duration=${duration}:size=320x240:rate=15`,
23
+ '-pix_fmt',
24
+ 'yuv420p',
25
+ '-c:v',
26
+ 'libx264',
27
+ dest,
28
+ ],
29
+ { stdout: 'ignore', stderr: 'ignore' },
30
+ );
31
+ await proc.exited;
32
+ if (proc.exitCode !== 0) throw new Error('failed to synthesize test clip');
33
+ }
34
+
35
+ // posterOnly is the memory-constrained-host path: a poster + duration, but NO heavy HLS/re-encode.
36
+ // SKIPPED on a host without ffmpeg/ffprobe — it synthesizes and decodes real media. Runs by default wherever ffmpeg is installed.
37
+ describe.skipIf(!haveFfmpeg)('processVideo posterOnly', () => {
38
+ test('extracts a poster + duration, ready, no HLS', async () => {
39
+ const root = await mkdtemp(path.join(tmpdir(), 'cb-poster-only-'));
40
+ try {
41
+ const store = createDiskStore(root);
42
+ const owner = 'u1';
43
+ const id = 'v1';
44
+ const srcLocal = path.join(root, 'in.mp4');
45
+ await makeSource(srcLocal);
46
+ await store.put(
47
+ fsSourceKey(owner, id),
48
+ new Uint8Array(await Bun.file(srcLocal).arrayBuffer()),
49
+ 'video/mp4',
50
+ );
51
+
52
+ const res = await processVideo({
53
+ store,
54
+ workDir: path.join(root, 'work'),
55
+ owner,
56
+ id,
57
+ sourceKey: fsSourceKey(owner, id),
58
+ posterOnly: true,
59
+ });
60
+
61
+ expect(res.status).toBe('ready');
62
+ expect(res.hasPoster).toBe(true);
63
+ expect(res.hasHls).toBe(false);
64
+ expect(res.hasSprite).toBe(false);
65
+ expect(res.durationSeconds).toBeGreaterThan(1);
66
+ // Poster is uploaded; the HLS master never is.
67
+ expect(await store.stat(fsPosterKey(owner, id))).not.toBeNull();
68
+ expect(await store.stat(fsHlsMasterKey(owner, id))).toBeNull();
69
+ } finally {
70
+ await rm(root, { recursive: true, force: true });
71
+ }
72
+ });
73
+
74
+ // Regression: a sub-1s clip (the 2026-07-04 prod `0:00` .mov) — the fixed `-ss 1` seek landed
75
+ // past EOF and produced no poster. extractPoster must clamp the seek / fall back to frame 0.
76
+ test('still extracts a poster for a very short (<1s) clip', async () => {
77
+ const root = await mkdtemp(path.join(tmpdir(), 'cb-poster-short-'));
78
+ try {
79
+ const store = createDiskStore(root);
80
+ const owner = 'u1';
81
+ const id = 'short1';
82
+ const srcLocal = path.join(root, 'in.mp4');
83
+ await makeSource(srcLocal, 0.5);
84
+ await store.put(
85
+ fsSourceKey(owner, id),
86
+ new Uint8Array(await Bun.file(srcLocal).arrayBuffer()),
87
+ 'video/mp4',
88
+ );
89
+
90
+ const res = await processVideo({
91
+ store,
92
+ workDir: path.join(root, 'work'),
93
+ owner,
94
+ id,
95
+ sourceKey: fsSourceKey(owner, id),
96
+ posterOnly: true,
97
+ });
98
+
99
+ expect(res.status).toBe('ready');
100
+ expect(res.hasPoster).toBe(true);
101
+ expect(await store.stat(fsPosterKey(owner, id))).not.toBeNull();
102
+ } finally {
103
+ await rm(root, { recursive: true, force: true });
104
+ }
105
+ });
106
+ });
@@ -0,0 +1,229 @@
1
+ import { mkdir, readdir, rm } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import type { ProcessingStatus } from 'cursedbelt-core/media';
4
+ import {
5
+ buildSpriteVtt,
6
+ canStreamCopy,
7
+ faststartArgs,
8
+ hlsLadderArgs,
9
+ isFfmpegAvailable,
10
+ LIBRARY_CRF,
11
+ planSprite,
12
+ posterArgs,
13
+ probeVideoStream,
14
+ resolveLadder,
15
+ runFfmpegProgress,
16
+ spriteArgs,
17
+ } from '../media-bun';
18
+ import {
19
+ fsHlsFileKey,
20
+ fsHlsMasterKey,
21
+ fsMp4Key,
22
+ fsPosterKey,
23
+ fsSpriteKey,
24
+ fsVttKey,
25
+ type MediaStore,
26
+ } from './types';
27
+
28
+ // Ingest→derivatives orchestrator: the store-aware glue on top of `server/media` (media-bun),
29
+ // which owns the argv/codec work but nothing about *where bytes live*. Transcodes a raw upload
30
+ // into web-safe derivatives (faststart MP4 fallback + HLS ladder + poster + sprite/VTT), uploads
31
+ // each into the MediaStore, and reports progress. Best-effort: if ffmpeg is unavailable it marks
32
+ // the video ready and lets it play raw (progressive Range).
33
+
34
+ export type ProcessProgress = (status: ProcessingStatus, pct: number) => void;
35
+
36
+ export type ProcessResult = {
37
+ status: ProcessingStatus;
38
+ durationSeconds: number;
39
+ hasHls: boolean;
40
+ hasSprite: boolean;
41
+ hasPoster: boolean;
42
+ };
43
+
44
+ export type ProcessOptions = {
45
+ store: MediaStore;
46
+ /** Scratch directory for ffmpeg inputs/outputs (cleaned up afterwards). */
47
+ workDir: string;
48
+ owner: string;
49
+ id: string;
50
+ /** Storage key of the already-persisted raw source. */
51
+ sourceKey: string;
52
+ /** HLS rung heights to attempt (clamped to the source height; never upscaled). */
53
+ ladderHeights?: number[];
54
+ /**
55
+ * Poster-only mode for memory-constrained hosts: probe duration + extract a single-frame
56
+ * poster (cheap, ~tens of MB peak) and skip the heavy re-encode/HLS/sprite work that OOM'd
57
+ * the 1GB prod box (2026-07-04). The raw source still plays progressively; this only buys a
58
+ * grid thumbnail + duration. Best-effort — a failed poster still lands the row `ready`.
59
+ */
60
+ posterOnly?: boolean;
61
+ onProgress?: ProcessProgress;
62
+ };
63
+
64
+ // Stream a produced local file into the store under `key` (no full heap copy).
65
+ async function uploadFile(store: MediaStore, localPath: string, key: string, mime: string) {
66
+ const file = Bun.file(localPath);
67
+ if (!(await file.exists())) return;
68
+ await store.putStream(key, file.stream(), mime, { contentLength: file.size });
69
+ }
70
+
71
+ // Recursively upload an HLS output tree (master.m3u8 + <height>/playlist.m3u8 + segments).
72
+ async function uploadHlsTree(store: MediaStore, dir: string, owner: string, id: string) {
73
+ const walk = async (rel: string): Promise<void> => {
74
+ const entries = await readdir(path.join(dir, rel), { withFileTypes: true });
75
+ for (const e of entries) {
76
+ const childRel = rel ? path.join(rel, e.name) : e.name;
77
+ if (e.isDirectory()) {
78
+ await walk(childRel);
79
+ } else {
80
+ const mime = e.name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t';
81
+ await uploadFile(store, path.join(dir, childRel), fsHlsFileKey(owner, id, childRel), mime);
82
+ }
83
+ }
84
+ };
85
+ await walk('');
86
+ }
87
+
88
+ // Extract a single JPEG poster, robust to very short clips: seek ~1s in to skip black intros, but
89
+ // clamp the seek inside a short clip and fall back to frame 0 if the seek lands past EOF (which
90
+ // otherwise decodes no frame and silently produces no poster — the 2026-07-04 `0:00` .mov case).
91
+ async function extractPoster(
92
+ inPath: string,
93
+ outPath: string,
94
+ durationSeconds: number,
95
+ ): Promise<boolean> {
96
+ let stderrTail = '';
97
+ const onError = (tail: string) => {
98
+ stderrTail = tail;
99
+ };
100
+ const at = durationSeconds > 1.2 ? 1 : 0;
101
+ if (await runFfmpegProgress(posterArgs(inPath, outPath, at), 0, undefined, onError)) return true;
102
+ if (at !== 0 && (await runFfmpegProgress(posterArgs(inPath, outPath, 0), 0, undefined, onError)))
103
+ return true;
104
+ // Real poster failures AND the malformed/short-clip fixtures test suites deliberately
105
+ // feed ffmpeg land here — stderr alone can't tell them apart, so the message is tagged
106
+ // `[expected-in-tests]` (grep-filterable) instead of silenced, so a real prod
107
+ // regression still surfaces.
108
+ console.warn(
109
+ `[videoProcessor][expected-in-tests] poster extraction failed (duration=${durationSeconds}s): ${stderrTail.slice(-400)}`,
110
+ );
111
+ return false;
112
+ }
113
+
114
+ export async function processVideo(opts: ProcessOptions): Promise<ProcessResult> {
115
+ const { store, workDir, owner, id, sourceKey, onProgress } = opts;
116
+ const report = (s: ProcessingStatus, p: number) => onProgress?.(s, p);
117
+ const idle: ProcessResult = {
118
+ status: 'ready',
119
+ durationSeconds: 0,
120
+ hasHls: false,
121
+ hasSprite: false,
122
+ hasPoster: false,
123
+ };
124
+
125
+ if (!(await isFfmpegAvailable())) {
126
+ report('ready', 100);
127
+ return idle;
128
+ }
129
+
130
+ const jobDir = path.join(workDir, owner, id);
131
+ await mkdir(jobDir, { recursive: true });
132
+
133
+ try {
134
+ report('processing', 1);
135
+
136
+ // 1. Materialise the source locally for ffmpeg (use the backend's own path if local).
137
+ let srcPath = (await store.localPath?.(sourceKey)) ?? null;
138
+ if (!srcPath) {
139
+ const bytes = await store.get(sourceKey);
140
+ if (!bytes) throw new Error(`source missing: ${sourceKey}`);
141
+ srcPath = path.join(jobDir, 'source.bin');
142
+ await Bun.write(srcPath, bytes);
143
+ }
144
+
145
+ const probe = await probeVideoStream(srcPath);
146
+ const durationSeconds = probe.duration;
147
+
148
+ // Poster-only mode: extract one frame + report ready. No re-encode, no HLS, no sprite —
149
+ // the memory-heavy steps that OOM'd the 1GB host stay off; the raw source plays as-is.
150
+ if (opts.posterOnly) {
151
+ report('processing', 50);
152
+ const posterPath = path.join(jobDir, 'poster.jpg');
153
+ const okPoster = await extractPoster(srcPath, posterPath, durationSeconds);
154
+ if (okPoster) await uploadFile(store, posterPath, fsPosterKey(owner, id), 'image/jpeg');
155
+ report('ready', 100);
156
+ return {
157
+ status: 'ready',
158
+ durationSeconds,
159
+ hasHls: false,
160
+ hasSprite: false,
161
+ hasPoster: okPoster,
162
+ };
163
+ }
164
+
165
+ // 2. Faststart MP4 — the progressive Range fallback (streamUrl). Stream-copy when the
166
+ // source is already web-safe, else re-encode to the shared web-safe ladder.
167
+ const mp4Path = path.join(jobDir, 'out.mp4');
168
+ const okMp4 = await runFfmpegProgress(
169
+ faststartArgs(srcPath, mp4Path, { copy: canStreamCopy(probe) }),
170
+ durationSeconds,
171
+ (pct) => report('processing', Math.round(pct * 0.4)),
172
+ );
173
+ if (okMp4) await uploadFile(store, mp4Path, fsMp4Key(owner, id), 'video/mp4');
174
+ // ffmpeg operates on the faststart output when it exists, else the raw source.
175
+ const encodeInput = okMp4 ? mp4Path : srcPath;
176
+
177
+ // 3. HLS ladder — the adaptive path (hlsUrl).
178
+ const heights = resolveLadder(opts.ladderHeights ?? [480, 720, 1080], probe.height);
179
+ const hlsDir = path.join(jobDir, 'hls');
180
+ await mkdir(hlsDir, { recursive: true });
181
+ const okHls = await runFfmpegProgress(
182
+ hlsLadderArgs(encodeInput, hlsDir, {
183
+ heights,
184
+ crf: Number(LIBRARY_CRF),
185
+ hasAudio: probe.hasAudio,
186
+ }),
187
+ durationSeconds,
188
+ (pct) => report('processing', 40 + Math.round(pct * 0.5)),
189
+ );
190
+ if (okHls) await uploadHlsTree(store, hlsDir, owner, id);
191
+
192
+ // 4. Poster (frame ~1s in).
193
+ report('processing', 92);
194
+ const posterPath = path.join(jobDir, 'poster.jpg');
195
+ const okPoster = await extractPoster(encodeInput, posterPath, durationSeconds);
196
+ if (okPoster) await uploadFile(store, posterPath, fsPosterKey(owner, id), 'image/jpeg');
197
+
198
+ // 5. Sprite sheet + WebVTT scrub track.
199
+ report('processing', 96);
200
+ let hasSprite = false;
201
+ if (durationSeconds > 0) {
202
+ const plan = planSprite(durationSeconds, 5, probe.width, probe.height);
203
+ const spritePath = path.join(jobDir, 'sprites.jpg');
204
+ if (await runFfmpegProgress(spriteArgs(encodeInput, spritePath, plan), 0)) {
205
+ await uploadFile(store, spritePath, fsSpriteKey(owner, id), 'image/jpeg');
206
+ await store.put(
207
+ fsVttKey(owner, id),
208
+ new TextEncoder().encode(buildSpriteVtt(plan, 'sprites.jpg')),
209
+ 'text/vtt',
210
+ );
211
+ hasSprite = true;
212
+ }
213
+ }
214
+
215
+ report('ready', 100);
216
+ return {
217
+ status: 'ready',
218
+ durationSeconds,
219
+ hasHls: okHls && (await store.stat(fsHlsMasterKey(owner, id))) !== null,
220
+ hasSprite,
221
+ hasPoster: okPoster,
222
+ };
223
+ } catch {
224
+ report('failed', 0);
225
+ return { ...idle, status: 'failed' };
226
+ } finally {
227
+ await rm(jobDir, { recursive: true, force: true }).catch(() => {});
228
+ }
229
+ }
@@ -0,0 +1,149 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { createSyncAlarm } from "./alarm";
6
+
7
+ /**
8
+ * 🔴 `tmpdir()`, not vault's `scratchDir()` — the seam moved when this engine did.
9
+ *
10
+ * In `apps/vault` this suite imported `scripts/testScratch`, an opt-in helper that
11
+ * hands out a directory under `$FORGE_STATE` and sweeps it. cursedbelt solves the
12
+ * same problem one level lower: `forceTestEnv.ts` is a bunfig PRELOAD that
13
+ * redirects `$TMPDIR` itself for every test process in the repo (rationale and the
14
+ * 4,950× measurement behind it are in `scripts/testTmpRoot.ts`). So a plain
15
+ * `mkdtempSync(join(tmpdir(), …))` here already lands in the small, swept root —
16
+ * and reaching back for the app's helper would be a cross-repo import that only
17
+ * resolves while the copy in vault still exists.
18
+ */
19
+ const dirs: string[] = [];
20
+ const scratchDir = (prefix: string): string => {
21
+ const dir = mkdtempSync(join(tmpdir(), `cb-${prefix}-`));
22
+ dirs.push(dir);
23
+ return dir;
24
+ };
25
+ afterAll(() => {
26
+ for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
27
+ });
28
+
29
+ function makeAlarm(overrides: { threshold?: number; reRaiseMs?: number } = {}) {
30
+ const dir = scratchDir("sync-alarm");
31
+ const raised: string[] = [];
32
+ let clock = 1_000_000;
33
+ const alarm = createSyncAlarm({
34
+ statePath: join(dir, "alarm.json"),
35
+ threshold: overrides.threshold ?? 3,
36
+ reRaiseMs: overrides.reRaiseMs ?? 60_000,
37
+ notify: (_title, body) => raised.push(body),
38
+ now: () => clock,
39
+ appName: "station",
40
+ });
41
+ return { alarm, raised, tick: (ms: number) => (clock += ms) };
42
+ }
43
+
44
+ describe("sync alarm", () => {
45
+ test("raises after the threshold of paused rounds, not before", () => {
46
+ const { alarm, raised } = makeAlarm();
47
+ alarm.record("paused", "doc.set");
48
+ alarm.record("paused", "doc.set");
49
+ expect(raised).toHaveLength(0);
50
+ alarm.record("paused", "doc.set");
51
+ expect(raised).toHaveLength(1);
52
+ expect(raised[0]).toContain("doc.set");
53
+ });
54
+
55
+ test("re-raises only after the re-raise window", () => {
56
+ const { alarm, raised, tick } = makeAlarm();
57
+ for (let i = 0; i < 5; i++) alarm.record("paused", "k");
58
+ expect(raised).toHaveLength(1);
59
+ tick(59_000);
60
+ alarm.record("paused", "k");
61
+ expect(raised).toHaveLength(1);
62
+ tick(2_000);
63
+ alarm.record("paused", "k");
64
+ expect(raised).toHaveLength(2);
65
+ });
66
+
67
+ test("ok resets the counter; unreachable neither counts nor resets", () => {
68
+ const { alarm, raised } = makeAlarm();
69
+ alarm.record("paused", "k");
70
+ alarm.record("paused", "k");
71
+ // The nightly window: unreachable rounds must not walk the counter to the
72
+ // threshold, and must not clear a genuine pause streak either.
73
+ alarm.record("unreachable");
74
+ alarm.record("paused", "k");
75
+ expect(raised).toHaveLength(1);
76
+ alarm.record("ok");
77
+ alarm.record("paused", "k");
78
+ alarm.record("paused", "k");
79
+ expect(raised).toHaveLength(1); // streak restarted from zero
80
+ });
81
+
82
+ test("auth refusals raise with the re-link message", () => {
83
+ const { alarm, raised } = makeAlarm({ threshold: 1 });
84
+ alarm.record("auth");
85
+ expect(raised[0]).toContain("Re-link");
86
+ });
87
+
88
+ test("state survives a process restart (same state file)", () => {
89
+ const dir = scratchDir("sync-alarm");
90
+ const statePath = join(dir, "alarm.json");
91
+ const raised: string[] = [];
92
+ const make = () =>
93
+ createSyncAlarm({
94
+ statePath,
95
+ threshold: 3,
96
+ notify: (_t, b) => raised.push(b),
97
+ now: () => 5_000,
98
+ });
99
+ make().record("paused", "k");
100
+ make().record("paused", "k");
101
+ make().record("paused", "k");
102
+ expect(raised).toHaveLength(1);
103
+ });
104
+ });
105
+
106
+ /**
107
+ * 🔴 The outcome that was missing, and the eight days it cost.
108
+ *
109
+ * The console's subdomain cutover left the Mac's sync dialer aimed at a host
110
+ * that had become a 301. It followed the redirect into a Cloudflare Access login
111
+ * page and threw a JSON parse error every round — neither `paused` (nothing was
112
+ * version-skewed), nor `auth` (no token was refused), nor `unreachable` (the
113
+ * peer answered). So the alarm recorded NOTHING while every mirrored report on
114
+ * prod froze, each still rendering a confident age.
115
+ */
116
+ describe("a sustained hard ERROR raises, exactly like a sustained pause", () => {
117
+ test("consecutive errors cross the threshold and the alarm names the real error", () => {
118
+ const { alarm, raised } = makeAlarm({ threshold: 3 });
119
+ alarm.record("error", "sync/info: 200 answered text/html, not JSON");
120
+ alarm.record("error", "sync/info: 200 answered text/html, not JSON");
121
+ expect(raised).toHaveLength(0);
122
+ alarm.record("error", "sync/info: 200 answered text/html, not JSON");
123
+ expect(raised).toHaveLength(1);
124
+ // The message carries the cause, because "failed (ignored)" being
125
+ // uninformative is the whole reason this outcome exists.
126
+ expect(raised[0]).toContain("text/html");
127
+ });
128
+
129
+ test("one good round clears the counter", () => {
130
+ const { alarm, raised } = makeAlarm({ threshold: 3 });
131
+ alarm.record("error", "boom");
132
+ alarm.record("error", "boom");
133
+ alarm.record("ok");
134
+ alarm.record("error", "boom");
135
+ alarm.record("error", "boom");
136
+ expect(raised).toHaveLength(0);
137
+ });
138
+
139
+ test("an UNREACHABLE peer between errors neither counts nor resets", () => {
140
+ // The nightly EC2 sleep sits in the middle of a real outage all the time.
141
+ const { alarm, raised } = makeAlarm({ threshold: 3 });
142
+ alarm.record("error", "boom");
143
+ alarm.record("unreachable");
144
+ alarm.record("error", "boom");
145
+ alarm.record("unreachable");
146
+ alarm.record("error", "boom");
147
+ expect(raised).toHaveLength(1);
148
+ });
149
+ });