happy2 0.0.5

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 (373) hide show
  1. package/README.md +82 -0
  2. package/package.json +74 -0
  3. package/packages/happy2-server/Dockerfile +18 -0
  4. package/packages/happy2-server/README.md +418 -0
  5. package/packages/happy2-server/bin/happy2.js +3 -0
  6. package/packages/happy2-server/dist/cli.d.ts +3 -0
  7. package/packages/happy2-server/dist/cli.d.ts.map +1 -0
  8. package/packages/happy2-server/dist/cli.js +44 -0
  9. package/packages/happy2-server/dist/cli.js.map +1 -0
  10. package/packages/happy2-server/dist/index.d.ts +24 -0
  11. package/packages/happy2-server/dist/index.d.ts.map +1 -0
  12. package/packages/happy2-server/dist/index.js +22 -0
  13. package/packages/happy2-server/dist/index.js.map +1 -0
  14. package/packages/happy2-server/dist/modules/agents/builtin-images.d.ts +8 -0
  15. package/packages/happy2-server/dist/modules/agents/builtin-images.d.ts.map +1 -0
  16. package/packages/happy2-server/dist/modules/agents/builtin-images.js +283 -0
  17. package/packages/happy2-server/dist/modules/agents/builtin-images.js.map +1 -0
  18. package/packages/happy2-server/dist/modules/agents/command.d.ts +3 -0
  19. package/packages/happy2-server/dist/modules/agents/command.d.ts.map +1 -0
  20. package/packages/happy2-server/dist/modules/agents/command.js +7 -0
  21. package/packages/happy2-server/dist/modules/agents/command.js.map +1 -0
  22. package/packages/happy2-server/dist/modules/agents/daemon.d.ts +106 -0
  23. package/packages/happy2-server/dist/modules/agents/daemon.d.ts.map +1 -0
  24. package/packages/happy2-server/dist/modules/agents/daemon.js +522 -0
  25. package/packages/happy2-server/dist/modules/agents/daemon.js.map +1 -0
  26. package/packages/happy2-server/dist/modules/agents/docker.d.ts +51 -0
  27. package/packages/happy2-server/dist/modules/agents/docker.d.ts.map +1 -0
  28. package/packages/happy2-server/dist/modules/agents/docker.js +256 -0
  29. package/packages/happy2-server/dist/modules/agents/docker.js.map +1 -0
  30. package/packages/happy2-server/dist/modules/agents/docker.test.d.ts +2 -0
  31. package/packages/happy2-server/dist/modules/agents/docker.test.d.ts.map +1 -0
  32. package/packages/happy2-server/dist/modules/agents/docker.test.js +155 -0
  33. package/packages/happy2-server/dist/modules/agents/docker.test.js.map +1 -0
  34. package/packages/happy2-server/dist/modules/agents/index.d.ts +5 -0
  35. package/packages/happy2-server/dist/modules/agents/index.d.ts.map +1 -0
  36. package/packages/happy2-server/dist/modules/agents/index.js +4 -0
  37. package/packages/happy2-server/dist/modules/agents/index.js.map +1 -0
  38. package/packages/happy2-server/dist/modules/agents/service.d.ts +140 -0
  39. package/packages/happy2-server/dist/modules/agents/service.d.ts.map +1 -0
  40. package/packages/happy2-server/dist/modules/agents/service.js +1283 -0
  41. package/packages/happy2-server/dist/modules/agents/service.js.map +1 -0
  42. package/packages/happy2-server/dist/modules/auth/cloudflare-access.d.ts +15 -0
  43. package/packages/happy2-server/dist/modules/auth/cloudflare-access.d.ts.map +1 -0
  44. package/packages/happy2-server/dist/modules/auth/cloudflare-access.js +49 -0
  45. package/packages/happy2-server/dist/modules/auth/cloudflare-access.js.map +1 -0
  46. package/packages/happy2-server/dist/modules/auth/crypto.d.ts +4 -0
  47. package/packages/happy2-server/dist/modules/auth/crypto.d.ts.map +1 -0
  48. package/packages/happy2-server/dist/modules/auth/crypto.js +31 -0
  49. package/packages/happy2-server/dist/modules/auth/crypto.js.map +1 -0
  50. package/packages/happy2-server/dist/modules/auth/crypto.test.d.ts +2 -0
  51. package/packages/happy2-server/dist/modules/auth/crypto.test.d.ts.map +1 -0
  52. package/packages/happy2-server/dist/modules/auth/crypto.test.js +11 -0
  53. package/packages/happy2-server/dist/modules/auth/crypto.test.js.map +1 -0
  54. package/packages/happy2-server/dist/modules/auth/email.d.ts +3 -0
  55. package/packages/happy2-server/dist/modules/auth/email.d.ts.map +1 -0
  56. package/packages/happy2-server/dist/modules/auth/email.js +22 -0
  57. package/packages/happy2-server/dist/modules/auth/email.js.map +1 -0
  58. package/packages/happy2-server/dist/modules/auth/metadata.d.ts +5 -0
  59. package/packages/happy2-server/dist/modules/auth/metadata.d.ts.map +1 -0
  60. package/packages/happy2-server/dist/modules/auth/metadata.js +28 -0
  61. package/packages/happy2-server/dist/modules/auth/metadata.js.map +1 -0
  62. package/packages/happy2-server/dist/modules/auth/metadata.test.d.ts +2 -0
  63. package/packages/happy2-server/dist/modules/auth/metadata.test.d.ts.map +1 -0
  64. package/packages/happy2-server/dist/modules/auth/metadata.test.js +10 -0
  65. package/packages/happy2-server/dist/modules/auth/metadata.test.js.map +1 -0
  66. package/packages/happy2-server/dist/modules/auth/methods.d.ts +10 -0
  67. package/packages/happy2-server/dist/modules/auth/methods.d.ts.map +1 -0
  68. package/packages/happy2-server/dist/modules/auth/methods.js +21 -0
  69. package/packages/happy2-server/dist/modules/auth/methods.js.map +1 -0
  70. package/packages/happy2-server/dist/modules/auth/methods.test.d.ts +2 -0
  71. package/packages/happy2-server/dist/modules/auth/methods.test.d.ts.map +1 -0
  72. package/packages/happy2-server/dist/modules/auth/methods.test.js +16 -0
  73. package/packages/happy2-server/dist/modules/auth/methods.test.js.map +1 -0
  74. package/packages/happy2-server/dist/modules/auth/oidc.d.ts +9 -0
  75. package/packages/happy2-server/dist/modules/auth/oidc.d.ts.map +1 -0
  76. package/packages/happy2-server/dist/modules/auth/oidc.js +68 -0
  77. package/packages/happy2-server/dist/modules/auth/oidc.js.map +1 -0
  78. package/packages/happy2-server/dist/modules/auth/service.d.ts +44 -0
  79. package/packages/happy2-server/dist/modules/auth/service.d.ts.map +1 -0
  80. package/packages/happy2-server/dist/modules/auth/service.js +208 -0
  81. package/packages/happy2-server/dist/modules/auth/service.js.map +1 -0
  82. package/packages/happy2-server/dist/modules/auth/tokens.d.ts +21 -0
  83. package/packages/happy2-server/dist/modules/auth/tokens.d.ts.map +1 -0
  84. package/packages/happy2-server/dist/modules/auth/tokens.js +79 -0
  85. package/packages/happy2-server/dist/modules/auth/tokens.js.map +1 -0
  86. package/packages/happy2-server/dist/modules/automation/repository.d.ts +127 -0
  87. package/packages/happy2-server/dist/modules/automation/repository.d.ts.map +1 -0
  88. package/packages/happy2-server/dist/modules/automation/repository.js +821 -0
  89. package/packages/happy2-server/dist/modules/automation/repository.js.map +1 -0
  90. package/packages/happy2-server/dist/modules/collaboration/repository.d.ts +839 -0
  91. package/packages/happy2-server/dist/modules/collaboration/repository.d.ts.map +1 -0
  92. package/packages/happy2-server/dist/modules/collaboration/repository.js +5795 -0
  93. package/packages/happy2-server/dist/modules/collaboration/repository.js.map +1 -0
  94. package/packages/happy2-server/dist/modules/collaboration/repository.test.d.ts +2 -0
  95. package/packages/happy2-server/dist/modules/collaboration/repository.test.d.ts.map +1 -0
  96. package/packages/happy2-server/dist/modules/collaboration/repository.test.js +620 -0
  97. package/packages/happy2-server/dist/modules/collaboration/repository.test.js.map +1 -0
  98. package/packages/happy2-server/dist/modules/collaboration/types.d.ts +243 -0
  99. package/packages/happy2-server/dist/modules/collaboration/types.d.ts.map +1 -0
  100. package/packages/happy2-server/dist/modules/collaboration/types.js +8 -0
  101. package/packages/happy2-server/dist/modules/collaboration/types.js.map +1 -0
  102. package/packages/happy2-server/dist/modules/config/defaults.d.ts +8 -0
  103. package/packages/happy2-server/dist/modules/config/defaults.d.ts.map +1 -0
  104. package/packages/happy2-server/dist/modules/config/defaults.js +70 -0
  105. package/packages/happy2-server/dist/modules/config/defaults.js.map +1 -0
  106. package/packages/happy2-server/dist/modules/config/defaults.test.d.ts +2 -0
  107. package/packages/happy2-server/dist/modules/config/defaults.test.d.ts.map +1 -0
  108. package/packages/happy2-server/dist/modules/config/defaults.test.js +27 -0
  109. package/packages/happy2-server/dist/modules/config/defaults.test.js.map +1 -0
  110. package/packages/happy2-server/dist/modules/config/environment.d.ts +4 -0
  111. package/packages/happy2-server/dist/modules/config/environment.d.ts.map +1 -0
  112. package/packages/happy2-server/dist/modules/config/environment.js +92 -0
  113. package/packages/happy2-server/dist/modules/config/environment.js.map +1 -0
  114. package/packages/happy2-server/dist/modules/config/loader.d.ts +5 -0
  115. package/packages/happy2-server/dist/modules/config/loader.d.ts.map +1 -0
  116. package/packages/happy2-server/dist/modules/config/loader.js +251 -0
  117. package/packages/happy2-server/dist/modules/config/loader.js.map +1 -0
  118. package/packages/happy2-server/dist/modules/config/loader.test.d.ts +2 -0
  119. package/packages/happy2-server/dist/modules/config/loader.test.d.ts.map +1 -0
  120. package/packages/happy2-server/dist/modules/config/loader.test.js +108 -0
  121. package/packages/happy2-server/dist/modules/config/loader.test.js.map +1 -0
  122. package/packages/happy2-server/dist/modules/config/paths.d.ts +7 -0
  123. package/packages/happy2-server/dist/modules/config/paths.d.ts.map +1 -0
  124. package/packages/happy2-server/dist/modules/config/paths.js +15 -0
  125. package/packages/happy2-server/dist/modules/config/paths.js.map +1 -0
  126. package/packages/happy2-server/dist/modules/config/paths.test.d.ts +2 -0
  127. package/packages/happy2-server/dist/modules/config/paths.test.d.ts.map +1 -0
  128. package/packages/happy2-server/dist/modules/config/paths.test.js +22 -0
  129. package/packages/happy2-server/dist/modules/config/paths.test.js.map +1 -0
  130. package/packages/happy2-server/dist/modules/config/runtime.d.ts +6 -0
  131. package/packages/happy2-server/dist/modules/config/runtime.d.ts.map +1 -0
  132. package/packages/happy2-server/dist/modules/config/runtime.js +14 -0
  133. package/packages/happy2-server/dist/modules/config/runtime.js.map +1 -0
  134. package/packages/happy2-server/dist/modules/config/type.d.ts +88 -0
  135. package/packages/happy2-server/dist/modules/config/type.d.ts.map +1 -0
  136. package/packages/happy2-server/dist/modules/config/type.js +2 -0
  137. package/packages/happy2-server/dist/modules/config/type.js.map +1 -0
  138. package/packages/happy2-server/dist/modules/database.d.ts +105 -0
  139. package/packages/happy2-server/dist/modules/database.d.ts.map +1 -0
  140. package/packages/happy2-server/dist/modules/database.js +583 -0
  141. package/packages/happy2-server/dist/modules/database.js.map +1 -0
  142. package/packages/happy2-server/dist/modules/drizzle.d.ts +11 -0
  143. package/packages/happy2-server/dist/modules/drizzle.d.ts.map +1 -0
  144. package/packages/happy2-server/dist/modules/drizzle.js +31 -0
  145. package/packages/happy2-server/dist/modules/drizzle.js.map +1 -0
  146. package/packages/happy2-server/dist/modules/files/lock.d.ts +3 -0
  147. package/packages/happy2-server/dist/modules/files/lock.d.ts.map +1 -0
  148. package/packages/happy2-server/dist/modules/files/lock.js +83 -0
  149. package/packages/happy2-server/dist/modules/files/lock.js.map +1 -0
  150. package/packages/happy2-server/dist/modules/files/media.d.ts +18 -0
  151. package/packages/happy2-server/dist/modules/files/media.d.ts.map +1 -0
  152. package/packages/happy2-server/dist/modules/files/media.js +352 -0
  153. package/packages/happy2-server/dist/modules/files/media.js.map +1 -0
  154. package/packages/happy2-server/dist/modules/files/provider.d.ts +130 -0
  155. package/packages/happy2-server/dist/modules/files/provider.d.ts.map +1 -0
  156. package/packages/happy2-server/dist/modules/files/provider.js +448 -0
  157. package/packages/happy2-server/dist/modules/files/provider.js.map +1 -0
  158. package/packages/happy2-server/dist/modules/files/quota.d.ts +58 -0
  159. package/packages/happy2-server/dist/modules/files/quota.d.ts.map +1 -0
  160. package/packages/happy2-server/dist/modules/files/quota.js +180 -0
  161. package/packages/happy2-server/dist/modules/files/quota.js.map +1 -0
  162. package/packages/happy2-server/dist/modules/files/routes.test.d.ts +2 -0
  163. package/packages/happy2-server/dist/modules/files/routes.test.d.ts.map +1 -0
  164. package/packages/happy2-server/dist/modules/files/routes.test.js +163 -0
  165. package/packages/happy2-server/dist/modules/files/routes.test.js.map +1 -0
  166. package/packages/happy2-server/dist/modules/files/scanner.d.ts +27 -0
  167. package/packages/happy2-server/dist/modules/files/scanner.d.ts.map +1 -0
  168. package/packages/happy2-server/dist/modules/files/scanner.js +66 -0
  169. package/packages/happy2-server/dist/modules/files/scanner.js.map +1 -0
  170. package/packages/happy2-server/dist/modules/files/storage.d.ts +95 -0
  171. package/packages/happy2-server/dist/modules/files/storage.d.ts.map +1 -0
  172. package/packages/happy2-server/dist/modules/files/storage.js +855 -0
  173. package/packages/happy2-server/dist/modules/files/storage.js.map +1 -0
  174. package/packages/happy2-server/dist/modules/files/storage.test.d.ts +2 -0
  175. package/packages/happy2-server/dist/modules/files/storage.test.d.ts.map +1 -0
  176. package/packages/happy2-server/dist/modules/files/storage.test.js +170 -0
  177. package/packages/happy2-server/dist/modules/files/storage.test.js.map +1 -0
  178. package/packages/happy2-server/dist/modules/integrations/index.d.ts +6 -0
  179. package/packages/happy2-server/dist/modules/integrations/index.d.ts.map +1 -0
  180. package/packages/happy2-server/dist/modules/integrations/index.js +6 -0
  181. package/packages/happy2-server/dist/modules/integrations/index.js.map +1 -0
  182. package/packages/happy2-server/dist/modules/integrations/repository.d.ts +136 -0
  183. package/packages/happy2-server/dist/modules/integrations/repository.d.ts.map +1 -0
  184. package/packages/happy2-server/dist/modules/integrations/repository.js +1229 -0
  185. package/packages/happy2-server/dist/modules/integrations/repository.js.map +1 -0
  186. package/packages/happy2-server/dist/modules/integrations/repository.test.d.ts +2 -0
  187. package/packages/happy2-server/dist/modules/integrations/repository.test.d.ts.map +1 -0
  188. package/packages/happy2-server/dist/modules/integrations/repository.test.js +345 -0
  189. package/packages/happy2-server/dist/modules/integrations/repository.test.js.map +1 -0
  190. package/packages/happy2-server/dist/modules/integrations/routes.test.d.ts +2 -0
  191. package/packages/happy2-server/dist/modules/integrations/routes.test.d.ts.map +1 -0
  192. package/packages/happy2-server/dist/modules/integrations/routes.test.js +161 -0
  193. package/packages/happy2-server/dist/modules/integrations/routes.test.js.map +1 -0
  194. package/packages/happy2-server/dist/modules/integrations/secrets.d.ts +19 -0
  195. package/packages/happy2-server/dist/modules/integrations/secrets.d.ts.map +1 -0
  196. package/packages/happy2-server/dist/modules/integrations/secrets.js +69 -0
  197. package/packages/happy2-server/dist/modules/integrations/secrets.js.map +1 -0
  198. package/packages/happy2-server/dist/modules/integrations/ssrf.d.ts +32 -0
  199. package/packages/happy2-server/dist/modules/integrations/ssrf.d.ts.map +1 -0
  200. package/packages/happy2-server/dist/modules/integrations/ssrf.js +185 -0
  201. package/packages/happy2-server/dist/modules/integrations/ssrf.js.map +1 -0
  202. package/packages/happy2-server/dist/modules/integrations/ssrf.test.d.ts +2 -0
  203. package/packages/happy2-server/dist/modules/integrations/ssrf.test.d.ts.map +1 -0
  204. package/packages/happy2-server/dist/modules/integrations/ssrf.test.js +56 -0
  205. package/packages/happy2-server/dist/modules/integrations/ssrf.test.js.map +1 -0
  206. package/packages/happy2-server/dist/modules/integrations/transport.d.ts +21 -0
  207. package/packages/happy2-server/dist/modules/integrations/transport.d.ts.map +1 -0
  208. package/packages/happy2-server/dist/modules/integrations/transport.js +108 -0
  209. package/packages/happy2-server/dist/modules/integrations/transport.js.map +1 -0
  210. package/packages/happy2-server/dist/modules/integrations/transport.test.d.ts +2 -0
  211. package/packages/happy2-server/dist/modules/integrations/transport.test.d.ts.map +1 -0
  212. package/packages/happy2-server/dist/modules/integrations/transport.test.js +66 -0
  213. package/packages/happy2-server/dist/modules/integrations/transport.test.js.map +1 -0
  214. package/packages/happy2-server/dist/modules/integrations/types.d.ts +155 -0
  215. package/packages/happy2-server/dist/modules/integrations/types.d.ts.map +1 -0
  216. package/packages/happy2-server/dist/modules/integrations/types.js +26 -0
  217. package/packages/happy2-server/dist/modules/integrations/types.js.map +1 -0
  218. package/packages/happy2-server/dist/modules/operations/export-worker.d.ts +16 -0
  219. package/packages/happy2-server/dist/modules/operations/export-worker.d.ts.map +1 -0
  220. package/packages/happy2-server/dist/modules/operations/export-worker.js +56 -0
  221. package/packages/happy2-server/dist/modules/operations/export-worker.js.map +1 -0
  222. package/packages/happy2-server/dist/modules/operations/repository.d.ts +215 -0
  223. package/packages/happy2-server/dist/modules/operations/repository.d.ts.map +1 -0
  224. package/packages/happy2-server/dist/modules/operations/repository.js +2011 -0
  225. package/packages/happy2-server/dist/modules/operations/repository.js.map +1 -0
  226. package/packages/happy2-server/dist/modules/operations/repository.test.d.ts +2 -0
  227. package/packages/happy2-server/dist/modules/operations/repository.test.d.ts.map +1 -0
  228. package/packages/happy2-server/dist/modules/operations/repository.test.js +610 -0
  229. package/packages/happy2-server/dist/modules/operations/repository.test.js.map +1 -0
  230. package/packages/happy2-server/dist/modules/operations/types.d.ts +144 -0
  231. package/packages/happy2-server/dist/modules/operations/types.d.ts.map +1 -0
  232. package/packages/happy2-server/dist/modules/operations/types.js +9 -0
  233. package/packages/happy2-server/dist/modules/operations/types.js.map +1 -0
  234. package/packages/happy2-server/dist/modules/realtime/events.d.ts +99 -0
  235. package/packages/happy2-server/dist/modules/realtime/events.d.ts.map +1 -0
  236. package/packages/happy2-server/dist/modules/realtime/events.js +173 -0
  237. package/packages/happy2-server/dist/modules/realtime/events.js.map +1 -0
  238. package/packages/happy2-server/dist/modules/realtime/index.d.ts +4 -0
  239. package/packages/happy2-server/dist/modules/realtime/index.d.ts.map +1 -0
  240. package/packages/happy2-server/dist/modules/realtime/index.js +4 -0
  241. package/packages/happy2-server/dist/modules/realtime/index.js.map +1 -0
  242. package/packages/happy2-server/dist/modules/realtime/local.d.ts +37 -0
  243. package/packages/happy2-server/dist/modules/realtime/local.d.ts.map +1 -0
  244. package/packages/happy2-server/dist/modules/realtime/local.js +181 -0
  245. package/packages/happy2-server/dist/modules/realtime/local.js.map +1 -0
  246. package/packages/happy2-server/dist/modules/realtime/local.test.d.ts +2 -0
  247. package/packages/happy2-server/dist/modules/realtime/local.test.d.ts.map +1 -0
  248. package/packages/happy2-server/dist/modules/realtime/local.test.js +186 -0
  249. package/packages/happy2-server/dist/modules/realtime/local.test.js.map +1 -0
  250. package/packages/happy2-server/dist/modules/realtime/pubsub.d.ts +26 -0
  251. package/packages/happy2-server/dist/modules/realtime/pubsub.d.ts.map +1 -0
  252. package/packages/happy2-server/dist/modules/realtime/pubsub.js +7 -0
  253. package/packages/happy2-server/dist/modules/realtime/pubsub.js.map +1 -0
  254. package/packages/happy2-server/dist/modules/request/database-idempotency.d.ts +15 -0
  255. package/packages/happy2-server/dist/modules/request/database-idempotency.d.ts.map +1 -0
  256. package/packages/happy2-server/dist/modules/request/database-idempotency.js +141 -0
  257. package/packages/happy2-server/dist/modules/request/database-idempotency.js.map +1 -0
  258. package/packages/happy2-server/dist/modules/request/idempotency-hook.d.ts +12 -0
  259. package/packages/happy2-server/dist/modules/request/idempotency-hook.d.ts.map +1 -0
  260. package/packages/happy2-server/dist/modules/request/idempotency-hook.js +98 -0
  261. package/packages/happy2-server/dist/modules/request/idempotency-hook.js.map +1 -0
  262. package/packages/happy2-server/dist/modules/request/idempotency.d.ts +146 -0
  263. package/packages/happy2-server/dist/modules/request/idempotency.d.ts.map +1 -0
  264. package/packages/happy2-server/dist/modules/request/idempotency.js +312 -0
  265. package/packages/happy2-server/dist/modules/request/idempotency.js.map +1 -0
  266. package/packages/happy2-server/dist/modules/request/idempotency.test.d.ts +2 -0
  267. package/packages/happy2-server/dist/modules/request/idempotency.test.d.ts.map +1 -0
  268. package/packages/happy2-server/dist/modules/request/idempotency.test.js +191 -0
  269. package/packages/happy2-server/dist/modules/request/idempotency.test.js.map +1 -0
  270. package/packages/happy2-server/dist/modules/request/index.d.ts +5 -0
  271. package/packages/happy2-server/dist/modules/request/index.d.ts.map +1 -0
  272. package/packages/happy2-server/dist/modules/request/index.js +5 -0
  273. package/packages/happy2-server/dist/modules/request/index.js.map +1 -0
  274. package/packages/happy2-server/dist/modules/request/rate-limit.d.ts +100 -0
  275. package/packages/happy2-server/dist/modules/request/rate-limit.d.ts.map +1 -0
  276. package/packages/happy2-server/dist/modules/request/rate-limit.js +239 -0
  277. package/packages/happy2-server/dist/modules/request/rate-limit.js.map +1 -0
  278. package/packages/happy2-server/dist/modules/request/rate-limit.test.d.ts +2 -0
  279. package/packages/happy2-server/dist/modules/request/rate-limit.test.d.ts.map +1 -0
  280. package/packages/happy2-server/dist/modules/request/rate-limit.test.js +123 -0
  281. package/packages/happy2-server/dist/modules/request/rate-limit.test.js.map +1 -0
  282. package/packages/happy2-server/dist/modules/schema.d.ts +31454 -0
  283. package/packages/happy2-server/dist/modules/schema.d.ts.map +1 -0
  284. package/packages/happy2-server/dist/modules/schema.js +1081 -0
  285. package/packages/happy2-server/dist/modules/schema.js.map +1 -0
  286. package/packages/happy2-server/dist/modules/workspace/index.d.ts +3 -0
  287. package/packages/happy2-server/dist/modules/workspace/index.d.ts.map +1 -0
  288. package/packages/happy2-server/dist/modules/workspace/index.js +3 -0
  289. package/packages/happy2-server/dist/modules/workspace/index.js.map +1 -0
  290. package/packages/happy2-server/dist/modules/workspace/service.d.ts +54 -0
  291. package/packages/happy2-server/dist/modules/workspace/service.d.ts.map +1 -0
  292. package/packages/happy2-server/dist/modules/workspace/service.js +997 -0
  293. package/packages/happy2-server/dist/modules/workspace/service.js.map +1 -0
  294. package/packages/happy2-server/dist/modules/workspace/types.d.ts +58 -0
  295. package/packages/happy2-server/dist/modules/workspace/types.d.ts.map +1 -0
  296. package/packages/happy2-server/dist/modules/workspace/types.js +11 -0
  297. package/packages/happy2-server/dist/modules/workspace/types.js.map +1 -0
  298. package/packages/happy2-server/dist/routes/agents.d.ts +5 -0
  299. package/packages/happy2-server/dist/routes/agents.d.ts.map +1 -0
  300. package/packages/happy2-server/dist/routes/agents.js +225 -0
  301. package/packages/happy2-server/dist/routes/agents.js.map +1 -0
  302. package/packages/happy2-server/dist/routes/auth.d.ts +5 -0
  303. package/packages/happy2-server/dist/routes/auth.d.ts.map +1 -0
  304. package/packages/happy2-server/dist/routes/auth.js +128 -0
  305. package/packages/happy2-server/dist/routes/auth.js.map +1 -0
  306. package/packages/happy2-server/dist/routes/automation.d.ts +6 -0
  307. package/packages/happy2-server/dist/routes/automation.d.ts.map +1 -0
  308. package/packages/happy2-server/dist/routes/automation.js +315 -0
  309. package/packages/happy2-server/dist/routes/automation.js.map +1 -0
  310. package/packages/happy2-server/dist/routes/basic.d.ts +3 -0
  311. package/packages/happy2-server/dist/routes/basic.d.ts.map +1 -0
  312. package/packages/happy2-server/dist/routes/basic.js +5 -0
  313. package/packages/happy2-server/dist/routes/basic.js.map +1 -0
  314. package/packages/happy2-server/dist/routes/collaboration.d.ts +7 -0
  315. package/packages/happy2-server/dist/routes/collaboration.d.ts.map +1 -0
  316. package/packages/happy2-server/dist/routes/collaboration.js +1285 -0
  317. package/packages/happy2-server/dist/routes/collaboration.js.map +1 -0
  318. package/packages/happy2-server/dist/routes/files.d.ts +9 -0
  319. package/packages/happy2-server/dist/routes/files.d.ts.map +1 -0
  320. package/packages/happy2-server/dist/routes/files.js +397 -0
  321. package/packages/happy2-server/dist/routes/files.js.map +1 -0
  322. package/packages/happy2-server/dist/routes/integrations.d.ts +6 -0
  323. package/packages/happy2-server/dist/routes/integrations.d.ts.map +1 -0
  324. package/packages/happy2-server/dist/routes/integrations.js +486 -0
  325. package/packages/happy2-server/dist/routes/integrations.js.map +1 -0
  326. package/packages/happy2-server/dist/routes/operations.d.ts +5 -0
  327. package/packages/happy2-server/dist/routes/operations.d.ts.map +1 -0
  328. package/packages/happy2-server/dist/routes/operations.js +588 -0
  329. package/packages/happy2-server/dist/routes/operations.js.map +1 -0
  330. package/packages/happy2-server/dist/routes/operations.test.d.ts +2 -0
  331. package/packages/happy2-server/dist/routes/operations.test.d.ts.map +1 -0
  332. package/packages/happy2-server/dist/routes/operations.test.js +106 -0
  333. package/packages/happy2-server/dist/routes/operations.test.js.map +1 -0
  334. package/packages/happy2-server/dist/routes/sync.d.ts +6 -0
  335. package/packages/happy2-server/dist/routes/sync.d.ts.map +1 -0
  336. package/packages/happy2-server/dist/routes/sync.js +549 -0
  337. package/packages/happy2-server/dist/routes/sync.js.map +1 -0
  338. package/packages/happy2-server/dist/routes/workspace.d.ts +5 -0
  339. package/packages/happy2-server/dist/routes/workspace.d.ts.map +1 -0
  340. package/packages/happy2-server/dist/routes/workspace.js +240 -0
  341. package/packages/happy2-server/dist/routes/workspace.js.map +1 -0
  342. package/packages/happy2-server/dist/runner.d.ts +3 -0
  343. package/packages/happy2-server/dist/runner.d.ts.map +1 -0
  344. package/packages/happy2-server/dist/runner.js +30 -0
  345. package/packages/happy2-server/dist/runner.js.map +1 -0
  346. package/packages/happy2-server/dist/server.d.ts +32 -0
  347. package/packages/happy2-server/dist/server.d.ts.map +1 -0
  348. package/packages/happy2-server/dist/server.js +284 -0
  349. package/packages/happy2-server/dist/server.js.map +1 -0
  350. package/packages/happy2-server/dist/standalone.d.ts +13 -0
  351. package/packages/happy2-server/dist/standalone.d.ts.map +1 -0
  352. package/packages/happy2-server/dist/standalone.js +99 -0
  353. package/packages/happy2-server/dist/standalone.js.map +1 -0
  354. package/packages/happy2-server/dist/web/assets/figtree-latin-wght-normal-D_ZTVpCC.woff2 +0 -0
  355. package/packages/happy2-server/dist/web/assets/happy-otter-Q2O_g-DT.png +0 -0
  356. package/packages/happy2-server/dist/web/assets/index-COvbX8JP.css +2 -0
  357. package/packages/happy2-server/dist/web/assets/index-DdpugxMO.js +57 -0
  358. package/packages/happy2-server/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  359. package/packages/happy2-server/dist/web/assets/onboarding-CRUpq9xl.png +0 -0
  360. package/packages/happy2-server/dist/web/index.html +13 -0
  361. package/packages/happy2-server/drizzle/0000_initial.sql +13 -0
  362. package/packages/happy2-server/drizzle/0001_accounts_profiles_files.sql +42 -0
  363. package/packages/happy2-server/drizzle/0002_file_visibility_and_uploader.sql +5 -0
  364. package/packages/happy2-server/drizzle/0003_collaboration.sql +565 -0
  365. package/packages/happy2-server/drizzle/0004_collaboration_expansion.sql +1099 -0
  366. package/packages/happy2-server/drizzle/0005_ai_agents.sql +85 -0
  367. package/packages/happy2-server/drizzle/0006_rig_global_event_queue.sql +10 -0
  368. package/packages/happy2-server/drizzle/0007_rename_server_identity.sql +3 -0
  369. package/packages/happy2-server/drizzle/0008_agent_message_streaming.sql +47 -0
  370. package/packages/happy2-server/drizzle/0009_chat_workspace_binding_lookup.sql +2 -0
  371. package/packages/happy2-server/drizzle/0010_rig_secret_assignments.sql +21 -0
  372. package/packages/happy2-server/drizzle/meta/_journal.json +83 -0
  373. package/packages/happy2-server/happy2.example.toml +104 -0
@@ -0,0 +1,2011 @@
1
+ import { createClient } from "@libsql/client";
2
+ import { createId } from "@paralleldrive/cuid2";
3
+ import { and, asc, desc, eq, gt, isNull, lt, lte, or, sql } from "drizzle-orm";
4
+ import { createDatabase, retrySqliteBusy, } from "../drizzle.js";
5
+ import { accountBans, accounts, auditLogEntries, authSessionEvents, authSessions, backupRecords, chatMembers, chatUpdates, chats, dataExportJobs, fileAccessGrants, files, messageAttachments, messageRevisions, messageSearchDocuments, messages, moderationActions, moderationReports, notifications, retentionRuns, serverSyncState, syncEvents, threadParticipants, threadUserStates, threads, users, } from "../schema.js";
6
+ import { OperationsError, } from "./types.js";
7
+ const auditSelection = {
8
+ id: auditLogEntries.id,
9
+ actor_user_id: auditLogEntries.actorUserId,
10
+ actor_integration_id: auditLogEntries.actorIntegrationId,
11
+ action: auditLogEntries.action,
12
+ target_type: auditLogEntries.targetType,
13
+ target_id: auditLogEntries.targetId,
14
+ chat_id: auditLogEntries.chatId,
15
+ before_json: auditLogEntries.beforeJson,
16
+ after_json: auditLogEntries.afterJson,
17
+ metadata_json: auditLogEntries.metadataJson,
18
+ client_ip: auditLogEntries.clientIp,
19
+ device: auditLogEntries.device,
20
+ app_version: auditLogEntries.appVersion,
21
+ user_agent: auditLogEntries.userAgent,
22
+ created_at: auditLogEntries.createdAt,
23
+ };
24
+ const banSelection = {
25
+ id: accountBans.id,
26
+ account_id: accountBans.accountId,
27
+ user_id: users.id,
28
+ username: users.username,
29
+ banned_by_user_id: accountBans.bannedByUserId,
30
+ reason: accountBans.reason,
31
+ banned_at: accountBans.bannedAt,
32
+ expires_at: accountBans.expiresAt,
33
+ revoked_at: accountBans.revokedAt,
34
+ revoked_by_user_id: accountBans.revokedByUserId,
35
+ revoke_reason: accountBans.revokeReason,
36
+ };
37
+ const reportSelection = {
38
+ id: moderationReports.id,
39
+ reported_by_user_id: moderationReports.reportedByUserId,
40
+ target_user_id: moderationReports.targetUserId,
41
+ chat_id: moderationReports.chatId,
42
+ message_id: moderationReports.messageId,
43
+ file_id: moderationReports.fileId,
44
+ reason: moderationReports.reason,
45
+ details: moderationReports.details,
46
+ status: moderationReports.status,
47
+ assigned_to_user_id: moderationReports.assignedToUserId,
48
+ resolution: moderationReports.resolution,
49
+ created_at: moderationReports.createdAt,
50
+ updated_at: moderationReports.updatedAt,
51
+ resolved_at: moderationReports.resolvedAt,
52
+ };
53
+ const moderationActionSelection = {
54
+ id: moderationActions.id,
55
+ report_id: moderationActions.reportId,
56
+ actor_user_id: moderationActions.actorUserId,
57
+ target_user_id: moderationActions.targetUserId,
58
+ chat_id: moderationActions.chatId,
59
+ message_id: moderationActions.messageId,
60
+ file_id: moderationActions.fileId,
61
+ action: moderationActions.action,
62
+ reason: moderationActions.reason,
63
+ metadata_json: moderationActions.metadataJson,
64
+ expires_at: moderationActions.expiresAt,
65
+ revoked_at: moderationActions.revokedAt,
66
+ created_at: moderationActions.createdAt,
67
+ };
68
+ const exportSelection = {
69
+ id: dataExportJobs.id,
70
+ requested_by_user_id: dataExportJobs.requestedByUserId,
71
+ kind: dataExportJobs.kind,
72
+ target_id: dataExportJobs.targetId,
73
+ status: dataExportJobs.status,
74
+ output_file_id: dataExportJobs.outputFileId,
75
+ options_json: dataExportJobs.optionsJson,
76
+ last_error: dataExportJobs.lastError,
77
+ expires_at: dataExportJobs.expiresAt,
78
+ created_at: dataExportJobs.createdAt,
79
+ started_at: dataExportJobs.startedAt,
80
+ completed_at: dataExportJobs.completedAt,
81
+ };
82
+ const backupSelection = {
83
+ id: backupRecords.id,
84
+ storage_provider: backupRecords.storageProvider,
85
+ storage_key: backupRecords.storageKey,
86
+ checksum_sha256: backupRecords.checksumSha256,
87
+ size: backupRecords.size,
88
+ status: backupRecords.status,
89
+ created_by_user_id: backupRecords.createdByUserId,
90
+ metadata_json: backupRecords.metadataJson,
91
+ last_error: backupRecords.lastError,
92
+ created_at: backupRecords.createdAt,
93
+ completed_at: backupRecords.completedAt,
94
+ retention_until: backupRecords.retentionUntil,
95
+ };
96
+ const retentionSelection = {
97
+ id: retentionRuns.id,
98
+ scope: retentionRuns.scope,
99
+ status: retentionRuns.status,
100
+ items_examined: retentionRuns.itemsExamined,
101
+ items_deleted: retentionRuns.itemsDeleted,
102
+ details_json: retentionRuns.detailsJson,
103
+ last_error: retentionRuns.lastError,
104
+ started_at: retentionRuns.startedAt,
105
+ completed_at: retentionRuns.completedAt,
106
+ };
107
+ /** Durable administrative and operational state. All authorization is rechecked in SQLite. */
108
+ export class OperationsRepository {
109
+ client;
110
+ db;
111
+ ownsClient;
112
+ constructor(source, authToken) {
113
+ this.ownsClient = typeof source === "string";
114
+ this.client =
115
+ typeof source === "string" ? createClient({ url: source, authToken }) : source;
116
+ this.db = createDatabase(this.client);
117
+ }
118
+ close() {
119
+ if (this.ownsClient)
120
+ this.client.close();
121
+ }
122
+ async listAuditLog(input) {
123
+ await this.requireAdminDb(this.db, input.actorUserId);
124
+ const cursor = decodeCursor(input.before);
125
+ const conditions = [];
126
+ if (input.action)
127
+ conditions.push(eq(auditLogEntries.action, input.action));
128
+ if (input.targetType)
129
+ conditions.push(eq(auditLogEntries.targetType, input.targetType));
130
+ if (input.targetId)
131
+ conditions.push(eq(auditLogEntries.targetId, input.targetId));
132
+ if (input.auditedActorUserId)
133
+ conditions.push(eq(auditLogEntries.actorUserId, input.auditedActorUserId));
134
+ if (cursor)
135
+ conditions.push(cursorCondition(auditLogEntries.createdAt, auditLogEntries.id, cursor));
136
+ const rows = await this.db
137
+ .select(auditSelection)
138
+ .from(auditLogEntries)
139
+ .where(and(...conditions))
140
+ .orderBy(desc(auditLogEntries.createdAt), desc(auditLogEntries.id))
141
+ .limit(input.limit + 1);
142
+ return page(rows, input.limit, asAudit);
143
+ }
144
+ async applyBan(input) {
145
+ const expiresAt = futureTimestamp(input.expiresAt, "expiresAt");
146
+ return this.writeDb(async (tx) => {
147
+ await this.requireAdminDb(tx, input.actorUserId);
148
+ if (input.actorUserId === input.targetUserId)
149
+ throw new OperationsError("forbidden", "Administrators cannot ban themselves");
150
+ const target = await this.accountTargetDb(tx, input.targetUserId);
151
+ await this.closeElapsedBan(tx, target);
152
+ if (target.bannedAt &&
153
+ (!target.banExpiresAt || Date.parse(target.banExpiresAt) > Date.now()))
154
+ throw new OperationsError("conflict", "User already has an active ban");
155
+ const id = createId();
156
+ await tx.insert(accountBans).values({
157
+ id,
158
+ accountId: target.accountId,
159
+ bannedByUserId: input.actorUserId,
160
+ reason: input.reason,
161
+ expiresAt,
162
+ });
163
+ await tx
164
+ .update(accounts)
165
+ .set({
166
+ bannedAt: sql `CURRENT_TIMESTAMP`,
167
+ banExpiresAt: expiresAt ?? null,
168
+ banReason: input.reason ?? null,
169
+ bannedByUserId: input.actorUserId,
170
+ })
171
+ .where(eq(accounts.id, target.accountId));
172
+ const sessions = await tx
173
+ .update(authSessions)
174
+ .set({ revokedAt: sql `CURRENT_TIMESTAMP` })
175
+ .where(and(eq(authSessions.accountId, target.accountId), isNull(authSessions.revokedAt)))
176
+ .returning({ id: authSessions.id });
177
+ await this.syncUserMutation(tx, input.actorUserId, target.userId, "user.banned");
178
+ const ban = await this.banDb(tx, id);
179
+ await this.appendAudit(tx, {
180
+ actorUserId: input.actorUserId,
181
+ action: "user.ban_applied",
182
+ targetType: "user",
183
+ targetId: target.userId,
184
+ before: accountTargetState(target),
185
+ after: ban,
186
+ context: mergeContext(input.context, {
187
+ revokedSessionCount: sessions.length,
188
+ }),
189
+ });
190
+ return ban;
191
+ });
192
+ }
193
+ async revokeBan(input) {
194
+ return this.writeDb(async (tx) => {
195
+ await this.requireAdminDb(tx, input.actorUserId);
196
+ const target = await this.accountTargetDb(tx, input.targetUserId);
197
+ const [current] = await tx
198
+ .select(banSelection)
199
+ .from(accountBans)
200
+ .leftJoin(users, eq(users.accountId, accountBans.accountId))
201
+ .where(and(eq(accountBans.accountId, target.accountId), isNull(accountBans.revokedAt)))
202
+ .orderBy(desc(accountBans.bannedAt), desc(accountBans.id))
203
+ .limit(1);
204
+ if (!current || !target.bannedAt)
205
+ throw new OperationsError("conflict", "User does not have an active ban");
206
+ await tx
207
+ .update(accountBans)
208
+ .set({
209
+ revokedAt: sql `CURRENT_TIMESTAMP`,
210
+ revokedByUserId: input.actorUserId,
211
+ revokeReason: input.reason ?? null,
212
+ })
213
+ .where(and(eq(accountBans.accountId, target.accountId), isNull(accountBans.revokedAt)));
214
+ await tx
215
+ .update(accounts)
216
+ .set({ bannedAt: null, banExpiresAt: null, banReason: null, bannedByUserId: null })
217
+ .where(eq(accounts.id, target.accountId));
218
+ await this.syncUserMutation(tx, input.actorUserId, target.userId, "user.unbanned");
219
+ const ban = await this.banDb(tx, text(current.id));
220
+ await this.appendAudit(tx, {
221
+ actorUserId: input.actorUserId,
222
+ action: "user.ban_revoked",
223
+ targetType: "user",
224
+ targetId: target.userId,
225
+ before: accountTargetState(target),
226
+ after: ban,
227
+ context: input.context,
228
+ });
229
+ return ban;
230
+ });
231
+ }
232
+ /** Clears elapsed bans durably. Safe for concurrent timers on multiple server instances. */
233
+ async expireDueBans(input) {
234
+ const now = new Date().toISOString();
235
+ const dueCondition = and(sql `${accounts.bannedAt} IS NOT NULL`, sql `${accounts.banExpiresAt} IS NOT NULL`, lte(accounts.banExpiresAt, now));
236
+ if (!input?.actorUserId) {
237
+ const [candidate] = await this.db
238
+ .select({ id: accounts.id })
239
+ .from(accounts)
240
+ .where(dueCondition)
241
+ .limit(1);
242
+ if (!candidate)
243
+ return 0;
244
+ }
245
+ return this.writeDb(async (tx) => {
246
+ if (input?.actorUserId)
247
+ await this.requireAdminDb(tx, input.actorUserId);
248
+ const due = await tx
249
+ .select({
250
+ accountId: accounts.id,
251
+ userId: users.id,
252
+ bannedAt: accounts.bannedAt,
253
+ banExpiresAt: accounts.banExpiresAt,
254
+ banReason: accounts.banReason,
255
+ bannedByUserId: accounts.bannedByUserId,
256
+ })
257
+ .from(accounts)
258
+ .leftJoin(users, eq(users.accountId, accounts.id))
259
+ .where(dueCondition);
260
+ for (const row of due) {
261
+ await tx
262
+ .update(accountBans)
263
+ .set({
264
+ revokedAt: sql `coalesce(${accountBans.revokedAt}, ${now})`,
265
+ revokeReason: sql `coalesce(${accountBans.revokeReason}, 'expired')`,
266
+ })
267
+ .where(and(eq(accountBans.accountId, row.accountId), isNull(accountBans.revokedAt), lte(accountBans.expiresAt, now)));
268
+ const updated = await tx
269
+ .update(accounts)
270
+ .set({
271
+ bannedAt: null,
272
+ banExpiresAt: null,
273
+ banReason: null,
274
+ bannedByUserId: null,
275
+ })
276
+ .where(and(eq(accounts.id, row.accountId), sql `${accounts.bannedAt} IS NOT NULL`, sql `${accounts.banExpiresAt} IS NOT NULL`, lte(accounts.banExpiresAt, now)))
277
+ .returning({ id: accounts.id });
278
+ if (!updated.length)
279
+ continue;
280
+ if (row.userId)
281
+ await this.syncUserMutation(tx, input?.actorUserId, row.userId, "user.unbanned");
282
+ await this.appendAudit(tx, {
283
+ actorUserId: input?.actorUserId,
284
+ action: "user.ban_expired",
285
+ targetType: "user",
286
+ targetId: row.userId ?? undefined,
287
+ before: {
288
+ bannedAt: row.bannedAt ?? undefined,
289
+ expiresAt: row.banExpiresAt ?? undefined,
290
+ reason: row.banReason ?? undefined,
291
+ bannedByUserId: row.bannedByUserId ?? undefined,
292
+ },
293
+ after: { banned: false },
294
+ context: input?.context,
295
+ });
296
+ }
297
+ return due.length;
298
+ });
299
+ }
300
+ async listBans(input) {
301
+ await this.requireAdminDb(this.db, input.actorUserId);
302
+ const cursor = decodeCursor(input.before);
303
+ const conditions = [];
304
+ if (input.targetUserId)
305
+ conditions.push(eq(users.id, input.targetUserId));
306
+ const now = new Date().toISOString();
307
+ if (input.status === "active") {
308
+ conditions.push(and(isNull(accountBans.revokedAt), or(isNull(accountBans.expiresAt), gt(accountBans.expiresAt, now))));
309
+ }
310
+ else if (input.status === "expired") {
311
+ conditions.push(and(isNull(accountBans.revokedAt), sql `${accountBans.expiresAt} IS NOT NULL`, lte(accountBans.expiresAt, now)));
312
+ }
313
+ else if (input.status === "revoked") {
314
+ conditions.push(sql `${accountBans.revokedAt} IS NOT NULL`);
315
+ }
316
+ if (cursor)
317
+ conditions.push(cursorCondition(accountBans.bannedAt, accountBans.id, cursor));
318
+ const rows = await this.db
319
+ .select(banSelection)
320
+ .from(accountBans)
321
+ .leftJoin(users, eq(users.accountId, accountBans.accountId))
322
+ .where(and(...conditions))
323
+ .orderBy(desc(accountBans.bannedAt), desc(accountBans.id))
324
+ .limit(input.limit + 1);
325
+ return page(rows, input.limit, asBan, (item) => item.bannedAt);
326
+ }
327
+ async createReport(input) {
328
+ if (![input.targetUserId, input.chatId, input.messageId, input.fileId].some(Boolean))
329
+ throw new OperationsError("invalid", "A report must identify at least one target");
330
+ return this.writeDb(async (tx) => {
331
+ await this.requireActiveUserDb(tx, input.actorUserId);
332
+ await this.requireReportTargetAccess(tx, input);
333
+ const id = createId();
334
+ await tx.insert(moderationReports).values({
335
+ id,
336
+ reportedByUserId: input.actorUserId,
337
+ targetUserId: input.targetUserId,
338
+ chatId: input.chatId,
339
+ messageId: input.messageId,
340
+ fileId: input.fileId,
341
+ reason: input.reason,
342
+ details: input.details,
343
+ });
344
+ const report = await this.reportDb(tx, id);
345
+ await this.appendAudit(tx, {
346
+ actorUserId: input.actorUserId,
347
+ action: "moderation.report_created",
348
+ targetType: "moderation_report",
349
+ targetId: id,
350
+ chatId: input.chatId,
351
+ after: report,
352
+ context: input.context,
353
+ });
354
+ return report;
355
+ });
356
+ }
357
+ async listReports(input) {
358
+ await this.requireAdminDb(this.db, input.actorUserId);
359
+ const cursor = decodeCursor(input.before);
360
+ const conditions = [];
361
+ if (input.status)
362
+ conditions.push(eq(moderationReports.status, input.status));
363
+ if (input.assignedToUserId)
364
+ conditions.push(eq(moderationReports.assignedToUserId, input.assignedToUserId));
365
+ if (cursor)
366
+ conditions.push(cursorCondition(moderationReports.createdAt, moderationReports.id, cursor));
367
+ const rows = await this.db
368
+ .select(reportSelection)
369
+ .from(moderationReports)
370
+ .where(and(...conditions))
371
+ .orderBy(desc(moderationReports.createdAt), desc(moderationReports.id))
372
+ .limit(input.limit + 1);
373
+ return page(rows, input.limit, asReport);
374
+ }
375
+ async updateReport(input) {
376
+ if (input.status === undefined &&
377
+ input.assignedToUserId === undefined &&
378
+ input.resolution === undefined)
379
+ throw new OperationsError("invalid", "At least one report field is required");
380
+ return this.writeDb(async (tx) => {
381
+ await this.requireAdminDb(tx, input.actorUserId);
382
+ const before = await this.reportDb(tx, input.reportId);
383
+ if (input.assignedToUserId)
384
+ await this.requireAdminDb(tx, input.assignedToUserId);
385
+ const status = input.status ?? before.status;
386
+ const assigned = input.assignedToUserId === undefined
387
+ ? before.assignedToUserId
388
+ : input.assignedToUserId;
389
+ const resolution = input.resolution === undefined ? before.resolution : input.resolution;
390
+ await tx
391
+ .update(moderationReports)
392
+ .set({
393
+ status,
394
+ assignedToUserId: assigned ?? null,
395
+ resolution: resolution ?? null,
396
+ resolvedAt: status === "resolved" || status === "dismissed"
397
+ ? sql `coalesce(${moderationReports.resolvedAt}, CURRENT_TIMESTAMP)`
398
+ : null,
399
+ updatedAt: sql `CURRENT_TIMESTAMP`,
400
+ })
401
+ .where(eq(moderationReports.id, input.reportId));
402
+ const after = await this.reportDb(tx, input.reportId);
403
+ await this.appendAudit(tx, {
404
+ actorUserId: input.actorUserId,
405
+ action: "moderation.report_updated",
406
+ targetType: "moderation_report",
407
+ targetId: input.reportId,
408
+ chatId: after.chatId,
409
+ before,
410
+ after,
411
+ context: input.context,
412
+ });
413
+ return after;
414
+ });
415
+ }
416
+ async takeModerationAction(input) {
417
+ const expiresAt = futureTimestamp(input.expiresAt, "expiresAt");
418
+ if (expiresAt && input.action !== "ban" && input.action !== "restrict")
419
+ throw new OperationsError("invalid", `${input.action} does not support expiresAt`);
420
+ return this.writeDb(async (tx) => {
421
+ await this.requireAdminDb(tx, input.actorUserId);
422
+ if (input.automationRunId) {
423
+ const [existing] = await tx
424
+ .select(moderationActionSelection)
425
+ .from(moderationActions)
426
+ .where(eq(moderationActions.automationRunId, input.automationRunId))
427
+ .limit(1);
428
+ if (existing) {
429
+ const action = asModerationAction(existing);
430
+ if (action.reportId !== input.reportId)
431
+ throw new OperationsError("conflict", "Automation run is already bound to another moderation report");
432
+ return {
433
+ report: await this.reportDb(tx, input.reportId),
434
+ action,
435
+ };
436
+ }
437
+ }
438
+ const before = await this.reportDb(tx, input.reportId);
439
+ if ((input.action === "ban" ||
440
+ input.action === "unban" ||
441
+ input.action === "delete_user" ||
442
+ input.action === "warn" ||
443
+ input.action === "restrict") &&
444
+ !before.targetUserId)
445
+ throw new OperationsError("invalid", `${input.action} requires a reported user`);
446
+ if (input.action === "remove_message" && !before.messageId)
447
+ throw new OperationsError("invalid", "remove_message requires a reported message");
448
+ if (input.action === "remove_file" && !before.fileId)
449
+ throw new OperationsError("invalid", "remove_file requires a reported file");
450
+ const actionId = createId();
451
+ let actionChatId = before.chatId;
452
+ let sync;
453
+ if (input.action === "ban")
454
+ sync = await this.applyBanInTransaction(tx, input.actorUserId, before.targetUserId, input.reason, expiresAt);
455
+ else if (input.action === "unban")
456
+ sync = await this.revokeBanInTransaction(tx, input.actorUserId, before.targetUserId, input.reason);
457
+ else if (input.action === "warn" || input.action === "restrict")
458
+ sync = await this.createModerationNotification(tx, {
459
+ actorUserId: input.actorUserId,
460
+ targetUserId: before.targetUserId,
461
+ chatId: before.chatId,
462
+ actionId,
463
+ reportId: input.reportId,
464
+ action: input.action,
465
+ reason: input.reason,
466
+ expiresAt,
467
+ });
468
+ else if (input.action === "remove_message") {
469
+ const removed = await this.removeMessageInTransaction(tx, input.actorUserId, before.messageId, input.reason);
470
+ actionChatId = removed.chatId;
471
+ sync = removed.sync;
472
+ }
473
+ else if (input.action === "remove_file")
474
+ sync = await this.removeFileInTransaction(tx, input.actorUserId, before.fileId, input.reason);
475
+ else if (input.action === "delete_user")
476
+ sync = await this.deleteUserInTransaction(tx, input.actorUserId, before.targetUserId);
477
+ await tx.insert(moderationActions).values({
478
+ id: actionId,
479
+ reportId: input.reportId,
480
+ actorUserId: input.actorUserId,
481
+ targetUserId: before.targetUserId,
482
+ chatId: actionChatId,
483
+ messageId: before.messageId,
484
+ fileId: before.fileId,
485
+ action: input.action,
486
+ reason: input.reason,
487
+ metadataJson: json(input.metadata),
488
+ automationRunId: input.automationRunId,
489
+ expiresAt,
490
+ });
491
+ await tx
492
+ .update(moderationReports)
493
+ .set({
494
+ status: "resolved",
495
+ assignedToUserId: input.actorUserId,
496
+ resolution: input.reason ?? input.action,
497
+ resolvedAt: sql `CURRENT_TIMESTAMP`,
498
+ updatedAt: sql `CURRENT_TIMESTAMP`,
499
+ })
500
+ .where(eq(moderationReports.id, input.reportId));
501
+ const report = await this.reportDb(tx, input.reportId);
502
+ const action = await this.moderationActionDb(tx, actionId);
503
+ await this.appendAudit(tx, {
504
+ actorUserId: input.actorUserId,
505
+ action: `moderation.${input.action}`,
506
+ targetType: "moderation_report",
507
+ targetId: input.reportId,
508
+ chatId: report.chatId,
509
+ before,
510
+ after: { report, action },
511
+ context: mergeContext(input.context, input.metadata),
512
+ });
513
+ return { report, action, sync };
514
+ });
515
+ }
516
+ async revokeModerationAction(input) {
517
+ return this.writeDb(async (tx) => {
518
+ await this.requireAdminDb(tx, input.actorUserId);
519
+ const before = await this.moderationActionDb(tx, input.actionId);
520
+ if (before.revokedAt)
521
+ throw new OperationsError("conflict", "Moderation action is already revoked");
522
+ if (before.action !== "restrict")
523
+ throw new OperationsError("conflict", "Only restrictions can be revoked");
524
+ if (!before.targetUserId)
525
+ throw new Error("Restriction is missing its target user");
526
+ await tx
527
+ .update(moderationActions)
528
+ .set({ revokedAt: sql `CURRENT_TIMESTAMP` })
529
+ .where(and(eq(moderationActions.id, input.actionId), isNull(moderationActions.revokedAt)));
530
+ const sync = await this.createModerationNotification(tx, {
531
+ actorUserId: input.actorUserId,
532
+ targetUserId: before.targetUserId,
533
+ chatId: before.chatId,
534
+ actionId: input.actionId,
535
+ reportId: before.reportId,
536
+ action: "restrict_revoked",
537
+ reason: input.reason,
538
+ });
539
+ const action = await this.moderationActionDb(tx, input.actionId);
540
+ await this.appendAudit(tx, {
541
+ actorUserId: input.actorUserId,
542
+ action: "moderation.action_revoked",
543
+ targetType: "moderation_action",
544
+ targetId: input.actionId,
545
+ chatId: before.chatId,
546
+ before,
547
+ after: action,
548
+ context: mergeContext(input.context, { reason: input.reason }),
549
+ });
550
+ return { action, sync };
551
+ });
552
+ }
553
+ async requestDataExport(input) {
554
+ const expiresAt = futureTimestamp(input.expiresAt, "expiresAt");
555
+ return this.writeDb(async (tx) => {
556
+ const actor = await this.requireActiveUserDb(tx, input.actorUserId);
557
+ let targetId = input.targetId;
558
+ if (input.kind === "user_data") {
559
+ targetId ??= input.actorUserId;
560
+ if (targetId !== input.actorUserId && actor.role !== "admin")
561
+ throw new OperationsError("forbidden", "Only administrators can export another user");
562
+ await this.requireExistingUserDb(tx, targetId);
563
+ }
564
+ else if (input.kind === "chat_history") {
565
+ if (!targetId)
566
+ throw new OperationsError("invalid", "chat_history requires targetId");
567
+ if (!(await this.canAccessChat(tx, input.actorUserId, targetId)))
568
+ throw new OperationsError("not_found", "Chat was not found");
569
+ }
570
+ else {
571
+ if (actor.role !== "admin")
572
+ throw new OperationsError("forbidden", "This export requires an administrator");
573
+ targetId = undefined;
574
+ }
575
+ const id = createId();
576
+ await tx.insert(dataExportJobs).values({
577
+ id,
578
+ requestedByUserId: input.actorUserId,
579
+ kind: input.kind,
580
+ targetId,
581
+ optionsJson: json(input.options),
582
+ expiresAt,
583
+ });
584
+ const job = await this.exportJobDb(tx, id);
585
+ await this.appendAudit(tx, {
586
+ actorUserId: input.actorUserId,
587
+ action: "data_export.requested",
588
+ targetType: "data_export",
589
+ targetId: id,
590
+ chatId: input.kind === "chat_history" ? targetId : undefined,
591
+ after: job,
592
+ context: input.context,
593
+ });
594
+ return job;
595
+ });
596
+ }
597
+ async getDataExport(actorUserId, jobId) {
598
+ const actor = await this.requireActiveUserDb(this.db, actorUserId);
599
+ const job = await this.exportJobDb(this.db, jobId);
600
+ if (actor.role !== "admin" && job.requestedByUserId !== actorUserId)
601
+ throw new OperationsError("not_found", "Data export was not found");
602
+ return job;
603
+ }
604
+ async listDataExports(input) {
605
+ const actor = await this.requireActiveUserDb(this.db, input.actorUserId);
606
+ if (!input.ownOnly && actor.role !== "admin")
607
+ throw new OperationsError("forbidden", "Administrator access is required");
608
+ const conditions = [];
609
+ if (input.ownOnly)
610
+ conditions.push(eq(dataExportJobs.requestedByUserId, input.actorUserId));
611
+ else if (input.requestedByUserId)
612
+ conditions.push(eq(dataExportJobs.requestedByUserId, input.requestedByUserId));
613
+ if (input.status)
614
+ conditions.push(eq(dataExportJobs.status, input.status));
615
+ const cursor = decodeCursor(input.before);
616
+ if (cursor)
617
+ conditions.push(cursorCondition(dataExportJobs.createdAt, dataExportJobs.id, cursor));
618
+ const rows = await this.db
619
+ .select(exportSelection)
620
+ .from(dataExportJobs)
621
+ .where(and(...conditions))
622
+ .orderBy(desc(dataExportJobs.createdAt), desc(dataExportJobs.id))
623
+ .limit(input.limit + 1);
624
+ return page(rows, input.limit, asExport);
625
+ }
626
+ async cancelDataExport(input) {
627
+ return this.writeDb(async (tx) => {
628
+ const actor = await this.requireActiveUserDb(tx, input.actorUserId);
629
+ const before = await this.exportJobDb(tx, input.jobId);
630
+ if (actor.role !== "admin" && before.requestedByUserId !== input.actorUserId)
631
+ throw new OperationsError("not_found", "Data export was not found");
632
+ if (before.status !== "pending" && before.status !== "running")
633
+ throw new OperationsError("conflict", "Data export can no longer be cancelled");
634
+ await tx
635
+ .update(dataExportJobs)
636
+ .set({ status: "cancelled", completedAt: sql `CURRENT_TIMESTAMP` })
637
+ .where(eq(dataExportJobs.id, input.jobId));
638
+ const after = await this.exportJobDb(tx, input.jobId);
639
+ await this.appendAudit(tx, {
640
+ actorUserId: input.actorUserId,
641
+ action: "data_export.cancelled",
642
+ targetType: "data_export",
643
+ targetId: input.jobId,
644
+ before,
645
+ after,
646
+ context: input.context,
647
+ });
648
+ return after;
649
+ });
650
+ }
651
+ async updateDataExport(input) {
652
+ const expiresAt = futureTimestamp(input.expiresAt, "expiresAt");
653
+ return this.writeDb(async (tx) => {
654
+ await this.requireAdminDb(tx, input.actorUserId);
655
+ const before = await this.exportJobDb(tx, input.jobId);
656
+ assertExportTransition(before.status, input.status);
657
+ if (input.status === "complete") {
658
+ if (!input.outputFileId)
659
+ throw new OperationsError("invalid", "A completed export requires outputFileId");
660
+ const [file] = await tx
661
+ .select({ id: files.id })
662
+ .from(files)
663
+ .where(and(eq(files.id, input.outputFileId), isNull(files.deletedAt), eq(files.uploadStatus, "complete")));
664
+ if (!file)
665
+ throw new OperationsError("not_found", "Output file was not found");
666
+ if (!before.requestedByUserId)
667
+ throw new OperationsError("conflict", "Export requester no longer exists; the artifact cannot be granted safely");
668
+ await tx
669
+ .insert(fileAccessGrants)
670
+ .values({
671
+ id: createId(),
672
+ fileId: input.outputFileId,
673
+ principalType: "user",
674
+ principalId: before.requestedByUserId,
675
+ grantedByUserId: input.actorUserId,
676
+ expiresAt: expiresAt ?? before.expiresAt,
677
+ })
678
+ .onConflictDoUpdate({
679
+ target: [
680
+ fileAccessGrants.fileId,
681
+ fileAccessGrants.principalType,
682
+ fileAccessGrants.principalId,
683
+ ],
684
+ targetWhere: isNull(fileAccessGrants.sourceMessageId),
685
+ set: {
686
+ expiresAt: sql `CASE WHEN ${fileAccessGrants.expiresAt} IS NULL OR excluded.expires_at IS NULL THEN NULL WHEN ${fileAccessGrants.expiresAt} > excluded.expires_at THEN ${fileAccessGrants.expiresAt} ELSE excluded.expires_at END`,
687
+ },
688
+ });
689
+ }
690
+ if (input.status === "failed" && !input.lastError)
691
+ throw new OperationsError("invalid", "A failed export requires lastError");
692
+ await tx
693
+ .update(dataExportJobs)
694
+ .set({
695
+ status: input.status,
696
+ outputFileId: input.outputFileId ?? sql `${dataExportJobs.outputFileId}`,
697
+ lastError: input.lastError ?? null,
698
+ expiresAt: expiresAt ?? sql `${dataExportJobs.expiresAt}`,
699
+ startedAt: input.status === "running"
700
+ ? sql `coalesce(${dataExportJobs.startedAt}, CURRENT_TIMESTAMP)`
701
+ : sql `${dataExportJobs.startedAt}`,
702
+ completedAt: ["complete", "failed", "cancelled", "expired"].includes(input.status)
703
+ ? sql `coalesce(${dataExportJobs.completedAt}, CURRENT_TIMESTAMP)`
704
+ : sql `${dataExportJobs.completedAt}`,
705
+ })
706
+ .where(eq(dataExportJobs.id, input.jobId));
707
+ const after = await this.exportJobDb(tx, input.jobId);
708
+ await this.appendAudit(tx, {
709
+ actorUserId: input.actorUserId,
710
+ action: `data_export.${input.status}`,
711
+ targetType: "data_export",
712
+ targetId: input.jobId,
713
+ before,
714
+ after,
715
+ context: input.context,
716
+ });
717
+ return after;
718
+ });
719
+ }
720
+ async claimPendingDataExports(limit = 5, leaseMs = 5 * 60_000) {
721
+ const claimedAt = new Date().toISOString();
722
+ const staleBefore = new Date(Date.now() - leaseMs).toISOString();
723
+ const claimable = or(eq(dataExportJobs.status, "pending"), and(eq(dataExportJobs.status, "running"), or(isNull(dataExportJobs.startedAt), lte(dataExportJobs.startedAt, staleBefore))));
724
+ const [candidate] = await this.db
725
+ .select({ id: dataExportJobs.id })
726
+ .from(dataExportJobs)
727
+ .where(claimable)
728
+ .limit(1);
729
+ if (!candidate)
730
+ return [];
731
+ return this.writeDb(async (tx) => {
732
+ const candidates = await tx
733
+ .select({ id: dataExportJobs.id })
734
+ .from(dataExportJobs)
735
+ .where(claimable)
736
+ .orderBy(dataExportJobs.createdAt, dataExportJobs.id)
737
+ .limit(limit);
738
+ const claimed = [];
739
+ for (const candidate of candidates) {
740
+ const [row] = await tx
741
+ .update(dataExportJobs)
742
+ .set({ status: "running", startedAt: claimedAt, lastError: null })
743
+ .where(and(eq(dataExportJobs.id, candidate.id), claimable))
744
+ .returning(exportSelection);
745
+ if (row)
746
+ claimed.push({ ...asExport(row), claimStartedAt: claimedAt });
747
+ }
748
+ return claimed;
749
+ });
750
+ }
751
+ async buildDataExportArtifact(claim) {
752
+ if (!claim.requestedByUserId)
753
+ throw new Error("Data export requester no longer exists");
754
+ const base = {
755
+ schemaVersion: 1,
756
+ exportId: claim.id,
757
+ kind: claim.kind,
758
+ requestedByUserId: claim.requestedByUserId,
759
+ targetId: claim.targetId,
760
+ createdAt: claim.createdAt,
761
+ generatedAt: new Date().toISOString(),
762
+ options: claim.options,
763
+ };
764
+ if (claim.kind === "user_data") {
765
+ const targetId = claim.targetId ?? claim.requestedByUserId;
766
+ const [profile] = await this.db
767
+ .select({
768
+ id: users.id,
769
+ username: users.username,
770
+ firstName: users.firstName,
771
+ lastName: users.lastName,
772
+ title: users.title,
773
+ email: accounts.email,
774
+ phone: users.phone,
775
+ photoFileId: users.photoFileId,
776
+ createdAt: users.createdAt,
777
+ })
778
+ .from(users)
779
+ .innerJoin(accounts, eq(accounts.id, users.accountId))
780
+ .where(eq(users.id, targetId));
781
+ if (!profile)
782
+ throw new Error("Data export target no longer exists");
783
+ const options = objectValue(claim.options);
784
+ const exportedFiles = options.includeFiles
785
+ ? await this.db
786
+ .select({
787
+ id: files.id,
788
+ kind: files.kind,
789
+ originalName: files.originalName,
790
+ contentType: files.contentType,
791
+ size: files.size,
792
+ createdAt: files.createdAt,
793
+ })
794
+ .from(files)
795
+ .where(and(eq(files.uploadedByUserId, targetId), isNull(files.deletedAt)))
796
+ .orderBy(files.createdAt, files.id)
797
+ : [];
798
+ return { ...base, data: { profile, files: exportedFiles } };
799
+ }
800
+ if (claim.kind === "chat_history") {
801
+ if (!claim.targetId ||
802
+ !(await this.canAccessChat(this.db, claim.requestedByUserId, claim.targetId)))
803
+ throw new Error("Data export chat is no longer accessible");
804
+ const [chat] = await this.db
805
+ .select({ id: chats.id, kind: chats.kind, name: chats.name, topic: chats.topic })
806
+ .from(chats)
807
+ .where(eq(chats.id, claim.targetId));
808
+ const exportedMessages = await this.db
809
+ .select({
810
+ id: messages.id,
811
+ sequence: messages.sequence,
812
+ senderUserId: messages.senderUserId,
813
+ senderBotId: messages.senderBotId,
814
+ kind: messages.kind,
815
+ text: messages.text,
816
+ threadRootMessageId: messages.threadRootMessageId,
817
+ createdAt: messages.createdAt,
818
+ editedAt: messages.editedAt,
819
+ deletedAt: messages.deletedAt,
820
+ })
821
+ .from(messages)
822
+ .where(eq(messages.chatId, claim.targetId))
823
+ .orderBy(messages.sequence);
824
+ return { ...base, data: { chat, messages: exportedMessages } };
825
+ }
826
+ await this.requireAdminDb(this.db, claim.requestedByUserId);
827
+ if (claim.kind === "audit_log") {
828
+ const entries = await this.db
829
+ .select(auditSelection)
830
+ .from(auditLogEntries)
831
+ .orderBy(auditLogEntries.createdAt, auditLogEntries.id);
832
+ return { ...base, data: { auditLog: entries.map(asAudit) } };
833
+ }
834
+ const [exportedUsers, exportedChats] = await Promise.all([
835
+ this.db
836
+ .select({
837
+ id: users.id,
838
+ username: users.username,
839
+ role: users.role,
840
+ createdAt: users.createdAt,
841
+ deletedAt: users.deletedAt,
842
+ })
843
+ .from(users)
844
+ .orderBy(users.createdAt, users.id),
845
+ this.db
846
+ .select({
847
+ id: chats.id,
848
+ kind: chats.kind,
849
+ name: chats.name,
850
+ createdAt: chats.createdAt,
851
+ deletedAt: chats.deletedAt,
852
+ })
853
+ .from(chats)
854
+ .orderBy(chats.createdAt, chats.id),
855
+ ]);
856
+ return { ...base, data: { users: exportedUsers, chats: exportedChats } };
857
+ }
858
+ async completeClaimedDataExport(claim, outputFileId) {
859
+ const requesterId = claim.requestedByUserId;
860
+ if (!requesterId)
861
+ return false;
862
+ return this.writeDb(async (tx) => {
863
+ const [completed] = await tx
864
+ .update(dataExportJobs)
865
+ .set({
866
+ status: "complete",
867
+ outputFileId,
868
+ lastError: null,
869
+ completedAt: sql `CURRENT_TIMESTAMP`,
870
+ })
871
+ .where(and(eq(dataExportJobs.id, claim.id), eq(dataExportJobs.status, "running"), eq(dataExportJobs.startedAt, claim.claimStartedAt)))
872
+ .returning({ id: dataExportJobs.id });
873
+ if (!completed)
874
+ return false;
875
+ await tx
876
+ .insert(fileAccessGrants)
877
+ .values({
878
+ id: createId(),
879
+ fileId: outputFileId,
880
+ principalType: "user",
881
+ principalId: requesterId,
882
+ grantedByUserId: requesterId,
883
+ expiresAt: claim.expiresAt,
884
+ })
885
+ .onConflictDoUpdate({
886
+ target: [
887
+ fileAccessGrants.fileId,
888
+ fileAccessGrants.principalType,
889
+ fileAccessGrants.principalId,
890
+ ],
891
+ targetWhere: isNull(fileAccessGrants.sourceMessageId),
892
+ set: { expiresAt: claim.expiresAt ?? null },
893
+ });
894
+ await this.appendAudit(tx, {
895
+ actorUserId: requesterId,
896
+ action: "data_export.complete",
897
+ targetType: "data_export",
898
+ targetId: claim.id,
899
+ after: { outputFileId },
900
+ });
901
+ return true;
902
+ });
903
+ }
904
+ async failClaimedDataExport(claim, error) {
905
+ const message = error instanceof Error ? error.message : String(error);
906
+ const [failed] = await this.db
907
+ .update(dataExportJobs)
908
+ .set({
909
+ status: "failed",
910
+ lastError: message.slice(0, 2_000),
911
+ completedAt: sql `CURRENT_TIMESTAMP`,
912
+ })
913
+ .where(and(eq(dataExportJobs.id, claim.id), eq(dataExportJobs.status, "running"), eq(dataExportJobs.startedAt, claim.claimStartedAt)))
914
+ .returning({ id: dataExportJobs.id });
915
+ return Boolean(failed);
916
+ }
917
+ async createBackup(input) {
918
+ const retentionUntil = futureTimestamp(input.retentionUntil, "retentionUntil");
919
+ return this.writeDb(async (tx) => {
920
+ await this.requireAdminDb(tx, input.actorUserId);
921
+ const id = createId();
922
+ try {
923
+ await tx.insert(backupRecords).values({
924
+ id,
925
+ storageProvider: input.storageProvider,
926
+ storageKey: input.storageKey,
927
+ createdByUserId: input.actorUserId,
928
+ metadataJson: json(input.metadata),
929
+ retentionUntil,
930
+ });
931
+ }
932
+ catch (error) {
933
+ if (isUniqueConstraint(error))
934
+ throw new OperationsError("conflict", "Backup storage key is already recorded");
935
+ throw error;
936
+ }
937
+ const backup = await this.backupDb(tx, id);
938
+ await this.appendAudit(tx, {
939
+ actorUserId: input.actorUserId,
940
+ action: "backup.created",
941
+ targetType: "backup",
942
+ targetId: id,
943
+ after: backup,
944
+ context: input.context,
945
+ });
946
+ return backup;
947
+ });
948
+ }
949
+ async listBackups(input) {
950
+ await this.requireAdminDb(this.db, input.actorUserId);
951
+ const conditions = [];
952
+ if (input.status)
953
+ conditions.push(eq(backupRecords.status, input.status));
954
+ const cursor = decodeCursor(input.before);
955
+ if (cursor)
956
+ conditions.push(cursorCondition(backupRecords.createdAt, backupRecords.id, cursor));
957
+ const rows = await this.db
958
+ .select(backupSelection)
959
+ .from(backupRecords)
960
+ .where(and(...conditions))
961
+ .orderBy(desc(backupRecords.createdAt), desc(backupRecords.id))
962
+ .limit(input.limit + 1);
963
+ return page(rows, input.limit, asBackup);
964
+ }
965
+ async updateBackup(input) {
966
+ const retentionUntil = futureTimestamp(input.retentionUntil, "retentionUntil");
967
+ return this.writeDb(async (tx) => {
968
+ await this.requireAdminDb(tx, input.actorUserId);
969
+ const before = await this.backupDb(tx, input.backupId);
970
+ assertBackupTransition(before.status, input.status);
971
+ if (input.status === "complete" && (!input.checksumSha256 || input.size === undefined))
972
+ throw new OperationsError("invalid", "A completed backup requires checksumSha256 and size");
973
+ if (input.status === "failed" && !input.lastError)
974
+ throw new OperationsError("invalid", "A failed backup requires lastError");
975
+ await tx
976
+ .update(backupRecords)
977
+ .set({
978
+ status: input.status,
979
+ checksumSha256: input.checksumSha256 ?? sql `${backupRecords.checksumSha256}`,
980
+ size: input.size ?? sql `${backupRecords.size}`,
981
+ lastError: input.lastError ?? null,
982
+ retentionUntil: retentionUntil ?? sql `${backupRecords.retentionUntil}`,
983
+ metadataJson: input.metadata === undefined
984
+ ? sql `${backupRecords.metadataJson}`
985
+ : json(input.metadata),
986
+ completedAt: ["complete", "failed", "deleted"].includes(input.status)
987
+ ? sql `coalesce(${backupRecords.completedAt}, CURRENT_TIMESTAMP)`
988
+ : sql `${backupRecords.completedAt}`,
989
+ })
990
+ .where(eq(backupRecords.id, input.backupId));
991
+ const after = await this.backupDb(tx, input.backupId);
992
+ await this.appendAudit(tx, {
993
+ actorUserId: input.actorUserId,
994
+ action: `backup.${input.status}`,
995
+ targetType: "backup",
996
+ targetId: input.backupId,
997
+ before,
998
+ after,
999
+ context: input.context,
1000
+ });
1001
+ return after;
1002
+ });
1003
+ }
1004
+ async startRetentionRun(input) {
1005
+ return this.writeDb(async (tx) => {
1006
+ await this.requireAdminDb(tx, input.actorUserId);
1007
+ const [active] = await tx
1008
+ .select({ id: retentionRuns.id })
1009
+ .from(retentionRuns)
1010
+ .where(and(eq(retentionRuns.scope, input.scope), eq(retentionRuns.status, "running")))
1011
+ .limit(1);
1012
+ if (active)
1013
+ throw new OperationsError("conflict", "A retention run is already active for this scope");
1014
+ const id = createId();
1015
+ await tx
1016
+ .insert(retentionRuns)
1017
+ .values({ id, scope: input.scope, detailsJson: json(input.details) });
1018
+ const run = await this.retentionRunDb(tx, id);
1019
+ await this.appendAudit(tx, {
1020
+ actorUserId: input.actorUserId,
1021
+ action: "retention.started",
1022
+ targetType: "retention_run",
1023
+ targetId: id,
1024
+ after: run,
1025
+ context: input.context,
1026
+ });
1027
+ return run;
1028
+ });
1029
+ }
1030
+ async listRetentionRuns(input) {
1031
+ await this.requireAdminDb(this.db, input.actorUserId);
1032
+ const conditions = [];
1033
+ if (input.scope)
1034
+ conditions.push(eq(retentionRuns.scope, input.scope));
1035
+ const cursor = decodeCursor(input.before);
1036
+ if (cursor)
1037
+ conditions.push(cursorCondition(retentionRuns.startedAt, retentionRuns.id, cursor));
1038
+ const rows = await this.db
1039
+ .select(retentionSelection)
1040
+ .from(retentionRuns)
1041
+ .where(and(...conditions))
1042
+ .orderBy(desc(retentionRuns.startedAt), desc(retentionRuns.id))
1043
+ .limit(input.limit + 1);
1044
+ return page(rows, input.limit, asRetention, (item) => item.startedAt);
1045
+ }
1046
+ async finishRetentionRun(input) {
1047
+ return this.writeDb(async (tx) => {
1048
+ await this.requireAdminDb(tx, input.actorUserId);
1049
+ const before = await this.retentionRunDb(tx, input.runId);
1050
+ if (before.status !== "running")
1051
+ throw new OperationsError("conflict", "Retention run is already finished");
1052
+ if (input.status === "failed" && !input.lastError)
1053
+ throw new OperationsError("invalid", "A failed retention run requires lastError");
1054
+ await tx
1055
+ .update(retentionRuns)
1056
+ .set({
1057
+ status: input.status,
1058
+ itemsExamined: input.itemsExamined,
1059
+ itemsDeleted: input.itemsDeleted,
1060
+ detailsJson: input.details === undefined
1061
+ ? sql `${retentionRuns.detailsJson}`
1062
+ : json(input.details),
1063
+ lastError: input.lastError ?? null,
1064
+ completedAt: sql `CURRENT_TIMESTAMP`,
1065
+ })
1066
+ .where(eq(retentionRuns.id, input.runId));
1067
+ const after = await this.retentionRunDb(tx, input.runId);
1068
+ await this.appendAudit(tx, {
1069
+ actorUserId: input.actorUserId,
1070
+ action: `retention.${input.status}`,
1071
+ targetType: "retention_run",
1072
+ targetId: input.runId,
1073
+ before,
1074
+ after,
1075
+ context: input.context,
1076
+ });
1077
+ return after;
1078
+ });
1079
+ }
1080
+ async listUserAccess(input) {
1081
+ await this.requireAdminDb(this.db, input.actorUserId);
1082
+ const cursor = decodeCursor(input.before);
1083
+ const recentEvent = (column) => this.db
1084
+ .select({ value: column })
1085
+ .from(authSessionEvents)
1086
+ .innerJoin(authSessions, eq(authSessions.id, authSessionEvents.sessionId))
1087
+ .where(eq(authSessions.accountId, accounts.id))
1088
+ .orderBy(desc(authSessionEvents.createdAt), desc(authSessionEvents.id))
1089
+ .limit(1);
1090
+ const conditions = [];
1091
+ const accessAt = sql `coalesce(${users.lastAccessAt}, '')`;
1092
+ if (cursor)
1093
+ conditions.push(or(lt(accessAt, cursor.at), and(eq(accessAt, cursor.at), lt(users.id, cursor.id))));
1094
+ const result = await this.db
1095
+ .select({
1096
+ id: users.id,
1097
+ username: users.username,
1098
+ email: accounts.email,
1099
+ role: users.role,
1100
+ last_access_at: users.lastAccessAt,
1101
+ banned_at: accounts.bannedAt,
1102
+ ban_expires_at: accounts.banExpiresAt,
1103
+ deleted_at: accounts.deletedAt,
1104
+ last_session_access_at: sql `max(${authSessions.lastSeenAt})`,
1105
+ active_session_count: sql `sum(case when ${authSessions.revokedAt} is null and ${authSessions.expiresAt} > CURRENT_TIMESTAMP then 1 else 0 end)`,
1106
+ last_client_ip: sql `(${recentEvent(authSessionEvents.ip)})`,
1107
+ last_device: sql `(${recentEvent(authSessionEvents.device)})`,
1108
+ last_app_version: sql `(${recentEvent(authSessionEvents.appVersion)})`,
1109
+ last_user_agent: sql `(${recentEvent(authSessionEvents.userAgent)})`,
1110
+ })
1111
+ .from(users)
1112
+ .innerJoin(accounts, eq(accounts.id, users.accountId))
1113
+ .leftJoin(authSessions, eq(authSessions.accountId, accounts.id))
1114
+ .where(and(...conditions))
1115
+ .groupBy(users.id, users.username, accounts.email, users.role, users.lastAccessAt, accounts.bannedAt, accounts.banExpiresAt, accounts.deletedAt)
1116
+ .orderBy(desc(accessAt), desc(users.id))
1117
+ .limit(input.limit + 1);
1118
+ return page(result, input.limit, asAccess, (item) => item.lastAccessAt ?? "", (item) => item.userId);
1119
+ }
1120
+ async requireActiveUserDb(executor, userId) {
1121
+ const [row] = await executor
1122
+ .select({ id: users.id, role: users.role })
1123
+ .from(users)
1124
+ .innerJoin(accounts, eq(accounts.id, users.accountId))
1125
+ .where(and(eq(users.id, userId), isNull(users.deletedAt), eq(accounts.active, 1), isNull(accounts.bannedAt), isNull(accounts.deletedAt)));
1126
+ if (!row)
1127
+ throw new OperationsError("not_found", "User was not found");
1128
+ return row;
1129
+ }
1130
+ async requireAdminDb(executor, userId) {
1131
+ const user = await this.requireActiveUserDb(executor, userId);
1132
+ if (user.role !== "admin")
1133
+ throw new OperationsError("forbidden", "Administrator access is required");
1134
+ }
1135
+ async requireExistingUserDb(executor, userId) {
1136
+ const [row] = await executor
1137
+ .select({ id: users.id })
1138
+ .from(users)
1139
+ .where(and(eq(users.id, userId), isNull(users.deletedAt)));
1140
+ if (!row)
1141
+ throw new OperationsError("not_found", "User was not found");
1142
+ }
1143
+ async accountTargetDb(executor, userId) {
1144
+ const [row] = await executor
1145
+ .select({
1146
+ accountId: accounts.id,
1147
+ userId: users.id,
1148
+ username: users.username,
1149
+ bannedAt: accounts.bannedAt,
1150
+ banExpiresAt: accounts.banExpiresAt,
1151
+ banReason: accounts.banReason,
1152
+ bannedByUserId: accounts.bannedByUserId,
1153
+ })
1154
+ .from(users)
1155
+ .innerJoin(accounts, eq(accounts.id, users.accountId))
1156
+ .where(and(eq(users.id, userId), isNull(users.deletedAt), isNull(accounts.deletedAt)));
1157
+ if (!row)
1158
+ throw new OperationsError("not_found", "User was not found");
1159
+ return {
1160
+ accountId: row.accountId,
1161
+ userId: row.userId,
1162
+ username: row.username,
1163
+ bannedAt: row.bannedAt ?? undefined,
1164
+ banExpiresAt: row.banExpiresAt ?? undefined,
1165
+ banReason: row.banReason ?? undefined,
1166
+ bannedByUserId: row.bannedByUserId ?? undefined,
1167
+ };
1168
+ }
1169
+ async applyBanInTransaction(tx, actorUserId, targetUserId, reason, expiresAt) {
1170
+ if (actorUserId === targetUserId)
1171
+ throw new OperationsError("forbidden", "Administrators cannot ban themselves");
1172
+ const target = await this.accountTargetDb(tx, targetUserId);
1173
+ await this.closeElapsedBan(tx, target);
1174
+ if (target.bannedAt &&
1175
+ (!target.banExpiresAt || Date.parse(target.banExpiresAt) > Date.now()))
1176
+ throw new OperationsError("conflict", "User already has an active ban");
1177
+ await tx.insert(accountBans).values({
1178
+ id: createId(),
1179
+ accountId: target.accountId,
1180
+ bannedByUserId: actorUserId,
1181
+ reason,
1182
+ expiresAt,
1183
+ });
1184
+ await tx
1185
+ .update(accounts)
1186
+ .set({
1187
+ bannedAt: sql `CURRENT_TIMESTAMP`,
1188
+ banExpiresAt: expiresAt ?? null,
1189
+ banReason: reason ?? null,
1190
+ bannedByUserId: actorUserId,
1191
+ })
1192
+ .where(eq(accounts.id, target.accountId));
1193
+ await tx
1194
+ .update(authSessions)
1195
+ .set({ revokedAt: sql `CURRENT_TIMESTAMP` })
1196
+ .where(and(eq(authSessions.accountId, target.accountId), isNull(authSessions.revokedAt)));
1197
+ return this.syncUserMutation(tx, actorUserId, target.userId, "user.banned");
1198
+ }
1199
+ async revokeBanInTransaction(tx, actorUserId, targetUserId, reason) {
1200
+ const target = await this.accountTargetDb(tx, targetUserId);
1201
+ if (!target.bannedAt)
1202
+ throw new OperationsError("conflict", "User does not have an active ban");
1203
+ await tx
1204
+ .update(accountBans)
1205
+ .set({
1206
+ revokedAt: sql `CURRENT_TIMESTAMP`,
1207
+ revokedByUserId: actorUserId,
1208
+ revokeReason: reason ?? null,
1209
+ })
1210
+ .where(and(eq(accountBans.accountId, target.accountId), isNull(accountBans.revokedAt)));
1211
+ await tx
1212
+ .update(accounts)
1213
+ .set({ bannedAt: null, banExpiresAt: null, banReason: null, bannedByUserId: null })
1214
+ .where(eq(accounts.id, target.accountId));
1215
+ return this.syncUserMutation(tx, actorUserId, target.userId, "user.unbanned");
1216
+ }
1217
+ async createModerationNotification(tx, input) {
1218
+ await this.requireExistingUserDb(tx, input.targetUserId);
1219
+ const sequence = await this.nextSequence(tx);
1220
+ const notificationId = createId();
1221
+ await tx.insert(notifications).values({
1222
+ id: notificationId,
1223
+ userId: input.targetUserId,
1224
+ kind: "moderation",
1225
+ chatId: input.chatId,
1226
+ actorUserId: input.actorUserId,
1227
+ payloadJson: JSON.stringify({
1228
+ actionId: input.actionId,
1229
+ reportId: input.reportId,
1230
+ action: input.action,
1231
+ reason: input.reason,
1232
+ expiresAt: input.expiresAt,
1233
+ }),
1234
+ syncSequence: sequence,
1235
+ });
1236
+ await this.insertSyncEvent(tx, {
1237
+ sequence,
1238
+ kind: "notification.created",
1239
+ entityId: notificationId,
1240
+ actorUserId: input.actorUserId,
1241
+ targetUserId: input.targetUserId,
1242
+ });
1243
+ return { sequence: String(sequence), chats: [], areas: ["notifications"] };
1244
+ }
1245
+ async removeMessageInTransaction(tx, actorUserId, messageId, reason) {
1246
+ const [message] = await tx
1247
+ .select({
1248
+ chatId: messages.chatId,
1249
+ deletedAt: messages.deletedAt,
1250
+ threadRootMessageId: messages.threadRootMessageId,
1251
+ })
1252
+ .from(messages)
1253
+ .where(eq(messages.id, messageId))
1254
+ .limit(1);
1255
+ if (!message)
1256
+ throw new OperationsError("not_found", "Message was not found");
1257
+ if (message.deletedAt !== null)
1258
+ throw new OperationsError("conflict", "Message is already removed");
1259
+ const chatId = message.chatId;
1260
+ const sequence = await this.nextSequence(tx);
1261
+ const pts = await this.advanceChatMutation(tx, {
1262
+ sequence,
1263
+ chatId,
1264
+ kind: "message.deleted",
1265
+ entityId: messageId,
1266
+ actorUserId,
1267
+ });
1268
+ await tx
1269
+ .update(messages)
1270
+ .set({
1271
+ text: "",
1272
+ contentJson: null,
1273
+ deletedAt: sql `CURRENT_TIMESTAMP`,
1274
+ deletedByUserId: actorUserId,
1275
+ deleteReason: reason ?? "moderation",
1276
+ changePts: pts,
1277
+ updatedAt: sql `CURRENT_TIMESTAMP`,
1278
+ })
1279
+ .where(and(eq(messages.id, messageId), isNull(messages.deletedAt)));
1280
+ await tx
1281
+ .delete(messageSearchDocuments)
1282
+ .where(eq(messageSearchDocuments.messageId, messageId));
1283
+ await tx.delete(messageRevisions).where(eq(messageRevisions.messageId, messageId));
1284
+ await tx.delete(notifications).where(eq(notifications.messageId, messageId));
1285
+ if (message.threadRootMessageId) {
1286
+ const threadRootMessageId = message.threadRootMessageId;
1287
+ await this.recomputeThreadProjection(tx, threadRootMessageId, pts);
1288
+ await tx
1289
+ .update(messages)
1290
+ .set({ changePts: pts })
1291
+ .where(eq(messages.id, threadRootMessageId));
1292
+ }
1293
+ return {
1294
+ chatId,
1295
+ sync: { sequence: String(sequence), chats: [{ chatId, pts: String(pts) }], areas: [] },
1296
+ };
1297
+ }
1298
+ async removeFileInTransaction(tx, actorUserId, fileId, reason) {
1299
+ const [file] = await tx
1300
+ .select({ deletedAt: files.deletedAt })
1301
+ .from(files)
1302
+ .where(eq(files.id, fileId))
1303
+ .limit(1);
1304
+ if (!file)
1305
+ throw new OperationsError("not_found", "File was not found");
1306
+ if (file.deletedAt !== null)
1307
+ throw new OperationsError("conflict", "File is already removed");
1308
+ const affectedChats = await tx
1309
+ .selectDistinct({ chatId: messages.chatId })
1310
+ .from(messageAttachments)
1311
+ .innerJoin(messages, eq(messages.id, messageAttachments.messageId))
1312
+ .innerJoin(chats, eq(chats.id, messages.chatId))
1313
+ .where(and(eq(messageAttachments.fileId, fileId), isNull(messages.deletedAt), isNull(chats.deletedAt)))
1314
+ .orderBy(messages.chatId);
1315
+ const sequence = await this.nextSequence(tx);
1316
+ const chatPoints = [];
1317
+ for (const row of affectedChats) {
1318
+ const chatId = row.chatId;
1319
+ const pts = await this.advanceChatMutation(tx, {
1320
+ sequence,
1321
+ chatId,
1322
+ kind: "file.removed",
1323
+ entityId: fileId,
1324
+ actorUserId,
1325
+ });
1326
+ chatPoints.push({ chatId, pts: String(pts) });
1327
+ }
1328
+ await tx
1329
+ .update(files)
1330
+ .set({
1331
+ deletedAt: sql `CURRENT_TIMESTAMP`,
1332
+ deletedByUserId: actorUserId,
1333
+ deleteReason: reason ?? "moderation",
1334
+ accessScope: "private",
1335
+ isPublic: 0,
1336
+ orphanedAt: sql `coalesce(${files.orphanedAt}, CURRENT_TIMESTAMP)`,
1337
+ })
1338
+ .where(and(eq(files.id, fileId), isNull(files.deletedAt)));
1339
+ await tx.delete(fileAccessGrants).where(eq(fileAccessGrants.fileId, fileId));
1340
+ await this.insertSyncEvent(tx, {
1341
+ sequence,
1342
+ kind: "file.removed",
1343
+ entityId: fileId,
1344
+ actorUserId,
1345
+ });
1346
+ return { sequence: String(sequence), chats: chatPoints, areas: ["files"] };
1347
+ }
1348
+ async deleteUserInTransaction(tx, actorUserId, targetUserId) {
1349
+ if (actorUserId === targetUserId)
1350
+ throw new OperationsError("forbidden", "Administrators cannot delete themselves");
1351
+ const [target] = await tx
1352
+ .select({ role: users.role, accountId: users.accountId })
1353
+ .from(users)
1354
+ .innerJoin(accounts, eq(accounts.id, users.accountId))
1355
+ .where(and(eq(users.id, targetUserId), isNull(users.deletedAt), isNull(accounts.deletedAt)))
1356
+ .limit(1);
1357
+ if (!target)
1358
+ throw new OperationsError("not_found", "User was not found");
1359
+ if (target.role === "admin") {
1360
+ const [otherAdmin] = await tx
1361
+ .select({ id: users.id })
1362
+ .from(users)
1363
+ .innerJoin(accounts, eq(accounts.id, users.accountId))
1364
+ .where(and(sql `${users.id} != ${targetUserId}`, eq(users.role, "admin"), isNull(users.deletedAt), eq(accounts.active, 1), isNull(accounts.bannedAt), isNull(accounts.deletedAt)))
1365
+ .limit(1);
1366
+ if (!otherAdmin)
1367
+ throw new OperationsError("forbidden", "The last active administrator cannot be deleted");
1368
+ }
1369
+ const sequence = await this.nextSequence(tx);
1370
+ const memberships = await tx
1371
+ .select({ chatId: chatMembers.chatId, role: chatMembers.role, kind: chats.kind })
1372
+ .from(chatMembers)
1373
+ .innerJoin(chats, eq(chats.id, chatMembers.chatId))
1374
+ .where(and(eq(chatMembers.userId, targetUserId), isNull(chatMembers.leftAt), isNull(chats.deletedAt)));
1375
+ const chatPoints = [];
1376
+ for (const membership of memberships) {
1377
+ const chatId = membership.chatId;
1378
+ let eventKind = "member.deleted";
1379
+ if (membership.kind !== "dm" && membership.role === "owner") {
1380
+ const [successor] = await tx
1381
+ .select({ userId: chatMembers.userId })
1382
+ .from(chatMembers)
1383
+ .innerJoin(users, eq(users.id, chatMembers.userId))
1384
+ .innerJoin(accounts, eq(accounts.id, users.accountId))
1385
+ .where(and(eq(chatMembers.chatId, chatId), sql `${chatMembers.userId} != ${targetUserId}`, isNull(chatMembers.leftAt), isNull(users.deletedAt), eq(accounts.active, 1), isNull(accounts.bannedAt), isNull(accounts.deletedAt)))
1386
+ .orderBy(sql `case ${chatMembers.role} when 'owner' then 0 when 'admin' then 1 else 2 end`, asc(chatMembers.joinedAt), asc(chatMembers.userId))
1387
+ .limit(1);
1388
+ if (successor) {
1389
+ const successorId = successor.userId;
1390
+ await tx
1391
+ .update(chatMembers)
1392
+ .set({
1393
+ role: "owner",
1394
+ syncSequence: sequence,
1395
+ updatedAt: sql `CURRENT_TIMESTAMP`,
1396
+ })
1397
+ .where(and(eq(chatMembers.chatId, chatId), eq(chatMembers.userId, successorId)));
1398
+ await tx
1399
+ .update(chats)
1400
+ .set({ ownerUserId: successorId })
1401
+ .where(eq(chats.id, chatId));
1402
+ eventKind = "member.deletedAndOwnershipTransferred";
1403
+ }
1404
+ else
1405
+ eventKind = "chat.deleted";
1406
+ }
1407
+ const pts = await this.advanceChatMutation(tx, {
1408
+ sequence,
1409
+ chatId,
1410
+ kind: eventKind,
1411
+ entityId: targetUserId,
1412
+ actorUserId,
1413
+ });
1414
+ chatPoints.push({ chatId, pts: String(pts) });
1415
+ if (membership.kind !== "dm")
1416
+ await tx
1417
+ .update(chatMembers)
1418
+ .set({
1419
+ leftAt: sql `CURRENT_TIMESTAMP`,
1420
+ removedByUserId: actorUserId,
1421
+ syncSequence: sequence,
1422
+ updatedAt: sql `CURRENT_TIMESTAMP`,
1423
+ })
1424
+ .where(and(eq(chatMembers.chatId, chatId), eq(chatMembers.userId, targetUserId), isNull(chatMembers.leftAt)));
1425
+ if (eventKind === "chat.deleted")
1426
+ await tx
1427
+ .update(chats)
1428
+ .set({
1429
+ deletedAt: sql `CURRENT_TIMESTAMP`,
1430
+ deletedByUserId: actorUserId,
1431
+ deleteReason: "last member deleted",
1432
+ ownerUserId: null,
1433
+ })
1434
+ .where(eq(chats.id, chatId));
1435
+ }
1436
+ if (!target.accountId)
1437
+ throw new Error("User account is missing");
1438
+ const accountId = target.accountId;
1439
+ await tx
1440
+ .update(authSessions)
1441
+ .set({ revokedAt: sql `CURRENT_TIMESTAMP` })
1442
+ .where(and(eq(authSessions.accountId, accountId), isNull(authSessions.revokedAt)));
1443
+ await tx
1444
+ .update(accounts)
1445
+ .set({
1446
+ deletedAt: sql `CURRENT_TIMESTAMP`,
1447
+ active: 0,
1448
+ passwordHash: null,
1449
+ bannedAt: null,
1450
+ banExpiresAt: null,
1451
+ banReason: null,
1452
+ bannedByUserId: null,
1453
+ email: sql `'deleted+' || ${accounts.id} || '@invalid.local'`,
1454
+ })
1455
+ .where(and(eq(accounts.id, accountId), isNull(accounts.deletedAt)));
1456
+ await tx
1457
+ .update(users)
1458
+ .set({
1459
+ deletedAt: sql `CURRENT_TIMESTAMP`,
1460
+ syncSequence: sequence,
1461
+ firstName: "Deleted",
1462
+ lastName: null,
1463
+ title: null,
1464
+ username: sql `'deleted_' || ${users.id}`,
1465
+ email: null,
1466
+ phone: null,
1467
+ photoFileId: null,
1468
+ })
1469
+ .where(and(eq(users.id, targetUserId), isNull(users.deletedAt)));
1470
+ await this.insertSyncEvent(tx, {
1471
+ sequence,
1472
+ kind: "user.deleted",
1473
+ entityId: targetUserId,
1474
+ actorUserId,
1475
+ });
1476
+ return { sequence: String(sequence), chats: chatPoints, areas: ["users"] };
1477
+ }
1478
+ async recomputeThreadProjection(tx, threadRootMessageId, pts) {
1479
+ await tx
1480
+ .delete(threadParticipants)
1481
+ .where(eq(threadParticipants.threadRootMessageId, threadRootMessageId));
1482
+ const participantRows = await tx
1483
+ .select({
1484
+ userId: messages.senderUserId,
1485
+ replyCount: sql `count(*)`,
1486
+ firstParticipatedAt: sql `min(${messages.createdAt})`,
1487
+ lastParticipatedAt: sql `max(${messages.createdAt})`,
1488
+ })
1489
+ .from(messages)
1490
+ .where(and(eq(messages.threadRootMessageId, threadRootMessageId), sql `${messages.senderUserId} IS NOT NULL`, isNull(messages.deletedAt), or(isNull(messages.expiresAt), sql `datetime(${messages.expiresAt}) > CURRENT_TIMESTAMP`)))
1491
+ .groupBy(messages.senderUserId);
1492
+ if (participantRows.length)
1493
+ await tx.insert(threadParticipants).values(participantRows.map((row) => ({
1494
+ threadRootMessageId,
1495
+ userId: row.userId,
1496
+ replyCount: row.replyCount,
1497
+ firstParticipatedAt: row.firstParticipatedAt,
1498
+ lastParticipatedAt: row.lastParticipatedAt,
1499
+ })));
1500
+ const activeReplies = and(eq(messages.threadRootMessageId, threadRootMessageId), isNull(messages.deletedAt), or(isNull(messages.expiresAt), sql `datetime(${messages.expiresAt}) > CURRENT_TIMESTAMP`));
1501
+ const [lastReply] = await tx
1502
+ .select({ id: messages.id, sequence: messages.sequence })
1503
+ .from(messages)
1504
+ .where(activeReplies)
1505
+ .orderBy(desc(messages.sequence))
1506
+ .limit(1);
1507
+ const [replyCount] = await tx
1508
+ .select({ count: sql `count(*)` })
1509
+ .from(messages)
1510
+ .where(activeReplies);
1511
+ const [participantCount] = await tx
1512
+ .select({ count: sql `count(*)` })
1513
+ .from(threadParticipants)
1514
+ .where(eq(threadParticipants.threadRootMessageId, threadRootMessageId));
1515
+ await tx
1516
+ .update(threads)
1517
+ .set({
1518
+ replyCount: replyCount?.count ?? 0,
1519
+ participantCount: participantCount?.count ?? 0,
1520
+ lastReplyMessageId: lastReply?.id ?? null,
1521
+ lastReplySequence: lastReply?.sequence ?? 0,
1522
+ lastPts: pts,
1523
+ updatedAt: sql `CURRENT_TIMESTAMP`,
1524
+ })
1525
+ .where(eq(threads.rootMessageId, threadRootMessageId));
1526
+ await tx
1527
+ .update(threadUserStates)
1528
+ .set({
1529
+ unreadCount: sql `(select count(*) from messages m where m.thread_root_message_id = ${threadRootMessageId} and m.deleted_at is null and (m.expires_at is null or datetime(m.expires_at) > CURRENT_TIMESTAMP) and m.sequence > ${threadUserStates.lastReadSequence} and (m.sender_user_id is null or m.sender_user_id != ${threadUserStates.userId}))`,
1530
+ mentionCount: sql `(select count(*) from message_mentions mm join messages m on m.id = mm.message_id where m.thread_root_message_id = ${threadRootMessageId} and m.deleted_at is null and m.sequence > ${threadUserStates.lastReadSequence} and mm.mentioned_user_id = ${threadUserStates.userId})`,
1531
+ updatedAt: sql `CURRENT_TIMESTAMP`,
1532
+ })
1533
+ .where(eq(threadUserStates.threadRootMessageId, threadRootMessageId));
1534
+ }
1535
+ async nextSequence(tx) {
1536
+ const [state] = await tx
1537
+ .update(serverSyncState)
1538
+ .set({
1539
+ sequence: sql `${serverSyncState.sequence} + 1`,
1540
+ updatedAt: sql `CURRENT_TIMESTAMP`,
1541
+ })
1542
+ .where(eq(serverSyncState.id, 1))
1543
+ .returning({ sequence: serverSyncState.sequence });
1544
+ if (!state)
1545
+ throw new Error("Sync state has not been initialized");
1546
+ return state.sequence;
1547
+ }
1548
+ async advanceChatMutation(tx, input) {
1549
+ const [chat] = await tx
1550
+ .update(chats)
1551
+ .set({
1552
+ pts: sql `${chats.pts} + 1`,
1553
+ lastChangeSequence: input.sequence,
1554
+ updatedAt: sql `CURRENT_TIMESTAMP`,
1555
+ })
1556
+ .where(and(eq(chats.id, input.chatId), isNull(chats.deletedAt)))
1557
+ .returning({ pts: chats.pts });
1558
+ if (!chat)
1559
+ throw new OperationsError("not_found", "Chat was not found");
1560
+ const pts = chat.pts;
1561
+ await tx.insert(chatUpdates).values({
1562
+ chatId: input.chatId,
1563
+ pts,
1564
+ ptsCount: 1,
1565
+ kind: input.kind,
1566
+ entityId: input.entityId,
1567
+ });
1568
+ await this.insertSyncEvent(tx, { ...input, chatPts: pts });
1569
+ return pts;
1570
+ }
1571
+ async insertSyncEvent(tx, input) {
1572
+ await tx.insert(syncEvents).values({
1573
+ sequence: input.sequence,
1574
+ kind: input.kind,
1575
+ chatId: input.chatId,
1576
+ chatPts: input.chatPts,
1577
+ entityId: input.entityId,
1578
+ actorUserId: input.actorUserId,
1579
+ targetUserId: input.targetUserId,
1580
+ });
1581
+ }
1582
+ async syncUserMutation(tx, actorUserId, targetUserId, kind) {
1583
+ const sequence = await this.nextSequence(tx);
1584
+ await tx.update(users).set({ syncSequence: sequence }).where(eq(users.id, targetUserId));
1585
+ await tx
1586
+ .insert(syncEvents)
1587
+ .values({ sequence, kind, entityId: targetUserId, actorUserId, targetUserId });
1588
+ return { sequence: String(sequence), chats: [], areas: ["users"] };
1589
+ }
1590
+ async closeElapsedBan(tx, target) {
1591
+ if (!target.bannedAt ||
1592
+ !target.banExpiresAt ||
1593
+ Date.parse(target.banExpiresAt) > Date.now())
1594
+ return;
1595
+ const now = new Date().toISOString();
1596
+ await tx
1597
+ .update(accountBans)
1598
+ .set({
1599
+ revokedAt: sql `coalesce(${accountBans.revokedAt}, ${now})`,
1600
+ revokeReason: sql `coalesce(${accountBans.revokeReason}, 'expired')`,
1601
+ })
1602
+ .where(and(eq(accountBans.accountId, target.accountId), isNull(accountBans.revokedAt), lte(accountBans.expiresAt, now)));
1603
+ await tx
1604
+ .update(accounts)
1605
+ .set({ bannedAt: null, banExpiresAt: null, banReason: null, bannedByUserId: null })
1606
+ .where(and(eq(accounts.id, target.accountId), sql `${accounts.banExpiresAt} IS NOT NULL`, lte(accounts.banExpiresAt, now)));
1607
+ }
1608
+ async requireReportTargetAccess(tx, input) {
1609
+ if (input.targetUserId)
1610
+ await this.requireExistingUserDb(tx, input.targetUserId);
1611
+ if (input.chatId && !(await this.canAccessChat(tx, input.actorUserId, input.chatId)))
1612
+ throw new OperationsError("not_found", "Chat was not found");
1613
+ if (input.messageId) {
1614
+ const [message] = await tx
1615
+ .select({ chatId: messages.chatId })
1616
+ .from(messages)
1617
+ .where(eq(messages.id, input.messageId))
1618
+ .limit(1);
1619
+ if (!message || !(await this.canAccessChat(tx, input.actorUserId, message.chatId)))
1620
+ throw new OperationsError("not_found", "Message was not found");
1621
+ if (input.chatId && input.chatId !== message.chatId)
1622
+ throw new OperationsError("invalid", "messageId does not belong to chatId");
1623
+ }
1624
+ if (input.fileId) {
1625
+ const grants = tx
1626
+ .select({ found: sql `1` })
1627
+ .from(fileAccessGrants)
1628
+ .where(and(eq(fileAccessGrants.fileId, files.id), or(and(eq(fileAccessGrants.principalType, "user"), eq(fileAccessGrants.principalId, input.actorUserId)), eq(fileAccessGrants.principalType, "server"), and(eq(fileAccessGrants.principalType, "chat"), sql `exists (select 1 from chats c left join chat_members cm on cm.chat_id = c.id and cm.user_id = ${input.actorUserId} and cm.left_at is null where c.id = ${fileAccessGrants.principalId} and c.deleted_at is null and (c.visibility = 'public' or cm.user_id is not null))`)), or(isNull(fileAccessGrants.expiresAt), gt(fileAccessGrants.expiresAt, sql `CURRENT_TIMESTAMP`))));
1629
+ const attachments = tx
1630
+ .select({ found: sql `1` })
1631
+ .from(messageAttachments)
1632
+ .innerJoin(messages, eq(messages.id, messageAttachments.messageId))
1633
+ .innerJoin(chats, eq(chats.id, messages.chatId))
1634
+ .leftJoin(chatMembers, and(eq(chatMembers.chatId, chats.id), eq(chatMembers.userId, input.actorUserId), isNull(chatMembers.leftAt)))
1635
+ .where(and(eq(messageAttachments.fileId, files.id), isNull(messages.deletedAt), isNull(chats.deletedAt), or(eq(chats.visibility, "public"), sql `${chatMembers.userId} IS NOT NULL`)));
1636
+ const [accessible] = await tx
1637
+ .select({ id: files.id })
1638
+ .from(files)
1639
+ .where(and(eq(files.id, input.fileId), isNull(files.deletedAt), or(eq(files.isPublic, 1), eq(files.uploadedByUserId, input.actorUserId), sql `exists ${grants}`, sql `exists ${attachments}`)))
1640
+ .limit(1);
1641
+ if (!accessible)
1642
+ throw new OperationsError("not_found", "File was not found");
1643
+ }
1644
+ }
1645
+ async canAccessChat(executor, userId, chatId) {
1646
+ const [row] = await executor
1647
+ .select({ id: chats.id })
1648
+ .from(chats)
1649
+ .leftJoin(chatMembers, and(eq(chatMembers.chatId, chats.id), eq(chatMembers.userId, userId), isNull(chatMembers.leftAt)))
1650
+ .where(and(eq(chats.id, chatId), isNull(chats.deletedAt), or(eq(chats.visibility, "public"), sql `${chatMembers.userId} IS NOT NULL`)))
1651
+ .limit(1);
1652
+ return Boolean(row);
1653
+ }
1654
+ async banDb(executor, id) {
1655
+ const [row] = await executor
1656
+ .select(banSelection)
1657
+ .from(accountBans)
1658
+ .leftJoin(users, eq(users.accountId, accountBans.accountId))
1659
+ .where(eq(accountBans.id, id))
1660
+ .limit(1);
1661
+ if (!row)
1662
+ throw new OperationsError("not_found", "Ban was not found");
1663
+ return asBan(row);
1664
+ }
1665
+ async reportDb(executor, id) {
1666
+ const [row] = await executor
1667
+ .select(reportSelection)
1668
+ .from(moderationReports)
1669
+ .where(eq(moderationReports.id, id))
1670
+ .limit(1);
1671
+ if (!row)
1672
+ throw new OperationsError("not_found", "Moderation report was not found");
1673
+ return asReport(row);
1674
+ }
1675
+ async moderationActionDb(executor, id) {
1676
+ const [row] = await executor
1677
+ .select(moderationActionSelection)
1678
+ .from(moderationActions)
1679
+ .where(eq(moderationActions.id, id))
1680
+ .limit(1);
1681
+ if (!row)
1682
+ throw new OperationsError("not_found", "Moderation action was not found");
1683
+ return asModerationAction(row);
1684
+ }
1685
+ async exportJobDb(executor, id) {
1686
+ const [row] = await executor
1687
+ .select(exportSelection)
1688
+ .from(dataExportJobs)
1689
+ .where(eq(dataExportJobs.id, id))
1690
+ .limit(1);
1691
+ if (!row)
1692
+ throw new OperationsError("not_found", "Data export was not found");
1693
+ return asExport(row);
1694
+ }
1695
+ async backupDb(executor, id) {
1696
+ const [row] = await executor
1697
+ .select(backupSelection)
1698
+ .from(backupRecords)
1699
+ .where(eq(backupRecords.id, id))
1700
+ .limit(1);
1701
+ if (!row)
1702
+ throw new OperationsError("not_found", "Backup was not found");
1703
+ return asBackup(row);
1704
+ }
1705
+ async retentionRunDb(executor, id) {
1706
+ const [row] = await executor
1707
+ .select(retentionSelection)
1708
+ .from(retentionRuns)
1709
+ .where(eq(retentionRuns.id, id))
1710
+ .limit(1);
1711
+ if (!row)
1712
+ throw new OperationsError("not_found", "Retention run was not found");
1713
+ return asRetention(row);
1714
+ }
1715
+ async appendAudit(tx, input) {
1716
+ const request = input.context?.request;
1717
+ const metadata = {
1718
+ ...input.context?.metadata,
1719
+ ...(request?.forwardedFor ? { forwardedFor: request.forwardedFor } : {}),
1720
+ ...(request?.location ? { location: request.location } : {}),
1721
+ };
1722
+ await tx.insert(auditLogEntries).values({
1723
+ id: createId(),
1724
+ actorUserId: input.actorUserId,
1725
+ action: input.action,
1726
+ targetType: input.targetType,
1727
+ targetId: input.targetId,
1728
+ chatId: input.chatId,
1729
+ beforeJson: json(input.before),
1730
+ afterJson: json(input.after),
1731
+ metadataJson: Object.keys(metadata).length ? JSON.stringify(metadata) : null,
1732
+ clientIp: request?.ip,
1733
+ device: request?.device,
1734
+ appVersion: request?.appVersion,
1735
+ userAgent: request?.userAgent,
1736
+ });
1737
+ }
1738
+ writeDb(operation) {
1739
+ return retrySqliteBusy(() => this.db.transaction(operation));
1740
+ }
1741
+ }
1742
+ function cursorCondition(timestampColumn, idColumn, cursor) {
1743
+ return or(lt(timestampColumn, cursor.at), and(eq(timestampColumn, cursor.at), lt(idColumn, cursor.id)));
1744
+ }
1745
+ function decodeCursor(value) {
1746
+ if (!value)
1747
+ return undefined;
1748
+ if (value.length > 1_024)
1749
+ throw new OperationsError("invalid", "Cursor is invalid");
1750
+ try {
1751
+ const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
1752
+ if (typeof parsed.at !== "string" ||
1753
+ typeof parsed.id !== "string" ||
1754
+ parsed.at.length > 64 ||
1755
+ parsed.id.length > 128)
1756
+ throw new Error("bad cursor");
1757
+ return { at: parsed.at, id: parsed.id };
1758
+ }
1759
+ catch {
1760
+ throw new OperationsError("invalid", "Cursor is invalid");
1761
+ }
1762
+ }
1763
+ function encodeCursor(at, id) {
1764
+ return Buffer.from(JSON.stringify({ at, id }), "utf8").toString("base64url");
1765
+ }
1766
+ function page(rows, limit, map, timestamp = (item) => item.createdAt, id = (item) => item.id) {
1767
+ const hasMore = rows.length > limit;
1768
+ const items = rows.slice(0, limit).map(map);
1769
+ const last = items.at(-1);
1770
+ return {
1771
+ items,
1772
+ nextCursor: hasMore && last ? encodeCursor(timestamp(last), id(last)) : undefined,
1773
+ };
1774
+ }
1775
+ function asAudit(row) {
1776
+ return {
1777
+ id: text(row.id),
1778
+ actorUserId: optionalText(row.actor_user_id),
1779
+ actorIntegrationId: optionalText(row.actor_integration_id),
1780
+ action: text(row.action),
1781
+ targetType: text(row.target_type),
1782
+ targetId: optionalText(row.target_id),
1783
+ chatId: optionalText(row.chat_id),
1784
+ before: parseJson(row.before_json),
1785
+ after: parseJson(row.after_json),
1786
+ metadata: parseJson(row.metadata_json),
1787
+ clientIp: optionalText(row.client_ip),
1788
+ device: optionalText(row.device),
1789
+ appVersion: optionalText(row.app_version),
1790
+ userAgent: optionalText(row.user_agent),
1791
+ createdAt: text(row.created_at),
1792
+ };
1793
+ }
1794
+ function asBan(row) {
1795
+ const revokedAt = optionalText(row.revoked_at);
1796
+ const expiresAt = optionalText(row.expires_at);
1797
+ return {
1798
+ id: text(row.id),
1799
+ accountId: text(row.account_id),
1800
+ userId: optionalText(row.user_id),
1801
+ username: optionalText(row.username),
1802
+ bannedByUserId: optionalText(row.banned_by_user_id),
1803
+ reason: optionalText(row.reason),
1804
+ bannedAt: text(row.banned_at),
1805
+ expiresAt,
1806
+ revokedAt,
1807
+ revokedByUserId: optionalText(row.revoked_by_user_id),
1808
+ revokeReason: optionalText(row.revoke_reason),
1809
+ status: revokedAt
1810
+ ? "revoked"
1811
+ : expiresAt && Date.parse(expiresAt) <= Date.now()
1812
+ ? "expired"
1813
+ : "active",
1814
+ };
1815
+ }
1816
+ function asReport(row) {
1817
+ return {
1818
+ id: text(row.id),
1819
+ reportedByUserId: optionalText(row.reported_by_user_id),
1820
+ targetUserId: optionalText(row.target_user_id),
1821
+ chatId: optionalText(row.chat_id),
1822
+ messageId: optionalText(row.message_id),
1823
+ fileId: optionalText(row.file_id),
1824
+ reason: text(row.reason),
1825
+ details: optionalText(row.details),
1826
+ status: text(row.status),
1827
+ assignedToUserId: optionalText(row.assigned_to_user_id),
1828
+ resolution: optionalText(row.resolution),
1829
+ createdAt: text(row.created_at),
1830
+ updatedAt: text(row.updated_at),
1831
+ resolvedAt: optionalText(row.resolved_at),
1832
+ };
1833
+ }
1834
+ function asModerationAction(row) {
1835
+ return {
1836
+ id: text(row.id),
1837
+ reportId: optionalText(row.report_id),
1838
+ actorUserId: optionalText(row.actor_user_id),
1839
+ targetUserId: optionalText(row.target_user_id),
1840
+ chatId: optionalText(row.chat_id),
1841
+ messageId: optionalText(row.message_id),
1842
+ fileId: optionalText(row.file_id),
1843
+ action: text(row.action),
1844
+ reason: optionalText(row.reason),
1845
+ metadata: parseJson(row.metadata_json),
1846
+ expiresAt: optionalText(row.expires_at),
1847
+ revokedAt: optionalText(row.revoked_at),
1848
+ createdAt: text(row.created_at),
1849
+ };
1850
+ }
1851
+ function asExport(row) {
1852
+ return {
1853
+ id: text(row.id),
1854
+ requestedByUserId: optionalText(row.requested_by_user_id),
1855
+ kind: text(row.kind),
1856
+ targetId: optionalText(row.target_id),
1857
+ status: text(row.status),
1858
+ outputFileId: optionalText(row.output_file_id),
1859
+ options: parseJson(row.options_json),
1860
+ lastError: optionalText(row.last_error),
1861
+ expiresAt: optionalText(row.expires_at),
1862
+ createdAt: text(row.created_at),
1863
+ startedAt: optionalText(row.started_at),
1864
+ completedAt: optionalText(row.completed_at),
1865
+ };
1866
+ }
1867
+ function asBackup(row) {
1868
+ return {
1869
+ id: text(row.id),
1870
+ storageProvider: text(row.storage_provider),
1871
+ storageKey: text(row.storage_key),
1872
+ checksumSha256: optionalText(row.checksum_sha256),
1873
+ size: optionalNumber(row.size),
1874
+ status: text(row.status),
1875
+ createdByUserId: optionalText(row.created_by_user_id),
1876
+ metadata: parseJson(row.metadata_json),
1877
+ lastError: optionalText(row.last_error),
1878
+ createdAt: text(row.created_at),
1879
+ completedAt: optionalText(row.completed_at),
1880
+ retentionUntil: optionalText(row.retention_until),
1881
+ };
1882
+ }
1883
+ function asRetention(row) {
1884
+ return {
1885
+ id: text(row.id),
1886
+ scope: text(row.scope),
1887
+ status: text(row.status),
1888
+ itemsExamined: number(row.items_examined),
1889
+ itemsDeleted: number(row.items_deleted),
1890
+ details: parseJson(row.details_json),
1891
+ lastError: optionalText(row.last_error),
1892
+ startedAt: text(row.started_at),
1893
+ completedAt: optionalText(row.completed_at),
1894
+ };
1895
+ }
1896
+ function asAccess(row) {
1897
+ return {
1898
+ userId: text(row.id),
1899
+ username: text(row.username),
1900
+ email: text(row.email),
1901
+ role: text(row.role),
1902
+ lastAccessAt: optionalText(row.last_access_at),
1903
+ lastSessionAccessAt: optionalText(row.last_session_access_at),
1904
+ activeSessionCount: number(row.active_session_count, 0),
1905
+ bannedAt: optionalText(row.banned_at),
1906
+ banExpiresAt: optionalText(row.ban_expires_at),
1907
+ deletedAt: optionalText(row.deleted_at),
1908
+ lastClientIp: optionalText(row.last_client_ip),
1909
+ lastDevice: optionalText(row.last_device),
1910
+ lastAppVersion: optionalText(row.last_app_version),
1911
+ lastUserAgent: optionalText(row.last_user_agent),
1912
+ };
1913
+ }
1914
+ function accountTargetState(target) {
1915
+ return {
1916
+ banned: Boolean(target.bannedAt),
1917
+ bannedAt: target.bannedAt,
1918
+ expiresAt: target.banExpiresAt,
1919
+ reason: target.banReason,
1920
+ bannedByUserId: target.bannedByUserId,
1921
+ };
1922
+ }
1923
+ function mergeContext(context, metadata) {
1924
+ return { request: context?.request, metadata: { ...context?.metadata, ...metadata } };
1925
+ }
1926
+ function futureTimestamp(value, name) {
1927
+ if (value === undefined)
1928
+ return undefined;
1929
+ const timestamp = Date.parse(value);
1930
+ if (!Number.isFinite(timestamp) || timestamp <= Date.now())
1931
+ throw new OperationsError("invalid", `${name} must be a future ISO timestamp`);
1932
+ return new Date(timestamp).toISOString();
1933
+ }
1934
+ function assertExportTransition(from, to) {
1935
+ const allowed = {
1936
+ pending: ["running", "failed", "cancelled"],
1937
+ running: ["complete", "failed", "cancelled"],
1938
+ complete: ["expired"],
1939
+ failed: [],
1940
+ cancelled: [],
1941
+ expired: [],
1942
+ };
1943
+ if (!allowed[from].includes(to))
1944
+ throw new OperationsError("conflict", `Cannot move a data export from ${from} to ${to}`);
1945
+ }
1946
+ function assertBackupTransition(from, to) {
1947
+ const allowed = {
1948
+ pending: ["running", "failed", "deleted"],
1949
+ running: ["complete", "failed", "deleted"],
1950
+ complete: ["deleted"],
1951
+ failed: ["deleted"],
1952
+ deleted: [],
1953
+ };
1954
+ if (!allowed[from].includes(to))
1955
+ throw new OperationsError("conflict", `Cannot move a backup from ${from} to ${to}`);
1956
+ }
1957
+ function json(value) {
1958
+ return value === undefined ? null : JSON.stringify(value);
1959
+ }
1960
+ function parseJson(value) {
1961
+ const raw = optionalText(value);
1962
+ if (raw === undefined)
1963
+ return undefined;
1964
+ try {
1965
+ return JSON.parse(raw);
1966
+ }
1967
+ catch {
1968
+ return undefined;
1969
+ }
1970
+ }
1971
+ function objectValue(value) {
1972
+ return value && typeof value === "object" && !Array.isArray(value)
1973
+ ? value
1974
+ : {};
1975
+ }
1976
+ function text(value) {
1977
+ if (typeof value === "string")
1978
+ return value;
1979
+ if (typeof value === "number" || typeof value === "bigint")
1980
+ return String(value);
1981
+ throw new Error("Expected database text value");
1982
+ }
1983
+ function optionalText(value) {
1984
+ return value === null || value === undefined ? undefined : text(value);
1985
+ }
1986
+ function number(value, fallback) {
1987
+ if (typeof value === "number")
1988
+ return value;
1989
+ if (typeof value === "bigint")
1990
+ return Number(value);
1991
+ if (typeof value === "string" && /^-?\d+$/.test(value))
1992
+ return Number(value);
1993
+ if (fallback !== undefined)
1994
+ return fallback;
1995
+ throw new Error("Expected database integer value");
1996
+ }
1997
+ function optionalNumber(value) {
1998
+ return value === null || value === undefined ? undefined : number(value);
1999
+ }
2000
+ function isUniqueConstraint(error) {
2001
+ let current = error;
2002
+ for (let depth = 0; current && depth < 5; depth += 1) {
2003
+ const value = current;
2004
+ const details = `${String(value.code ?? "")} ${String(value.message ?? "")}`.toLowerCase();
2005
+ if (details.includes("unique") || details.includes("constraint"))
2006
+ return true;
2007
+ current = value.cause;
2008
+ }
2009
+ return false;
2010
+ }
2011
+ //# sourceMappingURL=repository.js.map