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,31 @@
1
+ import type { ProcessingStatus } from 'cursedbelt-core/media';
2
+ import type { KeyMode } from './crypto';
3
+ export type PrivateItem = {
4
+ id: string;
5
+ owner: string;
6
+ sizeBytes: number;
7
+ createdAt: number;
8
+ status: ProcessingStatus;
9
+ keyMode: KeyMode;
10
+ /** AES-128 content key + IV, wrapped under the item's set key. */
11
+ wrappedContentKey: string;
12
+ /** AES-GCM envelope of { title, notes, tags, durationSeconds, … } under the item's set key. */
13
+ sealedMeta: string;
14
+ };
15
+ /** What the index endpoint exposes — clear, non-sensitive facts only. No title/thumb/notes/tags,
16
+ * no key mode, no wrapped key. */
17
+ export type PrivateIndexRow = {
18
+ id: string;
19
+ sizeBytes: number;
20
+ createdAt: number;
21
+ status: ProcessingStatus;
22
+ };
23
+ export declare const toPublic: (item: PrivateItem) => PrivateIndexRow;
24
+ export interface PrivateIndex {
25
+ add(item: PrivateItem): Promise<void>;
26
+ get(owner: string, id: string): Promise<PrivateItem | null>;
27
+ list(owner: string): Promise<PrivateItem[]>;
28
+ update(owner: string, id: string, patch: Partial<Omit<PrivateItem, 'id' | 'owner'>>): Promise<void>;
29
+ remove(owner: string, id: string): Promise<void>;
30
+ }
31
+ export declare function createMemoryPrivateIndex(): PrivateIndex;
@@ -0,0 +1,36 @@
1
+ export const toPublic = (item) => ({
2
+ id: item.id,
3
+ sizeBytes: item.sizeBytes,
4
+ createdAt: item.createdAt,
5
+ status: item.status,
6
+ });
7
+ export function createMemoryPrivateIndex() {
8
+ const byOwner = new Map();
9
+ const bucket = (owner) => {
10
+ let m = byOwner.get(owner);
11
+ if (!m) {
12
+ m = new Map();
13
+ byOwner.set(owner, m);
14
+ }
15
+ return m;
16
+ };
17
+ return {
18
+ async add(item) {
19
+ bucket(item.owner).set(item.id, item);
20
+ },
21
+ async get(owner, id) {
22
+ return bucket(owner).get(id) ?? null;
23
+ },
24
+ async list(owner) {
25
+ return [...bucket(owner).values()].sort((a, b) => b.createdAt - a.createdAt);
26
+ },
27
+ async update(owner, id, patch) {
28
+ const item = bucket(owner).get(id);
29
+ if (item)
30
+ bucket(owner).set(id, { ...item, ...patch });
31
+ },
32
+ async remove(owner, id) {
33
+ bucket(owner).delete(id);
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,26 @@
1
+ import type { Context, Hono } from 'hono';
2
+ import { type MediaStore } from '../storage/types';
3
+ import type { PrivateIndex } from './index-store';
4
+ import type { UnlockPasswordPolicy, UnlockService } from './unlock';
5
+ /** Access-Control-Allow-Origin resolver for split-origin private streaming (media on another
6
+ * origin than the SPA). Same shape as the storage layer's. */
7
+ export type CorsResolver = string | string[] | ((requestOrigin: string | undefined) => string | null | undefined);
8
+ export type MountPrivateOptions = {
9
+ store: MediaStore;
10
+ index: PrivateIndex;
11
+ unlock: UnlockService;
12
+ /** Path prefix (default '/_private'). */
13
+ basePath?: string;
14
+ /** App session auth → the caller's id + their per-user salt (created lazily, stable). Return
15
+ * null to reject. Gates unlock/index/reveal (the byte routes use the path token instead). */
16
+ authorize: (c: Context) => Promise<{
17
+ userId: string;
18
+ userSalt: string;
19
+ } | null>;
20
+ /** What a supplied password means (default: derive — deniable sets). `gate` verifies it
21
+ * against the app's accounts and mints the recoverable server-key grant instead. */
22
+ passwordPolicy?: UnlockPasswordPolicy;
23
+ /** Cross-origin access for split-origin deploys. */
24
+ cors?: CorsResolver;
25
+ };
26
+ export declare function mountPrivateRoutes(app: Hono, opts: MountPrivateOptions): void;
@@ -0,0 +1,219 @@
1
+ import { decodeRange, serveRange } from '../storage/streamRange';
2
+ import { privateHlsFileKey, privateSourceKey } from '../storage/types';
3
+ import { cryptBlobRangeStream, cryptBlobStream, openMeta, unwrapContentKey } from './crypto';
4
+ import { toPublic } from './index-store';
5
+ function resolveAcao(cors, reqOrigin) {
6
+ if (!cors)
7
+ return null;
8
+ if (typeof cors === 'string')
9
+ return cors;
10
+ if (Array.isArray(cors))
11
+ return reqOrigin && cors.includes(reqOrigin) ? reqOrigin : null;
12
+ return cors(reqOrigin) ?? null;
13
+ }
14
+ export function mountPrivateRoutes(app, opts) {
15
+ const base = opts.basePath ?? '/_private';
16
+ const { store, index, unlock } = opts;
17
+ const cors = (c, res) => {
18
+ const acao = resolveAcao(opts.cors, c.req.header('origin'));
19
+ if (acao) {
20
+ res.headers.set('Access-Control-Allow-Origin', acao);
21
+ res.headers.set('Access-Control-Expose-Headers', 'Content-Range, Content-Length, Accept-Ranges');
22
+ res.headers.append('Vary', 'Origin');
23
+ }
24
+ return res;
25
+ };
26
+ const policy = opts.passwordPolicy ?? { mode: 'derive' };
27
+ /** Resolve what the supplied password contributes under the active policy:
28
+ * derive ⇒ it feeds key derivation (may be undefined ⇒ standard set);
29
+ * gate ⇒ it must be present AND verify against the app's accounts, then the
30
+ * set key is the recoverable server one (password never feeds derivation).
31
+ * Returns an error Response to short-circuit with, or the derivation input. */
32
+ const resolvePassword = async (c, who, password) => {
33
+ if (policy.mode === 'derive')
34
+ return { derivePassword: password };
35
+ if (!password)
36
+ return { error: c.json({ error: 'password required' }, 401) };
37
+ if (!(await policy.verifyPassword(who, password))) {
38
+ return { error: c.json({ error: 'invalid password' }, 401) };
39
+ }
40
+ return {}; // verified — standard (server-key) set
41
+ };
42
+ // POST /unlock { password? } → a token sealing the set key (deniable w/ password, else
43
+ // standard; under the gate policy the password is verified and the set is always standard).
44
+ app.post(`${base}/unlock`, async (c) => {
45
+ const who = await opts.authorize(c);
46
+ if (!who)
47
+ return c.json({ error: 'unauthorized' }, 401);
48
+ const body = (await c.req.json().catch(() => ({})));
49
+ const resolved = await resolvePassword(c, who, body.password || undefined);
50
+ if ('error' in resolved)
51
+ return resolved.error;
52
+ const { token, mode } = await unlock.unlock({
53
+ userId: who.userId,
54
+ userSalt: who.userSalt,
55
+ password: resolved.derivePassword,
56
+ });
57
+ return c.json({ token, mode });
58
+ });
59
+ // GET /index → clear, non-sensitive rows only (size + createdAt + status). Never title/thumb/mode.
60
+ app.get(`${base}/index`, async (c) => {
61
+ const who = await opts.authorize(c);
62
+ if (!who)
63
+ return c.json({ error: 'unauthorized' }, 401);
64
+ return c.json({ items: (await index.list(who.userId)).map(toPublic) });
65
+ });
66
+ // Decrypt every descriptor a set key opens, silently skipping the rest (decrypt-success IS the
67
+ // membership test — deniability). Shared by the password and token reveal routes.
68
+ const revealWithKey = async (userId, setKey) => {
69
+ const revealed = [];
70
+ for (const item of await index.list(userId)) {
71
+ const meta = openMeta(setKey, item.sealedMeta);
72
+ if (meta !== null)
73
+ revealed.push({ id: item.id, meta });
74
+ }
75
+ return revealed;
76
+ };
77
+ // POST /reveal { password } → which items that password unlocks (GCM decrypt-success), with
78
+ // their now-decrypted metadata. Others stay opaque. This is the "try a password" cleanup UX.
79
+ // Under the gate policy the password must verify first and the standard set is revealed.
80
+ app.post(`${base}/reveal`, async (c) => {
81
+ const who = await opts.authorize(c);
82
+ if (!who)
83
+ return c.json({ error: 'unauthorized' }, 401);
84
+ const body = (await c.req.json().catch(() => ({})));
85
+ const resolved = await resolvePassword(c, who, body.password || undefined);
86
+ if ('error' in resolved)
87
+ return resolved.error;
88
+ const setKey = unlock.deriveSetKey({
89
+ userId: who.userId,
90
+ userSalt: who.userSalt,
91
+ password: resolved.derivePassword,
92
+ });
93
+ return c.json({ items: await revealWithKey(who.userId, setKey) });
94
+ });
95
+ // POST /reveal-unlocked { token } → the same reveal, keyed by an UNLOCK TOKEN instead of the
96
+ // password (the token seals the set key — same mechanism the content-key route uses). Lets an
97
+ // already-unlocked session list its items without re-typing the password. The token must belong
98
+ // to the session caller.
99
+ app.post(`${base}/reveal-unlocked`, async (c) => {
100
+ const who = await opts.authorize(c);
101
+ if (!who)
102
+ return c.json({ error: 'unauthorized' }, 401);
103
+ const body = (await c.req.json().catch(() => ({})));
104
+ if (!body.token)
105
+ return c.json({ error: 'token required' }, 400);
106
+ const grant = await unlock.verify(body.token);
107
+ if (!grant || grant.userId !== who.userId)
108
+ return c.json({ error: 'invalid token' }, 401);
109
+ return c.json({ items: await revealWithKey(who.userId, grant.setKey) });
110
+ });
111
+ // GET /u/:token/hls/:source/:video/* → the encrypted HLS tree. The `enc.key` sub-path returns
112
+ // the unwrapped AES-128 content key (gated, no-store); every other path serves the encrypted
113
+ // segment/playlist (edge-cacheable — hls.js decrypts with the key).
114
+ app.get(`${base}/u/:token/hls/:source/:video/*`, async (c) => {
115
+ const raw = c.req.param('token');
116
+ if (!raw)
117
+ return c.body(null, 401);
118
+ const grant = await unlock.verify(raw);
119
+ if (!grant)
120
+ return c.body(null, 401);
121
+ const video = c.req.param('video');
122
+ const prefix = `${base}/u/${raw}/hls/${c.req.param('source')}/${video}/`;
123
+ const rel = c.req.path.slice(prefix.length);
124
+ if (!rel || rel.includes('..'))
125
+ return c.body(null, 400);
126
+ if (rel.endsWith('enc.key')) {
127
+ const item = await index.get(grant.userId, video);
128
+ if (!item)
129
+ return c.body(null, 404);
130
+ const content = unwrapContentKey(grant.setKey, item.wrappedContentKey);
131
+ // null ⇒ this token's set key doesn't own the item (wrong password / wrong mode).
132
+ if (!content)
133
+ return c.body(null, 403);
134
+ // A fresh, tightly-sized ArrayBuffer copy of the 16-byte key — an unambiguous BodyInit
135
+ // (wrapping the Node Buffer directly can mis-size the response).
136
+ const keyBytes = Uint8Array.from(content.key);
137
+ return cors(c, new Response(keyBytes.buffer, {
138
+ status: 200,
139
+ headers: { 'Content-Type': 'application/octet-stream', 'Cache-Control': 'no-store' },
140
+ }));
141
+ }
142
+ const mime = rel.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t';
143
+ return cors(c, await serveRange(store, privateHlsFileKey(grant.userId, video, rel), mime, c.req.header('range')));
144
+ });
145
+ // GET /u/:token/file/:id → a private file stored as one AES-128-CTR ciphertext blob at
146
+ // privateSourceKey. Decrypted server-side per request under the token's set key; the mime
147
+ // comes from the sealed descriptor, so a token whose set key can't open the item (wrong
148
+ // password / wrong mode) gets a 403 and never any bytes.
149
+ //
150
+ // RANGE SUPPORT (genapp-100): CTR is positionally keyed, so a byte range decrypts from the
151
+ // block-aligned ciphertext offset with the IV counter advanced (`cryptBlobRangeStream`). This
152
+ // is what makes a big private VIDEO scrub on a remote-store host, where no local ffmpeg means
153
+ // no HLS derivatives and the blob route is the only byte path. Plaintext is never stored.
154
+ app.get(`${base}/u/:token/file/:id`, async (c) => {
155
+ const raw = c.req.param('token');
156
+ if (!raw)
157
+ return c.body(null, 401);
158
+ const grant = await unlock.verify(raw);
159
+ if (!grant)
160
+ return c.body(null, 401);
161
+ const id = c.req.param('id');
162
+ const item = await index.get(grant.userId, id);
163
+ if (!item)
164
+ return c.body(null, 404);
165
+ const content = unwrapContentKey(grant.setKey, item.wrappedContentKey);
166
+ if (!content)
167
+ return c.body(null, 403);
168
+ const meta = openMeta(grant.setKey, item.sealedMeta);
169
+ const baseHeaders = {
170
+ 'Content-Type': meta?.mime || 'application/octet-stream',
171
+ 'Cache-Control': 'no-store',
172
+ 'Accept-Ranges': 'bytes',
173
+ };
174
+ const key = privateSourceKey(grant.userId, id);
175
+ const range = item.sizeBytes > 0 ? decodeRange(c.req.header('range'), item.sizeBytes) : { kind: 'full' };
176
+ if (range.kind === 'unsatisfiable') {
177
+ return cors(c, new Response(null, { status: 416, headers: { 'Content-Range': `bytes */${item.sizeBytes}` } }));
178
+ }
179
+ if (range.kind === 'partial') {
180
+ // Fetch ciphertext from the BLOCK-ALIGNED start (CTR block = 16 bytes); the range
181
+ // stream drops the intra-block lead-in so the client sees exactly [start, end].
182
+ const alignedStart = range.start - (range.start % 16);
183
+ const stored = await store.getRange(key, { start: alignedStart, end: range.end });
184
+ if (!stored)
185
+ return c.body(null, 404);
186
+ return cors(c, new Response(cryptBlobRangeStream(content, stored.stream, range.start), {
187
+ status: 206,
188
+ headers: {
189
+ ...baseHeaders,
190
+ 'Content-Range': `bytes ${range.start}-${range.end}/${item.sizeBytes}`,
191
+ 'Content-Length': String(range.end - range.start + 1),
192
+ },
193
+ }));
194
+ }
195
+ const stored = await store.getRange(key);
196
+ if (!stored)
197
+ return c.body(null, 404);
198
+ const headers = { ...baseHeaders };
199
+ if (item.sizeBytes > 0)
200
+ headers['Content-Length'] = String(item.sizeBytes);
201
+ return cors(c, new Response(cryptBlobStream(content, stored.stream), { status: 200, headers }));
202
+ });
203
+ // Preflight for the cross-origin byte routes.
204
+ if (opts.cors) {
205
+ app.options(`${base}/u/*`, (c) => {
206
+ const acao = resolveAcao(opts.cors, c.req.header('origin'));
207
+ const headers = {
208
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
209
+ 'Access-Control-Allow-Headers': 'Range',
210
+ 'Access-Control-Max-Age': '86400',
211
+ };
212
+ if (acao) {
213
+ headers['Access-Control-Allow-Origin'] = acao;
214
+ headers.Vary = 'Origin';
215
+ }
216
+ return c.body(null, 204, headers);
217
+ });
218
+ }
219
+ }
@@ -0,0 +1,37 @@
1
+ import type { ProcessingStatus } from 'cursedbelt-core/media';
2
+ import { type MediaStore } from '../storage/types';
3
+ export type PrivateProcessResult = {
4
+ status: ProcessingStatus;
5
+ durationSeconds: number;
6
+ hasHls: boolean;
7
+ /** The AES-128 content key + IV, wrapped under the caller's set key. Null if processing failed. */
8
+ wrappedContentKey: string | null;
9
+ };
10
+ export type PrivateProcessOptions = {
11
+ store: MediaStore;
12
+ /** Scratch dir for ffmpeg I/O + the transient key file (cleaned up afterwards). */
13
+ workDir: string;
14
+ owner: string;
15
+ id: string;
16
+ /** Local PLAINTEXT source, staged by the caller; never persisted (deleted with the job dir). */
17
+ sourcePath: string;
18
+ /** The mode key (server-held or password-derived) the content key is wrapped under. */
19
+ setKey: Buffer;
20
+ ladderHeights?: number[];
21
+ onProgress?: (status: ProcessingStatus, pct: number) => void;
22
+ };
23
+ export declare function processPrivateVideo(opts: PrivateProcessOptions): Promise<PrivateProcessResult>;
24
+ export type PrivateBlobResult = {
25
+ status: ProcessingStatus;
26
+ sizeBytes: number;
27
+ /** The content key + IV, wrapped under the caller's set key. Null if processing failed. */
28
+ wrappedContentKey: string | null;
29
+ };
30
+ export declare function processPrivateBlob(opts: {
31
+ store: MediaStore;
32
+ owner: string;
33
+ id: string;
34
+ /** Local PLAINTEXT source, staged by the caller; never persisted (caller deletes it). */
35
+ sourcePath: string;
36
+ setKey: Buffer;
37
+ }): Promise<PrivateBlobResult>;
@@ -0,0 +1,97 @@
1
+ import { mkdir, readdir, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { hlsLadderArgs, isFfmpegAvailable, LIBRARY_CRF, probeVideoStream, resolveLadder, runFfmpegProgress, } from '../media-bun';
4
+ import { privateHlsFileKey, privateHlsMasterKey, privateSourceKey, } from '../storage/types';
5
+ import { cryptBlobStream, generateContentKey, wrapContentKey } from './crypto';
6
+ // ── Encrypted-HLS pipeline (private media) ──────────────────────────────────────
7
+ // Transcodes a private upload into an AES-128-encrypted HLS ladder: ffmpeg encrypts every
8
+ // segment with a per-item content key (via a key-info file), so the segments are ciphertext at
9
+ // rest (Spotlight-safe, opaque names) AND safe to edge-cache — the player fetches the content
10
+ // key from the gated key route and decrypts client-side (fast). The plaintext source is staged
11
+ // only in the work dir and never persisted; only the encrypted segments + playlists are stored.
12
+ // The content key is returned WRAPPED under the caller's set key (server-held or password-derived).
13
+ const KEY_URI = 'enc.key'; // relative in the playlist → the gated key route serves it per-video
14
+ async function uploadTree(store, dir, owner, id) {
15
+ const walk = async (rel) => {
16
+ const entries = await readdir(path.join(dir, rel), { withFileTypes: true });
17
+ for (const e of entries) {
18
+ const childRel = rel ? path.join(rel, e.name) : e.name;
19
+ if (e.isDirectory()) {
20
+ await walk(childRel);
21
+ }
22
+ else {
23
+ const mime = e.name.endsWith('.m3u8') ? 'application/vnd.apple.mpegurl' : 'video/mp2t';
24
+ const file = Bun.file(path.join(dir, childRel));
25
+ await store.putStream(privateHlsFileKey(owner, id, childRel), file.stream(), mime, {
26
+ contentLength: file.size,
27
+ });
28
+ }
29
+ }
30
+ };
31
+ await walk('');
32
+ }
33
+ export async function processPrivateVideo(opts) {
34
+ const { store, workDir, owner, id, sourcePath, setKey, onProgress } = opts;
35
+ const report = (s, p) => onProgress?.(s, p);
36
+ const failed = {
37
+ status: 'failed',
38
+ durationSeconds: 0,
39
+ hasHls: false,
40
+ wrappedContentKey: null,
41
+ };
42
+ // Private media MUST be encrypted — unlike public it can't degrade to a plaintext fallback.
43
+ if (!(await isFfmpegAvailable()))
44
+ return failed;
45
+ const jobDir = path.join(workDir, owner, id);
46
+ await mkdir(jobDir, { recursive: true });
47
+ try {
48
+ report('processing', 1);
49
+ const probe = await probeVideoStream(sourcePath);
50
+ // Mint the AES-128 content key + write the ffmpeg key-info file (key URI / key file / IV).
51
+ const content = generateContentKey();
52
+ const keyFile = path.join(jobDir, 'enc.key.bin');
53
+ await writeFile(keyFile, content.key);
54
+ const keyInfoFile = path.join(jobDir, 'enc.keyinfo');
55
+ await writeFile(keyInfoFile, `${KEY_URI}\n${keyFile}\n${content.iv.toString('hex')}\n`);
56
+ const heights = resolveLadder(opts.ladderHeights ?? [480, 720, 1080], probe.height);
57
+ const hlsDir = path.join(jobDir, 'hls');
58
+ await mkdir(hlsDir, { recursive: true });
59
+ const ok = await runFfmpegProgress(hlsLadderArgs(sourcePath, hlsDir, {
60
+ heights,
61
+ crf: Number(LIBRARY_CRF),
62
+ hasAudio: probe.hasAudio,
63
+ keyInfoFile,
64
+ }), probe.duration, (pct) => report('processing', Math.max(1, Math.min(98, pct))));
65
+ if (!ok)
66
+ return failed;
67
+ // Upload ONLY the encrypted segments + playlists — never the key file (served via the route).
68
+ await uploadTree(store, hlsDir, owner, id);
69
+ report('ready', 100);
70
+ return {
71
+ status: 'ready',
72
+ durationSeconds: probe.duration,
73
+ hasHls: (await store.stat(privateHlsMasterKey(owner, id))) !== null,
74
+ wrappedContentKey: wrapContentKey(setKey, content),
75
+ };
76
+ }
77
+ catch {
78
+ report('failed', 0);
79
+ return failed;
80
+ }
81
+ finally {
82
+ // Wipe the job dir — the plaintext source, the raw key file, and the outputs all vanish.
83
+ await rm(jobDir, { recursive: true, force: true }).catch(() => { });
84
+ }
85
+ }
86
+ export async function processPrivateBlob(opts) {
87
+ try {
88
+ const file = Bun.file(opts.sourcePath);
89
+ const sizeBytes = file.size; // CTR is length-preserving, so plaintext size == stored size
90
+ const content = generateContentKey();
91
+ await opts.store.putStream(privateSourceKey(opts.owner, opts.id), cryptBlobStream(content, file.stream()), 'application/octet-stream', { contentLength: sizeBytes });
92
+ return { status: 'ready', sizeBytes, wrappedContentKey: wrapContentKey(opts.setKey, content) };
93
+ }
94
+ catch {
95
+ return { status: 'failed', sizeBytes: 0, wrappedContentKey: null };
96
+ }
97
+ }
@@ -0,0 +1,52 @@
1
+ import { type KeyMode } from './crypto';
2
+ export type UnlockGrant = {
3
+ userId: string;
4
+ setKey: Buffer;
5
+ mode: KeyMode;
6
+ };
7
+ /**
8
+ * What a supplied password MEANS at the unlock/reveal/private-upload surfaces:
9
+ * • derive (default) — the password derives the set key (deniable sets; the
10
+ * historical behavior).
11
+ * • gate — the password is an ACCESS CHECK against the app's own accounts
12
+ * (verifyPassword, e.g. the login store's argon2id verify) and the set key
13
+ * stays the recoverable server-derived one. Password changes never orphan
14
+ * content, and a password is REQUIRED to unlock (no silent session unlock).
15
+ */
16
+ export type UnlockPasswordPolicy = {
17
+ mode: 'derive';
18
+ } | {
19
+ mode: 'gate';
20
+ verifyPassword: (who: {
21
+ userId: string;
22
+ userSalt: string;
23
+ }, password: string) => Promise<boolean>;
24
+ };
25
+ export interface UnlockService {
26
+ /** Mint an unlock token. Pass `password` for a deniable set; omit for the standard server-key
27
+ * set (the caller must already have authenticated the user). */
28
+ unlock(args: {
29
+ userId: string;
30
+ userSalt: string;
31
+ password?: string;
32
+ }): Promise<{
33
+ token: string;
34
+ mode: KeyMode;
35
+ }>;
36
+ /** Verify a token and recover the sealed set key, or null if invalid/expired/tampered. */
37
+ verify(token: string): Promise<UnlockGrant | null>;
38
+ /** Derive a set key without minting a token (for the reveal endpoint's per-item trials). */
39
+ deriveSetKey(args: {
40
+ userId: string;
41
+ userSalt: string;
42
+ password?: string;
43
+ }): Buffer;
44
+ }
45
+ export declare function createUnlockService(opts: {
46
+ /** Derives the recoverable (standard) set key. */
47
+ serverSecret: string;
48
+ /** Signs the unlock JWT AND (domain-separated) seals the set key inside it. */
49
+ unlockSecret: string;
50
+ /** Token lifetime — long enough to survive a paused-and-resumed private video. Default 6h. */
51
+ ttlSeconds?: number;
52
+ }): UnlockService;
@@ -0,0 +1,55 @@
1
+ import { decryptSecret, deriveKey, encryptSecret } from 'cwip/node';
2
+ // 🔴 `../auth/jwt`, NOT `../auth` — same reason as `storage/streamToken.ts`, and this is the
3
+ // SECOND edge of the pair: `cursedbelt-server/storage` reaches private-media, so the barrel
4
+ // import here put `otplib` (via `auth/index` → `totp.ts`) back into the storage graph after
5
+ // streamToken.ts had been fixed. Found by bundling the graph and reading the externals, not by
6
+ // grep — a two-hop edge is invisible to the obvious search.
7
+ import { signToken, verifyPurposeToken } from '../auth/jwt';
8
+ import { derivePasswordSetKey, deriveServerSetKey } from './crypto';
9
+ // ── Unlock service ──────────────────────────────────────────────────────────────
10
+ // Turns a private-mode unlock into a short-lived token that CARRIES the set key so later
11
+ // requests (the HLS content-key route, reveal) can unwrap without re-deriving. Two modes:
12
+ // • deniable — password given → setKey = scrypt(password, userSalt); the server holds no
13
+ // copy, so the token is the ONLY way back to that set key until it expires.
14
+ // • standard — no password → setKey = scrypt(serverSecret, userId); recoverable, session-gated.
15
+ // The set key is SEALED inside the token (AES-GCM under a server seal key), so the client can't
16
+ // read it out; only this server can. The JWT is signed via cursedbelt's one jose integration
17
+ // under the existing 'pm-unlock' purpose, so it can't be replayed as an access token.
18
+ const PURPOSE = 'pm-unlock';
19
+ export function createUnlockService(opts) {
20
+ const ttl = opts.ttlSeconds ?? 6 * 60 * 60;
21
+ const sealKey = deriveKey(opts.unlockSecret, 'pm-unlock-seal');
22
+ const deriveSetKey = ({ userId, userSalt, password, }) => password
23
+ ? derivePasswordSetKey(password, userSalt)
24
+ : deriveServerSetKey(opts.serverSecret, userId);
25
+ return {
26
+ deriveSetKey,
27
+ async unlock(args) {
28
+ const mode = args.password ? 'deniable' : 'standard';
29
+ const setKey = deriveSetKey(args);
30
+ const token = await signToken({
31
+ userId: args.userId,
32
+ sessionId: 'pm',
33
+ roles: [],
34
+ purpose: PURPOSE,
35
+ sk: encryptSecret(setKey.toString('base64'), sealKey),
36
+ m: mode,
37
+ }, opts.unlockSecret, ttl);
38
+ return { token, mode };
39
+ },
40
+ async verify(token) {
41
+ try {
42
+ const p = await verifyPurposeToken(token, opts.unlockSecret, PURPOSE);
43
+ const sealed = p.sk;
44
+ const mode = p.m;
45
+ if (typeof sealed !== 'string' || (mode !== 'standard' && mode !== 'deniable'))
46
+ return null;
47
+ const setKey = Buffer.from(decryptSecret(sealed, sealKey), 'base64');
48
+ return { userId: p.userId, setKey, mode };
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,2 @@
1
+ export { createSqliteResumeStore, MAX_LOCATION_LENGTH, type ResumeRecord, type ResumeStore, type ResumeStoreOptions, } from './store';
2
+ export { mountResumeRoutes, type ResumeRoutesOptions } from './routes';
@@ -0,0 +1,2 @@
1
+ export { createSqliteResumeStore, MAX_LOCATION_LENGTH, } from './store';
2
+ export { mountResumeRoutes } from './routes';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The three routes behind `useResume`:
3
+ *
4
+ * GET {base}?key=default → { location, updatedAt } | { location: null }
5
+ * PUT {base}?key=default → { ok: true } body { location }
6
+ * DELETE {base}?key=default → { ok: true }
7
+ *
8
+ * ── Caching, decided here rather than left to a proxy ───────────────────────
9
+ * Every response is `no-store, private`. The body is one person's last screen —
10
+ * it is per-user, it changes on every navigation, and a shared cache holding it
11
+ * would hand one reader another reader's place. This is the ONE endpoint the
12
+ * deep-link work adds; the deep links themselves are same-document hash URLs and
13
+ * change nothing about how the app shell is cached.
14
+ *
15
+ * Authorization is the app's, passed in: every satellite already knows how to
16
+ * turn a cookie into a user id, and a resume row is exactly as sensitive as the
17
+ * screen it names.
18
+ */
19
+ import type { Context, Hono } from 'hono';
20
+ import { type ResumeStore } from './store';
21
+ export interface ResumeRoutesOptions {
22
+ store: ResumeStore;
23
+ /** Resolve the caller to a user id, or null to refuse. */
24
+ authorize(c: Context): string | null | Promise<string | null>;
25
+ /** Default `/api/resume`. */
26
+ basePath?: string;
27
+ }
28
+ /**
29
+ * What a `?key=` may look like. 🔴 EXPORTED because a client that builds a key
30
+ * out of app data (`useTableViewMode`'s `table-view.<id>`) has no other way to
31
+ * know what this route will accept — and the failure is SILENT: the client
32
+ * swallows every error into "nothing stored", so a rejected key looks exactly
33
+ * like a preference that was never saved. `useTableViewMode.spec.tsx` asserts
34
+ * the keys that hook produces against this regex, so changing either end turns
35
+ * a test red instead of quietly un-remembering everybody's view.
36
+ *
37
+ * Note there is no `:` in the class, which is the separator a key builder
38
+ * reaches for first.
39
+ */
40
+ export declare const RESUME_KEY_PATTERN: RegExp;
41
+ export declare function mountResumeRoutes(app: Hono, options: ResumeRoutesOptions): void;
@@ -0,0 +1,50 @@
1
+ import { MAX_LOCATION_LENGTH } from './store';
2
+ const NO_STORE = { 'cache-control': 'no-store, private' };
3
+ /**
4
+ * What a `?key=` may look like. 🔴 EXPORTED because a client that builds a key
5
+ * out of app data (`useTableViewMode`'s `table-view.<id>`) has no other way to
6
+ * know what this route will accept — and the failure is SILENT: the client
7
+ * swallows every error into "nothing stored", so a rejected key looks exactly
8
+ * like a preference that was never saved. `useTableViewMode.spec.tsx` asserts
9
+ * the keys that hook produces against this regex, so changing either end turns
10
+ * a test red instead of quietly un-remembering everybody's view.
11
+ *
12
+ * Note there is no `:` in the class, which is the separator a key builder
13
+ * reaches for first.
14
+ */
15
+ export const RESUME_KEY_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
16
+ const keyOf = (c) => {
17
+ const raw = c.req.query('key') ?? 'default';
18
+ return RESUME_KEY_PATTERN.test(raw) ? raw : null;
19
+ };
20
+ export function mountResumeRoutes(app, options) {
21
+ const { store, authorize } = options;
22
+ const base = options.basePath ?? '/api/resume';
23
+ const withUser = (handler) => async (c) => {
24
+ const userId = await authorize(c);
25
+ if (!userId)
26
+ return c.json({ error: 'unauthorized' }, 401, NO_STORE);
27
+ const key = keyOf(c);
28
+ if (!key)
29
+ return c.json({ error: 'invalid key' }, 400, NO_STORE);
30
+ return handler(c, userId, key);
31
+ };
32
+ app.get(base, withUser((c, userId, key) => {
33
+ const record = store.read(userId, key);
34
+ return c.json(record ?? { location: null, updatedAt: null }, 200, NO_STORE);
35
+ }));
36
+ app.put(base, withUser(async (c, userId, key) => {
37
+ const body = (await c.req.json().catch(() => null));
38
+ const location = typeof body?.location === 'string' ? body.location.trim() : '';
39
+ if (!location)
40
+ return c.json({ error: 'location required' }, 400, NO_STORE);
41
+ if (location.length > MAX_LOCATION_LENGTH)
42
+ return c.json({ error: 'location too long' }, 413, NO_STORE);
43
+ store.write(userId, location, key);
44
+ return c.json({ ok: true }, 200, NO_STORE);
45
+ }));
46
+ app.delete(base, withUser((c, userId, key) => {
47
+ store.clear(userId, key);
48
+ return c.json({ ok: true }, 200, NO_STORE);
49
+ }));
50
+ }