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,523 @@
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 { clampIdleMs, isMasterLockKdfParams, windowRemaining, } from "cursedbelt-core/master-lock";
51
+ const TOKEN_BYTES = 24;
52
+ /**
53
+ * One app's lock. Constructed once at boot and held for the process's life.
54
+ *
55
+ * There is exactly ONE unlock at a time, not one per browser session. That is deliberate
56
+ * and it is the owner's model: *"entering it in one site will only apply it to that site"* —
57
+ * the unit is the SITE, and these apps have exactly one user. Minting per-session unlocks
58
+ * would mean the phone in someone else's hand stays open after the owner locks the desk,
59
+ * which inverts the manual-lock requirement (*"then no one can see anything until the master
60
+ * password is entered again"*).
61
+ */
62
+ export class MasterLock {
63
+ store;
64
+ now;
65
+ onLockedChange;
66
+ log;
67
+ /** Per-person mode — see {@link MasterLockOptions.enrollable}. */
68
+ enrollable;
69
+ record;
70
+ /**
71
+ * The live unlocks: token → when that token last saw real interaction. Moved only by
72
+ * {@link touch}.
73
+ *
74
+ * 🔴 A MAP, not one session, since 2026-08-25 — and the reason is the per-person mode.
75
+ * App-wide, one unlock was right: one person, one site, and the owner's *"entering it
76
+ * in one site will only apply it to that site"*. Per person it is wrong twice over.
77
+ * The owner asked to *"put in prompts for the orch and monitor it from our phones and
78
+ * other devices"* — so the desk and the phone are both his, both open at once, and a
79
+ * single-token model would have each unlock silently evict the other. And the guard
80
+ * now demands a PRESENTED token rather than reading `unlocked` (see the guard's own
81
+ * note), which is only possible if every device that typed the password holds one.
82
+ *
83
+ * {@link lock} still clears them ALL, which is what keeps the owner's original
84
+ * requirement true: *"then no one can see anything until the master password is
85
+ * entered again"* — one button, every device.
86
+ */
87
+ sessions = new Map();
88
+ expiryTimer = null;
89
+ failures = 0;
90
+ lastFailureAt = 0;
91
+ constructor(options) {
92
+ this.store = options.store;
93
+ this.now = options.now ?? Date.now;
94
+ this.onLockedChange = options.onLockedChange;
95
+ this.log = options.log ?? (() => undefined);
96
+ this.enrollable = options.enrollable === true;
97
+ this.record = readRecord(this.store.read());
98
+ if (!this.record && options.seedJson) {
99
+ const seeded = readRecord(options.seedJson);
100
+ if (seeded) {
101
+ this.record = seeded;
102
+ this.store.write(JSON.stringify(seeded));
103
+ this.log("master lock: seeded from MASTER_LOCK_SEED (the vault's master password)");
104
+ }
105
+ else {
106
+ // Loud, because the alternative is an app that boots with no lock and looks
107
+ // exactly like an app that was never meant to have one.
108
+ this.log("🔴 master lock: MASTER_LOCK_SEED is set but unusable — the lock is NOT armed");
109
+ }
110
+ }
111
+ }
112
+ /** Is a master password set at all? An app with `false` here refuses nothing. */
113
+ get configured() {
114
+ return this.record !== null;
115
+ }
116
+ /** The params the browser must derive under, or null. Public by design. */
117
+ get kdf() {
118
+ return this.record?.kdf ?? null;
119
+ }
120
+ get idleMs() {
121
+ return this.record?.idleMs ?? clampIdleMs(undefined);
122
+ }
123
+ /** Is this lock in per-person mode? */
124
+ get isEnrollable() {
125
+ return this.enrollable;
126
+ }
127
+ /** May a first password be chosen right now? Per-person mode, and no record yet. */
128
+ get awaitingEnrollment() {
129
+ return this.enrollable && this.record === null;
130
+ }
131
+ /**
132
+ * Is the site open right now?
133
+ *
134
+ * 🔴 An UNCONFIGURED app-wide lock reads as UNLOCKED, and that is the deliberate
135
+ * choice. A lock with no password cannot be opened by anybody, including the owner —
136
+ * failing closed would brick an app rather than protect it, and this guards a UI whose
137
+ * data is already behind the app's own owner-only auth. `/healthz` reports
138
+ * `configured` so a smoke can catch the state externally instead of the owner
139
+ * discovering it.
140
+ *
141
+ * 🔴 In PER-PERSON mode the same reasoning runs the other way and this reads as
142
+ * LOCKED. Nobody is bricked — {@link enroll} is open to a principal with no record —
143
+ * and "has not chosen a password yet" describes every new account, so opening for it
144
+ * would admit exactly the people the wall exists to stop. See
145
+ * {@link MasterLockOptions.enrollable}.
146
+ */
147
+ get unlocked() {
148
+ if (!this.record)
149
+ return !this.enrollable;
150
+ this.sweep();
151
+ return this.sessions.size > 0;
152
+ }
153
+ /** Milliseconds of idleness left before the unlock lapses; 0 when locked. */
154
+ /** Milliseconds of idleness left before the LONGEST-lived unlock lapses; 0 when none
155
+ * is live. The status page counts down with it, so it is the most generous of the
156
+ * open devices rather than an arbitrary one. */
157
+ get remainingMs() {
158
+ if (!this.record)
159
+ return 0;
160
+ this.sweep();
161
+ if (this.sessions.size === 0)
162
+ return 0;
163
+ const newest = Math.max(...this.sessions.values());
164
+ return Math.max(0, newest + this.record.idleMs - this.now());
165
+ }
166
+ status() {
167
+ return {
168
+ configured: this.configured,
169
+ enrollable: this.awaitingEnrollment,
170
+ locked: !this.unlocked,
171
+ kdf: this.kdf,
172
+ idleMs: this.idleMs,
173
+ remainingMs: this.remainingMs,
174
+ retryAfterMs: windowRemaining(this.failures, this.lastFailureAt, this.now()),
175
+ };
176
+ }
177
+ /** Does this request carry the live unlock? The cookie, or the header for a caller with
178
+ * no cookie jar. */
179
+ presents(req) {
180
+ this.sweep();
181
+ if (this.sessions.size === 0)
182
+ return false;
183
+ // Compared against every live token in constant time, and deliberately WITHOUT a
184
+ // `Map.has` shortcut: a hash lookup on attacker-supplied bytes leaks by timing what
185
+ // the comparison is written to hide.
186
+ for (const presented of presentedTokens(req)) {
187
+ for (const live of this.sessions.keys()) {
188
+ if (constantTimeEqual(presented, live))
189
+ return true;
190
+ }
191
+ }
192
+ return false;
193
+ }
194
+ /**
195
+ * Evaluate a verifier and, on a match, mint the unlock.
196
+ *
197
+ * Runs an argon2 verify on EVERY path — no record, throttled, wrong — so the answer's
198
+ * latency says nothing the answer itself does not.
199
+ */
200
+ async unlock(verifier) {
201
+ const now = this.now();
202
+ const retryAfterMs = windowRemaining(this.failures, this.lastFailureAt, now);
203
+ const record = this.record;
204
+ // The equalizing verify happens first and unconditionally. `verifier` may be empty
205
+ // or absurd; `verify` neither throws on that nor tells us anything, which is the point.
206
+ const genuine = retryAfterMs === 0 && record ? await verifyArgon(verifier, record.verifierHash) : null;
207
+ await equalize(verifier);
208
+ if (retryAfterMs > 0)
209
+ return { ok: false, retryAfterMs };
210
+ // `null` is a hash this build cannot parse. It is NOT a wrong password, and saying so
211
+ // is what stops the owner re-typing a correct password forever — the exact day
212
+ // `apps/collections` lost to a quoted hash in its secrets file.
213
+ if (genuine === null) {
214
+ if (record)
215
+ this.log("🔴 master lock: the stored verifier hash cannot be parsed — nothing can unlock");
216
+ return { ok: false, retryAfterMs: 0 };
217
+ }
218
+ if (!genuine) {
219
+ this.failures += 1;
220
+ this.lastFailureAt = now;
221
+ return { ok: false, retryAfterMs: windowRemaining(this.failures, this.lastFailureAt, now) };
222
+ }
223
+ this.failures = 0;
224
+ this.lastFailureAt = 0;
225
+ const token = randomToken();
226
+ const wasLocked = this.sessions.size === 0;
227
+ // ADDED, never replacing: the desk and the phone are the same person and both stay
228
+ // open. See the `sessions` note.
229
+ this.sessions.set(token, now);
230
+ this.scheduleExpiry();
231
+ if (wasLocked)
232
+ this.onLockedChange?.(false);
233
+ return { ok: true, token };
234
+ }
235
+ /**
236
+ * Choose the FIRST master password for this lock — per-person mode only.
237
+ *
238
+ * ── Why this is not a hole ─────────────────────────────────────────────────
239
+ * It looks like "anyone who can reach the endpoint sets the password", and the three
240
+ * things that make it not are all checked here rather than at the caller:
241
+ *
242
+ * 1. **It refuses once a record exists.** There is no overwrite branch and no force
243
+ * flag; rotation is {@link change}, which demands the current password re-typed.
244
+ * So this can only ever create the password that was never set.
245
+ * 2. **It refuses outside per-person mode**, so mounting the route in an app-wide
246
+ * app cannot silently arm a lock the owner did not choose.
247
+ * 3. **The principal is resolved from the app's own session, not from the request
248
+ * body.** That is the guard's job (`resolve`), and it is why a stranger cannot
249
+ * enroll on somebody else's behalf: to reach this at all they must already hold a
250
+ * live signed-in session for the account they are enrolling, and if they hold
251
+ * that, the master lock was never what was standing between them and the app.
252
+ *
253
+ * The unlock is minted with the record, deliberately: being asked to type a password
254
+ * back at the person who just chose it, twice, teaches them the app is broken.
255
+ */
256
+ async enroll(input) {
257
+ if (!this.enrollable)
258
+ return { ok: false, reason: "not-enrollable" };
259
+ // Re-read the store rather than trusting the field: two tabs can submit the
260
+ // choose-a-password form at once, and the second must lose rather than replace.
261
+ if (this.record || readRecord(this.store.read())) {
262
+ this.record ??= readRecord(this.store.read());
263
+ return { ok: false, reason: "already-configured" };
264
+ }
265
+ if (!isMasterLockKdfParams(input.kdf) || typeof input.verifier !== "string" || !input.verifier) {
266
+ return { ok: false, reason: "invalid" };
267
+ }
268
+ const next = {
269
+ kdf: input.kdf,
270
+ verifierHash: await Bun.password.hash(input.verifier, { algorithm: "argon2id" }),
271
+ idleMs: clampIdleMs(undefined),
272
+ };
273
+ this.record = next;
274
+ this.store.write(JSON.stringify(next));
275
+ this.failures = 0;
276
+ this.lastFailureAt = 0;
277
+ const token = randomToken();
278
+ this.sessions.set(token, this.now());
279
+ this.scheduleExpiry();
280
+ this.onLockedChange?.(false);
281
+ this.log("master lock: a first master password was chosen");
282
+ return { ok: true, token };
283
+ }
284
+ /**
285
+ * Record real user interaction. The ONE thing that moves the idle clock — see decision 2
286
+ * in the module note.
287
+ *
288
+ * A no-op on a lapsed session, deliberately: activity must never REVIVE an unlock, or a
289
+ * page left open would resurrect itself the moment the cat walked across the keyboard.
290
+ */
291
+ touch(req) {
292
+ this.sweep();
293
+ // Slides only the token that was presented. Two devices idle independently, so the
294
+ // phone left on the counter locks on its own schedule while the desk stays open —
295
+ // which is decision 2 applied per device rather than per person.
296
+ for (const presented of presentedTokens(req)) {
297
+ for (const live of this.sessions.keys()) {
298
+ if (constantTimeEqual(presented, live)) {
299
+ this.sessions.set(live, this.now());
300
+ this.scheduleExpiry();
301
+ return true;
302
+ }
303
+ }
304
+ }
305
+ return false;
306
+ }
307
+ /** The manual lock — the owner's *"I want a lock option to turn the site back to needing
308
+ * the master password"*. Also what a change of password does to every open page. */
309
+ lock() {
310
+ if (this.sessions.size === 0)
311
+ return;
312
+ // EVERY device this person has open, not the one that pressed the button. The
313
+ // owner's requirement is "no one can see anything until the master password is
314
+ // entered again", and a lock that left the phone open would not be that.
315
+ this.sessions.clear();
316
+ this.clearExpiry();
317
+ this.onLockedChange?.(true);
318
+ }
319
+ /**
320
+ * Rotate this app's master password. Requires the CURRENT one, re-typed — an open session
321
+ * proves somebody is at the keyboard, not that it is the owner, and the whole feature
322
+ * exists for the minutes when it is not.
323
+ *
324
+ * Per-app by construction: the record this writes is this app's own. The owner asked for
325
+ * exactly that, and for there to be no way to change them all at once.
326
+ */
327
+ async change(input) {
328
+ const record = this.record;
329
+ if (!record)
330
+ return { ok: false, reason: "unconfigured" };
331
+ if (!isMasterLockKdfParams(input.kdf) || typeof input.verifier !== "string" || !input.verifier) {
332
+ return { ok: false, reason: "invalid" };
333
+ }
334
+ const genuine = await verifyArgon(input.currentVerifier, record.verifierHash);
335
+ await equalize(input.currentVerifier);
336
+ if (genuine === null)
337
+ return { ok: false, reason: "unconfigured" };
338
+ if (!genuine)
339
+ return { ok: false, reason: "wrong" };
340
+ const next = {
341
+ kdf: input.kdf,
342
+ verifierHash: await Bun.password.hash(input.verifier, { algorithm: "argon2id" }),
343
+ idleMs: record.idleMs,
344
+ };
345
+ this.record = next;
346
+ this.store.write(JSON.stringify(next));
347
+ // Every open page dies with the old password — otherwise rotating it after a bad day
348
+ // leaves the session that worried you still holding the door.
349
+ this.lock();
350
+ this.log("master lock: master password rotated for this app");
351
+ return { ok: true };
352
+ }
353
+ /** The owner's *"customizable by me"* idle timeout, clamped to the usable band. */
354
+ setIdleMs(value) {
355
+ const record = this.record;
356
+ const idleMs = clampIdleMs(value);
357
+ if (!record)
358
+ return idleMs;
359
+ this.record = { ...record, idleMs };
360
+ this.store.write(JSON.stringify(this.record));
361
+ this.scheduleExpiry();
362
+ return idleMs;
363
+ }
364
+ /** Drop the in-memory unlock without notifying — for tests between cases. */
365
+ resetForTest() {
366
+ this.sessions.clear();
367
+ this.failures = 0;
368
+ this.lastFailureAt = 0;
369
+ this.clearExpiry();
370
+ }
371
+ /** Retire a session whose idle window has passed. Called on every read of the state, so
372
+ * correctness never depends on the timer having fired. */
373
+ sweep() {
374
+ const record = this.record;
375
+ if (!record || this.sessions.size === 0)
376
+ return;
377
+ const now = this.now();
378
+ const wasOpen = this.sessions.size > 0;
379
+ for (const [token, lastActivityAt] of this.sessions) {
380
+ // Each device expires on its OWN clock, so one going idle never shortens another.
381
+ if (now - lastActivityAt > record.idleMs)
382
+ this.sessions.delete(token);
383
+ }
384
+ if (wasOpen && this.sessions.size === 0) {
385
+ this.clearExpiry();
386
+ this.onLockedChange?.(true);
387
+ }
388
+ }
389
+ /**
390
+ * Wake up exactly when the unlock lapses.
391
+ *
392
+ * The sweep above already makes every READ correct, so this is not what enforces the
393
+ * timeout — it is what lets `onLockedChange` fire with nobody asking, which the
394
+ * binary-server route swap needs. `unref` where the runtime has it, so a timer never
395
+ * keeps a process alive.
396
+ */
397
+ scheduleExpiry() {
398
+ this.clearExpiry();
399
+ const record = this.record;
400
+ if (!record || this.sessions.size === 0)
401
+ return;
402
+ // The SOONEST lapse, so `onLockedChange` cannot be late for the device that goes
403
+ // first; the sweep it triggers re-arms for whichever is next.
404
+ const soonest = Math.min(...this.sessions.values());
405
+ const due = soonest + record.idleMs - this.now();
406
+ const timer = setTimeout(() => {
407
+ this.expiryTimer = null;
408
+ this.sweep();
409
+ }, Math.max(0, due) + 1);
410
+ timer.unref?.();
411
+ this.expiryTimer = timer;
412
+ }
413
+ clearExpiry() {
414
+ if (this.expiryTimer !== null) {
415
+ clearTimeout(this.expiryTimer);
416
+ this.expiryTimer = null;
417
+ }
418
+ }
419
+ }
420
+ /** Parse a stored/seeded record, or null. Every field is validated because all three
421
+ * sources — a database, an env seed, an older build — are outside this module. */
422
+ export function readRecord(json) {
423
+ if (!json)
424
+ return null;
425
+ let parsed;
426
+ try {
427
+ // 🔴 `unquote` FIRST, on the whole blob. The seed is written into a 0600 file that a
428
+ // shell sources, and the value contains `$` (an argon2id hash does), so it is written
429
+ // single-quoted — correctly. Every reader in the fleet strips that pair, but "every
430
+ // reader" has been wrong before: `apps/collections` shipped a quoted hash to prod in
431
+ // 2026-08-12 and spent a day being told its correct passphrase was wrong. A JSON
432
+ // document can never legitimately begin with a quote character, so this guesses at
433
+ // nothing and closes the class rather than the instance.
434
+ parsed = JSON.parse(unquote(json));
435
+ }
436
+ catch {
437
+ return null;
438
+ }
439
+ if (parsed === null || typeof parsed !== "object")
440
+ return null;
441
+ const value = parsed;
442
+ if (!isMasterLockKdfParams(value.kdf))
443
+ return null;
444
+ const hash = typeof value.verifierHash === "string" ? unquote(value.verifierHash) : "";
445
+ if (!hash.startsWith("$argon2"))
446
+ return null;
447
+ return { kdf: value.kdf, verifierHash: hash, idleMs: clampIdleMs(value.idleMs) };
448
+ }
449
+ /**
450
+ * Strip a wrapping quote pair.
451
+ *
452
+ * An argon2id PHC string contains `$`, so it is written SINGLE-QUOTED into the 0600 secrets
453
+ * file that a shell sources — and a `cut -d= -f2-` somewhere in the handover keeps the
454
+ * quotes. `apps/collections` lost a day to exactly that in 2026-08-12: the live site
455
+ * answered "that is not the passphrase" to the correct passphrase while its health check
456
+ * reported the lock armed. A hash can never legitimately begin with a quote, so nothing is
457
+ * guessed at here.
458
+ */
459
+ function unquote(value) {
460
+ const trimmed = value.trim();
461
+ const first = trimmed[0];
462
+ if ((first === '"' || first === "'") && trimmed.endsWith(first) && trimmed.length > 1) {
463
+ return trimmed.slice(1, -1);
464
+ }
465
+ return trimmed;
466
+ }
467
+ /** `true`/`false` for a verdict, `null` for a hash this build cannot parse — a distinction
468
+ * the caller must keep, because the two send the owner to different places. */
469
+ async function verifyArgon(candidate, hash) {
470
+ try {
471
+ return await Bun.password.verify(candidate, hash);
472
+ }
473
+ catch {
474
+ return null;
475
+ }
476
+ }
477
+ let equalizerHash = null;
478
+ async function equalize(candidate) {
479
+ equalizerHash ??= Bun.password.hash("master-lock-timing-equalizer", { algorithm: "argon2id" });
480
+ await Bun.password.verify(candidate || "x", await equalizerHash).catch(() => false);
481
+ }
482
+ function randomToken() {
483
+ const bytes = new Uint8Array(TOKEN_BYTES);
484
+ crypto.getRandomValues(bytes);
485
+ return Buffer.from(bytes).toString("base64url");
486
+ }
487
+ /**
488
+ * Every token this request offers — the header a non-browser caller uses, and EVERY
489
+ * `master_lock*` cookie in the jar.
490
+ *
491
+ * 🔴 Every one of them, not just this app's: a browser on `localhost` holds one cookie per
492
+ * locked app (see `masterLockCookieName` for why the name has to be per app), and this
493
+ * function has no idea which name belongs to the instance asking. Offering them all is
494
+ * safe and is not a widening — each candidate still has to survive the constant-time
495
+ * comparison against THIS lock's own live tokens, which are per-instance random. A foreign
496
+ * app's token is simply a token that does not match, exactly like a stale one.
497
+ */
498
+ function presentedTokens(req) {
499
+ const out = [];
500
+ const header = req.headers.get("x-master-lock");
501
+ if (header)
502
+ out.push(header);
503
+ const cookie = req.headers.get("cookie");
504
+ if (cookie) {
505
+ for (const part of cookie.split(";")) {
506
+ const [name, ...rest] = part.trim().split("=");
507
+ if (name === "master_lock" || name?.startsWith("master_lock_"))
508
+ out.push(rest.join("="));
509
+ }
510
+ }
511
+ return out;
512
+ }
513
+ function constantTimeEqual(a, b) {
514
+ const x = Buffer.from(a);
515
+ const y = Buffer.from(b);
516
+ // A length difference is already a non-match, and `timingSafeEqual` throws on one.
517
+ if (x.length !== y.length)
518
+ return false;
519
+ let diff = 0;
520
+ for (let i = 0; i < x.length; i += 1)
521
+ diff |= x[i] ^ y[i];
522
+ return diff === 0;
523
+ }
@@ -0,0 +1,76 @@
1
+ import { MasterLock } from "./masterLock";
2
+ /** Namespace for every per-person record, so one glance at the table says what they are. */
3
+ export declare const MASTER_LOCK_PRINCIPAL_KEY_PREFIX = "master_lock:principal:";
4
+ /**
5
+ * The storage key for one principal.
6
+ *
7
+ * SHA-256 hex of the id — see property 2 in the module note. It is not a security
8
+ * boundary (the id is not a secret and the hash is not salted); it is a SHAPE boundary,
9
+ * which is the thing that was actually at risk.
10
+ */
11
+ export declare function masterLockPrincipalKey(principalId: string): string;
12
+ export interface MasterLockDirectoryOptions {
13
+ /**
14
+ * The app's key/value surface. One row per principal, written under
15
+ * {@link masterLockPrincipalKey} — no migration, same shape as the app-wide record.
16
+ */
17
+ kv: {
18
+ get(key: string): string | null | undefined;
19
+ set(key: string, value: string): void;
20
+ };
21
+ /** Injected clock. The tests drive it; production omits it. */
22
+ now?: () => number;
23
+ /** Where to log a rotation or a first enrollment. Receives the principal LABEL. */
24
+ log?: (line: string) => void;
25
+ }
26
+ /**
27
+ * Every person's lock, made on first sight and held for the process's life.
28
+ *
29
+ * Locks live in memory alongside their unlock, exactly as the single-app engine's do, so
30
+ * a restart re-locks everybody. That is the correct default for a wall whose whole risk
31
+ * model is "this browser is somewhere it should not be".
32
+ */
33
+ export declare class MasterLockDirectory {
34
+ private readonly locks;
35
+ private readonly options;
36
+ constructor(options: MasterLockDirectoryOptions);
37
+ /**
38
+ * This principal's lock, created on first sight.
39
+ *
40
+ * 🔴 `principalId` must come from the request's own verified session and never from a
41
+ * header, a query parameter or a body field. The guard's `resolve` is the one place
42
+ * that decides it; see property 1 in the module note.
43
+ */
44
+ for(principalId: string): MasterLock;
45
+ /** Has this principal chosen a master password? Reads storage, so it is true of a
46
+ * person who enrolled in a previous boot and has not been seen in this one. */
47
+ isEnrolled(principalId: string): boolean;
48
+ /**
49
+ * Install a PRE-DERIVED record for one principal, if and only if it has none.
50
+ *
51
+ * ── What this is for, and what it is not ───────────────────────────────────
52
+ * A machine account — `apps/orch`'s deploy smoke is the only one so far. Its master
53
+ * password lives 0600 on the owner's Mac, so there is no browser to type it in, and
54
+ * the alternative shapes are both worse: letting the smoke `POST /__lock/enroll` makes
55
+ * it WRITE to a live production site (which `scripts/smokeCleanup.test.ts` refuses, on
56
+ * the owner's rule), and giving the smoke a bypass makes the wall unproven.
57
+ *
58
+ * 🔴 It is NOT an opening and cannot become one. `json` is `{kdf, verifierHash}` —
59
+ * already derived, the same shape `MASTER_LOCK_SEED` has carried since 2026-08-15 — so
60
+ * no password passes through this process, and whoever unlocks still has to present a
61
+ * verifier that matches. Seeding an account only decides that it HAS a password, never
62
+ * that anybody may skip it.
63
+ *
64
+ * Refuses when a record already exists, so it can never rotate one out from under the
65
+ * person holding it. Returns whether it installed anything.
66
+ */
67
+ seed(principalId: string, json: string): boolean;
68
+ /** Lock every open session — the "I am handing this laptop over" button, fleet-wide.
69
+ * Only ever reaches principals seen since boot, which is all that can be unlocked. */
70
+ lockAll(): void;
71
+ /** How many principals hold a live unlock right now. For `/healthz` and the boot log;
72
+ * a COUNT discloses nothing about who. */
73
+ unlockedCount(): number;
74
+ /** Drop every in-memory lock — for tests between cases. */
75
+ resetForTest(): void;
76
+ }