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,236 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { resolveAppDataHome } from "../paths/appDataHome";
6
+ import type { RetentionDeclaration } from "./policy";
7
+ import {
8
+ publishRetention,
9
+ readRetentionOverrides,
10
+ readRetentionRegistry,
11
+ resolveRetentionHome,
12
+ setRetentionHomeForTests,
13
+ writeRetentionOverride,
14
+ } from "./store";
15
+
16
+ const decl = (app: string, id: string): RetentionDeclaration => ({
17
+ app,
18
+ id,
19
+ label: id,
20
+ category: "run-log",
21
+ unit: "rows",
22
+ location: `${app}.sqlite · ${id}`,
23
+ consequence: "past run outcomes",
24
+ });
25
+
26
+ let home: string;
27
+
28
+ beforeEach(() => {
29
+ home = mkdtempSync(join(tmpdir(), "retention-home-"));
30
+ setRetentionHomeForTests(home);
31
+ });
32
+ afterEach(() => {
33
+ setRetentionHomeForTests(null);
34
+ rmSync(home, { recursive: true, force: true });
35
+ });
36
+
37
+ describe("publications — one file per app", () => {
38
+ test("an app publishes and can be read back", () => {
39
+ expect(
40
+ publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "events") }] }),
41
+ ).toBe(true);
42
+ const registry = readRetentionRegistry(home);
43
+ expect(registry).toHaveLength(1);
44
+ expect(registry[0]?.app).toBe("orch");
45
+ expect(registry[0]?.resources[0]?.declaration.id).toBe("events");
46
+ expect(registry[0]?.publishedAt).toBeGreaterThan(0);
47
+ });
48
+
49
+ test("two apps publishing concurrently cannot lose each other's records", () => {
50
+ // The reason this is one file per app: a shared registry that concurrent
51
+ // writers read-modify-write loses records, which is the exact bug the agent
52
+ // coordination board is one-file-per-record to avoid.
53
+ publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "events") }] });
54
+ publishRetention({ app: "notes", resources: [{ declaration: decl("notes", "ingest-runs") }] });
55
+ expect(
56
+ readRetentionRegistry(home)
57
+ .map((p) => p.app)
58
+ .sort(),
59
+ ).toEqual(["notes", "orch"]);
60
+ });
61
+
62
+ test("republishing replaces only that app's file", () => {
63
+ publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "events") }] });
64
+ publishRetention({ app: "notes", resources: [{ declaration: decl("notes", "ingest-runs") }] });
65
+ publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "job-runs") }] });
66
+ const orch = readRetentionRegistry(home).find((p) => p.app === "orch");
67
+ expect(orch?.resources.map((r) => r.declaration.id)).toEqual(["job-runs"]);
68
+ expect(readRetentionRegistry(home)).toHaveLength(2);
69
+ });
70
+
71
+ test("an app key that is not a safe file name is refused", () => {
72
+ expect(publishRetention({ app: "../escape", resources: [] })).toBe(false);
73
+ expect(readRetentionRegistry(home)).toHaveLength(0);
74
+ });
75
+
76
+ test("a corrupt publication is skipped, never fatal to the whole view", () => {
77
+ publishRetention({ app: "orch", resources: [{ declaration: decl("orch", "events") }] });
78
+ writeFileSync(join(home, "apps", "broken.json"), "{ this is not json");
79
+ expect(readRetentionRegistry(home).map((p) => p.app)).toEqual(["orch"]);
80
+ });
81
+
82
+ test("publications carry the last sweep so the UI can show it while the app is down", () => {
83
+ publishRetention({
84
+ app: "orch",
85
+ instance: "local",
86
+ resources: [
87
+ {
88
+ declaration: decl("orch", "events"),
89
+ measurement: { items: 3000, bytes: 4096 },
90
+ lastSweep: { at: 123, aged: 12, overflow: 0 },
91
+ },
92
+ ],
93
+ });
94
+ const row = readRetentionRegistry(home)[0]?.resources[0];
95
+ expect(row?.measurement).toEqual({ items: 3000, bytes: 4096 });
96
+ expect(row?.lastSweep).toEqual({ at: 123, aged: 12, overflow: 0 });
97
+ });
98
+ });
99
+
100
+ describe("overrides — the single-writer file", () => {
101
+ test("a write round-trips and stamps who and when", () => {
102
+ writeRetentionOverride("orch", "events", { days: 30 }, { home, updatedBy: "curt", now: 5 });
103
+ const file = readRetentionOverrides(home);
104
+ expect(file.overrides["orch:events"]).toMatchObject({
105
+ days: 30,
106
+ updatedBy: "curt",
107
+ updatedAt: 5,
108
+ });
109
+ expect(file.updatedAt).toBe(5);
110
+ });
111
+
112
+ test("editing one resource never loses another's setting", () => {
113
+ writeRetentionOverride("orch", "events", { days: 30 }, { home });
114
+ writeRetentionOverride("notes", "ingest-runs", { days: 3 }, { home });
115
+ const file = readRetentionOverrides(home);
116
+ expect(file.overrides["orch:events"]?.days).toBe(30);
117
+ expect(file.overrides["notes:ingest-runs"]?.days).toBe(3);
118
+ });
119
+
120
+ test("null CLEARS a field so the declaration's default becomes authoritative again", () => {
121
+ // Storing a value that merely equals today's default would freeze the
122
+ // resource at that number if the default ever moved.
123
+ writeRetentionOverride("orch", "events", { days: 30, maxItems: 500 }, { home });
124
+ writeRetentionOverride("orch", "events", { days: null }, { home });
125
+ expect(readRetentionOverrides(home).overrides["orch:events"]).toMatchObject({ maxItems: 500 });
126
+ expect(readRetentionOverrides(home).overrides["orch:events"]?.days).toBeUndefined();
127
+ });
128
+
129
+ test("a null patch removes the whole override — the UI's Reset", () => {
130
+ writeRetentionOverride("orch", "events", { days: 30 }, { home });
131
+ writeRetentionOverride("orch", "events", null, { home });
132
+ expect(readRetentionOverrides(home).overrides["orch:events"]).toBeUndefined();
133
+ });
134
+
135
+ test("an override left with nothing meaningful in it is dropped, not stored empty", () => {
136
+ writeRetentionOverride("orch", "events", { days: 30 }, { home });
137
+ writeRetentionOverride("orch", "events", { days: null }, { home });
138
+ expect(readRetentionOverrides(home).overrides["orch:events"]).toBeUndefined();
139
+ });
140
+
141
+ test("garbage values are ignored rather than written through to a sweep", () => {
142
+ writeRetentionOverride("orch", "events", { days: "thirty", maxItems: Number.NaN }, { home });
143
+ expect(readRetentionOverrides(home).overrides["orch:events"]).toBeUndefined();
144
+ });
145
+
146
+ test("enabled:false is storable and distinct from an absent value", () => {
147
+ writeRetentionOverride("orch", "events", { enabled: false }, { home });
148
+ expect(readRetentionOverrides(home).overrides["orch:events"]?.enabled).toBe(false);
149
+ });
150
+
151
+ test("a corrupt overrides file reads as no overrides — never a boot failure", () => {
152
+ writeFileSync(join(home, "overrides.json"), "}{");
153
+ expect(readRetentionOverrides(home).overrides).toEqual({});
154
+ });
155
+
156
+ test("the file is written whole and valid — a reader never sees half of it", () => {
157
+ writeRetentionOverride("orch", "events", { days: 30, note: "keeping a month" }, { home });
158
+ const raw = readFileSync(join(home, "overrides.json"), "utf8");
159
+ expect(() => JSON.parse(raw)).not.toThrow();
160
+ expect(raw.endsWith("\n")).toBe(true);
161
+ });
162
+ });
163
+
164
+ describe("resolveRetentionHome", () => {
165
+ afterEach(() => setRetentionHomeForTests(home));
166
+
167
+ test("RETENTION_HOME wins outright", () => {
168
+ setRetentionHomeForTests(null);
169
+ const explicit = join(home, "explicit");
170
+ expect(resolveRetentionHome({ RETENTION_HOME: explicit })).toBe(explicit);
171
+ });
172
+
173
+ test("APP_DATA_HOME — the one env var — is honored before any convention", () => {
174
+ setRetentionHomeForTests(null);
175
+ expect(resolveRetentionHome({ APP_DATA_HOME: home })).toBe(join(home, "retention"));
176
+ });
177
+
178
+ test("a configured home is used even before it exists — naming it is the statement", () => {
179
+ setRetentionHomeForTests(null);
180
+ const fresh = join(home, "not-yet");
181
+ expect(resolveRetentionHome({ APP_DATA_HOME: fresh, APP_DATA_DIR: join(home, "data") })).toBe(
182
+ join(fresh, "retention"),
183
+ );
184
+ });
185
+
186
+ test("the shared OS-convention home wins over APP_DATA_DIR so the station sees ONE fleet", () => {
187
+ // An app's own APP_DATA_DIR is private to it; publishing there instead of the
188
+ // shared home would hide it from every other app's view of the fleet.
189
+ setRetentionHomeForTests(null);
190
+ const conventional = resolveAppDataHome({ env: { HOME: home } });
191
+ mkdirSync(conventional, { recursive: true });
192
+ expect(resolveRetentionHome({ HOME: home, APP_DATA_DIR: join(home, "data") })).toBe(
193
+ join(conventional, "retention"),
194
+ );
195
+ });
196
+
197
+ test("a deployed box — no shared data home — falls through to APP_DATA_DIR", () => {
198
+ // prod-ec2 has no shared data home, and under ProtectSystem=strict only the
199
+ // data dir is writable. The app still sweeps; its fleet is that box's.
200
+ setRetentionHomeForTests(null);
201
+ const dataDir = join(home, "data");
202
+ expect(
203
+ resolveRetentionHome({ HOME: join(home, "nonexistent-home"), APP_DATA_DIR: dataDir }),
204
+ ).toBe(join(dataDir, "retention"));
205
+ });
206
+
207
+ test("an unwritable candidate is probed and skipped, never assumed", () => {
208
+ setRetentionHomeForTests(null);
209
+ const dataDir = join(home, "data2");
210
+ expect(
211
+ resolveRetentionHome({
212
+ APP_DATA_HOME: "/proc/definitely-not-writable",
213
+ HOME: join(home, "nonexistent-home"),
214
+ APP_DATA_DIR: dataDir,
215
+ }),
216
+ ).toBe(join(dataDir, "retention"));
217
+ });
218
+
219
+ /**
220
+ * 🔴 The published defect, asserted on the OUTPUT rather than on the source: up
221
+ * to 2026-09-13 this resolved a hardcoded dotfolder in the owner's home, so
222
+ * every installer of `cursedbelt` got a library reaching into a directory that
223
+ * had nothing to do with them.
224
+ */
225
+ test("resolves nothing from a previous generation, whatever the env says", () => {
226
+ for (const env of [
227
+ { HOME: join(home, "nonexistent-home") },
228
+ { HOME: join(home, "nonexistent-home"), APP_MANAGED_HOME: "/nowhere/inherited" },
229
+ ]) {
230
+ setRetentionHomeForTests(null);
231
+ const resolved = resolveRetentionHome(env);
232
+ expect(resolved).not.toContain("app-managed");
233
+ expect(resolved).not.toContain("/nowhere/inherited");
234
+ }
235
+ });
236
+ });
@@ -0,0 +1,338 @@
1
+ /**
2
+ * The fleet's retention SOURCE OF TRUTH on disk — where declarations are
3
+ * published and where the owner's overrides are read from.
4
+ *
5
+ * ── Why a file and not an API ───────────────────────────────────────────────
6
+ * The station has to show, and let the owner change, the policy for apps that
7
+ * are not running. Most satellites are started on demand; `orch` is a daemon but
8
+ * `notes`, `roms`, `vault` and the rest are usually down, and the whole point of
9
+ * the UI is to answer "what is keeping my disk full" without booting anything.
10
+ * An HTTP endpoint per app could not answer that, would need an auth model per
11
+ * app, and would make a stopped app look policy-less rather than idle.
12
+ *
13
+ * So an app PUBLISHES its declarations (a description of what it owns, plus what
14
+ * it last measured and swept) and READS overrides. The station reads every
15
+ * publication and is the only writer of overrides. Nothing has to be up for the
16
+ * owner to see the fleet, and no app parses another app's database.
17
+ *
18
+ * ── One file per app, deliberately ──────────────────────────────────────────
19
+ * `apps/<app>.json`, never one shared registry file. Several satellites boot at
20
+ * once (the station's start-all, a `bun run dev` fan-out, launchd at login), and
21
+ * a shared file that concurrent writers read-modify-write loses records — the
22
+ * exact bug the agent-coordination board is one-file-per-record to avoid. Each
23
+ * app owns exactly one path and can only ever clobber itself.
24
+ *
25
+ * `overrides.json` IS a single file, because it has a single writer (the
26
+ * station) and because the owner's settings for the whole fleet are one thing to
27
+ * back up, copy to a box, or read at a glance.
28
+ *
29
+ * ── Where it lives ──────────────────────────────────────────────────────────
30
+ * `<app-data-home>/retention` — one root shared by every local app, so the
31
+ * station sees one fleet. The home is CONFIGURATION ({@link resolveAppDataHome}):
32
+ * `$APP_DATA_HOME` when a machine keeps every app's data under one root, else the
33
+ * OS's own convention (`$XDG_DATA_HOME`, `~/Library/Application Support`).
34
+ *
35
+ * 🔴 It was `~/<a private dotfolder>` until 2026-09-13, hardcoded, in a PUBLISHED
36
+ * package — so anyone who installed cursedbelt got a library hunting for one
37
+ * person's directory layout. A library may know what its caller told it and what
38
+ * the OS says; it may not know the name of a folder in somebody's home.
39
+ *
40
+ * A deployed box under `ProtectSystem=strict` has only its own data dir writable,
41
+ * so it falls back to `$APP_DATA_DIR/retention`: the app still sweeps on the same
42
+ * code and the same defaults, and a station instance on that box reads that box's
43
+ * fleet. The resolution probes WRITABILITY rather than assuming, because the
44
+ * failure it prevents — an app whose every publish silently EROFS-es — is
45
+ * invisible until someone asks why the UI is empty.
46
+ */
47
+ import {
48
+ existsSync,
49
+ mkdirSync,
50
+ readdirSync,
51
+ readFileSync,
52
+ renameSync,
53
+ rmSync,
54
+ writeFileSync,
55
+ } from "node:fs";
56
+ import { join } from "node:path";
57
+ import { APP_DATA_HOME_ENV, resolveAppDataHome } from "../paths/appDataHome";
58
+ import type { RetentionDeclaration, RetentionOverride } from "./policy";
59
+ import { retentionKey } from "./policy";
60
+
61
+ /** What one sweep of one resource did. Published so the UI can show it. */
62
+ export interface RetentionSweepReport {
63
+ at: number;
64
+ /** Items removed because they aged past the cutoff. */
65
+ aged: number;
66
+ /** Items removed by the item-count backstop, liveness ignored. */
67
+ overflow: number;
68
+ /**
69
+ * Items the sweep deliberately left alone because they are still live — a
70
+ * running worker's transcript, an unsettled task's trail. Reported so a
71
+ * resource that stays large has an explanation rather than looking like a
72
+ * broken sweep.
73
+ */
74
+ protectedItems?: number;
75
+ bytesFreed?: number;
76
+ /**
77
+ * Items written to the archive before they were deleted — the `archived-history`
78
+ * category's receipt. Reported so the station can show "moved" rather than
79
+ * "removed": for an archiving resource those two words are the whole difference,
80
+ * and a UI that said the second would be describing data loss that did not happen.
81
+ */
82
+ archived?: number;
83
+ /** Bytes those archived items occupy in the archive. */
84
+ archivedBytes?: number;
85
+ /**
86
+ * Why this sweep deliberately removed nothing, when it otherwise would have.
87
+ * An archiving resource whose archive is not being collected REFUSES to trim,
88
+ * and a resource that quietly stops working is indistinguishable from one with
89
+ * nothing to do — so the reason is carried out to the UI rather than logged and
90
+ * lost.
91
+ */
92
+ blocked?: string;
93
+ error?: string;
94
+ }
95
+
96
+ /** A live measurement of a resource, taken at sweep time. */
97
+ export interface RetentionMeasurement {
98
+ items: number;
99
+ bytes?: number;
100
+ /** Timestamp of the oldest item, when the target can report it cheaply. */
101
+ oldestAt?: number;
102
+ }
103
+
104
+ /** One resource as published by its owning app. */
105
+ export interface PublishedResource {
106
+ declaration: RetentionDeclaration;
107
+ measurement?: RetentionMeasurement;
108
+ lastSweep?: RetentionSweepReport;
109
+ }
110
+
111
+ /** One app's publication file. */
112
+ export interface RetentionPublication {
113
+ app: string;
114
+ version: 1;
115
+ publishedAt: number;
116
+ /** The instance that published — `local`, `prod`, `dev`; purely informational. */
117
+ instance?: string;
118
+ /** True when the process that published it has sweeping disabled entirely. */
119
+ sweepDisabled?: boolean;
120
+ resources: PublishedResource[];
121
+ }
122
+
123
+ export interface RetentionOverridesFile {
124
+ version: 1;
125
+ updatedAt: number;
126
+ /** Keyed by `<app>:<id>` — `retentionKey()`. */
127
+ overrides: Record<string, RetentionOverride>;
128
+ }
129
+
130
+ /**
131
+ * A FUNCTION, not a shared constant. `writeRetentionOverride` mutates what
132
+ * `readRetentionOverrides` hands it, so a module-level empty object would be
133
+ * aliased into every caller: the first write on a machine with no overrides file
134
+ * would edit the constant itself, and a later read of an unrelated home would
135
+ * return those overrides. Caught by `store.test.ts`.
136
+ */
137
+ const emptyOverrides = (): RetentionOverridesFile => ({ version: 1, updatedAt: 0, overrides: {} });
138
+
139
+ let cachedHome: string | null = null;
140
+
141
+ /** Can we create and write inside this directory? Probed, never assumed. */
142
+ function isUsable(dir: string): boolean {
143
+ try {
144
+ mkdirSync(dir, { recursive: true });
145
+ const probe = join(dir, ".write-probe");
146
+ writeFileSync(probe, "");
147
+ rmSync(probe, { force: true });
148
+ return true;
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Resolve the retention home. See the module note for the ordering rationale.
156
+ * Cached per process — it is consulted on every sweep and cannot change under a
157
+ * running app.
158
+ */
159
+ export function resolveRetentionHome(
160
+ env: Record<string, string | undefined> = process.env,
161
+ ): string {
162
+ if (cachedHome) return cachedHome;
163
+ const candidates: string[] = [];
164
+ if (env.RETENTION_HOME) candidates.push(env.RETENTION_HOME);
165
+ // An explicitly CONFIGURED home is used whether or not it exists yet — the
166
+ // operator naming it is the statement that it is the right place.
167
+ const configured = env[APP_DATA_HOME_ENV]?.trim();
168
+ if (configured) candidates.push(join(configured, "retention"));
169
+ const conventional = resolveAppDataHome({ env });
170
+ // The OS convention is only PREFERRED when it is already there: on a deployed
171
+ // box creating it would either fail (read-only namespace) or strand a
172
+ // publication somewhere no station instance looks.
173
+ if (!configured && existsSync(conventional)) candidates.push(join(conventional, "retention"));
174
+ if (env.APP_DATA_DIR) candidates.push(join(env.APP_DATA_DIR, "retention"));
175
+ candidates.push(join(conventional, "retention"));
176
+
177
+ for (const candidate of candidates) {
178
+ if (isUsable(candidate)) {
179
+ cachedHome = candidate;
180
+ return candidate;
181
+ }
182
+ }
183
+ // Nothing is writable — return the conventional path anyway. Every writer here
184
+ // is best-effort and will log its failure; retention must not stop an app from
185
+ // booting.
186
+ cachedHome = join(conventional, "retention");
187
+ return cachedHome;
188
+ }
189
+
190
+ /** Test seam — point the home somewhere else for this process. */
191
+ export function setRetentionHomeForTests(home: string | null): void {
192
+ cachedHome = home;
193
+ }
194
+
195
+ const appsDir = (home: string): string => join(home, "apps");
196
+ const overridesPath = (home: string): string => join(home, "overrides.json");
197
+
198
+ /** A file name cannot be derived from an app key without pinning the charset. */
199
+ const SAFE_APP = /^[a-z0-9][a-z0-9-]*$/;
200
+
201
+ /**
202
+ * Write whole, atomically: a reader must never see half a file. `rename` inside
203
+ * one directory is atomic on every filesystem this fleet runs on, and the temp
204
+ * name carries the pid so two writers cannot share a scratch path.
205
+ */
206
+ function writeJsonAtomic(path: string, value: unknown): void {
207
+ const tmp = `${path}.${process.pid}.tmp`;
208
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
209
+ renameSync(tmp, path);
210
+ }
211
+
212
+ function readJson<T>(path: string, fallback: T): T {
213
+ try {
214
+ return JSON.parse(readFileSync(path, "utf8")) as T;
215
+ } catch {
216
+ // Absent is the normal first-run case; corrupt is a truncated write from a
217
+ // killed process. Both mean "no settings yet" — never a boot failure.
218
+ return fallback;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Publish one app's resources. Best-effort by contract: a retention publication
224
+ * that cannot be written is a missing row in a dashboard, and must never take
225
+ * down the app that owns the data.
226
+ */
227
+ export function publishRetention(
228
+ publication: Omit<RetentionPublication, "version" | "publishedAt">,
229
+ options: { home?: string; log?: (line: string) => void } = {},
230
+ ): boolean {
231
+ const home = options.home ?? resolveRetentionHome();
232
+ if (!SAFE_APP.test(publication.app)) return false;
233
+ try {
234
+ mkdirSync(appsDir(home), { recursive: true });
235
+ writeJsonAtomic(join(appsDir(home), `${publication.app}.json`), {
236
+ version: 1,
237
+ publishedAt: Date.now(),
238
+ ...publication,
239
+ } satisfies RetentionPublication);
240
+ return true;
241
+ } catch (error) {
242
+ options.log?.(
243
+ `⚠️ retention: could not publish ${publication.app} — ${error instanceof Error ? error.message : String(error)}`,
244
+ );
245
+ return false;
246
+ }
247
+ }
248
+
249
+ /** Every app's publication, newest-published first. */
250
+ export function readRetentionRegistry(home = resolveRetentionHome()): RetentionPublication[] {
251
+ const dir = appsDir(home);
252
+ let names: string[];
253
+ try {
254
+ names = readdirSync(dir).filter((n) => n.endsWith(".json"));
255
+ } catch {
256
+ return [];
257
+ }
258
+ const publications: RetentionPublication[] = [];
259
+ for (const name of names) {
260
+ const parsed = readJson<RetentionPublication | null>(join(dir, name), null);
261
+ if (parsed && Array.isArray(parsed.resources) && typeof parsed.app === "string") {
262
+ publications.push(parsed);
263
+ }
264
+ }
265
+ return publications.sort((a, b) => (b.publishedAt ?? 0) - (a.publishedAt ?? 0));
266
+ }
267
+
268
+ export function readRetentionOverrides(home = resolveRetentionHome()): RetentionOverridesFile {
269
+ const file = readJson<RetentionOverridesFile>(overridesPath(home), emptyOverrides());
270
+ if (!file || typeof file !== "object" || typeof file.overrides !== "object" || !file.overrides) {
271
+ return emptyOverrides();
272
+ }
273
+ return { version: 1, updatedAt: file.updatedAt ?? 0, overrides: file.overrides };
274
+ }
275
+
276
+ /**
277
+ * Apply one override. Read-modify-write, so an unrelated resource's setting is
278
+ * never lost to a concurrent edit of this one; the station is the only writer,
279
+ * which is what keeps that honest.
280
+ *
281
+ * A patch field set to `undefined` is left alone; set to `null` it is CLEARED,
282
+ * which is how the UI's "reset to default" works — deleting the key is the only
283
+ * way to make the declaration's default authoritative again, since a stored
284
+ * value that merely equals the default would freeze the resource at today's
285
+ * number if the default ever moves.
286
+ */
287
+ export function writeRetentionOverride(
288
+ app: string,
289
+ id: string,
290
+ patch: Partial<Record<"days" | "maxItems" | "maxBytes" | "enabled" | "note", unknown>> | null,
291
+ options: { home?: string; updatedBy?: string; now?: number } = {},
292
+ ): RetentionOverridesFile {
293
+ const home = options.home ?? resolveRetentionHome();
294
+ const now = options.now ?? Date.now();
295
+ const file = readRetentionOverrides(home);
296
+ const key = retentionKey(app, id);
297
+
298
+ if (patch === null) {
299
+ delete file.overrides[key];
300
+ } else {
301
+ const current: RetentionOverride = { ...(file.overrides[key] ?? {}) };
302
+ for (const field of ["days", "maxItems", "maxBytes"] as const) {
303
+ if (field in patch) {
304
+ const value = patch[field];
305
+ if (value === null) delete current[field];
306
+ else if (typeof value === "number" && Number.isFinite(value)) current[field] = value;
307
+ }
308
+ }
309
+ if ("enabled" in patch) {
310
+ const value = patch.enabled;
311
+ if (value === null) delete current.enabled;
312
+ else if (typeof value === "boolean") current.enabled = value;
313
+ }
314
+ if ("note" in patch) {
315
+ const value = patch.note;
316
+ if (value === null || value === "") delete current.note;
317
+ else if (typeof value === "string") current.note = value.slice(0, 500);
318
+ }
319
+ current.updatedAt = now;
320
+ if (options.updatedBy) current.updatedBy = options.updatedBy;
321
+ // An override with nothing left in it is not an override — drop the key so
322
+ // the resource reads as "default" rather than as a customized row that
323
+ // happens to change nothing.
324
+ const meaningful =
325
+ current.days !== undefined ||
326
+ current.maxItems !== undefined ||
327
+ current.maxBytes !== undefined ||
328
+ current.enabled !== undefined ||
329
+ current.note !== undefined;
330
+ if (meaningful) file.overrides[key] = current;
331
+ else delete file.overrides[key];
332
+ }
333
+
334
+ file.updatedAt = now;
335
+ mkdirSync(home, { recursive: true });
336
+ writeJsonAtomic(overridesPath(home), file);
337
+ return file;
338
+ }