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,84 @@
1
+ /**
2
+ * Where a machine keeps an app's data — as CONFIGURATION, never as a convention
3
+ * baked into the library.
4
+ *
5
+ * ## What this replaces, and why it is a defect rather than untidiness
6
+ *
7
+ * Until 2026-09-13 three live modules in this package joined the ambient home
8
+ * directory to a hardcoded dotfolder belonging to the previous generation: the
9
+ * retention store, the archive, and `resolveAppDataRoot`. This package is
10
+ * PUBLISHED. Anybody who installed it got a library that went looking for a
11
+ * directory in their home that has nothing to do with them, and — worse — wrote
12
+ * into it if it happened to exist.
13
+ *
14
+ * A library may not know the name of a directory in somebody's home. It may know
15
+ * two things: what the caller told it, and what the operating system says the
16
+ * convention is. That is the whole ladder below.
17
+ *
18
+ * ## The ladder
19
+ *
20
+ * 1. an explicit `override` — a `--data-root` flag, a host's own resolution;
21
+ * 2. `$APP_DATA_HOME` — the ONE env var, for a machine that keeps every app's
22
+ * data under one root so a fleet-wide tool can see all of it at once;
23
+ * 3. `$XDG_DATA_HOME` — the user already told the OS where data goes;
24
+ * 4. the platform default: `~/Library/Application Support` on macOS,
25
+ * `~/.local/share` everywhere else.
26
+ *
27
+ * `$XDG_DATA_HOME` is honored on macOS too. It is not the platform's own
28
+ * convention there, but a user who sets it has stated a preference, and a
29
+ * preference outranks a default on any platform.
30
+ *
31
+ * Nothing here touches the filesystem: it is a pure mapping from environment to
32
+ * path, so a caller can reason about the answer before creating anything, and a
33
+ * test can assert both platforms on one machine.
34
+ */
35
+ import { homedir } from "node:os";
36
+ import { join } from "node:path";
37
+
38
+ /** The one env var. A machine with a shared data root sets this and nothing else. */
39
+ export const APP_DATA_HOME_ENV = "APP_DATA_HOME";
40
+
41
+ /** The cross-desktop convention this honors ahead of the platform default. */
42
+ export const XDG_DATA_HOME_ENV = "XDG_DATA_HOME";
43
+
44
+ export interface ResolveAppDataHomeOptions {
45
+ /** An explicit answer from the caller. Wins outright — this is the "argument" half. */
46
+ readonly override?: string;
47
+ /** Environment to read. Injected by tests; defaults to the process's own. */
48
+ readonly env?: Record<string, string | undefined>;
49
+ /**
50
+ * Platform to resolve for. Injected by tests so both branches are proved on one
51
+ * machine; defaults to the running platform.
52
+ */
53
+ readonly platform?: string;
54
+ }
55
+
56
+ /** Trimmed, or undefined — an env var set to whitespace is not a configuration. */
57
+ function configured(value: string | undefined): string | undefined {
58
+ const trimmed = value?.trim();
59
+ return trimmed ? trimmed : undefined;
60
+ }
61
+
62
+ /**
63
+ * The machine's app-data home. See the module note for the ordering rationale.
64
+ */
65
+ export function resolveAppDataHome(options: ResolveAppDataHomeOptions = {}): string {
66
+ const env = options.env ?? process.env;
67
+ const override = configured(options.override);
68
+ if (override) return override;
69
+
70
+ const appDataHome = configured(env[APP_DATA_HOME_ENV]);
71
+ if (appDataHome) return appDataHome;
72
+
73
+ const xdg = configured(env[XDG_DATA_HOME_ENV]);
74
+ if (xdg) return xdg;
75
+
76
+ // `env.HOME` rather than `homedir()` alone, so a caller passing an env object
77
+ // gets a resolution it can actually control — every test below depends on it,
78
+ // and so does any host that boots an app under a different account.
79
+ const home = configured(env.HOME) ?? homedir();
80
+ const platform = options.platform ?? process.platform;
81
+ return platform === "darwin"
82
+ ? join(home, "Library", "Application Support")
83
+ : join(home, ".local", "share");
84
+ }
@@ -0,0 +1,165 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { Hono } from 'hono';
6
+ import { createDiskStore } from '../storage/diskStore';
7
+ import type { MediaStore } from '../storage/types';
8
+ import { generateUserSalt, sealMeta } from './crypto';
9
+ import { createMemoryPrivateIndex } from './index-store';
10
+ import { mountPrivateRoutes } from './mountPrivateRoutes';
11
+ import { processPrivateBlob } from './pipeline';
12
+ import { createUnlockService } from './unlock';
13
+
14
+ // The gated blob route: a private NON-video file (photo/doc) is one AES-128-CTR ciphertext at
15
+ // privateSourceKey; the route decrypts under the unlock token's set key and serves plaintext
16
+ // with the sealed descriptor's mime. A token whose set key doesn't own the item gets 403.
17
+
18
+ const unlock = createUnlockService({ serverSecret: 'srv', unlockSecret: 'unlock' });
19
+ const salt = generateUserSalt();
20
+ const PLAINTEXT = 'plain photo bytes ✻ not for strangers';
21
+
22
+ describe('private blob route', () => {
23
+ let root: string;
24
+ let store: MediaStore;
25
+ const index = createMemoryPrivateIndex();
26
+ const app = new Hono();
27
+
28
+ beforeAll(async () => {
29
+ root = await mkdtemp(path.join(tmpdir(), 'pm-blob-'));
30
+ store = createDiskStore(path.join(root, 'store'));
31
+ mountPrivateRoutes(app, {
32
+ store,
33
+ index,
34
+ unlock,
35
+ authorize: async () => ({ userId: 'u1', userSalt: salt }),
36
+ });
37
+
38
+ // Encrypt a staged plaintext under the 'alpha' set and index it, mirroring ingestPrivate.
39
+ const src = path.join(root, 'src.bin');
40
+ await writeFile(src, PLAINTEXT);
41
+ const setKey = unlock.deriveSetKey({ userId: 'u1', userSalt: salt, password: 'alpha' });
42
+ const res = await processPrivateBlob({ store, owner: 'u1', id: 'b1', sourcePath: src, setKey });
43
+ expect(res.status).toBe('ready');
44
+ await index.add({
45
+ id: 'b1',
46
+ owner: 'u1',
47
+ sizeBytes: res.sizeBytes,
48
+ createdAt: 1,
49
+ status: 'ready',
50
+ keyMode: 'deniable',
51
+ wrappedContentKey: res.wrappedContentKey ?? '',
52
+ sealedMeta: sealMeta(setKey, { name: 'secret.jpg', mime: 'image/jpeg', tags: [] }),
53
+ });
54
+ });
55
+ afterAll(async () => {
56
+ await rm(root, { recursive: true, force: true });
57
+ });
58
+
59
+ const tokenFor = async (password: string) =>
60
+ (await unlock.unlock({ userId: 'u1', userSalt: salt, password })).token;
61
+
62
+ test('serves decrypted plaintext with the sealed mime to the owning set', async () => {
63
+ const res = await app.request(`/_private/u/${await tokenFor('alpha')}/file/b1`);
64
+ expect(res.status).toBe(200);
65
+ expect(res.headers.get('content-type')).toBe('image/jpeg');
66
+ expect(res.headers.get('cache-control')).toBe('no-store');
67
+ expect(await res.text()).toBe(PLAINTEXT);
68
+ });
69
+
70
+ test("a valid token from the WRONG set can't open it (403, zero bytes)", async () => {
71
+ const res = await app.request(`/_private/u/${await tokenFor('bravo')}/file/b1`);
72
+ expect(res.status).toBe(403);
73
+ });
74
+
75
+ test('garbage token → 401; unknown id → 404', async () => {
76
+ expect((await app.request('/_private/u/not-a-token/file/b1')).status).toBe(401);
77
+ const res = await app.request(`/_private/u/${await tokenFor('alpha')}/file/nope`);
78
+ expect(res.status).toBe(404);
79
+ });
80
+ });
81
+
82
+ // ── Range/CTR-seek (genapp-100) ──────────────────────────────────────────────
83
+ // A big private VIDEO on a remote-store host has no HLS derivatives — this
84
+ // route is its only byte path, so Range is what makes it scrub. CTR decrypts
85
+ // from the block-aligned offset with the IV counter advanced; these pin the
86
+ // exact-slice contract at aligned, unaligned, suffix, and unsatisfiable ranges.
87
+ describe('private blob route — Range (CTR seek)', () => {
88
+ let root: string;
89
+ let store: MediaStore;
90
+ const index = createMemoryPrivateIndex();
91
+ const app = new Hono();
92
+ // >3 CTR blocks with an unaligned tail, so mid-block seeks are meaningful.
93
+ const BYTES = Buffer.from(Array.from({ length: 1000 }, (_, i) => i % 251));
94
+
95
+ beforeAll(async () => {
96
+ root = await mkdtemp(path.join(tmpdir(), 'pm-blob-range-'));
97
+ store = createDiskStore(path.join(root, 'store'));
98
+ mountPrivateRoutes(app, {
99
+ store,
100
+ index,
101
+ unlock,
102
+ authorize: async () => ({ userId: 'u1', userSalt: salt }),
103
+ });
104
+ const src = path.join(root, 'clip.bin');
105
+ await writeFile(src, BYTES);
106
+ const setKey = unlock.deriveSetKey({ userId: 'u1', userSalt: salt, password: 'alpha' });
107
+ const res = await processPrivateBlob({ store, owner: 'u1', id: 'v1', sourcePath: src, setKey });
108
+ expect(res.status).toBe('ready');
109
+ await index.add({
110
+ id: 'v1',
111
+ owner: 'u1',
112
+ sizeBytes: res.sizeBytes,
113
+ createdAt: 1,
114
+ status: 'ready',
115
+ keyMode: 'deniable',
116
+ wrappedContentKey: res.wrappedContentKey ?? '',
117
+ sealedMeta: sealMeta(setKey, { name: 'clip.mp4', mime: 'video/mp4', tags: [] }),
118
+ });
119
+ });
120
+ afterAll(async () => {
121
+ await rm(root, { recursive: true, force: true });
122
+ });
123
+
124
+ const token = async () => (await unlock.unlock({ userId: 'u1', userSalt: salt, password: 'alpha' })).token;
125
+ const get = async (range?: string) =>
126
+ app.request(`/_private/u/${await token()}/file/v1`, range ? { headers: { range } } : undefined);
127
+
128
+ test('a full read advertises Accept-Ranges and still decrypts whole', async () => {
129
+ const res = await get();
130
+ expect(res.status).toBe(200);
131
+ expect(res.headers.get('accept-ranges')).toBe('bytes');
132
+ expect(Buffer.from(await res.arrayBuffer()).equals(BYTES)).toBe(true);
133
+ });
134
+
135
+ test('a block-aligned range decrypts the exact slice as 206', async () => {
136
+ const res = await get('bytes=160-479');
137
+ expect(res.status).toBe(206);
138
+ expect(res.headers.get('content-range')).toBe('bytes 160-479/1000');
139
+ expect(res.headers.get('content-length')).toBe('320');
140
+ expect(Buffer.from(await res.arrayBuffer()).equals(BYTES.subarray(160, 480))).toBe(true);
141
+ });
142
+
143
+ test('an UNALIGNED mid-block range decrypts the exact slice (the CTR counter + lead-in drop)', async () => {
144
+ const res = await get('bytes=7-300');
145
+ expect(res.status).toBe(206);
146
+ expect(res.headers.get('content-range')).toBe('bytes 7-300/1000');
147
+ expect(Buffer.from(await res.arrayBuffer()).equals(BYTES.subarray(7, 301))).toBe(true);
148
+ });
149
+
150
+ test('an open-ended range runs to the last byte; a suffix range serves the tail', async () => {
151
+ const open = await get('bytes=990-');
152
+ expect(open.status).toBe(206);
153
+ expect(Buffer.from(await open.arrayBuffer()).equals(BYTES.subarray(990))).toBe(true);
154
+ const suffix = await get('bytes=-25');
155
+ expect(suffix.status).toBe(206);
156
+ expect(suffix.headers.get('content-range')).toBe('bytes 975-999/1000');
157
+ expect(Buffer.from(await suffix.arrayBuffer()).equals(BYTES.subarray(975))).toBe(true);
158
+ });
159
+
160
+ test('an out-of-bounds range answers 416 with the total', async () => {
161
+ const res = await get('bytes=1000-1200');
162
+ expect(res.status).toBe(416);
163
+ expect(res.headers.get('content-range')).toBe('bytes */1000');
164
+ });
165
+ });
@@ -0,0 +1,81 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ derivePasswordSetKey,
4
+ deriveServerSetKey,
5
+ generateContentKey,
6
+ generateUserSalt,
7
+ openMeta,
8
+ sealMeta,
9
+ unwrapContentKey,
10
+ wrapContentKey,
11
+ } from './crypto';
12
+
13
+ describe('private-media crypto core', () => {
14
+ const salt = generateUserSalt();
15
+
16
+ test('same password + salt → same key; different password → different key', () => {
17
+ const a = derivePasswordSetKey('hunter2', salt);
18
+ const a2 = derivePasswordSetKey('hunter2', salt);
19
+ const b = derivePasswordSetKey('other-pass', salt);
20
+ expect(a.equals(a2)).toBe(true);
21
+ expect(a.equals(b)).toBe(false);
22
+ expect(a.length).toBe(32);
23
+ });
24
+
25
+ test('the same password under different user salts yields different keys', () => {
26
+ const s2 = generateUserSalt();
27
+ expect(derivePasswordSetKey('pw', salt).equals(derivePasswordSetKey('pw', s2))).toBe(false);
28
+ });
29
+
30
+ test('server key is recoverable and distinct from any password key', () => {
31
+ const srv = deriveServerSetKey('server-secret', 'user-1');
32
+ expect(srv.equals(deriveServerSetKey('server-secret', 'user-1'))).toBe(true);
33
+ expect(srv.equals(deriveServerSetKey('server-secret', 'user-2'))).toBe(false);
34
+ expect(srv.equals(derivePasswordSetKey('server-secret', salt))).toBe(false);
35
+ });
36
+
37
+ test('sealMeta/openMeta round-trips; a wrong key returns null (the membership test)', () => {
38
+ const key = derivePasswordSetKey('pw-A', salt);
39
+ const wrong = derivePasswordSetKey('pw-B', salt);
40
+ const sealed = sealMeta(key, { title: 'Secret Clip', notes: 'private', tags: ['x'] });
41
+ expect(openMeta<{ title: string }>(key, sealed)?.title).toBe('Secret Clip');
42
+ expect(openMeta(wrong, sealed)).toBeNull(); // wrong password ⇒ not a member, no info leak
43
+ });
44
+
45
+ test('content key wrap/unwrap round-trips; wrong key ⇒ null', () => {
46
+ const key = derivePasswordSetKey('pw', salt);
47
+ const wrong = deriveServerSetKey('srv', 'u');
48
+ const content = generateContentKey();
49
+ expect(content.key.length).toBe(16); // AES-128
50
+ expect(content.iv.length).toBe(16);
51
+ const wrapped = wrapContentKey(key, content);
52
+ const unwrapped = unwrapContentKey(key, wrapped);
53
+ expect(unwrapped?.key.equals(content.key)).toBe(true);
54
+ expect(unwrapped?.iv.equals(content.iv)).toBe(true);
55
+ expect(unwrapContentKey(wrong, wrapped)).toBeNull();
56
+ });
57
+
58
+ test('a tampered envelope decrypts to null, not garbage', () => {
59
+ const key = derivePasswordSetKey('pw', salt);
60
+ const sealed = sealMeta(key, { title: 't' });
61
+ expect(openMeta(key, `${sealed}x`)).toBeNull();
62
+ });
63
+ });
64
+
65
+ describe('incrementIv (genapp-100 CTR seek)', () => {
66
+ test('adds blocks big-endian with byte carry', async () => {
67
+ const { incrementIv } = await import('./crypto');
68
+ const iv = Buffer.alloc(16);
69
+ iv[15] = 0xff;
70
+ const plus1 = incrementIv(iv, 1);
71
+ expect(plus1[15]).toBe(0x00);
72
+ expect(plus1[14]).toBe(0x01);
73
+ // A large jump carries across multiple bytes: 0x01_0000 blocks.
74
+ const jump = incrementIv(Buffer.alloc(16), 0x010000);
75
+ expect(jump[13]).toBe(0x01);
76
+ expect(jump[14]).toBe(0x00);
77
+ expect(jump[15]).toBe(0x00);
78
+ // The input IV is never mutated.
79
+ expect(iv[15]).toBe(0xff);
80
+ });
81
+ });
@@ -0,0 +1,155 @@
1
+ import { createCipheriv, randomBytes } from 'node:crypto';
2
+ import { decryptSecret, deriveKey, encryptSecret } from 'cwip/node';
3
+
4
+ // ── private-media crypto core ───────────────────────────────────────────────────
5
+ // The reusable heart of encrypted private media. Pure composition of cwip's `secretBox`
6
+ // (scrypt `deriveKey` + AES-256-GCM `encryptSecret`/`decryptSecret`) — whose own docs describe
7
+ // exactly our two key modes: derive from a SERVER SECRET (recoverable "standard") vs from the
8
+ // USER'S PASSWORD (zero-knowledge "deniable"). We use scrypt (slow, brute-force-resistant) for
9
+ // the password KDF — NOT cursedbelt's vault HKDF, which is fast and password-unsafe.
10
+ //
11
+ // Streaming stays fast because the bytes are AES-128 HLS segments the player decrypts (edge-
12
+ // cacheable); this module only mints/wraps the small 16-byte content key + seals metadata.
13
+
14
+ export type KeyMode = 'standard' | 'deniable';
15
+
16
+ /** A random per-user salt (non-secret, stored plaintext, stable once created). Domain-separates
17
+ * a user's password-derived keys so the same password yields different keys across users. */
18
+ export const generateUserSalt = (): string => randomBytes(16).toString('base64url');
19
+
20
+ /** Zero-knowledge set key: scrypt(password, per-user salt). The server never stores this (or a
21
+ * hash of it); it exists only transiently after an unlock. Different passwords → disjoint sets. */
22
+ export const derivePasswordSetKey = (password: string, userSalt: string): Buffer =>
23
+ deriveKey(password, `pmedia:${userSalt}`);
24
+
25
+ /** Recoverable set key: scrypt(serverSecret, userId). The server can always derive it, so
26
+ * `standard`-mode media is decryptable once private is unlocked (session-gated) — protects a
27
+ * stolen disk / Finder / backups, but a compromised running server can decrypt. */
28
+ export const deriveServerSetKey = (serverSecret: string, userId: string): Buffer =>
29
+ deriveKey(serverSecret, `pmedia-srv:${userId}`);
30
+
31
+ // ── Sealed metadata envelope (title / thumbnail key / notes / tags) ──────────────
32
+ // Encrypted under the set key; NEVER stored in the plaintext index. A successful decrypt is
33
+ // also the deniable MEMBERSHIP TEST — trying a password reveals only the items it opens.
34
+
35
+ export const sealMeta = (setKey: Buffer, meta: unknown): string =>
36
+ encryptSecret(JSON.stringify(meta), setKey);
37
+
38
+ /** Open a sealed envelope, or null if this key doesn't own it (wrong password / mode key).
39
+ * The null return IS the membership signal — no stored set identifier is needed. */
40
+ export function openMeta<T = unknown>(setKey: Buffer, sealed: string): T | null {
41
+ try {
42
+ return JSON.parse(decryptSecret(sealed, setKey)) as T;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ // ── AES-128 HLS content key: minted per item, WRAPPED under the set key ───────────
49
+ // The raw 16-byte key + 16-byte IV drive ffmpeg's `-hls_key_info_file` (AES-128 segments). Only
50
+ // the wrapped form is stored; the key route unwraps it (with the unlock-provided set key) and
51
+ // returns the raw key to hls.js, which decrypts segments client-side.
52
+
53
+ export type ContentKey = { key: Buffer; iv: Buffer };
54
+
55
+ export const generateContentKey = (): ContentKey => ({
56
+ key: randomBytes(16), // AES-128
57
+ iv: randomBytes(16),
58
+ });
59
+
60
+ export const wrapContentKey = (setKey: Buffer, content: ContentKey): string =>
61
+ encryptSecret(`${content.key.toString('base64')}.${content.iv.toString('base64')}`, setKey);
62
+
63
+ /** Unwrap a content key, or null if the set key can't open it (wrong password / mode key). */
64
+ export function unwrapContentKey(setKey: Buffer, wrapped: string): ContentKey | null {
65
+ try {
66
+ const [k, iv] = decryptSecret(wrapped, setKey).split('.');
67
+ if (!k || !iv) return null;
68
+ return { key: Buffer.from(k, 'base64'), iv: Buffer.from(iv, 'base64') };
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ // ── Whole-file blob cipher (non-video private files: photos, docs) ────────────────
75
+ // AES-128-CTR under the item's content key — the same key/IV material the HLS path wraps, so
76
+ // wrap/unwrap is shared. CTR is length-preserving and streaming (no padding, no buffering), and
77
+ // encrypt/decrypt are the same operation. Tamper-evidence comes from the GCM-sealed descriptor +
78
+ // the gated serve route (parity with the encrypted-HLS segments, which are CBC-without-MAC too).
79
+
80
+ /** Pipe a byte stream through the item's AES-128-CTR keystream. Symmetric: pass ciphertext to
81
+ * decrypt, plaintext to encrypt. */
82
+ /** Big-endian increment of a 16-byte CTR IV by `blocks` — the counter arithmetic that lets a
83
+ * decryptor start mid-stream (genapp-100: Range/seek on private blobs). */
84
+ export function incrementIv(iv: Buffer, blocks: number): Buffer {
85
+ const out = Buffer.from(iv);
86
+ let carry = BigInt(blocks);
87
+ for (let i = out.length - 1; i >= 0 && carry > 0n; i--) {
88
+ const sum = BigInt(out[i]) + (carry & 0xffn);
89
+ out[i] = Number(sum & 0xffn);
90
+ carry = (carry >> 8n) + (sum >> 8n);
91
+ }
92
+ return out;
93
+ }
94
+
95
+ /**
96
+ * Decrypt a CTR ciphertext stream that starts at the BLOCK-ALIGNED offset covering plaintext
97
+ * byte `startOffset`, dropping the intra-block lead-in — the seek half of {@link cryptBlobStream}.
98
+ * CTR is length-preserving and positionally keyed, so byte N of plaintext only needs ciphertext
99
+ * from block ⌊N/16⌋ onward with the IV counter advanced by that many blocks (genapp-100: this is
100
+ * what makes big private videos scrub on a remote-store host with no local derivatives).
101
+ */
102
+ export function cryptBlobRangeStream(
103
+ content: ContentKey,
104
+ stream: ReadableStream<Uint8Array>,
105
+ startOffset: number,
106
+ ): ReadableStream<Uint8Array> {
107
+ const BLOCK = 16;
108
+ const blocks = Math.floor(startOffset / BLOCK);
109
+ let drop = startOffset % BLOCK;
110
+ const cipher = createCipheriv('aes-128-ctr', content.key, incrementIv(content.iv, blocks));
111
+ return stream.pipeThrough(
112
+ new TransformStream<Uint8Array, Uint8Array>({
113
+ transform(chunk, controller) {
114
+ let out = cipher.update(chunk);
115
+ if (drop > 0) {
116
+ const skip = Math.min(drop, out.byteLength);
117
+ out = out.subarray(skip);
118
+ drop -= skip;
119
+ }
120
+ if (out.byteLength > 0) {
121
+ controller.enqueue(new Uint8Array(out.buffer, out.byteOffset, out.byteLength));
122
+ }
123
+ },
124
+ flush(controller) {
125
+ const out = cipher.final(); // empty for CTR — kept for cipher-contract completeness
126
+ if (out.byteLength > 0) {
127
+ controller.enqueue(new Uint8Array(out.buffer, out.byteOffset, out.byteLength));
128
+ }
129
+ },
130
+ }),
131
+ );
132
+ }
133
+
134
+ export function cryptBlobStream(
135
+ content: ContentKey,
136
+ stream: ReadableStream<Uint8Array>,
137
+ ): ReadableStream<Uint8Array> {
138
+ const cipher = createCipheriv('aes-128-ctr', content.key, content.iv);
139
+ return stream.pipeThrough(
140
+ new TransformStream<Uint8Array, Uint8Array>({
141
+ transform(chunk, controller) {
142
+ const out = cipher.update(chunk);
143
+ if (out.byteLength > 0) {
144
+ controller.enqueue(new Uint8Array(out.buffer, out.byteOffset, out.byteLength));
145
+ }
146
+ },
147
+ flush(controller) {
148
+ const out = cipher.final(); // empty for CTR — kept for cipher-contract completeness
149
+ if (out.byteLength > 0) {
150
+ controller.enqueue(new Uint8Array(out.buffer, out.byteOffset, out.byteLength));
151
+ }
152
+ },
153
+ }),
154
+ );
155
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { Hono } from 'hono';
3
+ import { createDiskStore } from '../storage/diskStore';
4
+ import { generateUserSalt, sealMeta } from './crypto';
5
+ import { createMemoryPrivateIndex } from './index-store';
6
+ import { mountPrivateRoutes } from './mountPrivateRoutes';
7
+ import { createUnlockService } from './unlock';
8
+
9
+ // The gate password policy: the password is an ACCESS CHECK against the app's own accounts
10
+ // (e.g. the login store), not a key-derivation input. Correct password ⇒ a STANDARD
11
+ // (recoverable, server-key) grant; wrong/absent password ⇒ 401; password changes never
12
+ // orphan content because the set key never depended on the password.
13
+
14
+ const unlock = createUnlockService({ serverSecret: 'srv', unlockSecret: 'unlock' });
15
+ const salt = generateUserSalt();
16
+
17
+ function gateApp(verify: (password: string) => boolean) {
18
+ const app = new Hono();
19
+ const index = createMemoryPrivateIndex();
20
+ mountPrivateRoutes(app, {
21
+ store: createDiskStore('/tmp/unused-pm-gate'),
22
+ index,
23
+ unlock,
24
+ authorize: async () => ({ userId: 'u1', userSalt: salt }),
25
+ passwordPolicy: {
26
+ mode: 'gate',
27
+ verifyPassword: async (_who, password) => verify(password),
28
+ },
29
+ });
30
+ return { app, index };
31
+ }
32
+
33
+ const post = (app: Hono, path: string, body: unknown) =>
34
+ app.request(path, {
35
+ method: 'POST',
36
+ headers: { 'content-type': 'application/json' },
37
+ body: JSON.stringify(body),
38
+ });
39
+
40
+ describe('unlock gate policy', () => {
41
+ test('correct password ⇒ standard grant (key never derived from the password)', async () => {
42
+ const { app } = gateApp((pw) => pw === 'login-pw');
43
+ const res = await post(app, '/_private/unlock', { password: 'login-pw' });
44
+ expect(res.status).toBe(200);
45
+ const { token, mode } = (await res.json()) as { token: string; mode: string };
46
+ expect(mode).toBe('standard');
47
+ const grant = await unlock.verify(token);
48
+ expect(grant?.mode).toBe('standard');
49
+ // The grant's key equals the server-derived one — NOT a password derivation.
50
+ expect(grant?.setKey.equals(unlock.deriveSetKey({ userId: 'u1', userSalt: salt }))).toBe(true);
51
+ });
52
+
53
+ test('wrong password ⇒ 401, no token', async () => {
54
+ const { app } = gateApp((pw) => pw === 'login-pw');
55
+ const res = await post(app, '/_private/unlock', { password: 'wrong' });
56
+ expect(res.status).toBe(401);
57
+ });
58
+
59
+ test('missing password ⇒ 401 (no silent session unlock under gate)', async () => {
60
+ const { app } = gateApp(() => true);
61
+ const res = await post(app, '/_private/unlock', {});
62
+ expect(res.status).toBe(401);
63
+ });
64
+
65
+ test('reveal under gate verifies then reveals the STANDARD set', async () => {
66
+ const { app, index } = gateApp((pw) => pw === 'login-pw');
67
+ const standardKey = unlock.deriveSetKey({ userId: 'u1', userSalt: salt });
68
+ await index.add({
69
+ id: 'item1',
70
+ owner: 'u1',
71
+ sealedMeta: sealMeta(standardKey, { name: 'secret.jpg', mime: 'image/jpeg', tags: [] }),
72
+ wrappedContentKey: 'unused',
73
+ sizeBytes: 3,
74
+ createdAt: Date.now(),
75
+ status: 'ready',
76
+ keyMode: 'standard',
77
+ });
78
+ const bad = await post(app, '/_private/reveal', { password: 'nope' });
79
+ expect(bad.status).toBe(401);
80
+ const good = await post(app, '/_private/reveal', { password: 'login-pw' });
81
+ expect(good.status).toBe(200);
82
+ const { items } = (await good.json()) as { items: Array<{ id: string }> };
83
+ expect(items.map((i) => i.id)).toEqual(['item1']);
84
+ });
85
+
86
+ test('derive policy (default) still mints deniable sets from passwords', async () => {
87
+ const app = new Hono();
88
+ mountPrivateRoutes(app, {
89
+ store: createDiskStore('/tmp/unused-pm-gate'),
90
+ index: createMemoryPrivateIndex(),
91
+ unlock,
92
+ authorize: async () => ({ userId: 'u1', userSalt: salt }),
93
+ });
94
+ const res = await post(app, '/_private/unlock', { password: 'anything' });
95
+ expect(res.status).toBe(200);
96
+ expect(((await res.json()) as { mode: string }).mode).toBe('deniable');
97
+ });
98
+ });
@@ -0,0 +1,83 @@
1
+ import type { ProcessingStatus } from 'cursedbelt-core/media';
2
+ import type { KeyMode } from './crypto';
3
+
4
+ // ── Private index ───────────────────────────────────────────────────────────────
5
+ // Per-user rows for encrypted media. The row holds ONLY non-sensitive facts in the clear
6
+ // (size, created-at, status) so a user can see "N locked files, X GB" and prune ones whose
7
+ // password they've lost. The sensitive parts — title / thumbnail / notes / tags — live in the
8
+ // AES-GCM `sealedMeta` envelope, and the AES-128 content key lives in `wrappedContentKey`; both
9
+ // are opaque without the set key. The public projection (`toPublic`) is what the index endpoint
10
+ // returns — it strips every secret, including the key MODE (so the clear index can't even hint
11
+ // which rows are deniable). An app swaps the in-memory store for a SQLite-backed one (see
12
+ // cursedbelt-server/sqlite) without touching the routes.
13
+
14
+ export type PrivateItem = {
15
+ id: string;
16
+ owner: string;
17
+ sizeBytes: number;
18
+ createdAt: number;
19
+ status: ProcessingStatus;
20
+ keyMode: KeyMode;
21
+ /** AES-128 content key + IV, wrapped under the item's set key. */
22
+ wrappedContentKey: string;
23
+ /** AES-GCM envelope of { title, notes, tags, durationSeconds, … } under the item's set key. */
24
+ sealedMeta: string;
25
+ };
26
+
27
+ /** What the index endpoint exposes — clear, non-sensitive facts only. No title/thumb/notes/tags,
28
+ * no key mode, no wrapped key. */
29
+ export type PrivateIndexRow = {
30
+ id: string;
31
+ sizeBytes: number;
32
+ createdAt: number;
33
+ status: ProcessingStatus;
34
+ };
35
+
36
+ export const toPublic = (item: PrivateItem): PrivateIndexRow => ({
37
+ id: item.id,
38
+ sizeBytes: item.sizeBytes,
39
+ createdAt: item.createdAt,
40
+ status: item.status,
41
+ });
42
+
43
+ export interface PrivateIndex {
44
+ add(item: PrivateItem): Promise<void>;
45
+ get(owner: string, id: string): Promise<PrivateItem | null>;
46
+ list(owner: string): Promise<PrivateItem[]>;
47
+ update(
48
+ owner: string,
49
+ id: string,
50
+ patch: Partial<Omit<PrivateItem, 'id' | 'owner'>>,
51
+ ): Promise<void>;
52
+ remove(owner: string, id: string): Promise<void>;
53
+ }
54
+
55
+ export function createMemoryPrivateIndex(): PrivateIndex {
56
+ const byOwner = new Map<string, Map<string, PrivateItem>>();
57
+ const bucket = (owner: string) => {
58
+ let m = byOwner.get(owner);
59
+ if (!m) {
60
+ m = new Map();
61
+ byOwner.set(owner, m);
62
+ }
63
+ return m;
64
+ };
65
+ return {
66
+ async add(item) {
67
+ bucket(item.owner).set(item.id, item);
68
+ },
69
+ async get(owner, id) {
70
+ return bucket(owner).get(id) ?? null;
71
+ },
72
+ async list(owner) {
73
+ return [...bucket(owner).values()].sort((a, b) => b.createdAt - a.createdAt);
74
+ },
75
+ async update(owner, id, patch) {
76
+ const item = bucket(owner).get(id);
77
+ if (item) bucket(owner).set(id, { ...item, ...patch });
78
+ },
79
+ async remove(owner, id) {
80
+ bucket(owner).delete(id);
81
+ },
82
+ };
83
+ }