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,255 @@
1
+ /**
2
+ * What this suite pins, in order of how much it costs to get wrong:
3
+ *
4
+ * 1. **The SQL projection and the TypeScript adapter agree.** `shareEventsSource`
5
+ * and `activityFromShareEvent` are two implementations of ONE mapping, and a
6
+ * shape that drifts is exactly the "second audit trail" this primitive exists
7
+ * to prevent. The test compares them field for field on a real grant.
8
+ * 2. **A missing source table is an empty source.** An app can register the
9
+ * sharing source before it has ever made a grant.
10
+ * 3. **Append only.** The store exposes no verb that removes an event.
11
+ * 4. **The filters mean what they say**, including the two that are easy to get
12
+ * subtly wrong: a `%` in a search term is a literal, and an action prefix is
13
+ * a prefix rather than a substring.
14
+ */
15
+ import { Database } from "bun:sqlite";
16
+ import { describe, expect, test } from "bun:test";
17
+ import { activityFromShareEvent } from "cursedbelt-core/activity/model";
18
+ import { createShareStore, user } from "../sharing/store";
19
+ import { shareEventsSource } from "./shareSource";
20
+ import { createActivityLog } from "./store";
21
+
22
+ const open = (): Database => new Database(":memory:");
23
+
24
+ /** A clock that ticks one minute per call, so `at` ordering is deterministic. */
25
+ function clock(startIso = "2026-08-11T00:00:00.000Z"): () => string {
26
+ let at = Date.parse(startIso);
27
+ return () => {
28
+ const value = new Date(at).toISOString();
29
+ at += 60_000;
30
+ return value;
31
+ };
32
+ }
33
+
34
+ describe("the activity log", () => {
35
+ test("records and reads back one event", () => {
36
+ const log = createActivityLog({ db: open(), app: "admin", now: clock() });
37
+ const written = log.record({
38
+ actor: "curt@example.com",
39
+ action: "backup.delete",
40
+ resource: { kind: "database", id: "notes.sqlite" },
41
+ note: "superseded",
42
+ data: { bytes: 4096 },
43
+ });
44
+
45
+ expect(written.id).toBe("log:1");
46
+ expect(written.source).toBe("log");
47
+ expect(written.app).toBe("admin");
48
+ expect(written.resource).toEqual({ app: "admin", kind: "database", id: "notes.sqlite" });
49
+
50
+ const page = log.query();
51
+ expect(page.events).toEqual([written]);
52
+ expect(page.truncated).toBe(false);
53
+ expect(page.limit).toBe(100);
54
+ });
55
+
56
+ test("refuses an event that cannot be filtered or attributed", () => {
57
+ const log = createActivityLog({ db: open(), app: "admin" });
58
+ expect(() => log.record({ actor: "curt@example.com", action: "Backup Deleted" })).toThrow(
59
+ /not an action/,
60
+ );
61
+ expect(() => log.record({ actor: "curt@example.com", action: "backup" })).toThrow(
62
+ /not an action/,
63
+ );
64
+ expect(() => log.record({ actor: " ", action: "backup.delete" })).toThrow(/no actor/);
65
+ });
66
+
67
+ test("is append only — no verb here can remove an event", () => {
68
+ const log = createActivityLog({ db: open(), app: "admin" });
69
+ const verbs = Object.keys(log);
70
+ expect(verbs.some((verb) => /delete|remove|purge|clear|prune|truncate/i.test(verb))).toBe(
71
+ false,
72
+ );
73
+ });
74
+
75
+ test("newest first, bounded, and honest about there being more", () => {
76
+ const log = createActivityLog({ db: open(), app: "admin", now: clock() });
77
+ for (let i = 0; i < 5; i++) {
78
+ log.record({ actor: "curt@example.com", action: "backup.create", note: `#${i}` });
79
+ }
80
+ const page = log.query({ limit: 2 });
81
+ expect(page.events.map((e) => e.note)).toEqual(["#4", "#3"]);
82
+ expect(page.truncated).toBe(true);
83
+ expect(log.query({ limit: 99 }).truncated).toBe(false);
84
+ expect(log.count()).toBe(5);
85
+ // A limit past the cap is capped, never taken literally.
86
+ expect(log.query({ limit: 10_000 }).limit).toBe(1000);
87
+ });
88
+
89
+ test("filters: window, actor case, action prefix, resource, outcome", () => {
90
+ const log = createActivityLog({ db: open(), app: "notes", now: clock() });
91
+ log.record({ actor: "Curt@Example.com", action: "note.create", resource: { kind: "note", id: "a" } });
92
+ log.record({ actor: "other@example.com", action: "note.delete", resource: { kind: "note", id: "b" } });
93
+ log.record({ actor: "curt@example.com", action: "backup.create", outcome: "error" });
94
+
95
+ expect(log.query({ actor: "curt@example.com" }).events.map((e) => e.action)).toEqual([
96
+ "backup.create",
97
+ "note.create",
98
+ ]);
99
+ // A trailing dot is a PREFIX; without it the match is exact.
100
+ expect(log.query({ action: "note." }).events).toHaveLength(2);
101
+ expect(log.query({ action: "note.create" }).events).toHaveLength(1);
102
+ expect(log.query({ kind: "note", resourceId: "b" }).events).toHaveLength(1);
103
+ expect(log.query({ outcome: "error" }).events).toHaveLength(1);
104
+ expect(log.query({ since: "2026-08-11T00:01:00.000Z" }).events).toHaveLength(2);
105
+ expect(log.query({ until: "2026-08-11T00:00:30.000Z" }).events).toHaveLength(1);
106
+ });
107
+
108
+ test("a wildcard in a search term is a literal", () => {
109
+ const log = createActivityLog({ db: open(), app: "notes", now: clock() });
110
+ log.record({ actor: "curt@example.com", action: "note.create", note: "100% done" });
111
+ log.record({ actor: "curt@example.com", action: "note.create", note: "nothing like it" });
112
+
113
+ expect(log.query({ q: "100%" }).events).toHaveLength(1);
114
+ // Were `%` a wildcard, "%d" would match both.
115
+ expect(log.query({ q: "%d" }).events).toHaveLength(0);
116
+ expect(log.query({ q: "note.create" }).events).toHaveLength(2);
117
+ });
118
+
119
+ test("facets say what is in the result, bounded", () => {
120
+ const log = createActivityLog({ db: open(), app: "notes", now: clock() });
121
+ log.record({ actor: "a@example.com", action: "note.create", resource: { kind: "note", id: "1" } });
122
+ log.record({ actor: "a@example.com", action: "note.create", resource: { kind: "note", id: "2" } });
123
+ log.record({ actor: "b@example.com", action: "note.delete", resource: { kind: "note", id: "1" } });
124
+
125
+ const facets = log.facets();
126
+ expect(facets.actions).toEqual([
127
+ { value: "note.create", count: 2 },
128
+ { value: "note.delete", count: 1 },
129
+ ]);
130
+ expect(facets.actors[0]).toEqual({ value: "a@example.com", count: 2 });
131
+ expect(facets.kinds).toEqual([{ value: "note", count: 3 }]);
132
+ // A facet respects the filter it is drawn under.
133
+ expect(log.facets({ actor: "b@example.com" }).actions).toEqual([
134
+ { value: "note.delete", count: 1 },
135
+ ]);
136
+ });
137
+ });
138
+
139
+ describe("the sharing primitive as a source", () => {
140
+ test("a share event reaches the stream, mapped exactly as the vocabulary maps it", () => {
141
+ const db = open();
142
+ const at = clock();
143
+ const shares = createShareStore({ db, app: "family", now: at });
144
+ const log = createActivityLog({
145
+ db,
146
+ app: "family",
147
+ now: at,
148
+ sources: [shareEventsSource({ app: "family" })],
149
+ });
150
+
151
+ const grant = shares.grant({
152
+ kind: "person",
153
+ id: "3f2",
154
+ subject: user("Aunt@Example.com"),
155
+ level: "read",
156
+ grantedBy: "curt@example.com",
157
+ note: "she asked",
158
+ });
159
+ expect(grant.level).toBe("read");
160
+
161
+ const [event] = shares.events({ kind: "person", id: "3f2" });
162
+ expect(event).toBeDefined();
163
+ const expected = activityFromShareEvent(event);
164
+
165
+ const [streamed] = log.query().events;
166
+ expect(streamed).toBeDefined();
167
+ expect(streamed.source).toBe("share");
168
+ expect(streamed.id).toBe(`share:${event.id}`);
169
+ // 🔴 The pin: the SQL projection and the adapter are one mapping.
170
+ expect({
171
+ at: streamed.at,
172
+ actor: streamed.actor,
173
+ action: streamed.action,
174
+ resource: streamed.resource
175
+ ? { kind: streamed.resource.kind, id: streamed.resource.id }
176
+ : null,
177
+ outcome: streamed.outcome,
178
+ note: streamed.note,
179
+ data: streamed.data,
180
+ }).toEqual({
181
+ at: expected.at as string,
182
+ actor: expected.actor,
183
+ action: expected.action,
184
+ resource: expected.resource ?? null,
185
+ outcome: expected.outcome ?? "ok",
186
+ note: expected.note ?? "",
187
+ data: expected.data ?? null,
188
+ });
189
+ });
190
+
191
+ test("a group event is labeled with the log's app rather than left unattributed", () => {
192
+ const db = open();
193
+ const shares = createShareStore({ db, app: "family", now: clock() });
194
+ const log = createActivityLog({
195
+ db,
196
+ app: "family",
197
+ sources: [shareEventsSource({ app: "family" })],
198
+ });
199
+ shares.createGroup({ id: "trustees", name: "The trustees", createdBy: "curt@example.com" });
200
+
201
+ const [event] = log.query().events;
202
+ expect(event.app).toBe("family");
203
+ expect(event.action).toBe("share.group.create");
204
+ expect(event.resource).toBeNull();
205
+ });
206
+
207
+ test("own events and share events interleave by time, newest first", () => {
208
+ const db = open();
209
+ const at = clock();
210
+ const shares = createShareStore({ db, app: "family", now: at });
211
+ const log = createActivityLog({
212
+ db,
213
+ app: "family",
214
+ now: at,
215
+ sources: [shareEventsSource({ app: "family" })],
216
+ });
217
+ log.record({ actor: "curt@example.com", action: "person.create", resource: { kind: "person", id: "1" } });
218
+ shares.grant({
219
+ kind: "person",
220
+ id: "1",
221
+ subject: user("aunt@example.com"),
222
+ level: "read",
223
+ grantedBy: "curt@example.com",
224
+ });
225
+ log.record({ actor: "curt@example.com", action: "person.update", resource: { kind: "person", id: "1" } });
226
+
227
+ expect(log.query().events.map((e) => e.action)).toEqual([
228
+ "person.update",
229
+ "share.grant",
230
+ "person.create",
231
+ ]);
232
+ expect(log.query({ source: "share" }).events.map((e) => e.action)).toEqual(["share.grant"]);
233
+ expect(log.query({ source: "log" }).events).toHaveLength(2);
234
+ expect(log.count()).toBe(3);
235
+ });
236
+
237
+ test("a source whose table does not exist is empty, not a crash", () => {
238
+ const db = open();
239
+ const log = createActivityLog({
240
+ db,
241
+ app: "notes",
242
+ sources: [shareEventsSource({ app: "notes" })],
243
+ });
244
+ log.record({ actor: "curt@example.com", action: "note.create" });
245
+
246
+ // No sharing tables in this database at all.
247
+ expect(log.sources()).toEqual(["log"]);
248
+ expect(log.query().events).toHaveLength(1);
249
+ expect(log.facets().actions).toHaveLength(1);
250
+
251
+ // …and it joins the stream the moment the app starts sharing.
252
+ createShareStore({ db, app: "notes" });
253
+ expect(log.sources()).toEqual(["log", "share"]);
254
+ });
255
+ });
@@ -0,0 +1,368 @@
1
+ /**
2
+ * The activity store: one append-only table, plus any number of read-only
3
+ * SOURCES projected into the same shape, queried as one stream.
4
+ *
5
+ * ── How an app uses it ──────────────────────────────────────────────────────
6
+ * ```ts
7
+ * const activity = createActivityLog({
8
+ * db,
9
+ * app: "family",
10
+ * // The sharing primitive already audits itself. Read those rows where they
11
+ * // are instead of copying them — see `shareEventsSource`.
12
+ * sources: [shareEventsSource()],
13
+ * });
14
+ *
15
+ * activity.record({ actor: viewer.email, action: "person.delete",
16
+ * resource: { kind: "person", id }, note: "duplicate" });
17
+ *
18
+ * activity.query({ since, actor, action: "share.", limit: 100 });
19
+ * ```
20
+ *
21
+ * ── 🔴 Four properties this file exists to guarantee ────────────────────────
22
+ * 1. **Append only.** No update verb, no delete verb, no `TRUNCATE`. Ageing
23
+ * rows out is the fleet retention standard's job, through a declaration the
24
+ * owner can see (`satellite-kit/retention`) — never a silent cutoff here.
25
+ * 2. **A source is READ, never copied.** `share_events` stays the canonical
26
+ * sharing trail; this store selects from it. Two tables holding the same
27
+ * audit rows is two tables that will one day disagree, and the one people
28
+ * would trust is whichever the screen happened to read.
29
+ * 3. **A missing source table is an empty source, not a 500.** An app may
30
+ * register `shareEventsSource()` before it has ever created a grant, and a
31
+ * dashboard must not die on the app that has done nothing yet. (The same
32
+ * lesson `orch`'s `plainjob_jobs` retention target learned at boot.)
33
+ * 4. **`action` is validated at the write.** An action vocabulary that drifts
34
+ * cannot be filtered, and filtering is the whole point of the column.
35
+ */
36
+ import type { Database, SQLQueryBindings } from "bun:sqlite";
37
+ import {
38
+ type ActivityEvent,
39
+ type ActivityEventInput,
40
+ type ActivityOutcome,
41
+ isActivityAction,
42
+ isActivityOutcome,
43
+ } from "cursedbelt-core/activity/model";
44
+ import { runMigrations } from "../migration/runner";
45
+ import { ACTIVITY_EVENTS_TABLE, ACTIVITY_MIGRATIONS } from "./migrations";
46
+
47
+ /**
48
+ * A read-only stream contributor: a SELECT projecting some other table into the
49
+ * union's eleven columns, in this order and with these names —
50
+ *
51
+ * `source, seq, at, app, actor, action, kind, resource_id, outcome, note, data`
52
+ *
53
+ * `seq` is that table's own row id: it breaks ties within one `at`, and it is
54
+ * what makes the event id (`"<source>:<seq>"`) stable and unique across sources.
55
+ */
56
+ export interface ActivitySource {
57
+ /** Short, stable, unique within the log — it prefixes every id it produces. */
58
+ name: string;
59
+ /** The projection. See the column contract above. */
60
+ sql: string;
61
+ /** Bound in order, before any filter parameter. */
62
+ params?: readonly SQLQueryBindings[];
63
+ /**
64
+ * A table this projection needs. When it does not exist the source is skipped
65
+ * for that call rather than throwing — see property 3 in the header.
66
+ */
67
+ requiresTable?: string;
68
+ }
69
+
70
+ /** Everything the stream can be narrowed by. Every field is optional; the empty
71
+ * query is "the newest `limit` events". */
72
+ export interface ActivityQuery {
73
+ /** ISO-8601, inclusive. */
74
+ since?: string;
75
+ /** ISO-8601, inclusive. */
76
+ until?: string;
77
+ /** Case-insensitive exact match on the account. */
78
+ actor?: string;
79
+ /** Exact, or a PREFIX when it ends in a dot: `"share."` matches `share.grant`. */
80
+ action?: string;
81
+ kind?: string;
82
+ resourceId?: string;
83
+ outcome?: ActivityOutcome;
84
+ /** Restrict to one contributor (`"log"`, `"share"`). */
85
+ source?: string;
86
+ /** Substring, over actor + action + note + resource id. */
87
+ q?: string;
88
+ /** Default 100, hard cap 1000. */
89
+ limit?: number;
90
+ }
91
+
92
+ export interface ActivityPage {
93
+ events: ActivityEvent[];
94
+ /** The limit actually applied, after defaulting and capping. */
95
+ limit: number;
96
+ /** There is at least one more row older than the last one returned. */
97
+ truncated: boolean;
98
+ }
99
+
100
+ /** One dimension of "what is in this result", for the filter UI. */
101
+ export interface ActivityFacet {
102
+ value: string;
103
+ count: number;
104
+ }
105
+
106
+ export interface ActivityFacets {
107
+ actions: ActivityFacet[];
108
+ actors: ActivityFacet[];
109
+ kinds: ActivityFacet[];
110
+ }
111
+
112
+ export interface ActivityLogOptions {
113
+ db: Database;
114
+ /** The satellite this log speaks for. Fixed at construction, so a caller
115
+ * cannot write an event under another app's name. */
116
+ app: string;
117
+ /** Read-only contributors merged into the stream. */
118
+ sources?: readonly ActivitySource[];
119
+ /** Injectable clock, ISO-8601. */
120
+ now?: () => string;
121
+ }
122
+
123
+ export interface ActivityLog {
124
+ /** The app this log speaks for. */
125
+ readonly app: string;
126
+ /** Append one event. Returns it as it will be read back. */
127
+ record(input: ActivityEventInput): ActivityEvent;
128
+ /** Newest first, bounded. */
129
+ query(query?: ActivityQuery): ActivityPage;
130
+ /** How many events match — the unbounded count behind a bounded page. */
131
+ count(query?: ActivityQuery): number;
132
+ /** What is in the result, per dimension, for a filter UI. */
133
+ facets(query?: ActivityQuery, limit?: number): ActivityFacets;
134
+ /** Which contributors are live right now (a source whose table is missing is
135
+ * absent, which is what the read surface reports). */
136
+ sources(): string[];
137
+ }
138
+
139
+ interface StreamRow {
140
+ source: string;
141
+ seq: number;
142
+ at: string;
143
+ app: string;
144
+ actor: string;
145
+ action: string;
146
+ kind: string | null;
147
+ resource_id: string | null;
148
+ outcome: ActivityOutcome;
149
+ note: string;
150
+ data: string | null;
151
+ }
152
+
153
+ const DEFAULT_LIMIT = 100;
154
+ const MAX_LIMIT = 1000;
155
+ const MAX_FACET = 25;
156
+
157
+ /** `%` and `_` are wildcards and `\` is the escape — a note containing any of
158
+ * them must still search literally. */
159
+ const likeContains = (value: string): string =>
160
+ `%${value.replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
161
+
162
+ const toEvent = (row: StreamRow): ActivityEvent => ({
163
+ id: `${row.source}:${row.seq}`,
164
+ source: row.source,
165
+ at: row.at,
166
+ app: row.app,
167
+ actor: row.actor,
168
+ action: row.action,
169
+ resource:
170
+ row.kind === null || row.resource_id === null
171
+ ? null
172
+ : { app: row.app, kind: row.kind, id: row.resource_id },
173
+ outcome: isActivityOutcome(row.outcome) ? row.outcome : "ok",
174
+ note: row.note ?? "",
175
+ data: parseData(row.data),
176
+ });
177
+
178
+ /** A row whose `data` is unreadable must not take the page down with it — the
179
+ * event still says who did what, which is the part that matters. */
180
+ function parseData(raw: string | null): Record<string, unknown> | null {
181
+ if (!raw) return null;
182
+ try {
183
+ const parsed = JSON.parse(raw) as unknown;
184
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
185
+ ? (parsed as Record<string, unknown>)
186
+ : { value: parsed };
187
+ } catch {
188
+ return { unparsed: raw };
189
+ }
190
+ }
191
+
192
+ export function createActivityLog(options: ActivityLogOptions): ActivityLog {
193
+ const { db, app } = options;
194
+ const now = options.now ?? (() => new Date().toISOString());
195
+ const registered = options.sources ?? [];
196
+
197
+ runMigrations(db, ACTIVITY_MIGRATIONS);
198
+
199
+ const insert = db.query(
200
+ `INSERT INTO ${ACTIVITY_EVENTS_TABLE}
201
+ (at, app, actor, action, kind, resource_id, outcome, note, data)
202
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
203
+ );
204
+
205
+ const tableExists = (name: string): boolean =>
206
+ !!db
207
+ .query("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ?")
208
+ .get(name);
209
+
210
+ /** The contributors that can actually answer right now. */
211
+ const liveSources = (): ActivitySource[] =>
212
+ registered.filter((source) => !source.requiresTable || tableExists(source.requiresTable));
213
+
214
+ const OWN_SELECT = `SELECT 'log' AS source, event_id AS seq, at, app, actor, action,
215
+ kind, resource_id, outcome, note, data
216
+ FROM ${ACTIVITY_EVENTS_TABLE}`;
217
+
218
+ /** The union, plus the WHERE the caller asked for, plus their parameters in
219
+ * bind order (sources first — they are inside the FROM). */
220
+ const stream = (query: ActivityQuery): { sql: string; params: SQLQueryBindings[] } => {
221
+ const params: SQLQueryBindings[] = [];
222
+ const selects = [OWN_SELECT];
223
+ for (const source of liveSources()) {
224
+ if (query.source && query.source !== source.name) continue;
225
+ selects.push(source.sql);
226
+ params.push(...(source.params ?? []));
227
+ }
228
+ const where: string[] = [];
229
+ if (query.source) {
230
+ where.push("source = ?");
231
+ params.push(query.source);
232
+ }
233
+ if (query.since) {
234
+ where.push("at >= ?");
235
+ params.push(query.since);
236
+ }
237
+ if (query.until) {
238
+ where.push("at <= ?");
239
+ params.push(query.until);
240
+ }
241
+ if (query.actor) {
242
+ where.push("LOWER(actor) = LOWER(?)");
243
+ params.push(query.actor);
244
+ }
245
+ if (query.action) {
246
+ if (query.action.endsWith(".")) {
247
+ where.push("action LIKE ? ESCAPE '\\'");
248
+ params.push(`${query.action.replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`);
249
+ } else {
250
+ where.push("action = ?");
251
+ params.push(query.action);
252
+ }
253
+ }
254
+ if (query.kind) {
255
+ where.push("kind = ?");
256
+ params.push(query.kind);
257
+ }
258
+ if (query.resourceId) {
259
+ where.push("resource_id = ?");
260
+ params.push(query.resourceId);
261
+ }
262
+ if (query.outcome) {
263
+ where.push("outcome = ?");
264
+ params.push(query.outcome);
265
+ }
266
+ if (query.q) {
267
+ where.push(
268
+ "(actor LIKE ? ESCAPE '\\' OR action LIKE ? ESCAPE '\\' OR note LIKE ? ESCAPE '\\' OR IFNULL(resource_id, '') LIKE ? ESCAPE '\\')",
269
+ );
270
+ const like = likeContains(query.q);
271
+ params.push(like, like, like, like);
272
+ }
273
+ // `query.source` naming a contributor that is not live leaves only the own
274
+ // select, filtered to a source it cannot be — an empty result, which is the
275
+ // honest answer rather than every row.
276
+ const sql = `SELECT * FROM (${selects.join("\n UNION ALL\n")}) AS stream${
277
+ where.length ? ` WHERE ${where.join(" AND ")}` : ""
278
+ }`;
279
+ return { sql, params };
280
+ };
281
+
282
+ return {
283
+ app,
284
+
285
+ record(input: ActivityEventInput): ActivityEvent {
286
+ if (!isActivityAction(input.action)) {
287
+ throw new Error(
288
+ `activity: "${input.action}" is not an action — use lowercase subject.verb, e.g. "backup.delete"`,
289
+ );
290
+ }
291
+ const actor = input.actor?.trim();
292
+ if (!actor) {
293
+ // An event that cannot say who is a log line, not an audit trail.
294
+ throw new Error(`activity: ${input.action} has no actor`);
295
+ }
296
+ const at = input.at ?? now();
297
+ const outcome = input.outcome ?? "ok";
298
+ const data = input.data ? JSON.stringify(input.data) : null;
299
+ const info = insert.run(
300
+ at,
301
+ app,
302
+ actor,
303
+ input.action,
304
+ input.resource?.kind ?? null,
305
+ input.resource?.id ?? null,
306
+ outcome,
307
+ input.note ?? "",
308
+ data,
309
+ );
310
+ return {
311
+ id: `log:${Number(info.lastInsertRowid)}`,
312
+ source: "log",
313
+ at,
314
+ app,
315
+ actor,
316
+ action: input.action,
317
+ resource: input.resource ? { app, ...input.resource } : null,
318
+ outcome,
319
+ note: input.note ?? "",
320
+ data: input.data ?? null,
321
+ };
322
+ },
323
+
324
+ query(query: ActivityQuery = {}): ActivityPage {
325
+ const limit = Math.min(MAX_LIMIT, Math.max(1, Math.floor(query.limit ?? DEFAULT_LIMIT)));
326
+ const { sql, params } = stream(query);
327
+ // One row past the limit, so "there is more" is a fact rather than a
328
+ // guess made from `rows.length === limit`.
329
+ const rows = db
330
+ .query(`${sql} ORDER BY at DESC, seq DESC LIMIT ?`)
331
+ .all(...params, limit + 1) as StreamRow[];
332
+ const truncated = rows.length > limit;
333
+ return { events: rows.slice(0, limit).map(toEvent), limit, truncated };
334
+ },
335
+
336
+ count(query: ActivityQuery = {}): number {
337
+ const { sql, params } = stream(query);
338
+ const row = db.query(`SELECT COUNT(*) AS n FROM (${sql})`).get(...params) as {
339
+ n: number;
340
+ } | null;
341
+ return row?.n ?? 0;
342
+ },
343
+
344
+ facets(query: ActivityQuery = {}, limit = MAX_FACET): ActivityFacets {
345
+ const { sql, params } = stream(query);
346
+ const capped = Math.min(MAX_FACET, Math.max(1, Math.floor(limit)));
347
+ const dimension = (column: string): ActivityFacet[] =>
348
+ (
349
+ db
350
+ .query(
351
+ `SELECT ${column} AS value, COUNT(*) AS count FROM (${sql})
352
+ WHERE ${column} IS NOT NULL AND ${column} <> ''
353
+ GROUP BY ${column} ORDER BY count DESC, value ASC LIMIT ?`,
354
+ )
355
+ .all(...params, capped) as { value: string; count: number }[]
356
+ ).map((row) => ({ value: row.value, count: row.count }));
357
+ return {
358
+ actions: dimension("action"),
359
+ actors: dimension("actor"),
360
+ kinds: dimension("kind"),
361
+ };
362
+ },
363
+
364
+ sources(): string[] {
365
+ return ["log", ...liveSources().map((source) => source.name)];
366
+ },
367
+ };
368
+ }