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,181 @@
1
+ /**
2
+ * The archive's contract is a promise about ORDER: the bytes are provably on
3
+ * disk before anything is deleted. Every test here is that promise from a
4
+ * different angle, and the load-bearing ones are the failures — a sink that
5
+ * silently succeeded when it had not written would be indistinguishable from one
6
+ * that worked, right up until the day it mattered.
7
+ */
8
+ import { afterEach, describe, expect, test } from "bun:test";
9
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import { resolveAppDataHome } from "../paths/appDataHome";
13
+ import {
14
+ archiveStamp,
15
+ createJsonlArchive,
16
+ resolveArchiveHome,
17
+ verifyArchive,
18
+ type ArchiveHome,
19
+ } from "./archive";
20
+
21
+ const temps: string[] = [];
22
+ const tmp = (): string => {
23
+ const dir = mkdtempSync(join(tmpdir(), "cb-archive-"));
24
+ temps.push(dir);
25
+ return dir;
26
+ };
27
+ afterEach(() => {
28
+ for (const dir of temps.splice(0)) rmSync(dir, { recursive: true, force: true });
29
+ });
30
+
31
+ const home = (dir: string, terminal = true): ArchiveHome => ({ dir, terminal });
32
+ const AT = Date.UTC(2026, 7, 25, 11, 42, 33);
33
+
34
+ describe("createJsonlArchive", () => {
35
+ test("a sealed segment is on disk, one JSON row per line, and named for its resource", () => {
36
+ const dir = tmp();
37
+ const sink = createJsonlArchive({ app: "notes", resource: "sync-history", home: home(dir) });
38
+ const segment = sink.write([{ seq: 1, kind: "item.upsert" }, { seq: 2, kind: "tag.set" }], AT);
39
+
40
+ expect(segment.file).toBe(`sync-history-${archiveStamp(new Date(AT))}.jsonl`);
41
+ expect(segment.rows).toBe(2);
42
+ const body = readFileSync(segment.path, "utf8");
43
+ expect(body.split("\n").filter(Boolean).map((l) => JSON.parse(l))).toEqual([
44
+ { seq: 1, kind: "item.upsert" },
45
+ { seq: 2, kind: "tag.set" },
46
+ ]);
47
+ });
48
+
49
+ test("every segment is catalogued in an APPEND-only index, and verifyArchive re-proves them", () => {
50
+ const dir = tmp();
51
+ const sink = createJsonlArchive({ app: "notes", resource: "sync-history", home: home(dir) });
52
+ sink.write([{ seq: 1 }], AT);
53
+ sink.write([{ seq: 2 }, { seq: 3 }], AT + 60_000);
54
+
55
+ expect(verifyArchive(dir)).toEqual({ segments: 2, rows: 3, bytes: expect.any(Number), bad: [] });
56
+ });
57
+
58
+ test("🔴 corruption after the fact is REPORTED — an archive nobody can check is a belief", () => {
59
+ const dir = tmp();
60
+ const sink = createJsonlArchive({ app: "notes", resource: "sync-history", home: home(dir) });
61
+ const segment = sink.write([{ seq: 1 }], AT);
62
+ writeFileSync(segment.path, '{"seq":99}\n');
63
+
64
+ const report = verifyArchive(dir);
65
+ expect(report.bad).toEqual([`${segment.file}: sha256 mismatch`]);
66
+ });
67
+
68
+ test("a collected segment reads as absent rather than as corruption", () => {
69
+ // The healthy steady state on a deployed box: the Mac pulled it and removed
70
+ // the box's copy. Calling that corruption would make the normal case alarming.
71
+ const dir = tmp();
72
+ const sink = createJsonlArchive({ app: "notes", resource: "sync-history", home: home(dir, false) });
73
+ const segment = sink.write([{ seq: 1 }], AT);
74
+ rmSync(segment.path);
75
+ expect(verifyArchive(dir).bad).toEqual([`${segment.file}: absent (collected, or lost)`]);
76
+ });
77
+
78
+ test("🔴 an unwritable home THROWS — the caller must never read that as 'nothing to archive'", () => {
79
+ const dir = tmp();
80
+ const locked = join(dir, "locked");
81
+ mkdirSync(locked);
82
+ chmodSync(locked, 0o500);
83
+ const sink = createJsonlArchive({
84
+ app: "notes",
85
+ resource: "sync-history",
86
+ home: home(join(locked, "archive")),
87
+ });
88
+ expect(() => sink.write([{ seq: 1 }], AT)).toThrow();
89
+ chmodSync(locked, 0o700);
90
+ });
91
+
92
+ test("an empty segment is refused — a sweep with nothing to move must not seal a file", () => {
93
+ const sink = createJsonlArchive({ app: "notes", resource: "sync-history", home: home(tmp()) });
94
+ expect(() => sink.write([], AT)).toThrow(/empty segment/);
95
+ });
96
+
97
+ test("no half-written segment is ever visible: the temp file does not survive", () => {
98
+ const dir = tmp();
99
+ const sink = createJsonlArchive({ app: "notes", resource: "sync-history", home: home(dir) });
100
+ sink.write([{ seq: 1 }], AT);
101
+ const strays = readFileSync(join(dir, "index.jsonl"), "utf8");
102
+ expect(strays).toContain("sync-history");
103
+ expect(existsSync(join(dir, `sync-history-${archiveStamp(new Date(AT))}.jsonl.${process.pid}.tmp`))).toBe(
104
+ false,
105
+ );
106
+ });
107
+ });
108
+
109
+ describe("pendingBytes — the staleness signal a deployed box can actually observe", () => {
110
+ test("a TERMINAL archive owes nobody anything, however large it gets", () => {
111
+ const dir = tmp();
112
+ const sink = createJsonlArchive({ app: "notes", resource: "sync-history", home: home(dir, true) });
113
+ sink.write(Array.from({ length: 500 }, (_, i) => ({ seq: i })), AT);
114
+ expect(sink.pendingBytes()).toBe(0);
115
+ });
116
+
117
+ test("a STAGED archive counts every uncollected segment, and forgets one that was taken", () => {
118
+ const dir = tmp();
119
+ const sink = createJsonlArchive({ app: "notes", resource: "sync-history", home: home(dir, false) });
120
+ const first = sink.write([{ seq: 1 }], AT);
121
+ const second = sink.write([{ seq: 2 }], AT + 60_000);
122
+ expect(sink.pendingBytes()).toBe(first.bytes + second.bytes);
123
+
124
+ rmSync(first.path); // the Mac pulled it
125
+ expect(sink.pendingBytes()).toBe(second.bytes);
126
+ });
127
+
128
+ test("the catalogue is not counted as pending — it is not history, it is the list", () => {
129
+ const dir = tmp();
130
+ const sink = createJsonlArchive({ app: "notes", resource: "sync-history", home: home(dir, false) });
131
+ const only = sink.write([{ seq: 1 }], AT);
132
+ expect(sink.pendingBytes()).toBe(only.bytes);
133
+ });
134
+ });
135
+
136
+ describe("resolveArchiveHome", () => {
137
+ test("the shared OS-convention home is TERMINAL — the archive simply grows there", () => {
138
+ const root = tmp();
139
+ const conventional = resolveAppDataHome({ env: { HOME: root } });
140
+ mkdirSync(conventional, { recursive: true });
141
+ const resolved = resolveArchiveHome("notes", { HOME: root });
142
+ expect(resolved).toEqual({
143
+ dir: join(conventional, "cursed-satellites", "archive", "notes"),
144
+ terminal: true,
145
+ });
146
+ });
147
+
148
+ test("APP_DATA_HOME is honored before the convention, and before it exists", () => {
149
+ const configured = join(tmp(), "managed");
150
+ expect(resolveArchiveHome("notes", { APP_DATA_HOME: configured })).toEqual({
151
+ dir: join(configured, "cursed-satellites", "archive", "notes"),
152
+ terminal: true,
153
+ });
154
+ });
155
+
156
+ /**
157
+ * 🔴 Up to 2026-09-13 this resolved a hardcoded dotfolder in the owner's home —
158
+ * in a PUBLISHED package. Asserted on the output, so it still holds if the
159
+ * folder comes back through a variable.
160
+ */
161
+ test("resolves nothing from a previous generation, whatever the env says", () => {
162
+ const root = tmp();
163
+ const resolved = resolveArchiveHome("notes", { HOME: root, APP_MANAGED_HOME: "/nowhere/inherited" });
164
+ expect(resolved.dir).not.toContain("app-managed");
165
+ expect(resolved.dir).not.toContain("/nowhere/inherited");
166
+ });
167
+
168
+ test("🔴 a deployed box falls back to the data dir and is STAGED — something must collect it", () => {
169
+ const root = tmp(); // no shared data home, which is every deployed box
170
+ const dataDir = join(tmp(), "data");
171
+ mkdirSync(dataDir, { recursive: true });
172
+ expect(resolveArchiveHome("notes", { HOME: root, APP_DATA_DIR: dataDir })).toEqual({
173
+ dir: join(dataDir, "archive"),
174
+ terminal: false,
175
+ });
176
+ });
177
+
178
+ test("an app key that could escape its directory is refused", () => {
179
+ expect(() => resolveArchiveHome("notes/../etc")).toThrow(/lower-kebab/);
180
+ });
181
+ });
@@ -0,0 +1,353 @@
1
+ /**
2
+ * The ARCHIVE half of `archived-history` — where the rows go before they are
3
+ * allowed to leave the instance.
4
+ *
5
+ * ── The ordering rule IS the safety argument, and this fleet has broken it ───
6
+ * prod's `diskGuard.sh` rule 1 deletes the oldest `VACUUM INTO` sqlite backups
7
+ * to free space, and its own comment calls those files "NOT regenerable". A
8
+ * cleanup routine that runs before the copy is data loss wearing housekeeping's
9
+ * name, and it is the exact failure this module exists to make impossible. So:
10
+ *
11
+ * 1. serialize the rows,
12
+ * 2. write them to a NEW file, fsync it,
13
+ * 3. RE-READ that file off the disk and prove it — byte length, sha256, line
14
+ * count, and that every line parses back to an object,
15
+ * 4. only then may the caller delete those rows.
16
+ *
17
+ * `write()` throws rather than returning a failure, because the one thing a
18
+ * caller must never be able to do is treat "the archive did not happen" as
19
+ * "nothing to delete". A throw ends the sweep for that resource with the rows
20
+ * still in the table, which is the correct outcome every time.
21
+ *
22
+ * ── A segment per sweep, never an appended file ─────────────────────────────
23
+ * "Append-only" is a property of the DIRECTORY here: each sweep seals its own
24
+ * `<resource>-<stamp>.jsonl` and nothing is ever rewritten. That is not a style
25
+ * choice. On a deployed box the archive is a WAY-STATION — the Mac's
26
+ * `backups:pull-prod` copies segments down and removes the box's copy — and a
27
+ * collector that scp'd a file the app was still appending to would take a
28
+ * prefix and then delete the rest. A sealed file cannot be half a file.
29
+ *
30
+ * ── Terminal vs staged, and the staleness refusal ───────────────────────────
31
+ * On a workstation the archive is home: `<app-data-home>/cursed-satellites/
32
+ * archive/<app>/`, it is `terminal`, and it simply grows. The home is
33
+ * CONFIGURATION ({@link resolveAppDataHome}) — `$APP_DATA_HOME`, else the OS's own
34
+ * convention — never a folder this library invented in somebody's home; that was
35
+ * the published defect fixed 2026-09-13. On a deployed box nothing outside the
36
+ * app's own data dir is writable, so it resolves to `$APP_DATA_DIR/archive` and
37
+ * is `staged` — something else has to come and get it. That distinction is what lets the
38
+ * sweeper honor "refuse to trim if the last successful pull is stale" using a
39
+ * fact the box can actually observe: how many bytes of sealed segments are still
40
+ * sitting there uncollected. If the collector stops, the archive fills to its
41
+ * budget and the trim STOPS — bounded archive, complete table — rather than the
42
+ * table trimming into an archive nobody is taking away.
43
+ */
44
+ import { createHash } from "node:crypto";
45
+ import {
46
+ appendFileSync,
47
+ closeSync,
48
+ existsSync,
49
+ fsyncSync,
50
+ mkdirSync,
51
+ openSync,
52
+ readdirSync,
53
+ readFileSync,
54
+ renameSync,
55
+ rmSync,
56
+ statSync,
57
+ writeFileSync,
58
+ writeSync,
59
+ } from "node:fs";
60
+ import { join } from "node:path";
61
+ import { APP_DATA_HOME_ENV, resolveAppDataHome } from "../paths/appDataHome";
62
+
63
+ /** One sealed archive file. */
64
+ export interface ArchiveSegment {
65
+ /** Absolute path on this machine. */
66
+ path: string;
67
+ /** Basename — what a collector and the index line name it by. */
68
+ file: string;
69
+ rows: number;
70
+ bytes: number;
71
+ sha256: string;
72
+ at: number;
73
+ }
74
+
75
+ export interface ArchiveSink {
76
+ /** Where segments are written. */
77
+ readonly dir: string;
78
+ /** True when THIS machine is the archive's final home (no collector is owed). */
79
+ readonly terminal: boolean;
80
+ /**
81
+ * Bytes of sealed segments still waiting for a collector. Always 0 when
82
+ * `terminal` — a home archive is never "pending", it is the archive.
83
+ */
84
+ pendingBytes(): number;
85
+ /**
86
+ * Seal one segment, proven on disk. THROWS if it cannot prove it: the caller
87
+ * treats that as "these rows stay in the table".
88
+ */
89
+ write(records: ReadonlyArray<Record<string, unknown>>, at: number): ArchiveSegment;
90
+ }
91
+
92
+ /** The default budget of uncollected archive on a staged (deployed-box) home.
93
+ * Generous next to the tables it governs — notes' whole `ops` log is ~15 MB —
94
+ * and small next to a 6.7 GB root. Reached only if the collector has stopped. */
95
+ export const DEFAULT_PENDING_ARCHIVE_BUDGET_BYTES = 64 * 1024 * 1024;
96
+
97
+ const SAFE_NAME = /^[a-z0-9][a-z0-9-]*$/;
98
+
99
+ /** `20260825-114233` — sortable, second-resolution, and the same shape the
100
+ * fleet's dated backup dirs already use, so one eye reads both. */
101
+ export function archiveStamp(date: Date): string {
102
+ const p = (n: number, w = 2) => String(n).padStart(w, "0");
103
+ return (
104
+ `${date.getUTCFullYear()}${p(date.getUTCMonth() + 1)}${p(date.getUTCDate())}-` +
105
+ `${p(date.getUTCHours())}${p(date.getUTCMinutes())}${p(date.getUTCSeconds())}`
106
+ );
107
+ }
108
+
109
+ export interface ArchiveHome {
110
+ dir: string;
111
+ terminal: boolean;
112
+ }
113
+
114
+ /** Can we create and write inside this directory? Probed, never assumed —
115
+ * the same reasoning as `resolveRetentionHome`, and the same failure it
116
+ * prevents: a writer that silently EROFS-es every time. */
117
+ function isUsable(dir: string): boolean {
118
+ try {
119
+ mkdirSync(dir, { recursive: true });
120
+ const probe = join(dir, ".write-probe");
121
+ writeFileSync(probe, "");
122
+ rmSync(probe, { force: true });
123
+ return true;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Where this app's archive lives on THIS machine, and whether the machine keeps
131
+ * it.
132
+ *
133
+ * `<app-data-home>/cursed-satellites/archive/<app>/`, reached through the same env
134
+ * ladder the retention home uses, so a test, a container and a deployed box each
135
+ * land somewhere writable rather than somewhere assumed.
136
+ */
137
+ export function resolveArchiveHome(
138
+ app: string,
139
+ env: Record<string, string | undefined> = process.env,
140
+ ): ArchiveHome {
141
+ if (!SAFE_NAME.test(app)) throw new Error(`archive: app key "${app}" must be lower-kebab`);
142
+ const candidates: ArchiveHome[] = [];
143
+ if (env.RETENTION_ARCHIVE_HOME) {
144
+ candidates.push({ dir: join(env.RETENTION_ARCHIVE_HOME, app), terminal: true });
145
+ }
146
+ // An explicitly CONFIGURED home is used whether or not it exists yet — the
147
+ // operator naming it is the statement that it is the right place.
148
+ const configured = env[APP_DATA_HOME_ENV]?.trim();
149
+ if (configured) {
150
+ candidates.push({
151
+ dir: join(configured, "cursed-satellites", "archive", app),
152
+ terminal: true,
153
+ });
154
+ }
155
+ const conventional = resolveAppDataHome({ env });
156
+ // The OS convention is only PREFERRED when it is already there: creating it on
157
+ // a deployed box would strand the archive where no collector looks.
158
+ if (!configured && existsSync(conventional)) {
159
+ candidates.push({
160
+ dir: join(conventional, "cursed-satellites", "archive", app),
161
+ terminal: true,
162
+ });
163
+ }
164
+ // A deployed box: only the app's own data dir is writable under
165
+ // `ProtectSystem=strict`, and something else has to come and collect it.
166
+ if (env.APP_DATA_DIR) candidates.push({ dir: join(env.APP_DATA_DIR, "archive"), terminal: false });
167
+
168
+ for (const candidate of candidates) {
169
+ if (isUsable(candidate.dir)) return candidate;
170
+ }
171
+ // Nothing writable. Return the conventional path and let `write()` throw when
172
+ // it is actually used — which, by this module's contract, means nothing is
173
+ // deleted. Never a boot failure.
174
+ return { dir: join(conventional, "cursed-satellites", "archive", app), terminal: true };
175
+ }
176
+
177
+ export interface JsonlArchiveOptions {
178
+ app: string;
179
+ /** Stable, app-scoped resource id — the segment name's prefix. */
180
+ resource: string;
181
+ /** Overrides the resolved home. Tests and callers that already resolved it. */
182
+ home?: ArchiveHome;
183
+ env?: Record<string, string | undefined>;
184
+ }
185
+
186
+ /**
187
+ * The fleet's archive sink: newline-delimited JSON segments plus an append-only
188
+ * `index.jsonl` naming every segment ever sealed here.
189
+ *
190
+ * The index is what makes the archive answerable without reading gigabytes —
191
+ * "what is in here, how many rows, and does it still hash the same" — and it is
192
+ * appended rather than rewritten so a crash mid-write can cost at most the last
193
+ * line, never the catalogue.
194
+ */
195
+ export function createJsonlArchive(options: JsonlArchiveOptions): ArchiveSink {
196
+ const { app, resource } = options;
197
+ if (!SAFE_NAME.test(resource)) {
198
+ throw new Error(`archive: resource id "${resource}" must be lower-kebab`);
199
+ }
200
+ const home = options.home ?? resolveArchiveHome(app, options.env);
201
+ const dir = home.dir;
202
+ const terminal = home.terminal;
203
+ const indexPath = join(dir, "index.jsonl");
204
+
205
+ return {
206
+ dir,
207
+ terminal,
208
+
209
+ pendingBytes() {
210
+ if (terminal) return 0;
211
+ let total = 0;
212
+ let names: string[];
213
+ try {
214
+ names = readdirSync(dir);
215
+ } catch {
216
+ return 0; // not created yet — nothing is owed
217
+ }
218
+ for (const name of names) {
219
+ if (!name.endsWith(".jsonl") || name === "index.jsonl") continue;
220
+ try {
221
+ total += statSync(join(dir, name)).size;
222
+ } catch {
223
+ // vanished between readdir and stat — the collector took it
224
+ }
225
+ }
226
+ return total;
227
+ },
228
+
229
+ write(records, at) {
230
+ if (records.length === 0) throw new Error("archive: refusing to seal an empty segment");
231
+ const body = `${records.map((row) => JSON.stringify(row)).join("\n")}\n`;
232
+ const bytes = Buffer.byteLength(body);
233
+ const sha256 = createHash("sha256").update(body).digest("hex");
234
+ const file = `${resource}-${archiveStamp(new Date(at))}.jsonl`;
235
+ const path = join(dir, file);
236
+ const tmp = `${path}.${process.pid}.tmp`;
237
+
238
+ mkdirSync(dir, { recursive: true });
239
+
240
+ // Write + fsync THE FILE. Without the fsync the bytes can be in the page
241
+ // cache while the rows are already gone from a database that fsyncs its
242
+ // own WAL — a power cut between the two loses exactly the rows this
243
+ // module exists to keep.
244
+ const fd = openSync(tmp, "w");
245
+ try {
246
+ writeSync(fd, body);
247
+ fsyncSync(fd);
248
+ } finally {
249
+ closeSync(fd);
250
+ }
251
+
252
+ // Prove it, off the disk, before anything is deleted. Re-reading is the
253
+ // only check that covers a short write, a full disk that reported
254
+ // success, and a filesystem that lied.
255
+ let readBack: string;
256
+ try {
257
+ readBack = readFileSync(tmp, "utf8");
258
+ } catch (error) {
259
+ rmSync(tmp, { force: true });
260
+ throw new Error(
261
+ `archive: could not read back ${tmp} — ${error instanceof Error ? error.message : String(error)}`,
262
+ );
263
+ }
264
+ const verify = (ok: boolean, what: string): void => {
265
+ if (ok) return;
266
+ rmSync(tmp, { force: true });
267
+ throw new Error(`archive: ${file} failed verification (${what}) — nothing was deleted`);
268
+ };
269
+ verify(Buffer.byteLength(readBack) === bytes, `expected ${bytes} bytes`);
270
+ verify(createHash("sha256").update(readBack).digest("hex") === sha256, "sha256 mismatch");
271
+ const lines = readBack.split("\n").filter((line) => line.length > 0);
272
+ verify(lines.length === records.length, `expected ${records.length} rows, read ${lines.length}`);
273
+ for (const line of lines) {
274
+ let parsed: unknown;
275
+ try {
276
+ parsed = JSON.parse(line);
277
+ } catch {
278
+ verify(false, "a row did not parse back");
279
+ return undefined as never;
280
+ }
281
+ verify(typeof parsed === "object" && parsed !== null, "a row read back as a non-object");
282
+ }
283
+
284
+ // Sealed. `rename` inside one directory is atomic on every filesystem
285
+ // this fleet runs on, so a collector never sees a partial segment.
286
+ renameSync(tmp, path);
287
+
288
+ const segment: ArchiveSegment = { path, file, rows: records.length, bytes, sha256, at };
289
+ try {
290
+ appendFileSync(
291
+ indexPath,
292
+ `${JSON.stringify({ app, resource, ...segment, path: undefined, file })}\n`,
293
+ );
294
+ } catch {
295
+ // The catalogue is a convenience; the segment is the archive. A failed
296
+ // index append must never make a sealed segment look unsealed and send
297
+ // the same rows through again.
298
+ }
299
+ return segment;
300
+ },
301
+ };
302
+ }
303
+
304
+ /**
305
+ * Re-verify every segment in an archive directory against the sha256 the index
306
+ * recorded — the answer to "is this archive still real?" without a restore.
307
+ *
308
+ * Exists because an archive nobody can check is a belief, and this whole
309
+ * category is a promise that the rows still exist somewhere.
310
+ */
311
+ export function verifyArchive(dir: string): {
312
+ segments: number;
313
+ rows: number;
314
+ bytes: number;
315
+ bad: string[];
316
+ } {
317
+ const result = { segments: 0, rows: 0, bytes: 0, bad: [] as string[] };
318
+ let index: string;
319
+ try {
320
+ index = readFileSync(join(dir, "index.jsonl"), "utf8");
321
+ } catch {
322
+ return result;
323
+ }
324
+ for (const line of index.split("\n")) {
325
+ if (!line.trim()) continue;
326
+ let entry: { file?: string; rows?: number; bytes?: number; sha256?: string };
327
+ try {
328
+ entry = JSON.parse(line);
329
+ } catch {
330
+ result.bad.push(`unparseable index line: ${line.slice(0, 80)}`);
331
+ continue;
332
+ }
333
+ if (!entry.file || !entry.sha256) continue;
334
+ result.segments++;
335
+ const path = join(dir, entry.file);
336
+ let body: string;
337
+ try {
338
+ body = readFileSync(path, "utf8");
339
+ } catch {
340
+ // On a STAGED archive a collected segment is the healthy outcome, so
341
+ // absence is reported as a fact rather than as corruption.
342
+ result.bad.push(`${entry.file}: absent (collected, or lost)`);
343
+ continue;
344
+ }
345
+ if (createHash("sha256").update(body).digest("hex") !== entry.sha256) {
346
+ result.bad.push(`${entry.file}: sha256 mismatch`);
347
+ continue;
348
+ }
349
+ result.rows += entry.rows ?? 0;
350
+ result.bytes += entry.bytes ?? 0;
351
+ }
352
+ return result;
353
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The read model behind the station's Retention view: every app's published
3
+ * resources joined to the owner's overrides, with the effective policy resolved
4
+ * by the SAME function the sweeps use.
5
+ *
6
+ * That last point is the whole reason this is a shared function rather than a
7
+ * few lines in the station's route handler. A UI that computes "effective days"
8
+ * its own way eventually disagrees with the sweeper, and the disagreement shows
9
+ * up as data that vanished earlier than the screen said it would — which is the
10
+ * one failure mode that would make the owner stop trusting the control.
11
+ */
12
+ import type { EffectiveRetentionPolicy, RetentionOverride } from "./policy";
13
+ import { RETENTION_CATEGORIES, resolveRetentionPolicy } from "./policy";
14
+ import type { PublishedResource, RetentionPublication } from "./store";
15
+ import { readRetentionOverrides, readRetentionRegistry, resolveRetentionHome } from "./store";
16
+
17
+ export interface RetentionFleetRow extends PublishedResource {
18
+ key: string;
19
+ app: string;
20
+ policy: EffectiveRetentionPolicy;
21
+ override?: RetentionOverride;
22
+ /** The category's description, denormalized so the UI needs no second lookup. */
23
+ category: { label: string; defaultDays: number; summary: string };
24
+ /** Default days for this resource before any override — what "Reset" returns to. */
25
+ defaultDays: number;
26
+ /** The publishing app's last heartbeat, so the UI can label a stale row. */
27
+ publishedAt: number;
28
+ instance?: string;
29
+ sweepDisabled?: boolean;
30
+ }
31
+
32
+ export interface RetentionFleetView {
33
+ home: string;
34
+ updatedAt: number;
35
+ rows: RetentionFleetRow[];
36
+ totals: { resources: number; apps: number; items: number; bytes: number };
37
+ /** Overrides with no matching declaration — an app renamed or retired a resource. */
38
+ orphanedOverrides: Array<{ key: string; override: RetentionOverride }>;
39
+ }
40
+
41
+ export function buildRetentionFleetView(options: { home?: string } = {}): RetentionFleetView {
42
+ const home = options.home ?? resolveRetentionHome();
43
+ const publications: RetentionPublication[] = readRetentionRegistry(home);
44
+ const overridesFile = readRetentionOverrides(home);
45
+ const rows: RetentionFleetRow[] = [];
46
+ const claimed = new Set<string>();
47
+
48
+ for (const publication of publications) {
49
+ for (const resource of publication.resources) {
50
+ const declaration = resource.declaration;
51
+ if (!declaration || typeof declaration.app !== "string") continue;
52
+ const key = `${declaration.app}:${declaration.id}`;
53
+ const override = overridesFile.overrides[key];
54
+ let policy: EffectiveRetentionPolicy;
55
+ try {
56
+ policy = resolveRetentionPolicy(declaration, override);
57
+ } catch {
58
+ // A publication written by an older/newer app version than this reader
59
+ // understands is skipped rather than allowed to break the whole view.
60
+ continue;
61
+ }
62
+ claimed.add(key);
63
+ const category = RETENTION_CATEGORIES[declaration.category];
64
+ rows.push({
65
+ ...resource,
66
+ key,
67
+ app: declaration.app,
68
+ policy,
69
+ ...(override ? { override } : {}),
70
+ category,
71
+ defaultDays: declaration.defaultDays ?? category.defaultDays,
72
+ publishedAt: publication.publishedAt,
73
+ ...(publication.instance ? { instance: publication.instance } : {}),
74
+ ...(publication.sweepDisabled ? { sweepDisabled: true } : {}),
75
+ });
76
+ }
77
+ }
78
+
79
+ // Biggest first: the view exists to answer "what is filling my disk".
80
+ rows.sort((a, b) => {
81
+ const bytes = (b.measurement?.bytes ?? 0) - (a.measurement?.bytes ?? 0);
82
+ if (bytes !== 0) return bytes;
83
+ const items = (b.measurement?.items ?? 0) - (a.measurement?.items ?? 0);
84
+ if (items !== 0) return items;
85
+ return a.key.localeCompare(b.key);
86
+ });
87
+
88
+ const orphanedOverrides = Object.entries(overridesFile.overrides)
89
+ .filter(([key]) => !claimed.has(key))
90
+ .map(([key, override]) => ({ key, override }));
91
+
92
+ return {
93
+ home,
94
+ updatedAt: overridesFile.updatedAt,
95
+ rows,
96
+ totals: {
97
+ resources: rows.length,
98
+ apps: new Set(rows.map((r) => r.app)).size,
99
+ items: rows.reduce((sum, r) => sum + (r.measurement?.items ?? 0), 0),
100
+ bytes: rows.reduce((sum, r) => sum + (r.measurement?.bytes ?? 0), 0),
101
+ },
102
+ orphanedOverrides,
103
+ };
104
+ }