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,321 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
3
+ import { mkdtempSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { buildRetentionFleetView } from "./fleet";
7
+ import { DAY_MS, type RetentionDeclaration } from "./policy";
8
+ import { readRetentionRegistry, setRetentionHomeForTests, writeRetentionOverride } from "./store";
9
+ import { createRetentionSweeper } from "./sweeper";
10
+ import { createSqliteTableTarget, type RetentionTarget } from "./target";
11
+
12
+ const NOW = 1_800_000_000_000;
13
+
14
+ const decl = (id: string, over: Partial<RetentionDeclaration> = {}): RetentionDeclaration => ({
15
+ app: "orch",
16
+ id,
17
+ label: id,
18
+ category: "run-log",
19
+ unit: "rows",
20
+ location: `orch.sqlite · ${id}`,
21
+ consequence: "past run outcomes",
22
+ ...over,
23
+ });
24
+
25
+ let home: string;
26
+ let db: Database;
27
+ const logs: string[] = [];
28
+ const log = (line: string): void => {
29
+ logs.push(line);
30
+ };
31
+
32
+ function seed(table: string, ages: number[]): void {
33
+ db.run(`CREATE TABLE ${table} (id INTEGER PRIMARY KEY, at INTEGER)`);
34
+ for (const days of ages) {
35
+ db.query(`INSERT INTO ${table} (at) VALUES ($at)`).run({ $at: NOW - days * DAY_MS } as never);
36
+ }
37
+ }
38
+
39
+ const target = (
40
+ id: string,
41
+ table: string,
42
+ over: Partial<RetentionDeclaration> = {},
43
+ ): RetentionTarget =>
44
+ createSqliteTableTarget({ db, declaration: decl(id, over), table, timeColumn: "at" });
45
+
46
+ beforeEach(() => {
47
+ home = mkdtempSync(join(tmpdir(), "retention-sweeper-"));
48
+ setRetentionHomeForTests(home);
49
+ db = new Database(":memory:");
50
+ logs.length = 0;
51
+ });
52
+ afterEach(() => {
53
+ setRetentionHomeForTests(null);
54
+ db.close();
55
+ rmSync(home, { recursive: true, force: true });
56
+ });
57
+
58
+ describe("createRetentionSweeper", () => {
59
+ test("sweeps every resource and totals what it removed", () => {
60
+ seed("job_runs", [40, 30, 1]);
61
+ seed("check_runs", [90, 2]);
62
+ const sweeper = createRetentionSweeper({
63
+ app: "orch",
64
+ home,
65
+ log,
66
+ targets: [target("job-runs", "job_runs"), target("check-runs", "check_runs")],
67
+ });
68
+ const report = sweeper.sweepNow(NOW);
69
+ expect(report.totals.aged).toBe(3);
70
+ expect(report.resources.map((r) => r.key)).toEqual(["orch:job-runs", "orch:check-runs"]);
71
+ });
72
+
73
+ test("publishes measurements and the last sweep, so the UI works while the app is down", () => {
74
+ seed("job_runs", [40, 1]);
75
+ const sweeper = createRetentionSweeper({
76
+ app: "orch",
77
+ home,
78
+ log,
79
+ targets: [target("job-runs", "job_runs")],
80
+ });
81
+ sweeper.sweepNow(NOW);
82
+
83
+ const published = readRetentionRegistry(home)[0]?.resources[0];
84
+ expect(published?.measurement?.items).toBe(1);
85
+ expect(published?.lastSweep).toMatchObject({ aged: 1, overflow: 0 });
86
+ expect(published?.declaration.consequence).toBeTruthy();
87
+ });
88
+
89
+ test("the owner's override is what the sweep runs with", () => {
90
+ seed("job_runs", [40, 30, 20, 1]);
91
+ writeRetentionOverride("orch", "job-runs", { days: 25 }, { home });
92
+ const sweeper = createRetentionSweeper({
93
+ app: "orch",
94
+ home,
95
+ log,
96
+ targets: [target("job-runs", "job_runs")],
97
+ });
98
+ // The 14-day default would have taken three; the override keeps the 20-day row.
99
+ expect(sweeper.sweepNow(NOW).totals.aged).toBe(2);
100
+ expect(db.query("SELECT COUNT(*) AS n FROM job_runs").get()).toEqual({ n: 2 });
101
+ });
102
+
103
+ test("a disabled override keeps everything and says so in the resolved policy", () => {
104
+ seed("job_runs", [400, 300]);
105
+ writeRetentionOverride("orch", "job-runs", { enabled: false }, { home });
106
+ const sweeper = createRetentionSweeper({
107
+ app: "orch",
108
+ home,
109
+ log,
110
+ targets: [target("job-runs", "job_runs")],
111
+ });
112
+ const report = sweeper.sweepNow(NOW);
113
+ expect(report.totals.aged).toBe(0);
114
+ expect(report.resources[0]?.policy.enabled).toBe(false);
115
+ });
116
+
117
+ test("one broken resource never stops the others", () => {
118
+ seed("job_runs", [400]);
119
+ const exploding: RetentionTarget = {
120
+ declaration: decl("broken"),
121
+ measure: () => ({ items: 0 }),
122
+ preview: () => ({ aged: 0, overflow: 0 }),
123
+ sweep: () => {
124
+ throw new Error("database is locked");
125
+ },
126
+ };
127
+ const sweeper = createRetentionSweeper({
128
+ app: "orch",
129
+ home,
130
+ log,
131
+ targets: [exploding, target("job-runs", "job_runs")],
132
+ });
133
+ const report = sweeper.sweepNow(NOW);
134
+ expect(report.totals.errors).toBe(1);
135
+ expect(report.totals.aged).toBe(1); // the healthy one still ran
136
+ expect(report.resources.find((r) => r.key === "orch:broken")?.report.error).toMatch(/locked/);
137
+ expect(logs.some((l) => l.includes("sweep failed"))).toBe(true);
138
+ });
139
+
140
+ test("RETENTION_DISABLED still measures and publishes — a silent kill switch is a broken timer", () => {
141
+ seed("job_runs", [400, 300]);
142
+ const sweeper = createRetentionSweeper({
143
+ app: "orch",
144
+ home,
145
+ log,
146
+ disabled: true,
147
+ targets: [target("job-runs", "job_runs")],
148
+ });
149
+ expect(sweeper.sweepNow(NOW).totals.aged).toBe(0);
150
+ expect(readRetentionRegistry(home)[0]?.sweepDisabled).toBe(true);
151
+ expect(readRetentionRegistry(home)[0]?.resources[0]?.measurement?.items).toBe(2);
152
+ expect(sweeper.status().disabled).toBe(true);
153
+ });
154
+
155
+ test("an overflow is logged loudly — a bound that declines to bind is not a bound", () => {
156
+ seed("job_runs", [1, 1, 1, 1, 1]);
157
+ const sweeper = createRetentionSweeper({
158
+ app: "orch",
159
+ home,
160
+ log,
161
+ targets: [target("job-runs", "job_runs", { defaultMaxItems: 2 })],
162
+ });
163
+ expect(sweeper.sweepNow(NOW).totals.overflow).toBe(3);
164
+ expect(logs.some((l) => l.includes("OVERFLOW"))).toBe(true);
165
+ });
166
+
167
+ test("a silent sweep stays silent — logging every no-op sweep trains people to ignore the log", () => {
168
+ seed("job_runs", [1]);
169
+ const sweeper = createRetentionSweeper({
170
+ app: "orch",
171
+ home,
172
+ log,
173
+ targets: [target("job-runs", "job_runs")],
174
+ });
175
+ sweeper.sweepNow(NOW);
176
+ expect(logs.filter((l) => l.includes("pruned"))).toHaveLength(0);
177
+ });
178
+
179
+ test("preview reports what WOULD go, and changes nothing", () => {
180
+ seed("job_runs", [40, 30, 1]);
181
+ const sweeper = createRetentionSweeper({
182
+ app: "orch",
183
+ home,
184
+ log,
185
+ targets: [target("job-runs", "job_runs")],
186
+ });
187
+ expect(sweeper.preview(NOW)).toEqual([
188
+ { key: "orch:job-runs", policy: expect.objectContaining({ days: 14 }), aged: 2, overflow: 0 },
189
+ ]);
190
+ expect(db.query("SELECT COUNT(*) AS n FROM job_runs").get()).toEqual({ n: 3 });
191
+ });
192
+
193
+ test("duplicate resource ids are refused at construction, not at the first sweep", () => {
194
+ seed("job_runs", [1]);
195
+ expect(() =>
196
+ createRetentionSweeper({
197
+ app: "orch",
198
+ home,
199
+ log,
200
+ targets: [target("job-runs", "job_runs"), target("job-runs", "job_runs")],
201
+ }),
202
+ ).toThrow(/duplicate resource key/);
203
+ });
204
+
205
+ test("a malformed declaration is refused at construction", () => {
206
+ seed("job_runs", [1]);
207
+ expect(() =>
208
+ createRetentionSweeper({
209
+ app: "orch",
210
+ home,
211
+ log,
212
+ targets: [target("job-runs", "job_runs", { category: "saved-notes" as never })],
213
+ }),
214
+ ).toThrow(/unknown category/);
215
+ });
216
+
217
+ test("the liveness kick sweeps when the timer has gone silent", () => {
218
+ seed("job_runs", [400]);
219
+ let clock = NOW;
220
+ const sweeper = createRetentionSweeper({
221
+ app: "orch",
222
+ home,
223
+ log,
224
+ intervalMs: 60_000,
225
+ targets: [target("job-runs", "job_runs")],
226
+ now: () => clock,
227
+ });
228
+ sweeper.start();
229
+ clock += 10 * 60_000; // the machine slept through several intervals
230
+ sweeper.ensureAlive();
231
+ sweeper.stop();
232
+ expect(logs.some((l) => l.includes("LIVENESS KICK"))).toBe(true);
233
+ expect(db.query("SELECT COUNT(*) AS n FROM job_runs").get()).toEqual({ n: 0 });
234
+ });
235
+
236
+ test("an overrides edit is noticed within a pulse, not within an interval", async () => {
237
+ // Otherwise the station's control appears to do nothing for most of a day.
238
+ seed("job_runs", [40, 30, 2]);
239
+ const sweeper = createRetentionSweeper({
240
+ app: "orch",
241
+ home,
242
+ log,
243
+ intervalMs: 10 * 60_000,
244
+ bootDelayMs: 10 * 60_000,
245
+ pulseMs: 10,
246
+ // The seeded ages are relative to NOW, so the sweep must read the same
247
+ // clock — the pulse itself compares real file mtimes and is unaffected.
248
+ now: () => NOW,
249
+ targets: [target("job-runs", "job_runs")],
250
+ });
251
+ sweeper.start();
252
+ writeRetentionOverride("orch", "job-runs", { days: 1 }, { home });
253
+ await Bun.sleep(120);
254
+ sweeper.stop();
255
+ expect(db.query("SELECT COUNT(*) AS n FROM job_runs").get()).toEqual({ n: 0 });
256
+ expect(logs.some((l) => l.includes("policy changed"))).toBe(true);
257
+ });
258
+
259
+ test("start publishes immediately — a dashboard blank after boot reads as broken", () => {
260
+ seed("job_runs", [1, 1]);
261
+ const sweeper = createRetentionSweeper({
262
+ app: "orch",
263
+ home,
264
+ log,
265
+ intervalMs: 10 * 60_000,
266
+ bootDelayMs: 10 * 60_000,
267
+ targets: [target("job-runs", "job_runs")],
268
+ });
269
+ sweeper.start();
270
+ sweeper.stop();
271
+ expect(readRetentionRegistry(home)[0]?.resources[0]?.measurement?.items).toBe(2);
272
+ });
273
+ });
274
+
275
+ describe("buildRetentionFleetView", () => {
276
+ test("joins publications to overrides and resolves the SAME policy the sweep uses", () => {
277
+ seed("job_runs", [1, 1]);
278
+ const sweeper = createRetentionSweeper({
279
+ app: "orch",
280
+ home,
281
+ log,
282
+ targets: [target("job-runs", "job_runs")],
283
+ });
284
+ sweeper.publish();
285
+ writeRetentionOverride("orch", "job-runs", { days: 30, note: "keeping a month" }, { home });
286
+
287
+ const view = buildRetentionFleetView({ home });
288
+ expect(view.rows).toHaveLength(1);
289
+ const row = view.rows[0];
290
+ expect(row?.policy.days).toBe(30);
291
+ expect(row?.policy.sources.days).toBe("override");
292
+ expect(row?.defaultDays).toBe(14);
293
+ expect(row?.override?.note).toBe("keeping a month");
294
+ expect(row?.category.label).toBe("Run log");
295
+ expect(view.totals).toMatchObject({ resources: 1, apps: 1, items: 2 });
296
+ });
297
+
298
+ test("an override with no declaration is surfaced as orphaned, not silently dropped", () => {
299
+ // An app renamed or retired a resource; the owner's setting is now inert and
300
+ // the UI has to be able to say so.
301
+ writeRetentionOverride("orch", "gone-away", { days: 30 }, { home });
302
+ const view = buildRetentionFleetView({ home });
303
+ expect(view.orphanedOverrides.map((o) => o.key)).toEqual(["orch:gone-away"]);
304
+ });
305
+
306
+ test("rows sort biggest-first — the view answers 'what is filling my disk'", () => {
307
+ seed("small", [1]);
308
+ seed("big", [1, 1, 1, 1]);
309
+ const sweeper = createRetentionSweeper({
310
+ app: "orch",
311
+ home,
312
+ log,
313
+ targets: [target("small", "small"), target("big", "big")],
314
+ });
315
+ sweeper.publish();
316
+ expect(buildRetentionFleetView({ home }).rows.map((r) => r.key)).toEqual([
317
+ "orch:big",
318
+ "orch:small",
319
+ ]);
320
+ });
321
+ });
@@ -0,0 +1,367 @@
1
+ /**
2
+ * The sweep RUNTIME — one timer per app that applies the fleet policy to every
3
+ * resource that app declared, publishes what it found, and never takes the app
4
+ * down when it fails.
5
+ *
6
+ * Generalized from `apps/orch/src/server/eventRetention.ts`, which had all of
7
+ * this already for exactly one table: the boot delay, the liveness kick, the
8
+ * never-throw sweep, the status readout. Those were not incidental — each is a
9
+ * failure that had already happened once — so they are kept verbatim in spirit
10
+ * and made to serve every resource in the fleet instead of one.
11
+ *
12
+ * ── Overrides take effect within a minute, not within an interval ───────────
13
+ * A sweep runs every 6 hours by default. If the owner's change in the station UI
14
+ * only applied on that cadence, the control would appear to do nothing for most
15
+ * of a day — the "button that visibly does nothing" failure this fleet has
16
+ * already paid for elsewhere. So a light PULSE timer stats `overrides.json` once
17
+ * a minute and sweeps early when it has changed since the last applied read.
18
+ * One `stat` a minute is free; a UI whose settings look inert is not.
19
+ */
20
+ import { statSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import type { EffectiveRetentionPolicy } from "./policy";
23
+ import { assertRetentionDeclaration, resolveRetentionPolicy, retentionKey } from "./policy";
24
+ import type { PublishedResource, RetentionSweepReport } from "./store";
25
+ import { publishRetention, readRetentionOverrides, resolveRetentionHome } from "./store";
26
+ import type { RetentionTarget } from "./target";
27
+
28
+ /** Default cadence. Diagnostic data is not urgent; four sweeps a day is plenty. */
29
+ export const DEFAULT_SWEEP_INTERVAL_MS = 6 * 60 * 60 * 1000;
30
+ /** How often to notice an overrides edit. */
31
+ export const OVERRIDES_PULSE_MS = 60_000;
32
+
33
+ export interface ResourceOutcome {
34
+ key: string;
35
+ policy: EffectiveRetentionPolicy;
36
+ report: RetentionSweepReport;
37
+ }
38
+
39
+ export interface RetentionRunReport {
40
+ at: number;
41
+ resources: ResourceOutcome[];
42
+ /** Totals across every resource, for one honest log line. */
43
+ totals: { aged: number; overflow: number; bytesFreed: number; errors: number };
44
+ }
45
+
46
+ export interface RetentionSweeper {
47
+ start(): void;
48
+ stop(): void;
49
+ /** A supervisor's liveness poke — rebuild a timer lost to sleep/hibernate. */
50
+ ensureAlive(): void;
51
+ /** Run one sweep synchronously. Never throws. */
52
+ sweepNow(now?: number): RetentionRunReport;
53
+ /** Measure and republish without deleting anything — what the UI reads. */
54
+ publish(): void;
55
+ /** What each resource WOULD remove right now, under the current policy. */
56
+ preview(
57
+ now?: number,
58
+ ): Array<{ key: string; policy: EffectiveRetentionPolicy; aged: number; overflow: number }>;
59
+ status(): {
60
+ app: string;
61
+ home: string;
62
+ disabled: boolean;
63
+ lastRun: RetentionRunReport | null;
64
+ nextRunAt: number | null;
65
+ lastError: string | null;
66
+ };
67
+ }
68
+
69
+ export interface CreateRetentionSweeperOptions {
70
+ app: string;
71
+ targets: RetentionTarget[];
72
+ intervalMs?: number;
73
+ bootDelayMs?: number;
74
+ /** How often to notice an overrides edit. Injectable so tests need not wait a minute. */
75
+ pulseMs?: number;
76
+ /** `local` / `prod` / `dev` — informational, shown in the UI. */
77
+ instance?: string;
78
+ /**
79
+ * Kill switch. `RETENTION_DISABLED=1` in the app's environment stops every
80
+ * sweep while still publishing measurements, so the UI shows the sizes AND
81
+ * says plainly that nothing is being pruned. A silent kill switch would be
82
+ * indistinguishable from a broken timer.
83
+ */
84
+ disabled?: boolean;
85
+ home?: string;
86
+ log?: (line: string) => void;
87
+ now?: () => number;
88
+ }
89
+
90
+ const formatBytes = (bytes: number): string => {
91
+ if (bytes < 1024) return `${bytes} B`;
92
+ const units = ["KB", "MB", "GB"];
93
+ let value = bytes / 1024;
94
+ let unit = 0;
95
+ while (value >= 1024 && unit < units.length - 1) {
96
+ value /= 1024;
97
+ unit++;
98
+ }
99
+ return `${value.toFixed(1)} ${units[unit]}`;
100
+ };
101
+
102
+ export function createRetentionSweeper(options: CreateRetentionSweeperOptions): RetentionSweeper {
103
+ const { app, targets } = options;
104
+ const intervalMs = options.intervalMs ?? DEFAULT_SWEEP_INTERVAL_MS;
105
+ const bootDelayMs = options.bootDelayMs ?? 60_000;
106
+ const home = options.home ?? resolveRetentionHome();
107
+ const log = options.log ?? ((line: string) => console.log(`[${app}] ${line}`));
108
+ const now = options.now ?? (() => Date.now());
109
+ const disabled = options.disabled ?? process.env.RETENTION_DISABLED === "1";
110
+
111
+ // Fail at construction, where the stack names the app — not at the first sweep
112
+ // six hours later, in a daemon nobody is watching. A duplicate key is the
113
+ // nastier of the two: two resources sharing one override would let the owner
114
+ // change a number and silently govern the wrong thing.
115
+ const seenKeys = new Set<string>();
116
+ for (const target of targets) {
117
+ assertRetentionDeclaration(target.declaration);
118
+ const key = retentionKey(target.declaration.app, target.declaration.id);
119
+ if (seenKeys.has(key)) {
120
+ throw new Error(
121
+ `retention: duplicate resource key "${key}" — ids must be unique within an app`,
122
+ );
123
+ }
124
+ seenKeys.add(key);
125
+ }
126
+
127
+ let timer: ReturnType<typeof setTimeout> | null = null;
128
+ let pulse: ReturnType<typeof setInterval> | null = null;
129
+ let stopped = true;
130
+ let nextRunAt: number | null = null;
131
+ let lastRun: RetentionRunReport | null = null;
132
+ let lastError: string | null = null;
133
+ let lastBeatAt = now();
134
+ let appliedOverridesAt = 0;
135
+
136
+ const overridesFile = join(home, "overrides.json");
137
+
138
+ function policiesNow(): Map<string, EffectiveRetentionPolicy> {
139
+ const file = readRetentionOverrides(home);
140
+ appliedOverridesAt = Math.max(appliedOverridesAt, file.updatedAt ?? 0);
141
+ const resolved = new Map<string, EffectiveRetentionPolicy>();
142
+ for (const target of targets) {
143
+ const declaration = target.declaration;
144
+ const policy = resolveRetentionPolicy(
145
+ declaration,
146
+ file.overrides[`${declaration.app}:${declaration.id}`],
147
+ );
148
+ resolved.set(policy.key, policy);
149
+ }
150
+ return resolved;
151
+ }
152
+
153
+ /** Measure every resource and write the publication the station UI reads. */
154
+ function publish(lastReports?: Map<string, RetentionSweepReport>): void {
155
+ const resources: PublishedResource[] = [];
156
+ for (const target of targets) {
157
+ const key = `${target.declaration.app}:${target.declaration.id}`;
158
+ let measurement: PublishedResource["measurement"];
159
+ try {
160
+ measurement = target.measure();
161
+ } catch (error) {
162
+ log(
163
+ `⚠️ retention: could not measure ${key} — ${error instanceof Error ? error.message : String(error)}`,
164
+ );
165
+ }
166
+ const previous = lastRun?.resources.find((r) => r.key === key)?.report;
167
+ const lastSweep = lastReports?.get(key) ?? previous;
168
+ resources.push({
169
+ declaration: target.declaration,
170
+ ...(measurement ? { measurement } : {}),
171
+ ...(lastSweep ? { lastSweep } : {}),
172
+ });
173
+ }
174
+ publishRetention(
175
+ {
176
+ app,
177
+ ...(options.instance ? { instance: options.instance } : {}),
178
+ ...(disabled ? { sweepDisabled: true } : {}),
179
+ resources,
180
+ },
181
+ { home, log },
182
+ );
183
+ }
184
+
185
+ function sweepNow(at = now()): RetentionRunReport {
186
+ lastBeatAt = at;
187
+ const outcomes: ResourceOutcome[] = [];
188
+ const totals = { aged: 0, overflow: 0, bytesFreed: 0, errors: 0 };
189
+ const reports = new Map<string, RetentionSweepReport>();
190
+ let policies: Map<string, EffectiveRetentionPolicy>;
191
+ try {
192
+ policies = policiesNow();
193
+ } catch (error) {
194
+ // A malformed declaration must not wedge the timer — report and skip.
195
+ lastError = error instanceof Error ? error.message : String(error);
196
+ log(`⚠️ retention: cannot resolve policies — ${lastError}`);
197
+ const report: RetentionRunReport = { at, resources: [], totals: { ...totals, errors: 1 } };
198
+ lastRun = report;
199
+ return report;
200
+ }
201
+
202
+ for (const target of targets) {
203
+ const key = `${target.declaration.app}:${target.declaration.id}`;
204
+ const policy = policies.get(key);
205
+ if (!policy) continue;
206
+ let report: RetentionSweepReport;
207
+ if (disabled) {
208
+ report = { at, aged: 0, overflow: 0 };
209
+ } else {
210
+ try {
211
+ report = target.sweep(policy, at);
212
+ } catch (error) {
213
+ // A busy/locked database is a transient the next sweep clears; one
214
+ // broken resource must never stop the others.
215
+ const message = error instanceof Error ? error.message : String(error);
216
+ report = { at, aged: 0, overflow: 0, error: message };
217
+ totals.errors++;
218
+ log(`⚠️ retention: ${key} sweep failed — ${message}`);
219
+ }
220
+ }
221
+ totals.aged += report.aged;
222
+ totals.overflow += report.overflow;
223
+ totals.bytesFreed += report.bytesFreed ?? 0;
224
+ reports.set(key, report);
225
+ outcomes.push({ key, policy, report });
226
+
227
+ // An archiving resource that refuses to trim looks EXACTLY like one with
228
+ // nothing to do — same zeros, same silence — so the refusal has to be the
229
+ // noisy case. It means the archive's collector has stopped, which is a
230
+ // disk problem on its way to becoming a data problem.
231
+ if (report.blocked) {
232
+ log(`⚠️ retention: ${key} did NOT trim — ${report.blocked}`);
233
+ }
234
+ if (report.overflow > 0) {
235
+ log(
236
+ `⚠️ retention: ${key} OVERFLOW — ${report.overflow} oldest item(s) dropped to hold it at ` +
237
+ `${policy.maxItems}. These went regardless of the age rule: something is writing far ` +
238
+ "faster than expected. A bound that declines to bind is not a bound.",
239
+ );
240
+ }
241
+ }
242
+
243
+ const report: RetentionRunReport = { at, resources: outcomes, totals };
244
+ lastRun = report;
245
+ if (totals.errors === 0) lastError = null;
246
+ // Silence is the healthy steady state — log only a sweep that changed something.
247
+ if (totals.aged > 0 || totals.overflow > 0) {
248
+ // "pruned" and "archived" are not the same event and must not read as one:
249
+ // the first is data the owner agreed to lose, the second is data that moved.
250
+ const archived = outcomes.reduce((sum, o) => sum + (o.report.archived ?? 0), 0);
251
+ log(
252
+ `retention: pruned ${totals.aged + totals.overflow - archived} item(s)` +
253
+ (archived > 0 ? `, archived ${archived}` : "") +
254
+ (totals.bytesFreed > 0 ? `, ${formatBytes(totals.bytesFreed)} reclaimed` : "") +
255
+ ` across ${outcomes.filter((o) => o.report.aged + o.report.overflow > 0).length} resource(s)`,
256
+ );
257
+ }
258
+ publish(reports);
259
+ return report;
260
+ }
261
+
262
+ function schedule(delayMs = intervalMs): void {
263
+ if (stopped) return;
264
+ if (timer) clearTimeout(timer);
265
+ nextRunAt = now() + delayMs;
266
+ timer = setTimeout(() => {
267
+ timer = null;
268
+ sweepNow();
269
+ schedule();
270
+ }, delayMs);
271
+ // Unref'd for the same reason the pulse is, and it was the one that mattered:
272
+ // a 6-hour `setTimeout` is a 6-hour reason for a process not to exit. Every
273
+ // caller that owns a drain calls `stop()`, but `apps/vault` deliberately has
274
+ // no signal handler (launchd restarts it), and without this a SIGTERM there
275
+ // would hang until the timer fired or the supervisor lost patience and
276
+ // SIGKILLed — which is exactly how a WAL gets left half-closed. Housekeeping
277
+ // must never be the last thing holding a process open.
278
+ (timer as { unref?: () => void }).unref?.();
279
+ }
280
+
281
+ /** Notice an overrides edit made by the station and act on it promptly. */
282
+ function checkOverrides(): void {
283
+ if (stopped) return;
284
+ try {
285
+ const mtime = statSync(overridesFile).mtimeMs;
286
+ if (mtime > appliedOverridesAt) {
287
+ appliedOverridesAt = mtime;
288
+ log("retention: policy changed — sweeping now");
289
+ sweepNow();
290
+ schedule();
291
+ }
292
+ } catch {
293
+ // No overrides file yet is the normal case on a fresh machine.
294
+ }
295
+ }
296
+
297
+ return {
298
+ start() {
299
+ if (!stopped) return;
300
+ stopped = false;
301
+ lastBeatAt = now();
302
+ // Publish immediately so the UI has the fleet's sizes without waiting for
303
+ // the first sweep — a dashboard that is blank for a minute after boot
304
+ // reads as broken.
305
+ try {
306
+ publish();
307
+ } catch (error) {
308
+ log(
309
+ `⚠️ retention: initial publish failed — ${error instanceof Error ? error.message : String(error)}`,
310
+ );
311
+ }
312
+ try {
313
+ appliedOverridesAt = statSync(overridesFile).mtimeMs;
314
+ } catch {
315
+ appliedOverridesAt = 0;
316
+ }
317
+ schedule(Math.min(bootDelayMs, intervalMs));
318
+ pulse = setInterval(checkOverrides, options.pulseMs ?? OVERRIDES_PULSE_MS);
319
+ // Never hold the process open for housekeeping.
320
+ (pulse as { unref?: () => void }).unref?.();
321
+ },
322
+ stop() {
323
+ stopped = true;
324
+ if (timer) clearTimeout(timer);
325
+ if (pulse) clearInterval(pulse);
326
+ timer = null;
327
+ pulse = null;
328
+ nextRunAt = null;
329
+ },
330
+ ensureAlive() {
331
+ if (stopped) return;
332
+ // ~3 missed intervals, floored so a fast test cadence is not declared
333
+ // wedged the moment it is a beat late — the watchdog's shape.
334
+ const staleAfterMs = Math.max(intervalMs * 3, 90_000);
335
+ if (now() - lastBeatAt <= staleAfterMs) return;
336
+ log(
337
+ "⚠️ retention: LIVENESS KICK — sweep timer silent (system sleep/hibernate?) — sweeping now",
338
+ );
339
+ sweepNow();
340
+ schedule();
341
+ },
342
+ sweepNow,
343
+ publish: () => publish(),
344
+ preview(at = now()) {
345
+ const policies = policiesNow();
346
+ const rows: Array<{
347
+ key: string;
348
+ policy: EffectiveRetentionPolicy;
349
+ aged: number;
350
+ overflow: number;
351
+ }> = [];
352
+ for (const target of targets) {
353
+ const key = `${target.declaration.app}:${target.declaration.id}`;
354
+ const policy = policies.get(key);
355
+ if (!policy) continue;
356
+ try {
357
+ const { aged, overflow } = target.preview(policy, at);
358
+ rows.push({ key, policy, aged, overflow });
359
+ } catch {
360
+ rows.push({ key, policy, aged: 0, overflow: 0 });
361
+ }
362
+ }
363
+ return rows;
364
+ },
365
+ status: () => ({ app, home, disabled, lastRun, nextRunAt, lastError }),
366
+ };
367
+ }