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,265 @@
1
+ import type { Context, Hono } from 'hono';
2
+ import { decodeRange, serveRange } from '../storage/streamRange';
3
+ import { type MediaStore, privateHlsFileKey, privateSourceKey } from '../storage/types';
4
+ import { cryptBlobRangeStream, cryptBlobStream, openMeta, unwrapContentKey } from './crypto';
5
+ import type { PrivateIndex } from './index-store';
6
+ import { toPublic } from './index-store';
7
+ import type { UnlockPasswordPolicy, UnlockService } from './unlock';
8
+
9
+ // Mounts the private-media routes. The unlock / index / reveal endpoints are session-guarded (a
10
+ // fetch CAN send the app's Bearer). The byte routes under /u/<token>/ are UN-guarded (hls.js
11
+ // can't send a Bearer) and authorized by the path-embedded unlock token instead — the same token
12
+ // carries the sealed set key, so the content-key route can unwrap the item's AES-128 key. The
13
+ // encrypted segments themselves are edge-cacheable ciphertext; only the tiny key is gated + no-store.
14
+
15
+ /** Access-Control-Allow-Origin resolver for split-origin private streaming (media on another
16
+ * origin than the SPA). Same shape as the storage layer's. */
17
+ export type CorsResolver =
18
+ | string
19
+ | string[]
20
+ | ((requestOrigin: string | undefined) => string | null | undefined);
21
+
22
+ function resolveAcao(cors: CorsResolver | undefined, reqOrigin: string | undefined): string | null {
23
+ if (!cors) return null;
24
+ if (typeof cors === 'string') return cors;
25
+ if (Array.isArray(cors)) return reqOrigin && cors.includes(reqOrigin) ? reqOrigin : null;
26
+ return cors(reqOrigin) ?? null;
27
+ }
28
+
29
+ export type MountPrivateOptions = {
30
+ store: MediaStore;
31
+ index: PrivateIndex;
32
+ unlock: UnlockService;
33
+ /** Path prefix (default '/_private'). */
34
+ basePath?: string;
35
+ /** App session auth → the caller's id + their per-user salt (created lazily, stable). Return
36
+ * null to reject. Gates unlock/index/reveal (the byte routes use the path token instead). */
37
+ authorize: (c: Context) => Promise<{ userId: string; userSalt: string } | null>;
38
+ /** What a supplied password means (default: derive — deniable sets). `gate` verifies it
39
+ * against the app's accounts and mints the recoverable server-key grant instead. */
40
+ passwordPolicy?: UnlockPasswordPolicy;
41
+ /** Cross-origin access for split-origin deploys. */
42
+ cors?: CorsResolver;
43
+ };
44
+
45
+ export function mountPrivateRoutes(app: Hono, opts: MountPrivateOptions): void {
46
+ const base = opts.basePath ?? '/_private';
47
+ const { store, index, unlock } = opts;
48
+ const cors = (c: Context, res: Response): Response => {
49
+ const acao = resolveAcao(opts.cors, c.req.header('origin'));
50
+ if (acao) {
51
+ res.headers.set('Access-Control-Allow-Origin', acao);
52
+ res.headers.set(
53
+ 'Access-Control-Expose-Headers',
54
+ 'Content-Range, Content-Length, Accept-Ranges',
55
+ );
56
+ res.headers.append('Vary', 'Origin');
57
+ }
58
+ return res;
59
+ };
60
+
61
+ const policy = opts.passwordPolicy ?? { mode: 'derive' as const };
62
+
63
+ /** Resolve what the supplied password contributes under the active policy:
64
+ * derive ⇒ it feeds key derivation (may be undefined ⇒ standard set);
65
+ * gate ⇒ it must be present AND verify against the app's accounts, then the
66
+ * set key is the recoverable server one (password never feeds derivation).
67
+ * Returns an error Response to short-circuit with, or the derivation input. */
68
+ const resolvePassword = async (
69
+ c: Context,
70
+ who: { userId: string; userSalt: string },
71
+ password: string | undefined,
72
+ ): Promise<{ derivePassword?: string } | { error: Response }> => {
73
+ if (policy.mode === 'derive') return { derivePassword: password };
74
+ if (!password) return { error: c.json({ error: 'password required' }, 401) };
75
+ if (!(await policy.verifyPassword(who, password))) {
76
+ return { error: c.json({ error: 'invalid password' }, 401) };
77
+ }
78
+ return {}; // verified — standard (server-key) set
79
+ };
80
+
81
+ // POST /unlock { password? } → a token sealing the set key (deniable w/ password, else
82
+ // standard; under the gate policy the password is verified and the set is always standard).
83
+ app.post(`${base}/unlock`, async (c) => {
84
+ const who = await opts.authorize(c);
85
+ if (!who) return c.json({ error: 'unauthorized' }, 401);
86
+ const body = (await c.req.json().catch(() => ({}))) as { password?: string };
87
+ const resolved = await resolvePassword(c, who, body.password || undefined);
88
+ if ('error' in resolved) return resolved.error;
89
+ const { token, mode } = await unlock.unlock({
90
+ userId: who.userId,
91
+ userSalt: who.userSalt,
92
+ password: resolved.derivePassword,
93
+ });
94
+ return c.json({ token, mode });
95
+ });
96
+
97
+ // GET /index → clear, non-sensitive rows only (size + createdAt + status). Never title/thumb/mode.
98
+ app.get(`${base}/index`, async (c) => {
99
+ const who = await opts.authorize(c);
100
+ if (!who) return c.json({ error: 'unauthorized' }, 401);
101
+ return c.json({ items: (await index.list(who.userId)).map(toPublic) });
102
+ });
103
+
104
+ // Decrypt every descriptor a set key opens, silently skipping the rest (decrypt-success IS the
105
+ // membership test — deniability). Shared by the password and token reveal routes.
106
+ const revealWithKey = async (userId: string, setKey: Buffer) => {
107
+ const revealed: Array<{ id: string; meta: unknown }> = [];
108
+ for (const item of await index.list(userId)) {
109
+ const meta = openMeta(setKey, item.sealedMeta);
110
+ if (meta !== null) revealed.push({ id: item.id, meta });
111
+ }
112
+ return revealed;
113
+ };
114
+
115
+ // POST /reveal { password } → which items that password unlocks (GCM decrypt-success), with
116
+ // their now-decrypted metadata. Others stay opaque. This is the "try a password" cleanup UX.
117
+ // Under the gate policy the password must verify first and the standard set is revealed.
118
+ app.post(`${base}/reveal`, async (c) => {
119
+ const who = await opts.authorize(c);
120
+ if (!who) return c.json({ error: 'unauthorized' }, 401);
121
+ const body = (await c.req.json().catch(() => ({}))) as { password?: string };
122
+ const resolved = await resolvePassword(c, who, body.password || undefined);
123
+ if ('error' in resolved) return resolved.error;
124
+ const setKey = unlock.deriveSetKey({
125
+ userId: who.userId,
126
+ userSalt: who.userSalt,
127
+ password: resolved.derivePassword,
128
+ });
129
+ return c.json({ items: await revealWithKey(who.userId, setKey) });
130
+ });
131
+
132
+ // POST /reveal-unlocked { token } → the same reveal, keyed by an UNLOCK TOKEN instead of the
133
+ // password (the token seals the set key — same mechanism the content-key route uses). Lets an
134
+ // already-unlocked session list its items without re-typing the password. The token must belong
135
+ // to the session caller.
136
+ app.post(`${base}/reveal-unlocked`, async (c) => {
137
+ const who = await opts.authorize(c);
138
+ if (!who) return c.json({ error: 'unauthorized' }, 401);
139
+ const body = (await c.req.json().catch(() => ({}))) as { token?: string };
140
+ if (!body.token) return c.json({ error: 'token required' }, 400);
141
+ const grant = await unlock.verify(body.token);
142
+ if (!grant || grant.userId !== who.userId) return c.json({ error: 'invalid token' }, 401);
143
+ return c.json({ items: await revealWithKey(who.userId, grant.setKey) });
144
+ });
145
+
146
+ // GET /u/:token/hls/:source/:video/* → the encrypted HLS tree. The `enc.key` sub-path returns
147
+ // the unwrapped AES-128 content key (gated, no-store); every other path serves the encrypted
148
+ // segment/playlist (edge-cacheable — hls.js decrypts with the key).
149
+ app.get(`${base}/u/:token/hls/:source/:video/*`, async (c) => {
150
+ const raw = c.req.param('token');
151
+ if (!raw) return c.body(null, 401);
152
+ const grant = await unlock.verify(raw);
153
+ if (!grant) return c.body(null, 401);
154
+ const video = c.req.param('video');
155
+ const prefix = `${base}/u/${raw}/hls/${c.req.param('source')}/${video}/`;
156
+ const rel = c.req.path.slice(prefix.length);
157
+ if (!rel || rel.includes('..')) return c.body(null, 400);
158
+
159
+ if (rel.endsWith('enc.key')) {
160
+ const item = await index.get(grant.userId, video);
161
+ if (!item) return c.body(null, 404);
162
+ const content = unwrapContentKey(grant.setKey, item.wrappedContentKey);
163
+ // null ⇒ this token's set key doesn't own the item (wrong password / wrong mode).
164
+ if (!content) return c.body(null, 403);
165
+ // A fresh, tightly-sized ArrayBuffer copy of the 16-byte key — an unambiguous BodyInit
166
+ // (wrapping the Node Buffer directly can mis-size the response).
167
+ const keyBytes = Uint8Array.from(content.key);
168
+ return cors(
169
+ c,
170
+ new Response(keyBytes.buffer, {
171
+ status: 200,
172
+ headers: { 'Content-Type': 'application/octet-stream', 'Cache-Control': 'no-store' },
173
+ }),
174
+ );
175
+ }
176
+
177
+ const mime = rel.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t';
178
+ return cors(
179
+ c,
180
+ await serveRange(
181
+ store,
182
+ privateHlsFileKey(grant.userId, video, rel),
183
+ mime,
184
+ c.req.header('range'),
185
+ ),
186
+ );
187
+ });
188
+
189
+ // GET /u/:token/file/:id → a private file stored as one AES-128-CTR ciphertext blob at
190
+ // privateSourceKey. Decrypted server-side per request under the token's set key; the mime
191
+ // comes from the sealed descriptor, so a token whose set key can't open the item (wrong
192
+ // password / wrong mode) gets a 403 and never any bytes.
193
+ //
194
+ // RANGE SUPPORT (genapp-100): CTR is positionally keyed, so a byte range decrypts from the
195
+ // block-aligned ciphertext offset with the IV counter advanced (`cryptBlobRangeStream`). This
196
+ // is what makes a big private VIDEO scrub on a remote-store host, where no local ffmpeg means
197
+ // no HLS derivatives and the blob route is the only byte path. Plaintext is never stored.
198
+ app.get(`${base}/u/:token/file/:id`, async (c) => {
199
+ const raw = c.req.param('token');
200
+ if (!raw) return c.body(null, 401);
201
+ const grant = await unlock.verify(raw);
202
+ if (!grant) return c.body(null, 401);
203
+ const id = c.req.param('id');
204
+ const item = await index.get(grant.userId, id);
205
+ if (!item) return c.body(null, 404);
206
+ const content = unwrapContentKey(grant.setKey, item.wrappedContentKey);
207
+ if (!content) return c.body(null, 403);
208
+ const meta = openMeta<{ mime?: string }>(grant.setKey, item.sealedMeta);
209
+ const baseHeaders: Record<string, string> = {
210
+ 'Content-Type': meta?.mime || 'application/octet-stream',
211
+ 'Cache-Control': 'no-store',
212
+ 'Accept-Ranges': 'bytes',
213
+ };
214
+
215
+ const key = privateSourceKey(grant.userId, id);
216
+ const range = item.sizeBytes > 0 ? decodeRange(c.req.header('range'), item.sizeBytes) : { kind: 'full' as const };
217
+ if (range.kind === 'unsatisfiable') {
218
+ return cors(
219
+ c,
220
+ new Response(null, { status: 416, headers: { 'Content-Range': `bytes */${item.sizeBytes}` } }),
221
+ );
222
+ }
223
+ if (range.kind === 'partial') {
224
+ // Fetch ciphertext from the BLOCK-ALIGNED start (CTR block = 16 bytes); the range
225
+ // stream drops the intra-block lead-in so the client sees exactly [start, end].
226
+ const alignedStart = range.start - (range.start % 16);
227
+ const stored = await store.getRange(key, { start: alignedStart, end: range.end });
228
+ if (!stored) return c.body(null, 404);
229
+ return cors(
230
+ c,
231
+ new Response(cryptBlobRangeStream(content, stored.stream, range.start), {
232
+ status: 206,
233
+ headers: {
234
+ ...baseHeaders,
235
+ 'Content-Range': `bytes ${range.start}-${range.end}/${item.sizeBytes}`,
236
+ 'Content-Length': String(range.end - range.start + 1),
237
+ },
238
+ }),
239
+ );
240
+ }
241
+
242
+ const stored = await store.getRange(key);
243
+ if (!stored) return c.body(null, 404);
244
+ const headers = { ...baseHeaders };
245
+ if (item.sizeBytes > 0) headers['Content-Length'] = String(item.sizeBytes);
246
+ return cors(c, new Response(cryptBlobStream(content, stored.stream), { status: 200, headers }));
247
+ });
248
+
249
+ // Preflight for the cross-origin byte routes.
250
+ if (opts.cors) {
251
+ app.options(`${base}/u/*`, (c) => {
252
+ const acao = resolveAcao(opts.cors, c.req.header('origin'));
253
+ const headers: Record<string, string> = {
254
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
255
+ 'Access-Control-Allow-Headers': 'Range',
256
+ 'Access-Control-Max-Age': '86400',
257
+ };
258
+ if (acao) {
259
+ headers['Access-Control-Allow-Origin'] = acao;
260
+ headers.Vary = 'Origin';
261
+ }
262
+ return c.body(null, 204, headers);
263
+ });
264
+ }
265
+ }
@@ -0,0 +1,111 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
2
+ import { createDecipheriv } from 'node:crypto';
3
+ import { mkdtemp, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import path from 'node:path';
6
+ import { isFfmpegAvailable } from '../media-bun';
7
+ import { createDiskStore } from '../storage/diskStore';
8
+ import { privateHlsFileKey, privateHlsMasterKey } from '../storage/types';
9
+ import { deriveServerSetKey, unwrapContentKey } from './crypto';
10
+ import { processPrivateVideo } from './pipeline';
11
+
12
+ // Integration test — needs ffmpeg on PATH. When absent (CI image without it) the tests no-op so
13
+ // the suite still passes; the pipeline itself returns 'failed' when ffmpeg is missing (private
14
+ // media can't degrade to plaintext), which is the intended behavior.
15
+ describe('processPrivateVideo (encrypted HLS)', () => {
16
+ let root: string;
17
+ let work: string;
18
+ let clip: string;
19
+ let hasFfmpeg = false;
20
+
21
+ beforeAll(async () => {
22
+ hasFfmpeg = await isFfmpegAvailable();
23
+ root = await mkdtemp(path.join(tmpdir(), 'pm-store-'));
24
+ work = await mkdtemp(path.join(tmpdir(), 'pm-work-'));
25
+ if (!hasFfmpeg) return;
26
+ clip = path.join(work, 'src.mp4');
27
+ const proc = Bun.spawn(
28
+ [
29
+ 'ffmpeg',
30
+ '-y',
31
+ '-f',
32
+ 'lavfi',
33
+ '-i',
34
+ 'testsrc=size=854x480:rate=15:duration=3',
35
+ '-pix_fmt',
36
+ 'yuv420p',
37
+ '-c:v',
38
+ 'libx264',
39
+ '-preset',
40
+ 'ultrafast',
41
+ '-t',
42
+ '3',
43
+ clip,
44
+ ],
45
+ { stdout: 'ignore', stderr: 'ignore' },
46
+ );
47
+ await proc.exited;
48
+ });
49
+ afterAll(async () => {
50
+ await rm(root, { recursive: true, force: true });
51
+ await rm(work, { recursive: true, force: true });
52
+ });
53
+
54
+ test('encrypts HLS segments; the wrapped key unwraps and decrypts a segment to valid MPEG-TS', async () => {
55
+ if (!hasFfmpeg) {
56
+ console.warn('[pipeline.spec] ffmpeg absent — skipping encrypted-HLS integration test');
57
+ return;
58
+ }
59
+ const store = createDiskStore(root);
60
+ const setKey = deriveServerSetKey('server-secret', 'user-1');
61
+
62
+ const res = await processPrivateVideo({
63
+ store,
64
+ workDir: work,
65
+ owner: 't1',
66
+ id: 'v1',
67
+ sourcePath: clip,
68
+ setKey,
69
+ });
70
+
71
+ expect(res.status).toBe('ready');
72
+ expect(res.hasHls).toBe(true);
73
+ expect(res.wrappedContentKey).not.toBeNull();
74
+
75
+ // The master + a 480p variant playlist exist, and the playlist declares AES-128 encryption.
76
+ expect(await store.stat(privateHlsMasterKey('t1', 'v1'))).not.toBeNull();
77
+ const playlistBytes = await store.get(privateHlsFileKey('t1', 'v1', '480/playlist.m3u8'));
78
+ expect(playlistBytes).not.toBeNull();
79
+ const playlist = new TextDecoder().decode(playlistBytes as Uint8Array);
80
+ expect(playlist).toContain('#EXT-X-KEY:METHOD=AES-128');
81
+ expect(playlist).toContain('URI="enc.key"');
82
+
83
+ // The raw content key never leaves wrapped — but unwrapping with the set key recovers it,
84
+ // and it correctly AES-128-CBC-decrypts a real segment back to an MPEG-TS stream (0x47 sync).
85
+ const content = unwrapContentKey(setKey, res.wrappedContentKey as string);
86
+ expect(content).not.toBeNull();
87
+ expect(content?.key.length).toBe(16);
88
+
89
+ const seg = await store.get(privateHlsFileKey('t1', 'v1', '480/seg_00000.ts'));
90
+ expect(seg).not.toBeNull();
91
+ const d = createDecipheriv('aes-128-cbc', content?.key as Buffer, content?.iv as Buffer);
92
+ const plain = Buffer.concat([d.update(Buffer.from(seg as Uint8Array)), d.final()]);
93
+ expect(plain[0]).toBe(0x47); // MPEG-TS sync byte ⇒ decryption + key are correct
94
+ }, 30_000);
95
+
96
+ test('returns failed when ffmpeg is unavailable (private media cannot go plaintext)', async () => {
97
+ // Documents the contract; only meaningfully exercised on a machine without ffmpeg.
98
+ if (hasFfmpeg) return;
99
+ const store = createDiskStore(root);
100
+ const res = await processPrivateVideo({
101
+ store,
102
+ workDir: work,
103
+ owner: 't',
104
+ id: 'v',
105
+ sourcePath: '/nonexistent',
106
+ setKey: deriveServerSetKey('s', 'u'),
107
+ });
108
+ expect(res.status).toBe('failed');
109
+ expect(res.wrappedContentKey).toBeNull();
110
+ });
111
+ });
@@ -0,0 +1,170 @@
1
+ import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import type { ProcessingStatus } from 'cursedbelt-core/media';
4
+ import {
5
+ hlsLadderArgs,
6
+ isFfmpegAvailable,
7
+ LIBRARY_CRF,
8
+ probeVideoStream,
9
+ resolveLadder,
10
+ runFfmpegProgress,
11
+ } from '../media-bun';
12
+ import {
13
+ type MediaStore,
14
+ privateHlsFileKey,
15
+ privateHlsMasterKey,
16
+ privateSourceKey,
17
+ } from '../storage/types';
18
+ import { cryptBlobStream, generateContentKey, wrapContentKey } from './crypto';
19
+
20
+ // ── Encrypted-HLS pipeline (private media) ──────────────────────────────────────
21
+ // Transcodes a private upload into an AES-128-encrypted HLS ladder: ffmpeg encrypts every
22
+ // segment with a per-item content key (via a key-info file), so the segments are ciphertext at
23
+ // rest (Spotlight-safe, opaque names) AND safe to edge-cache — the player fetches the content
24
+ // key from the gated key route and decrypts client-side (fast). The plaintext source is staged
25
+ // only in the work dir and never persisted; only the encrypted segments + playlists are stored.
26
+ // The content key is returned WRAPPED under the caller's set key (server-held or password-derived).
27
+
28
+ const KEY_URI = 'enc.key'; // relative in the playlist → the gated key route serves it per-video
29
+
30
+ export type PrivateProcessResult = {
31
+ status: ProcessingStatus;
32
+ durationSeconds: number;
33
+ hasHls: boolean;
34
+ /** The AES-128 content key + IV, wrapped under the caller's set key. Null if processing failed. */
35
+ wrappedContentKey: string | null;
36
+ };
37
+
38
+ export type PrivateProcessOptions = {
39
+ store: MediaStore;
40
+ /** Scratch dir for ffmpeg I/O + the transient key file (cleaned up afterwards). */
41
+ workDir: string;
42
+ owner: string;
43
+ id: string;
44
+ /** Local PLAINTEXT source, staged by the caller; never persisted (deleted with the job dir). */
45
+ sourcePath: string;
46
+ /** The mode key (server-held or password-derived) the content key is wrapped under. */
47
+ setKey: Buffer;
48
+ ladderHeights?: number[];
49
+ onProgress?: (status: ProcessingStatus, pct: number) => void;
50
+ };
51
+
52
+ async function uploadTree(store: MediaStore, dir: string, owner: string, id: string) {
53
+ const walk = async (rel: string): Promise<void> => {
54
+ const entries = await readdir(path.join(dir, rel), { withFileTypes: true });
55
+ for (const e of entries) {
56
+ const childRel = rel ? path.join(rel, e.name) : e.name;
57
+ if (e.isDirectory()) {
58
+ await walk(childRel);
59
+ } else {
60
+ const mime = e.name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t';
61
+ const file = Bun.file(path.join(dir, childRel));
62
+ await store.putStream(privateHlsFileKey(owner, id, childRel), file.stream(), mime, {
63
+ contentLength: file.size,
64
+ });
65
+ }
66
+ }
67
+ };
68
+ await walk('');
69
+ }
70
+
71
+ export async function processPrivateVideo(
72
+ opts: PrivateProcessOptions,
73
+ ): Promise<PrivateProcessResult> {
74
+ const { store, workDir, owner, id, sourcePath, setKey, onProgress } = opts;
75
+ const report = (s: ProcessingStatus, p: number) => onProgress?.(s, p);
76
+ const failed: PrivateProcessResult = {
77
+ status: 'failed',
78
+ durationSeconds: 0,
79
+ hasHls: false,
80
+ wrappedContentKey: null,
81
+ };
82
+
83
+ // Private media MUST be encrypted — unlike public it can't degrade to a plaintext fallback.
84
+ if (!(await isFfmpegAvailable())) return failed;
85
+
86
+ const jobDir = path.join(workDir, owner, id);
87
+ await mkdir(jobDir, { recursive: true });
88
+
89
+ try {
90
+ report('processing', 1);
91
+ const probe = await probeVideoStream(sourcePath);
92
+
93
+ // Mint the AES-128 content key + write the ffmpeg key-info file (key URI / key file / IV).
94
+ const content = generateContentKey();
95
+ const keyFile = path.join(jobDir, 'enc.key.bin');
96
+ await writeFile(keyFile, content.key);
97
+ const keyInfoFile = path.join(jobDir, 'enc.keyinfo');
98
+ await writeFile(keyInfoFile, `${KEY_URI}\n${keyFile}\n${content.iv.toString('hex')}\n`);
99
+
100
+ const heights = resolveLadder(opts.ladderHeights ?? [480, 720, 1080], probe.height);
101
+ const hlsDir = path.join(jobDir, 'hls');
102
+ await mkdir(hlsDir, { recursive: true });
103
+
104
+ const ok = await runFfmpegProgress(
105
+ hlsLadderArgs(sourcePath, hlsDir, {
106
+ heights,
107
+ crf: Number(LIBRARY_CRF),
108
+ hasAudio: probe.hasAudio,
109
+ keyInfoFile,
110
+ }),
111
+ probe.duration,
112
+ (pct) => report('processing', Math.max(1, Math.min(98, pct))),
113
+ );
114
+ if (!ok) return failed;
115
+
116
+ // Upload ONLY the encrypted segments + playlists — never the key file (served via the route).
117
+ await uploadTree(store, hlsDir, owner, id);
118
+
119
+ report('ready', 100);
120
+ return {
121
+ status: 'ready',
122
+ durationSeconds: probe.duration,
123
+ hasHls: (await store.stat(privateHlsMasterKey(owner, id))) !== null,
124
+ wrappedContentKey: wrapContentKey(setKey, content),
125
+ };
126
+ } catch {
127
+ report('failed', 0);
128
+ return failed;
129
+ } finally {
130
+ // Wipe the job dir — the plaintext source, the raw key file, and the outputs all vanish.
131
+ await rm(jobDir, { recursive: true, force: true }).catch(() => {});
132
+ }
133
+ }
134
+
135
+ // ── Encrypted-blob pipeline (private NON-video files: photos, docs, audio) ────────
136
+ // A non-video private upload has no HLS ladder to build — it becomes ONE AES-128-CTR ciphertext
137
+ // blob at privateSourceKey, under the same per-item content-key material the HLS path wraps.
138
+ // Serving is the gated blob route in mountPrivateRoutes (decrypts server-side per request); the
139
+ // plaintext staging file is the caller's to wipe, mirroring processPrivateVideo.
140
+
141
+ export type PrivateBlobResult = {
142
+ status: ProcessingStatus;
143
+ sizeBytes: number;
144
+ /** The content key + IV, wrapped under the caller's set key. Null if processing failed. */
145
+ wrappedContentKey: string | null;
146
+ };
147
+
148
+ export async function processPrivateBlob(opts: {
149
+ store: MediaStore;
150
+ owner: string;
151
+ id: string;
152
+ /** Local PLAINTEXT source, staged by the caller; never persisted (caller deletes it). */
153
+ sourcePath: string;
154
+ setKey: Buffer;
155
+ }): Promise<PrivateBlobResult> {
156
+ try {
157
+ const file = Bun.file(opts.sourcePath);
158
+ const sizeBytes = file.size; // CTR is length-preserving, so plaintext size == stored size
159
+ const content = generateContentKey();
160
+ await opts.store.putStream(
161
+ privateSourceKey(opts.owner, opts.id),
162
+ cryptBlobStream(content, file.stream()),
163
+ 'application/octet-stream',
164
+ { contentLength: sizeBytes },
165
+ );
166
+ return { status: 'ready', sizeBytes, wrappedContentKey: wrapContentKey(opts.setKey, content) };
167
+ } catch {
168
+ return { status: 'failed', sizeBytes: 0, wrappedContentKey: null };
169
+ }
170
+ }