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,627 @@
1
+ /**
2
+ * The sitewide MASTER LOCK — a second wall, in front of a whole app, that the app's own
3
+ * sign-in cannot open.
4
+ *
5
+ * ── What the owner asked for (2026-08-15, their words) ──────────────────────
6
+ * *"I want to be able to share my laptop without certain apps being at risk of other people
7
+ * viewing them while I am still logged in … Until the master password is entered a user
8
+ * logged in as me cannot see anything about the site, not even text or images, nothing at
9
+ * all."*
10
+ *
11
+ * So: standard auth still happens FIRST and is unchanged. This sits behind it. A request
12
+ * that has passed the app's own gate and carries no live unlock is answered with the lock
13
+ * page (for a document) or a 401 (for anything else) — never with the app.
14
+ *
15
+ * ── 🔴 BE HONEST ABOUT WHAT THIS BUYS ───────────────────────────────────────
16
+ * In `apps/vault` the master password IS the encryption key: the server holds ciphertext
17
+ * and a total breach yields noise. **This cannot work that way, and must never be described
18
+ * as if it does.** The apps it guards serve their bytes to browsers; those bytes are
19
+ * plaintext at rest by nature. This is a LOCK ON THE DOOR — an unlocked laptop, a borrowed
20
+ * phone, a hijacked session still cannot open the UI. The owner said the same thing back:
21
+ * *"I am aware that a tech savvy user of my laptop could still get around it by changing
22
+ * code. This work does not worry about that, we are only securing app UI access."*
23
+ * The same distinction is written in `fleet/rules/owner-uis-are-one-ui.md` and in
24
+ * binary-server's `src/admin/passcode.ts`, which is this feature's nearest relative.
25
+ *
26
+ * ── The four decisions that are not obvious ─────────────────────────────────
27
+ *
28
+ * **1. Sessions live in MEMORY, so a restart re-locks.** The correct default for a surface
29
+ * whose whole risk model is "the browser is somewhere it should not be". Same call as the
30
+ * inspector passcode.
31
+ *
32
+ * **2. The idle clock is slid by INTERACTION, never by traffic.** The owner said *"if I
33
+ * leave the site up but don't interact with it"* — and every app here polls. Sliding on any
34
+ * request would let a background refetch hold the lock open all night with nobody at the
35
+ * desk, which is exactly the scenario the feature exists for. So an ordinary request only
36
+ * READS the clock; `touch()` — reached solely by `POST /__lock/ping`, which the browser
37
+ * sends on real pointer/key/touch activity — is the only thing that moves it.
38
+ *
39
+ * **3. The password is never stored, and this process never sees it.** The browser derives
40
+ * `verifier = PBKDF2(KEK ‖ password)` (see `../../core/master-lock/kdf.ts`); the record
41
+ * holds `argon2id(verifier)`. It is seeded from the vault's OWN record so the owner's
42
+ * existing vault master password works on day one without anyone knowing it.
43
+ *
44
+ * **4. Every refusal is the same refusal.** Wrong verifier, throttled, unconfigured and
45
+ * "no unlock" answer with the identical status and body, and a dummy argon2 verify runs on
46
+ * every path so the TIMING does not separate them either. The one thing that varies is the
47
+ * `retryAfterMs` a throttled caller is told — which they already know, since they are the
48
+ * one who has been guessing.
49
+ */
50
+ import {
51
+ type MasterLockKdfParams,
52
+ type MasterLockStatus,
53
+ clampIdleMs,
54
+ isMasterLockKdfParams,
55
+ windowRemaining,
56
+ } from "cursedbelt-core/master-lock";
57
+
58
+ /** The persisted record. Three public values — none of them is a secret, and the two that
59
+ * look like one are a salt and a one-way hash. */
60
+ export interface MasterLockRecord {
61
+ kdf: MasterLockKdfParams;
62
+ /** `argon2id(verifier)`. The PHC string is self-describing, so there is no salt column. */
63
+ verifierHash: string;
64
+ /** The owner's configured idle timeout for THIS app. */
65
+ idleMs: number;
66
+ }
67
+
68
+ /**
69
+ * Where the record rests. One blob, so an app can keep it in whatever it already has —
70
+ * binary-server's `settings` table, a satellite's `app_settings` row, a JSON file.
71
+ *
72
+ * Synchronous because every consumer's storage is `bun:sqlite`, and an async read here
73
+ * would make the request-path guard async for no reason any caller could observe.
74
+ */
75
+ export interface MasterLockStore {
76
+ read(): string | null;
77
+ write(json: string): void;
78
+ }
79
+
80
+ export interface MasterLockOptions {
81
+ store: MasterLockStore;
82
+ /**
83
+ * The BOOTSTRAP record, as JSON, used only when the store is empty — the same shape as
84
+ * {@link MasterLockRecord} with `idleMs` optional.
85
+ *
86
+ * It arrives as `MASTER_LOCK_SEED` in the app's own 0600 deploy-secrets env file,
87
+ * copied from the vault's own `vault_keys` row, so the lock is armed from the first
88
+ * request rather than from whenever somebody remembers to set one. A lock that has to
89
+ * be configured before it protects anything protects nothing on the day it matters —
90
+ * the reasoning `bootstrapPasscode` records in binary-server.
91
+ *
92
+ * A change made in the app afterwards WINS, permanently: this is consulted only when
93
+ * the store has nothing.
94
+ */
95
+ seedJson?: string | null;
96
+ /** Injected clock. The tests drive it; production omits it. */
97
+ now?: () => number;
98
+ /**
99
+ * Called whenever this app flips locked ⇄ unlocked, including when the idle timer fires.
100
+ *
101
+ * `binary-server`'s inspector needs it: its `/` is a Bun `HTMLBundle` matched BEFORE the
102
+ * `fetch` handler, so the only way to keep the app shell off the wire while locked is to
103
+ * swap that route with `server.reload()` — which needs a signal, not a poll.
104
+ */
105
+ onLockedChange?: (locked: boolean) => void;
106
+ /** Where to log the few things worth a line (a seed adopted, a rotation). */
107
+ log?: (line: string) => void;
108
+ /**
109
+ * PER-PERSON mode. Inverts decision 5 below: with no record this lock reads as
110
+ * LOCKED rather than open, and {@link MasterLock.enroll} is the way out.
111
+ *
112
+ * 🔴 It is an option and not the default because the two modes fail in opposite
113
+ * directions, and each is right for exactly one situation:
114
+ *
115
+ * - **App-wide (`false`, the default).** One record for the whole app, seeded
116
+ * from `MASTER_LOCK_SEED`. Unconfigured must read as OPEN, or a missing seed
117
+ * bricks an app nobody can get into — see `unlocked`.
118
+ * - **Per-person (`true`).** One record per signed-in account, and there is no
119
+ * seed: each person chooses their own. Unconfigured must read as LOCKED,
120
+ * because "this person has not chosen a password yet" is the state of every
121
+ * NEW account, and treating it as open would mean the wall admits everybody
122
+ * it has never met — the precise opposite of the feature.
123
+ *
124
+ * Nothing bricks in per-person mode, because enrollment is always available to
125
+ * a principal with no record. That is what makes failing closed affordable here
126
+ * and unaffordable there.
127
+ */
128
+ enrollable?: boolean;
129
+ }
130
+
131
+ /** What an unlock attempt answers. `ok` carries the token to set as a cookie. */
132
+ export type MasterLockUnlockResult =
133
+ | { ok: true; token: string }
134
+ | { ok: false; retryAfterMs: number };
135
+
136
+ /** What choosing a FIRST master password answers. `ok` carries the unlock, so the
137
+ * person who just set it is not asked to type it again in the next breath. */
138
+ export type MasterLockEnrollResult =
139
+ | { ok: true; token: string }
140
+ | { ok: false; reason: "not-enrollable" | "already-configured" | "invalid" };
141
+
142
+ const TOKEN_BYTES = 24;
143
+
144
+ /**
145
+ * One app's lock. Constructed once at boot and held for the process's life.
146
+ *
147
+ * There is exactly ONE unlock at a time, not one per browser session. That is deliberate
148
+ * and it is the owner's model: *"entering it in one site will only apply it to that site"* —
149
+ * the unit is the SITE, and these apps have exactly one user. Minting per-session unlocks
150
+ * would mean the phone in someone else's hand stays open after the owner locks the desk,
151
+ * which inverts the manual-lock requirement (*"then no one can see anything until the master
152
+ * password is entered again"*).
153
+ */
154
+ export class MasterLock {
155
+ private readonly store: MasterLockStore;
156
+ private readonly now: () => number;
157
+ private readonly onLockedChange: ((locked: boolean) => void) | undefined;
158
+ private readonly log: (line: string) => void;
159
+ /** Per-person mode — see {@link MasterLockOptions.enrollable}. */
160
+ private readonly enrollable: boolean;
161
+
162
+ private record: MasterLockRecord | null;
163
+ /**
164
+ * The live unlocks: token → when that token last saw real interaction. Moved only by
165
+ * {@link touch}.
166
+ *
167
+ * 🔴 A MAP, not one session, since 2026-08-25 — and the reason is the per-person mode.
168
+ * App-wide, one unlock was right: one person, one site, and the owner's *"entering it
169
+ * in one site will only apply it to that site"*. Per person it is wrong twice over.
170
+ * The owner asked to *"put in prompts for the orch and monitor it from our phones and
171
+ * other devices"* — so the desk and the phone are both his, both open at once, and a
172
+ * single-token model would have each unlock silently evict the other. And the guard
173
+ * now demands a PRESENTED token rather than reading `unlocked` (see the guard's own
174
+ * note), which is only possible if every device that typed the password holds one.
175
+ *
176
+ * {@link lock} still clears them ALL, which is what keeps the owner's original
177
+ * requirement true: *"then no one can see anything until the master password is
178
+ * entered again"* — one button, every device.
179
+ */
180
+ private sessions = new Map<string, number>();
181
+ private expiryTimer: ReturnType<typeof setTimeout> | null = null;
182
+
183
+ private failures = 0;
184
+ private lastFailureAt = 0;
185
+
186
+ constructor(options: MasterLockOptions) {
187
+ this.store = options.store;
188
+ this.now = options.now ?? Date.now;
189
+ this.onLockedChange = options.onLockedChange;
190
+ this.log = options.log ?? (() => undefined);
191
+ this.enrollable = options.enrollable === true;
192
+ this.record = readRecord(this.store.read());
193
+ if (!this.record && options.seedJson) {
194
+ const seeded = readRecord(options.seedJson);
195
+ if (seeded) {
196
+ this.record = seeded;
197
+ this.store.write(JSON.stringify(seeded));
198
+ this.log("master lock: seeded from MASTER_LOCK_SEED (the vault's master password)");
199
+ } else {
200
+ // Loud, because the alternative is an app that boots with no lock and looks
201
+ // exactly like an app that was never meant to have one.
202
+ this.log("🔴 master lock: MASTER_LOCK_SEED is set but unusable — the lock is NOT armed");
203
+ }
204
+ }
205
+ }
206
+
207
+ /** Is a master password set at all? An app with `false` here refuses nothing. */
208
+ get configured(): boolean {
209
+ return this.record !== null;
210
+ }
211
+
212
+ /** The params the browser must derive under, or null. Public by design. */
213
+ get kdf(): MasterLockKdfParams | null {
214
+ return this.record?.kdf ?? null;
215
+ }
216
+
217
+ get idleMs(): number {
218
+ return this.record?.idleMs ?? clampIdleMs(undefined);
219
+ }
220
+
221
+ /** Is this lock in per-person mode? */
222
+ get isEnrollable(): boolean {
223
+ return this.enrollable;
224
+ }
225
+
226
+ /** May a first password be chosen right now? Per-person mode, and no record yet. */
227
+ get awaitingEnrollment(): boolean {
228
+ return this.enrollable && this.record === null;
229
+ }
230
+
231
+ /**
232
+ * Is the site open right now?
233
+ *
234
+ * 🔴 An UNCONFIGURED app-wide lock reads as UNLOCKED, and that is the deliberate
235
+ * choice. A lock with no password cannot be opened by anybody, including the owner —
236
+ * failing closed would brick an app rather than protect it, and this guards a UI whose
237
+ * data is already behind the app's own owner-only auth. `/healthz` reports
238
+ * `configured` so a smoke can catch the state externally instead of the owner
239
+ * discovering it.
240
+ *
241
+ * 🔴 In PER-PERSON mode the same reasoning runs the other way and this reads as
242
+ * LOCKED. Nobody is bricked — {@link enroll} is open to a principal with no record —
243
+ * and "has not chosen a password yet" describes every new account, so opening for it
244
+ * would admit exactly the people the wall exists to stop. See
245
+ * {@link MasterLockOptions.enrollable}.
246
+ */
247
+ get unlocked(): boolean {
248
+ if (!this.record) return !this.enrollable;
249
+ this.sweep();
250
+ return this.sessions.size > 0;
251
+ }
252
+
253
+ /** Milliseconds of idleness left before the unlock lapses; 0 when locked. */
254
+ /** Milliseconds of idleness left before the LONGEST-lived unlock lapses; 0 when none
255
+ * is live. The status page counts down with it, so it is the most generous of the
256
+ * open devices rather than an arbitrary one. */
257
+ get remainingMs(): number {
258
+ if (!this.record) return 0;
259
+ this.sweep();
260
+ if (this.sessions.size === 0) return 0;
261
+ const newest = Math.max(...this.sessions.values());
262
+ return Math.max(0, newest + this.record.idleMs - this.now());
263
+ }
264
+
265
+ status(): MasterLockStatus {
266
+ return {
267
+ configured: this.configured,
268
+ enrollable: this.awaitingEnrollment,
269
+ locked: !this.unlocked,
270
+ kdf: this.kdf,
271
+ idleMs: this.idleMs,
272
+ remainingMs: this.remainingMs,
273
+ retryAfterMs: windowRemaining(this.failures, this.lastFailureAt, this.now()),
274
+ };
275
+ }
276
+
277
+ /** Does this request carry the live unlock? The cookie, or the header for a caller with
278
+ * no cookie jar. */
279
+ presents(req: Request): boolean {
280
+ this.sweep();
281
+ if (this.sessions.size === 0) return false;
282
+ // Compared against every live token in constant time, and deliberately WITHOUT a
283
+ // `Map.has` shortcut: a hash lookup on attacker-supplied bytes leaks by timing what
284
+ // the comparison is written to hide.
285
+ for (const presented of presentedTokens(req)) {
286
+ for (const live of this.sessions.keys()) {
287
+ if (constantTimeEqual(presented, live)) return true;
288
+ }
289
+ }
290
+ return false;
291
+ }
292
+
293
+ /**
294
+ * Evaluate a verifier and, on a match, mint the unlock.
295
+ *
296
+ * Runs an argon2 verify on EVERY path — no record, throttled, wrong — so the answer's
297
+ * latency says nothing the answer itself does not.
298
+ */
299
+ async unlock(verifier: string): Promise<MasterLockUnlockResult> {
300
+ const now = this.now();
301
+ const retryAfterMs = windowRemaining(this.failures, this.lastFailureAt, now);
302
+ const record = this.record;
303
+ // The equalizing verify happens first and unconditionally. `verifier` may be empty
304
+ // or absurd; `verify` neither throws on that nor tells us anything, which is the point.
305
+ const genuine =
306
+ retryAfterMs === 0 && record ? await verifyArgon(verifier, record.verifierHash) : null;
307
+ await equalize(verifier);
308
+
309
+ if (retryAfterMs > 0) return { ok: false, retryAfterMs };
310
+ // `null` is a hash this build cannot parse. It is NOT a wrong password, and saying so
311
+ // is what stops the owner re-typing a correct password forever — the exact day
312
+ // `apps/collections` lost to a quoted hash in its secrets file.
313
+ if (genuine === null) {
314
+ if (record) this.log("🔴 master lock: the stored verifier hash cannot be parsed — nothing can unlock");
315
+ return { ok: false, retryAfterMs: 0 };
316
+ }
317
+ if (!genuine) {
318
+ this.failures += 1;
319
+ this.lastFailureAt = now;
320
+ return { ok: false, retryAfterMs: windowRemaining(this.failures, this.lastFailureAt, now) };
321
+ }
322
+
323
+ this.failures = 0;
324
+ this.lastFailureAt = 0;
325
+ const token = randomToken();
326
+ const wasLocked = this.sessions.size === 0;
327
+ // ADDED, never replacing: the desk and the phone are the same person and both stay
328
+ // open. See the `sessions` note.
329
+ this.sessions.set(token, now);
330
+ this.scheduleExpiry();
331
+ if (wasLocked) this.onLockedChange?.(false);
332
+ return { ok: true, token };
333
+ }
334
+
335
+ /**
336
+ * Choose the FIRST master password for this lock — per-person mode only.
337
+ *
338
+ * ── Why this is not a hole ─────────────────────────────────────────────────
339
+ * It looks like "anyone who can reach the endpoint sets the password", and the three
340
+ * things that make it not are all checked here rather than at the caller:
341
+ *
342
+ * 1. **It refuses once a record exists.** There is no overwrite branch and no force
343
+ * flag; rotation is {@link change}, which demands the current password re-typed.
344
+ * So this can only ever create the password that was never set.
345
+ * 2. **It refuses outside per-person mode**, so mounting the route in an app-wide
346
+ * app cannot silently arm a lock the owner did not choose.
347
+ * 3. **The principal is resolved from the app's own session, not from the request
348
+ * body.** That is the guard's job (`resolve`), and it is why a stranger cannot
349
+ * enroll on somebody else's behalf: to reach this at all they must already hold a
350
+ * live signed-in session for the account they are enrolling, and if they hold
351
+ * that, the master lock was never what was standing between them and the app.
352
+ *
353
+ * The unlock is minted with the record, deliberately: being asked to type a password
354
+ * back at the person who just chose it, twice, teaches them the app is broken.
355
+ */
356
+ async enroll(input: {
357
+ kdf: MasterLockKdfParams;
358
+ verifier: string;
359
+ }): Promise<MasterLockEnrollResult> {
360
+ if (!this.enrollable) return { ok: false, reason: "not-enrollable" };
361
+ // Re-read the store rather than trusting the field: two tabs can submit the
362
+ // choose-a-password form at once, and the second must lose rather than replace.
363
+ if (this.record || readRecord(this.store.read())) {
364
+ this.record ??= readRecord(this.store.read());
365
+ return { ok: false, reason: "already-configured" };
366
+ }
367
+ if (!isMasterLockKdfParams(input.kdf) || typeof input.verifier !== "string" || !input.verifier) {
368
+ return { ok: false, reason: "invalid" };
369
+ }
370
+ const next: MasterLockRecord = {
371
+ kdf: input.kdf,
372
+ verifierHash: await Bun.password.hash(input.verifier, { algorithm: "argon2id" }),
373
+ idleMs: clampIdleMs(undefined),
374
+ };
375
+ this.record = next;
376
+ this.store.write(JSON.stringify(next));
377
+ this.failures = 0;
378
+ this.lastFailureAt = 0;
379
+ const token = randomToken();
380
+ this.sessions.set(token, this.now());
381
+ this.scheduleExpiry();
382
+ this.onLockedChange?.(false);
383
+ this.log("master lock: a first master password was chosen");
384
+ return { ok: true, token };
385
+ }
386
+
387
+ /**
388
+ * Record real user interaction. The ONE thing that moves the idle clock — see decision 2
389
+ * in the module note.
390
+ *
391
+ * A no-op on a lapsed session, deliberately: activity must never REVIVE an unlock, or a
392
+ * page left open would resurrect itself the moment the cat walked across the keyboard.
393
+ */
394
+ touch(req: Request): boolean {
395
+ this.sweep();
396
+ // Slides only the token that was presented. Two devices idle independently, so the
397
+ // phone left on the counter locks on its own schedule while the desk stays open —
398
+ // which is decision 2 applied per device rather than per person.
399
+ for (const presented of presentedTokens(req)) {
400
+ for (const live of this.sessions.keys()) {
401
+ if (constantTimeEqual(presented, live)) {
402
+ this.sessions.set(live, this.now());
403
+ this.scheduleExpiry();
404
+ return true;
405
+ }
406
+ }
407
+ }
408
+ return false;
409
+ }
410
+
411
+ /** The manual lock — the owner's *"I want a lock option to turn the site back to needing
412
+ * the master password"*. Also what a change of password does to every open page. */
413
+ lock(): void {
414
+ if (this.sessions.size === 0) return;
415
+ // EVERY device this person has open, not the one that pressed the button. The
416
+ // owner's requirement is "no one can see anything until the master password is
417
+ // entered again", and a lock that left the phone open would not be that.
418
+ this.sessions.clear();
419
+ this.clearExpiry();
420
+ this.onLockedChange?.(true);
421
+ }
422
+
423
+ /**
424
+ * Rotate this app's master password. Requires the CURRENT one, re-typed — an open session
425
+ * proves somebody is at the keyboard, not that it is the owner, and the whole feature
426
+ * exists for the minutes when it is not.
427
+ *
428
+ * Per-app by construction: the record this writes is this app's own. The owner asked for
429
+ * exactly that, and for there to be no way to change them all at once.
430
+ */
431
+ async change(input: {
432
+ currentVerifier: string;
433
+ kdf: MasterLockKdfParams;
434
+ verifier: string;
435
+ }): Promise<{ ok: true } | { ok: false; reason: "wrong" | "invalid" | "unconfigured" }> {
436
+ const record = this.record;
437
+ if (!record) return { ok: false, reason: "unconfigured" };
438
+ if (!isMasterLockKdfParams(input.kdf) || typeof input.verifier !== "string" || !input.verifier) {
439
+ return { ok: false, reason: "invalid" };
440
+ }
441
+ const genuine = await verifyArgon(input.currentVerifier, record.verifierHash);
442
+ await equalize(input.currentVerifier);
443
+ if (genuine === null) return { ok: false, reason: "unconfigured" };
444
+ if (!genuine) return { ok: false, reason: "wrong" };
445
+
446
+ const next: MasterLockRecord = {
447
+ kdf: input.kdf,
448
+ verifierHash: await Bun.password.hash(input.verifier, { algorithm: "argon2id" }),
449
+ idleMs: record.idleMs,
450
+ };
451
+ this.record = next;
452
+ this.store.write(JSON.stringify(next));
453
+ // Every open page dies with the old password — otherwise rotating it after a bad day
454
+ // leaves the session that worried you still holding the door.
455
+ this.lock();
456
+ this.log("master lock: master password rotated for this app");
457
+ return { ok: true };
458
+ }
459
+
460
+ /** The owner's *"customizable by me"* idle timeout, clamped to the usable band. */
461
+ setIdleMs(value: number): number {
462
+ const record = this.record;
463
+ const idleMs = clampIdleMs(value);
464
+ if (!record) return idleMs;
465
+ this.record = { ...record, idleMs };
466
+ this.store.write(JSON.stringify(this.record));
467
+ this.scheduleExpiry();
468
+ return idleMs;
469
+ }
470
+
471
+ /** Drop the in-memory unlock without notifying — for tests between cases. */
472
+ resetForTest(): void {
473
+ this.sessions.clear();
474
+ this.failures = 0;
475
+ this.lastFailureAt = 0;
476
+ this.clearExpiry();
477
+ }
478
+
479
+ /** Retire a session whose idle window has passed. Called on every read of the state, so
480
+ * correctness never depends on the timer having fired. */
481
+ private sweep(): void {
482
+ const record = this.record;
483
+ if (!record || this.sessions.size === 0) return;
484
+ const now = this.now();
485
+ const wasOpen = this.sessions.size > 0;
486
+ for (const [token, lastActivityAt] of this.sessions) {
487
+ // Each device expires on its OWN clock, so one going idle never shortens another.
488
+ if (now - lastActivityAt > record.idleMs) this.sessions.delete(token);
489
+ }
490
+ if (wasOpen && this.sessions.size === 0) {
491
+ this.clearExpiry();
492
+ this.onLockedChange?.(true);
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Wake up exactly when the unlock lapses.
498
+ *
499
+ * The sweep above already makes every READ correct, so this is not what enforces the
500
+ * timeout — it is what lets `onLockedChange` fire with nobody asking, which the
501
+ * binary-server route swap needs. `unref` where the runtime has it, so a timer never
502
+ * keeps a process alive.
503
+ */
504
+ private scheduleExpiry(): void {
505
+ this.clearExpiry();
506
+ const record = this.record;
507
+ if (!record || this.sessions.size === 0) return;
508
+ // The SOONEST lapse, so `onLockedChange` cannot be late for the device that goes
509
+ // first; the sweep it triggers re-arms for whichever is next.
510
+ const soonest = Math.min(...this.sessions.values());
511
+ const due = soonest + record.idleMs - this.now();
512
+ const timer = setTimeout(() => {
513
+ this.expiryTimer = null;
514
+ this.sweep();
515
+ }, Math.max(0, due) + 1);
516
+ (timer as { unref?: () => void }).unref?.();
517
+ this.expiryTimer = timer;
518
+ }
519
+
520
+ private clearExpiry(): void {
521
+ if (this.expiryTimer !== null) {
522
+ clearTimeout(this.expiryTimer);
523
+ this.expiryTimer = null;
524
+ }
525
+ }
526
+ }
527
+
528
+ /** Parse a stored/seeded record, or null. Every field is validated because all three
529
+ * sources — a database, an env seed, an older build — are outside this module. */
530
+ export function readRecord(json: string | null | undefined): MasterLockRecord | null {
531
+ if (!json) return null;
532
+ let parsed: unknown;
533
+ try {
534
+ // 🔴 `unquote` FIRST, on the whole blob. The seed is written into a 0600 file that a
535
+ // shell sources, and the value contains `$` (an argon2id hash does), so it is written
536
+ // single-quoted — correctly. Every reader in the fleet strips that pair, but "every
537
+ // reader" has been wrong before: `apps/collections` shipped a quoted hash to prod in
538
+ // 2026-08-12 and spent a day being told its correct passphrase was wrong. A JSON
539
+ // document can never legitimately begin with a quote character, so this guesses at
540
+ // nothing and closes the class rather than the instance.
541
+ parsed = JSON.parse(unquote(json));
542
+ } catch {
543
+ return null;
544
+ }
545
+ if (parsed === null || typeof parsed !== "object") return null;
546
+ const value = parsed as Record<string, unknown>;
547
+ if (!isMasterLockKdfParams(value.kdf)) return null;
548
+ const hash = typeof value.verifierHash === "string" ? unquote(value.verifierHash) : "";
549
+ if (!hash.startsWith("$argon2")) return null;
550
+ return { kdf: value.kdf, verifierHash: hash, idleMs: clampIdleMs(value.idleMs) };
551
+ }
552
+
553
+ /**
554
+ * Strip a wrapping quote pair.
555
+ *
556
+ * An argon2id PHC string contains `$`, so it is written SINGLE-QUOTED into the 0600 secrets
557
+ * file that a shell sources — and a `cut -d= -f2-` somewhere in the handover keeps the
558
+ * quotes. `apps/collections` lost a day to exactly that in 2026-08-12: the live site
559
+ * answered "that is not the passphrase" to the correct passphrase while its health check
560
+ * reported the lock armed. A hash can never legitimately begin with a quote, so nothing is
561
+ * guessed at here.
562
+ */
563
+ function unquote(value: string): string {
564
+ const trimmed = value.trim();
565
+ const first = trimmed[0];
566
+ if ((first === '"' || first === "'") && trimmed.endsWith(first) && trimmed.length > 1) {
567
+ return trimmed.slice(1, -1);
568
+ }
569
+ return trimmed;
570
+ }
571
+
572
+ /** `true`/`false` for a verdict, `null` for a hash this build cannot parse — a distinction
573
+ * the caller must keep, because the two send the owner to different places. */
574
+ async function verifyArgon(candidate: string, hash: string): Promise<boolean | null> {
575
+ try {
576
+ return await Bun.password.verify(candidate, hash);
577
+ } catch {
578
+ return null;
579
+ }
580
+ }
581
+
582
+ let equalizerHash: Promise<string> | null = null;
583
+ async function equalize(candidate: string): Promise<void> {
584
+ equalizerHash ??= Bun.password.hash("master-lock-timing-equalizer", { algorithm: "argon2id" });
585
+ await Bun.password.verify(candidate || "x", await equalizerHash).catch(() => false);
586
+ }
587
+
588
+ function randomToken(): string {
589
+ const bytes = new Uint8Array(TOKEN_BYTES);
590
+ crypto.getRandomValues(bytes);
591
+ return Buffer.from(bytes).toString("base64url");
592
+ }
593
+
594
+ /**
595
+ * Every token this request offers — the header a non-browser caller uses, and EVERY
596
+ * `master_lock*` cookie in the jar.
597
+ *
598
+ * 🔴 Every one of them, not just this app's: a browser on `localhost` holds one cookie per
599
+ * locked app (see `masterLockCookieName` for why the name has to be per app), and this
600
+ * function has no idea which name belongs to the instance asking. Offering them all is
601
+ * safe and is not a widening — each candidate still has to survive the constant-time
602
+ * comparison against THIS lock's own live tokens, which are per-instance random. A foreign
603
+ * app's token is simply a token that does not match, exactly like a stale one.
604
+ */
605
+ function presentedTokens(req: Request): string[] {
606
+ const out: string[] = [];
607
+ const header = req.headers.get("x-master-lock");
608
+ if (header) out.push(header);
609
+ const cookie = req.headers.get("cookie");
610
+ if (cookie) {
611
+ for (const part of cookie.split(";")) {
612
+ const [name, ...rest] = part.trim().split("=");
613
+ if (name === "master_lock" || name?.startsWith("master_lock_")) out.push(rest.join("="));
614
+ }
615
+ }
616
+ return out;
617
+ }
618
+
619
+ function constantTimeEqual(a: string, b: string): boolean {
620
+ const x = Buffer.from(a);
621
+ const y = Buffer.from(b);
622
+ // A length difference is already a non-match, and `timingSafeEqual` throws on one.
623
+ if (x.length !== y.length) return false;
624
+ let diff = 0;
625
+ for (let i = 0; i < x.length; i += 1) diff |= x[i] ^ y[i];
626
+ return diff === 0;
627
+ }