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,613 @@
1
+ /**
2
+ * The two shapes a governed resource can take — a sqlite table and a directory
3
+ * of files — behind one interface the sweeper drives.
4
+ *
5
+ * Everything policy-shaped (how old, how many, what the floor is) is decided in
6
+ * `policy.ts` and arrives here already resolved. A target's only job is to know
7
+ * how to count, preview and delete ITS medium. That split is what stops the next
8
+ * app from needing a fourth hand-rolled sweeper.
9
+ *
10
+ * ── `preview` is not a nicety ───────────────────────────────────────────────
11
+ * The station UI shows "this change would remove N items now" BEFORE the owner
12
+ * saves it. That number has to come from the same predicate the sweep runs, or
13
+ * it is a guess wearing a number's clothes — so `preview` and `sweep` share
14
+ * their WHERE clause by construction here, rather than by two functions that
15
+ * were correct on the day they were written.
16
+ */
17
+ import type { Database } from "bun:sqlite";
18
+ import { readdirSync, rmSync, statSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import type { ArchiveSink } from "./archive";
21
+ import { DEFAULT_PENDING_ARCHIVE_BUDGET_BYTES } from "./archive";
22
+ import type { EffectiveRetentionPolicy, RetentionDeclaration } from "./policy";
23
+ import { categoryArchivesBeforeDelete, cutoffFor, retentionKey } from "./policy";
24
+ import type { RetentionMeasurement, RetentionSweepReport } from "./store";
25
+
26
+ export interface RetentionPreview {
27
+ /** Items the age rule would remove. */
28
+ aged: number;
29
+ /** Items the item-count backstop would remove on top of that. */
30
+ overflow: number;
31
+ }
32
+
33
+ export interface RetentionTarget {
34
+ readonly declaration: RetentionDeclaration;
35
+ measure(): RetentionMeasurement;
36
+ preview(policy: EffectiveRetentionPolicy, now: number): RetentionPreview;
37
+ sweep(policy: EffectiveRetentionPolicy, now: number): RetentionSweepReport;
38
+ }
39
+
40
+ /**
41
+ * Table and column names cannot be bound as parameters, so they are
42
+ * interpolated — and therefore must be proven safe rather than trusted. Every
43
+ * caller is first-party today; this is what keeps that true after someone wires
44
+ * a name in from a config file.
45
+ */
46
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
47
+ function ident(kind: string, name: string): string {
48
+ if (!IDENTIFIER.test(name))
49
+ throw new Error(`retention: unsafe ${kind} name ${JSON.stringify(name)}`);
50
+ return `"${name}"`;
51
+ }
52
+
53
+ /**
54
+ * 🔴 The structural half of the `archived-history` promise.
55
+ *
56
+ * The category says the rows are written somewhere safe before they go. These
57
+ * two targets DELETE — that is all they can do — so a declaration carrying an
58
+ * archiving category must not be constructible on one. Without this refusal the
59
+ * category would degrade, silently and at the worst possible moment, into
60
+ * "delete after a LONGER window than the standard allows", which is strictly
61
+ * worse than having no category at all.
62
+ *
63
+ * It throws at CONSTRUCTION, where the stack names the app and the resource,
64
+ * rather than at the first sweep hours later in a daemon nobody is watching.
65
+ */
66
+ function refuseArchivingCategory(declaration: RetentionDeclaration, target: string): void {
67
+ if (!categoryArchivesBeforeDelete(declaration.category)) return;
68
+ throw new Error(
69
+ `retention: "${retentionKey(declaration.app, declaration.id)}" declares the ` +
70
+ `"${declaration.category}" category, which promises the rows are archived before they are ` +
71
+ `deleted — and ${target} only deletes. Use createArchivingSqliteTableTarget with an ` +
72
+ "ArchiveSink, or pick a category that honestly describes throwing the data away.",
73
+ );
74
+ }
75
+
76
+ export interface SqliteTableTargetOptions {
77
+ db: Database;
78
+ declaration: RetentionDeclaration;
79
+ table: string;
80
+ /** Column holding the row's age. Rows with NULL here are treated as ageless and kept. */
81
+ timeColumn: string;
82
+ /**
83
+ * How `timeColumn` stores time. `epoch-ms` is the default; `iso` covers the
84
+ * `2026-08-01T22:43:05.317Z` TEXT columns that are just as common across this
85
+ * fleet (`orch`'s `job_runs`, and anything written by a scheduler that reasoned
86
+ * in `Date().toISOString()`).
87
+ *
88
+ * ISO-8601 UTC strings sort lexicographically in the same order as the instants
89
+ * they name, so the comparison stays a plain `<` against a bound parameter — no
90
+ * per-row `strftime`, and the column's index is still usable.
91
+ */
92
+ timeFormat?: "epoch-ms" | "iso";
93
+ /** Primary key used to address rows. Must be NOT NULL — `NOT IN` with a NULL yields NULL. */
94
+ idColumn?: string;
95
+ /**
96
+ * A SQL predicate that, when TRUE, exempts a row from the AGE sweep — the
97
+ * "still working state" rule. `apps/orch` uses it so a task parked for months
98
+ * keeps the whole trail that explains why it parked; an age-only sweep would
99
+ * delete exactly the rows the owner comes back to read.
100
+ *
101
+ * The item-count backstop deliberately IGNORES it: if the protected set alone
102
+ * can overflow the table, the bound has to win or it is not a bound. When that
103
+ * happens the sweeper says so loudly.
104
+ */
105
+ protectWhere?: string;
106
+ }
107
+
108
+ export function createSqliteTableTarget(options: SqliteTableTargetOptions): RetentionTarget {
109
+ const { db, declaration } = options;
110
+ refuseArchivingCategory(declaration, "createSqliteTableTarget");
111
+ const table = ident("table", options.table);
112
+ const time = ident("column", options.timeColumn);
113
+ const id = ident("column", options.idColumn ?? "id");
114
+ const protect = options.protectWhere ? `(${options.protectWhere})` : null;
115
+
116
+ // The floor — "never delete the newest N, at any age" — is expressed once, as
117
+ // an exclusion of the newest N rows of the WHOLE table (not of the deletable
118
+ // subset): a floor that only counted deletable rows would let a table of
119
+ // entirely-protected rows plus one old row delete that row while claiming to
120
+ // keep three.
121
+ const newestN = `${id} NOT IN (SELECT ${id} FROM ${table} ORDER BY ${time} DESC LIMIT $floor)`;
122
+ const ageWhere =
123
+ `${time} IS NOT NULL AND ${time} < $cutoff` +
124
+ (protect ? ` AND NOT ${protect}` : "") +
125
+ ` AND ${newestN}`;
126
+
127
+ // Statements are prepared LAZILY, and a missing table is a resource that does
128
+ // not exist YET rather than an error.
129
+ //
130
+ // This is not defensive padding: `orch` declares `plainjob_jobs` and
131
+ // `job_runs`, which its job library and scheduler create on first use — so at
132
+ // the moment the composition root builds its sweeper, neither table exists.
133
+ // Preparing eagerly threw `no such table` out of `createRetentionSweeper` and
134
+ // would have taken the whole daemon down at boot on a fresh machine, to bound
135
+ // a table with nothing in it. Any app whose schema is created by a dependency
136
+ // hits the same thing.
137
+ const tableExists = db.query(
138
+ "SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = $name",
139
+ );
140
+ const present = (): boolean => tableExists.get({ $name: options.table } as never) !== null;
141
+
142
+ const statements = new Map<string, ReturnType<Database["query"]>>();
143
+ const prepared = (sql: string): ReturnType<Database["query"]> => {
144
+ let statement = statements.get(sql);
145
+ if (!statement) {
146
+ statement = db.query(sql);
147
+ statements.set(sql, statement);
148
+ }
149
+ return statement;
150
+ };
151
+
152
+ const countAll = () => prepared(`SELECT COUNT(*) AS n FROM ${table}`);
153
+ const oldest = () => prepared(`SELECT MIN(${time}) AS t FROM ${table}`);
154
+ const countAged = () => prepared(`SELECT COUNT(*) AS n FROM ${table} WHERE ${ageWhere}`);
155
+ const deleteAged = () =>
156
+ prepared(`DELETE FROM ${table} WHERE ${id} IN (SELECT ${id} FROM ${table} WHERE ${ageWhere})`);
157
+ const deleteOldest = () =>
158
+ prepared(
159
+ `DELETE FROM ${table} WHERE ${id} IN (SELECT ${id} FROM ${table} ORDER BY ${time} ASC LIMIT $excess)`,
160
+ );
161
+
162
+ const count = (): number => (countAll().get() as { n: number }).n;
163
+
164
+ const isIso = options.timeFormat === "iso";
165
+ /** The cutoff in the column's own units, so the comparison stays a plain `<`. */
166
+ const asColumnTime = (ms: number): number | string => (isIso ? new Date(ms).toISOString() : ms);
167
+ /** …and back, for the UI's "oldest item" readout. */
168
+ const asMs = (value: unknown): number | undefined => {
169
+ if (typeof value === "number") return value;
170
+ if (typeof value === "string") {
171
+ const parsed = Date.parse(value);
172
+ return Number.isNaN(parsed) ? undefined : parsed;
173
+ }
174
+ return undefined;
175
+ };
176
+
177
+ /** How many the backstop removes once the age rule has run. */
178
+ function overflowExcess(policy: EffectiveRetentionPolicy, remaining: number): number {
179
+ if (!policy.enabled || policy.maxItems <= 0) return 0;
180
+ const overCap = remaining - policy.maxItems;
181
+ // Never below the floor, even to honor the cap: the floor is the one rule
182
+ // with no exceptions.
183
+ const abovefloor = remaining - policy.floorItems;
184
+ return Math.max(0, Math.min(overCap, abovefloor));
185
+ }
186
+
187
+ return {
188
+ declaration,
189
+ measure() {
190
+ if (!present()) return { items: 0 };
191
+ const oldestAt = asMs((oldest().get() as { t: unknown }).t);
192
+ return { items: count(), ...(oldestAt === undefined ? {} : { oldestAt }) };
193
+ },
194
+ preview(policy, now) {
195
+ if (!policy.enabled || !present()) return { aged: 0, overflow: 0 };
196
+ const cutoff = cutoffFor(policy, now);
197
+ const aged =
198
+ cutoff === null
199
+ ? 0
200
+ : (
201
+ countAged().get({
202
+ $cutoff: asColumnTime(cutoff),
203
+ $floor: policy.floorItems,
204
+ } as never) as { n: number }
205
+ ).n;
206
+ return { aged, overflow: overflowExcess(policy, count() - aged) };
207
+ },
208
+ sweep(policy, now) {
209
+ if (!policy.enabled || !present()) return { at: now, aged: 0, overflow: 0 };
210
+ let aged = 0;
211
+ const cutoff = cutoffFor(policy, now);
212
+ if (cutoff !== null) {
213
+ aged = deleteAged().run({
214
+ $cutoff: asColumnTime(cutoff),
215
+ $floor: policy.floorItems,
216
+ } as never).changes;
217
+ }
218
+ const excess = overflowExcess(policy, count());
219
+ const overflow = excess > 0 ? deleteOldest().run({ $excess: excess } as never).changes : 0;
220
+ return { at: now, aged, overflow };
221
+ },
222
+ };
223
+ }
224
+
225
+ export interface FileDirTargetOptions {
226
+ declaration: RetentionDeclaration;
227
+ dir: string;
228
+ /** Only files matching this are governed. Anything else in the dir is invisible. */
229
+ match?: RegExp;
230
+ /** Injectable for tests; defaults to the file's mtime. */
231
+ timeOf?: (path: string, stat: { mtimeMs: number }) => number;
232
+ /**
233
+ * Files this sweep must never delete, whatever the policy says — the
234
+ * filesystem counterpart of `protectWhere`.
235
+ *
236
+ * Called once per sweep and given every governed file, so the caller can
237
+ * resolve the protected set with ONE query rather than one per file (`orch`
238
+ * asks its database for the attempts that are still live or whose task has not
239
+ * settled). Unlike `protectWhere`, this holds against the byte and item caps
240
+ * TOO: a bound that deletes a running worker's live transcript to save disk has
241
+ * traded a disk problem for a debugging one, and unlinking a file that is still
242
+ * being appended to loses the writer's output with no error anywhere.
243
+ */
244
+ protect?: (
245
+ files: ReadonlyArray<{ name: string; path: string; at: number; bytes: number }>,
246
+ ) => Set<string>;
247
+ }
248
+
249
+ interface FileEntry {
250
+ name: string;
251
+ path: string;
252
+ at: number;
253
+ bytes: number;
254
+ }
255
+
256
+ /**
257
+ * A directory of files — attempt transcripts, generated art, exports.
258
+ *
259
+ * Only files DIRECTLY in `dir` and matching `match` are ever considered: a tool
260
+ * that deletes things gets to delete only what it can prove it owns, which is
261
+ * the rule `deploy-engine/backupRetention.ts` arrived at after the same
262
+ * question. Subdirectories are left entirely alone.
263
+ */
264
+ export function createFileDirTarget(options: FileDirTargetOptions): RetentionTarget {
265
+ const { declaration, dir } = options;
266
+ refuseArchivingCategory(declaration, "createFileDirTarget");
267
+ const match = options.match;
268
+ const timeOf = options.timeOf ?? ((_p, stat) => stat.mtimeMs);
269
+
270
+ function list(): FileEntry[] {
271
+ let names: string[];
272
+ try {
273
+ names = readdirSync(dir);
274
+ } catch {
275
+ return []; // not created yet — a resource with nothing in it
276
+ }
277
+ const entries: FileEntry[] = [];
278
+ for (const name of names) {
279
+ if (match && !match.test(name)) continue;
280
+ const path = join(dir, name);
281
+ try {
282
+ const stat = statSync(path);
283
+ if (!stat.isFile()) continue;
284
+ entries.push({ name, path, at: timeOf(path, stat), bytes: stat.size });
285
+ } catch {
286
+ // vanished between readdir and stat — nothing to govern
287
+ }
288
+ }
289
+ return entries.sort((a, b) => b.at - a.at); // newest first
290
+ }
291
+
292
+ /** The plan, shared by preview and sweep so they cannot disagree. */
293
+ function plan(
294
+ policy: EffectiveRetentionPolicy,
295
+ now: number,
296
+ ): { aged: FileEntry[]; overflow: FileEntry[]; protectedCount: number } {
297
+ const entries = list();
298
+ if (!policy.enabled) return { aged: [], overflow: [], protectedCount: 0 };
299
+
300
+ // Protected by the caller (live writer / unsettled task), then by the floor.
301
+ // Both are absolute: neither the age rule nor either cap may touch them.
302
+ const kept = new Set(options.protect ? options.protect(entries) : []);
303
+ for (const entry of entries.slice(0, policy.floorItems)) kept.add(entry.path);
304
+ const deletable = entries.filter((e) => !kept.has(e.path));
305
+
306
+ const cutoff = cutoffFor(policy, now);
307
+ const aged = cutoff === null ? [] : deletable.filter((e) => e.at < cutoff);
308
+ const agedPaths = new Set(aged.map((e) => e.path));
309
+
310
+ // Oldest first: the cap always gives up the least recently useful thing.
311
+ const candidates = deletable.filter((e) => !agedPaths.has(e.path)).sort((a, b) => a.at - b.at);
312
+ const overflow: FileEntry[] = [];
313
+
314
+ if (policy.maxItems > 0) {
315
+ let remaining = entries.length - aged.length;
316
+ for (const entry of candidates) {
317
+ if (remaining <= policy.maxItems) break;
318
+ overflow.push(entry);
319
+ remaining--;
320
+ }
321
+ }
322
+ if (policy.maxBytes > 0) {
323
+ const chosen = new Set(overflow.map((e) => e.path));
324
+ let bytes = entries.reduce(
325
+ (sum, e) => (agedPaths.has(e.path) || chosen.has(e.path) ? sum : sum + e.bytes),
326
+ 0,
327
+ );
328
+ for (const entry of candidates) {
329
+ if (bytes <= policy.maxBytes) break;
330
+ if (chosen.has(entry.path)) continue;
331
+ overflow.push(entry);
332
+ chosen.add(entry.path);
333
+ bytes -= entry.bytes;
334
+ }
335
+ }
336
+ return { aged, overflow, protectedCount: entries.length - deletable.length };
337
+ }
338
+
339
+ return {
340
+ declaration,
341
+ measure() {
342
+ const entries = list();
343
+ const result: RetentionMeasurement = {
344
+ items: entries.length,
345
+ bytes: entries.reduce((sum, e) => sum + e.bytes, 0),
346
+ };
347
+ const last = entries[entries.length - 1];
348
+ if (last) result.oldestAt = last.at;
349
+ return result;
350
+ },
351
+ preview(policy, now) {
352
+ const { aged, overflow } = plan(policy, now);
353
+ return { aged: aged.length, overflow: overflow.length };
354
+ },
355
+ sweep(policy, now) {
356
+ const { aged, overflow, protectedCount } = plan(policy, now);
357
+ let bytesFreed = 0;
358
+ let agedRemoved = 0;
359
+ let overflowRemoved = 0;
360
+ for (const [entries, isAged] of [
361
+ [aged, true],
362
+ [overflow, false],
363
+ ] as const) {
364
+ for (const entry of entries) {
365
+ try {
366
+ rmSync(entry.path, { force: true });
367
+ bytesFreed += entry.bytes;
368
+ if (isAged) agedRemoved++;
369
+ else overflowRemoved++;
370
+ } catch {
371
+ // A file that will not delete is a housekeeping problem, never a
372
+ // reason to abandon the rest of the sweep.
373
+ }
374
+ }
375
+ }
376
+ return {
377
+ at: now,
378
+ aged: agedRemoved,
379
+ overflow: overflowRemoved,
380
+ bytesFreed,
381
+ protectedItems: protectedCount,
382
+ };
383
+ },
384
+ };
385
+ }
386
+
387
+ // ── archive-then-trim ────────────────────────────────────────────────────────
388
+
389
+ export interface ArchivingSqliteTableTargetOptions
390
+ extends Omit<SqliteTableTargetOptions, "declaration"> {
391
+ declaration: RetentionDeclaration;
392
+ /** Where the rows go before they leave the table. */
393
+ archive: ArchiveSink;
394
+ /**
395
+ * Columns written to the archive, in order. Defaults to every column the
396
+ * table has at construction — which is what an append-only history wants: the
397
+ * archive is the row, not a projection somebody chose on a Tuesday.
398
+ */
399
+ columns?: string[];
400
+ /**
401
+ * Most rows one sweep will move. The sweep is synchronous and the rows are
402
+ * held in memory to be serialized, so a first run against years of history
403
+ * must not try to do it in one bite. Successive sweeps drain the rest.
404
+ */
405
+ maxBatch?: number;
406
+ /**
407
+ * How many bytes of uncollected archive a STAGED home may hold before this
408
+ * target refuses to trim. See `archive.ts` — this is the "refuse if the last
409
+ * successful pull is stale" rule, expressed in the one fact a deployed box can
410
+ * observe for itself.
411
+ */
412
+ pendingBudgetBytes?: number;
413
+ }
414
+
415
+ /**
416
+ * A sqlite table whose aged rows are ARCHIVED and then deleted — the runtime for
417
+ * the `archived-history` category.
418
+ *
419
+ * Everything about WHICH rows is shared with `createSqliteTableTarget`: the same
420
+ * age predicate, the same `protectWhere` liveness exemption, the same floor, the
421
+ * same cap. What differs is the one step in the middle, and it is the whole
422
+ * point: nothing is deleted until `archive.write()` has proven the bytes are on
423
+ * disk, and `write()` throws rather than reporting failure so there is no path
424
+ * where "the archive did not happen" reaches a DELETE.
425
+ *
426
+ * 🔴 The item-count backstop archives too. A cap that dumped the oldest rows
427
+ * straight to /dev/null the moment a table overflowed would mean the ONE case
428
+ * where the history is growing fastest is the one case it is destroyed — an
429
+ * archiving resource that has an unarchived escape hatch is not an archiving
430
+ * resource.
431
+ */
432
+ export function createArchivingSqliteTableTarget(
433
+ options: ArchivingSqliteTableTargetOptions,
434
+ ): RetentionTarget {
435
+ const { db, declaration, archive } = options;
436
+ if (!categoryArchivesBeforeDelete(declaration.category)) {
437
+ throw new Error(
438
+ `retention: "${retentionKey(declaration.app, declaration.id)}" uses ` +
439
+ "createArchivingSqliteTableTarget but its category does not archive before deleting " +
440
+ `(${declaration.category}). Use createSqliteTableTarget, or declare "archived-history".`,
441
+ );
442
+ }
443
+ const key = retentionKey(declaration.app, declaration.id);
444
+ const table = ident("table", options.table);
445
+ const time = ident("column", options.timeColumn);
446
+ const idColumn = options.idColumn ?? "id";
447
+ const id = ident("column", idColumn);
448
+ const protect = options.protectWhere ? `(${options.protectWhere})` : null;
449
+ const maxBatch = options.maxBatch ?? 50_000;
450
+ const pendingBudget = options.pendingBudgetBytes ?? DEFAULT_PENDING_ARCHIVE_BUDGET_BYTES;
451
+
452
+ // Identical to the plain target's, deliberately: the predicate that decides
453
+ // what ages out must not fork, or the preview the owner reads and the sweep
454
+ // that runs stop being the same question.
455
+ const newestN = `${id} NOT IN (SELECT ${id} FROM ${table} ORDER BY ${time} DESC LIMIT $floor)`;
456
+ const ageWhere =
457
+ `${time} IS NOT NULL AND ${time} < $cutoff` +
458
+ (protect ? ` AND NOT ${protect}` : "") +
459
+ ` AND ${newestN}`;
460
+
461
+ const tableExists = db.query(
462
+ "SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = $name",
463
+ );
464
+ const present = (): boolean => tableExists.get({ $name: options.table } as never) !== null;
465
+
466
+ const statements = new Map<string, ReturnType<Database["query"]>>();
467
+ const prepared = (sql: string): ReturnType<Database["query"]> => {
468
+ let statement = statements.get(sql);
469
+ if (!statement) {
470
+ statement = db.query(sql);
471
+ statements.set(sql, statement);
472
+ }
473
+ return statement;
474
+ };
475
+
476
+ /** Resolved lazily for the same reason statements are: the table may not exist yet. */
477
+ let columnList: string | null = null;
478
+ const columns = (): string => {
479
+ if (columnList) return columnList;
480
+ if (options.columns?.length) {
481
+ columnList = options.columns.map((c) => ident("column", c)).join(", ");
482
+ return columnList;
483
+ }
484
+ const info = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
485
+ columnList = info.map((row) => ident("column", row.name)).join(", ") || "*";
486
+ return columnList;
487
+ };
488
+
489
+ const count = (): number =>
490
+ (prepared(`SELECT COUNT(*) AS n FROM ${table}`).get() as { n: number }).n;
491
+
492
+ const isIso = options.timeFormat === "iso";
493
+ const asColumnTime = (ms: number): number | string => (isIso ? new Date(ms).toISOString() : ms);
494
+ const asMs = (value: unknown): number | undefined => {
495
+ if (typeof value === "number") return value;
496
+ if (typeof value === "string") {
497
+ const parsed = Date.parse(value);
498
+ return Number.isNaN(parsed) ? undefined : parsed;
499
+ }
500
+ return undefined;
501
+ };
502
+
503
+ function overflowExcess(policy: EffectiveRetentionPolicy, remaining: number): number {
504
+ if (!policy.enabled || policy.maxItems <= 0) return 0;
505
+ return Math.max(0, Math.min(remaining - policy.maxItems, remaining - policy.floorItems));
506
+ }
507
+
508
+ const agedRows = (cutoff: number, floor: number, limit: number): Array<Record<string, unknown>> =>
509
+ prepared(
510
+ `SELECT ${columns()} FROM ${table} WHERE ${ageWhere} ORDER BY ${time} ASC LIMIT $limit`,
511
+ ).all({ $cutoff: asColumnTime(cutoff), $floor: floor, $limit: limit } as never) as Array<
512
+ Record<string, unknown>
513
+ >;
514
+
515
+ const oldestRows = (excess: number): Array<Record<string, unknown>> =>
516
+ prepared(
517
+ `SELECT ${columns()} FROM ${table} ORDER BY ${time} ASC LIMIT $excess`,
518
+ ).all({ $excess: excess } as never) as Array<Record<string, unknown>>;
519
+
520
+ /**
521
+ * Archive the rows, THEN delete exactly those ids. Never a WHERE re-run: the
522
+ * rows that were archived and the rows that are deleted have to be the same
523
+ * set, and a predicate evaluated twice against a live table is two sets.
524
+ */
525
+ function archiveThenDelete(rows: Array<Record<string, unknown>>, at: number): number {
526
+ const ids = rows.map((row) => row[idColumn]);
527
+ if (ids.some((value) => value === null || value === undefined)) {
528
+ throw new Error(
529
+ `retention: ${key} cannot archive rows whose "${idColumn}" is NULL — a row this sweep ` +
530
+ "could not address afterwards must not be archived, or the archive would grow a copy " +
531
+ "of a row that stays in the table for ever.",
532
+ );
533
+ }
534
+ const segment = archive.write(rows, at);
535
+ const placeholders = ids.map(() => "?").join(", ");
536
+ db.query(`DELETE FROM ${table} WHERE ${id} IN (${placeholders})`).run(...(ids as never[]));
537
+ return segment.bytes;
538
+ }
539
+
540
+ return {
541
+ declaration,
542
+ measure() {
543
+ if (!present()) return { items: 0 };
544
+ const oldestAt = asMs(
545
+ (prepared(`SELECT MIN(${time}) AS t FROM ${table}`).get() as { t: unknown }).t,
546
+ );
547
+ return { items: count(), ...(oldestAt === undefined ? {} : { oldestAt }) };
548
+ },
549
+ preview(policy, now) {
550
+ if (!policy.enabled || !present()) return { aged: 0, overflow: 0 };
551
+ const cutoff = cutoffFor(policy, now);
552
+ const aged =
553
+ cutoff === null
554
+ ? 0
555
+ : (
556
+ prepared(`SELECT COUNT(*) AS n FROM ${table} WHERE ${ageWhere}`).get({
557
+ $cutoff: asColumnTime(cutoff),
558
+ $floor: policy.floorItems,
559
+ } as never) as { n: number }
560
+ ).n;
561
+ return { aged: Math.min(aged, maxBatch), overflow: overflowExcess(policy, count() - aged) };
562
+ },
563
+ sweep(policy, now) {
564
+ if (!policy.enabled || !present()) return { at: now, aged: 0, overflow: 0 };
565
+
566
+ // 🔴 The staleness refusal, before a single row is read. A staged archive
567
+ // that nobody is collecting means the box would be trading a bounded table
568
+ // for an unbounded archive on the same disk — and, worse, doing it while
569
+ // the ONLY copy of the trimmed history sits on the box the trim was
570
+ // supposed to protect. Stop, and say why.
571
+ const pending = archive.pendingBytes();
572
+ if (!archive.terminal && pending > pendingBudget) {
573
+ return {
574
+ at: now,
575
+ aged: 0,
576
+ overflow: 0,
577
+ blocked:
578
+ `${Math.round(pending / 1048576)} MB of archived history in ${archive.dir} has not been ` +
579
+ `collected (budget ${Math.round(pendingBudget / 1048576)} MB). Nothing was trimmed: the ` +
580
+ "archive is this history's only other copy, so a stale collector must stop the trim, " +
581
+ "not be outrun by it. Run the Mac's `bun run backups:pull-prod`.",
582
+ };
583
+ }
584
+
585
+ let aged = 0;
586
+ let archivedBytes = 0;
587
+ const cutoff = cutoffFor(policy, now);
588
+ if (cutoff !== null) {
589
+ const rows = agedRows(cutoff, policy.floorItems, maxBatch);
590
+ if (rows.length > 0) {
591
+ archivedBytes += archiveThenDelete(rows, now);
592
+ aged = rows.length;
593
+ }
594
+ }
595
+ let overflow = 0;
596
+ const excess = Math.min(overflowExcess(policy, count()), maxBatch);
597
+ if (excess > 0) {
598
+ const rows = oldestRows(excess);
599
+ if (rows.length > 0) {
600
+ archivedBytes += archiveThenDelete(rows, now + 1);
601
+ overflow = rows.length;
602
+ }
603
+ }
604
+ const moved = aged + overflow;
605
+ return {
606
+ at: now,
607
+ aged,
608
+ overflow,
609
+ ...(moved > 0 ? { archived: moved, archivedBytes } : {}),
610
+ };
611
+ },
612
+ };
613
+ }
@@ -0,0 +1,12 @@
1
+ // The saas search tier (04 §3) — the far side of `SearchProvider` for a pooled
2
+ // Postgres. `createSaasHostServices` is the near side a deployment actually
3
+ // calls; these are exported for a host that wires the tier by hand and for the
4
+ // migration step that creates the schema.
5
+ export { createPgSearchProvider, type PgSearchProviderOptions } from './pgSearchProvider';
6
+ export {
7
+ ensureSaasSearchSchema,
8
+ PG_RANK_WEIGHTS,
9
+ PG_SEARCH_CHUNKS_DDL,
10
+ PG_SEARCH_DDL,
11
+ PG_SEARCH_DOCS_DDL,
12
+ } from './pgSearchSchema';