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,1285 @@
1
+ import { CollaborationError } from "../modules/collaboration/types.js";
2
+ import { realtimeTopics, } from "../modules/realtime/index.js";
3
+ const MAX_ID_LENGTH = 128;
4
+ const MAX_MESSAGE_LENGTH = 40_000;
5
+ const MAX_ATTACHMENTS = 20;
6
+ const MAX_FORWARD_TARGETS = 20;
7
+ const MAX_SELF_DESTRUCT_SECONDS = 31_536_000;
8
+ export function registerCollaborationRoutes(app, auth, repository, pubsub, agents) {
9
+ app.get("/v0/chats", authenticated(auth, async (request, _reply, userId) => {
10
+ emptyQuery(request);
11
+ return { chats: await repository.listChats(userId) };
12
+ }));
13
+ app.get("/v0/chats/:chatId", authenticated(auth, async (request, _reply, userId) => {
14
+ emptyQuery(request);
15
+ return { chat: await repository.getChat(userId, pathId(request, "chatId")) };
16
+ }));
17
+ app.get("/v0/chats/:chatId/members", authenticated(auth, async (request, _reply, userId) => {
18
+ emptyQuery(request);
19
+ const memberships = await repository.listChatMemberships(userId, pathId(request, "chatId"));
20
+ return {
21
+ users: memberships.map(({ user }) => user),
22
+ memberships,
23
+ };
24
+ }));
25
+ app.get("/v0/chats/:chatId/messages", authenticated(auth, async (request, _reply, userId) => {
26
+ const chatId = pathId(request, "chatId");
27
+ const page = messagePage(request, ["beforeSequence", "afterSequence", "limit"]);
28
+ return repository.listMessages({ userId, chatId, ...page });
29
+ }));
30
+ app.get("/v0/messages/:messageId", authenticated(auth, async (request, _reply, userId) => {
31
+ emptyQuery(request);
32
+ return {
33
+ message: await repository.getMessage(userId, pathId(request, "messageId")),
34
+ };
35
+ }));
36
+ app.get("/v0/messages/:messageId/thread", authenticated(auth, async (request, _reply, userId) => {
37
+ const selected = await repository.getMessage(userId, pathId(request, "messageId"));
38
+ const root = selected.threadRootMessageId
39
+ ? await repository.getMessage(userId, selected.threadRootMessageId)
40
+ : selected;
41
+ const page = messagePage(request, ["beforeSequence", "afterSequence", "limit"]);
42
+ const result = await repository.listMessages({
43
+ userId,
44
+ chatId: root.chatId,
45
+ threadRootMessageId: root.id,
46
+ ...page,
47
+ });
48
+ return { root, ...result };
49
+ }));
50
+ app.get("/v0/threads", authenticated(auth, async (request, _reply, userId) => {
51
+ const query = requestQuery(request, ["before", "unreadOnly", "limit"]);
52
+ return repository.listMyThreads({
53
+ userId,
54
+ before: optionalQueryString(query, "before", MAX_ID_LENGTH),
55
+ unreadOnly: optionalQueryBoolean(query, "unreadOnly"),
56
+ limit: queryLimit(query, "limit", 50, 100),
57
+ });
58
+ }));
59
+ app.get("/v0/notifications", authenticated(auth, async (request, _reply, userId) => {
60
+ const query = requestQuery(request, ["before", "unreadOnly", "limit"]);
61
+ return repository.listNotifications({
62
+ userId,
63
+ before: optionalQueryString(query, "before", MAX_ID_LENGTH),
64
+ unreadOnly: optionalQueryBoolean(query, "unreadOnly"),
65
+ limit: queryLimit(query, "limit", 50, 100),
66
+ });
67
+ }));
68
+ app.post("/v0/notifications/markRead", authenticated(auth, async (request, _reply, userId) => {
69
+ const body = requestBody(request, ["notificationIds", "all"]);
70
+ const result = await repository.markNotificationsRead({
71
+ actorUserId: userId,
72
+ notificationIds: has(body, "notificationIds")
73
+ ? idArrayField(body, "notificationIds", 500, true)
74
+ : undefined,
75
+ all: has(body, "all") ? booleanField(body, "all") : undefined,
76
+ });
77
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
78
+ return { sync: result.hint };
79
+ }));
80
+ app.post("/v0/chats/createDirectMessage", authenticated(auth, async (request, reply, userId) => {
81
+ const body = requestBody(request, ["userId"]);
82
+ const otherUserId = idField(body, "userId");
83
+ const result = await repository.createDirectMessage(userId, otherUserId);
84
+ if (result.hint)
85
+ await publishHints(request, pubsub, [result.hint], {
86
+ userIds: [userId, otherUserId],
87
+ });
88
+ return reply.code(result.hint ? 201 : 200).send({
89
+ chat: result.chat,
90
+ sync: result.hint,
91
+ });
92
+ }));
93
+ app.post("/v0/chats/createGroupDirectMessage", authenticated(auth, async (request, reply, userId) => {
94
+ const body = requestBody(request, ["userIds", "name"]);
95
+ const result = await repository.createGroupDirectMessage({
96
+ actorUserId: userId,
97
+ userIds: idArrayField(body, "userIds", 49, false),
98
+ name: has(body, "name") ? trimmedString(body, "name", 100) : undefined,
99
+ });
100
+ if (result.hint)
101
+ await publishHints(request, pubsub, [result.hint], {
102
+ userIds: result.memberUserIds,
103
+ });
104
+ return reply.code(result.hint ? 201 : 200).send({
105
+ chat: result.chat,
106
+ sync: result.hint,
107
+ });
108
+ }));
109
+ app.post("/v0/chats/createAgent", authenticated(auth, async (request, reply, userId) => {
110
+ if (!agents)
111
+ return reply.code(503).send({
112
+ error: "agents_unavailable",
113
+ message: "AI agents are not enabled on this Happy (2) server.",
114
+ });
115
+ const body = requestBody(request, ["name", "username"]);
116
+ const result = await agents.createAgent({
117
+ actorUserId: userId,
118
+ name: trimmedString(body, "name", 100),
119
+ username: agentUsername(body),
120
+ });
121
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
122
+ return reply.code(201).send({ chat: result.chat, sync: result.hint });
123
+ }));
124
+ app.post("/v0/chats/createChannel", authenticated(auth, async (request, reply, userId) => {
125
+ const body = requestBody(request, ["kind", "name", "slug", "topic"]);
126
+ const kind = enumField(body, "kind", ["public_channel", "private_channel"]);
127
+ const result = await repository.createChannel({
128
+ actorUserId: userId,
129
+ kind,
130
+ name: trimmedString(body, "name", 100),
131
+ slug: channelSlug(body),
132
+ topic: nullableTrimmedString(body, "topic", 500),
133
+ });
134
+ await publishHints(request, pubsub, [result.hint], {
135
+ server: kind === "public_channel",
136
+ userIds: [userId],
137
+ });
138
+ return reply.code(201).send({ chat: result.chat, sync: result.hint });
139
+ }));
140
+ app.post("/v0/chats/:chatId/updateTopic", authenticated(auth, async (request, _reply, userId) => {
141
+ const body = requestBody(request, ["topic"]);
142
+ requireField(body, "topic");
143
+ const result = await repository.updateTopic(userId, pathId(request, "chatId"), nullableTrimmedString(body, "topic", 500));
144
+ await publishHints(request, pubsub, [result.hint]);
145
+ return { chat: result.chat, sync: result.hint };
146
+ }));
147
+ app.post("/v0/chats/:chatId/updateChannel", authenticated(auth, async (request, _reply, userId) => {
148
+ const body = requestBody(request, [
149
+ "name",
150
+ "slug",
151
+ "topic",
152
+ "kind",
153
+ "photoFileId",
154
+ "isListed",
155
+ ]);
156
+ if (Object.keys(body).length === 0)
157
+ throw new InvalidRequest("At least one channel field is required");
158
+ const result = await repository.updateChannel({
159
+ actorUserId: userId,
160
+ chatId: pathId(request, "chatId"),
161
+ name: has(body, "name") ? trimmedString(body, "name", 100) : undefined,
162
+ slug: has(body, "slug") ? channelSlug(body) : undefined,
163
+ topic: has(body, "topic")
164
+ ? body.topic === null
165
+ ? null
166
+ : (nullableTrimmedString(body, "topic", 500) ?? null)
167
+ : undefined,
168
+ kind: optionalEnumField(body, "kind", [
169
+ "public_channel",
170
+ "private_channel",
171
+ ]),
172
+ photoFileId: has(body, "photoFileId")
173
+ ? body.photoFileId === null
174
+ ? null
175
+ : id(body.photoFileId, "photoFileId")
176
+ : undefined,
177
+ isListed: has(body, "isListed") ? booleanField(body, "isListed") : undefined,
178
+ });
179
+ await publishHints(request, pubsub, [result.hint], {
180
+ server: result.chat.kind === "public_channel",
181
+ });
182
+ return { chat: result.chat, sync: result.hint };
183
+ }));
184
+ app.post("/v0/chats/:chatId/updatePolicies", authenticated(auth, async (request, _reply, userId) => {
185
+ const body = requestBody(request, [
186
+ "retentionMode",
187
+ "retentionSeconds",
188
+ "defaultExpiryMode",
189
+ "defaultSelfDestructSeconds",
190
+ "defaultAfterReadScope",
191
+ ]);
192
+ if (Object.keys(body).length === 0)
193
+ throw new InvalidRequest("At least one policy field is required");
194
+ const result = await repository.updateChannelPolicies({
195
+ actorUserId: userId,
196
+ chatId: pathId(request, "chatId"),
197
+ retentionMode: optionalEnumField(body, "retentionMode", [
198
+ "inherit",
199
+ "forever",
200
+ "duration",
201
+ ]),
202
+ retentionSeconds: optionalNullablePositiveIntegerField(body, "retentionSeconds", MAX_SELF_DESTRUCT_SECONDS * 100),
203
+ defaultExpiryMode: optionalEnumField(body, "defaultExpiryMode", [
204
+ "none",
205
+ "after_send",
206
+ "after_read",
207
+ ]),
208
+ defaultSelfDestructSeconds: optionalNullablePositiveIntegerField(body, "defaultSelfDestructSeconds", MAX_SELF_DESTRUCT_SECONDS),
209
+ defaultAfterReadScope: optionalEnumField(body, "defaultAfterReadScope", [
210
+ "any_reader",
211
+ "all_readers",
212
+ ]),
213
+ });
214
+ await publishHints(request, pubsub, [result.hint]);
215
+ return { chat: result.chat, sync: result.hint };
216
+ }));
217
+ for (const [path, archived] of [
218
+ ["archiveChannel", true],
219
+ ["unarchiveChannel", false],
220
+ ])
221
+ app.post(`/v0/chats/:chatId/${path}`, authenticated(auth, async (request, _reply, userId) => {
222
+ const body = request.body === undefined || request.body === null
223
+ ? {}
224
+ : requestBody(request, ["reason"]);
225
+ const result = await repository.setChannelArchived({
226
+ actorUserId: userId,
227
+ chatId: pathId(request, "chatId"),
228
+ archived,
229
+ reason: has(body, "reason")
230
+ ? (nullableTrimmedString(body, "reason", 500) ?? undefined)
231
+ : undefined,
232
+ });
233
+ await publishHints(request, pubsub, [result.hint], {
234
+ server: result.chat.kind === "public_channel",
235
+ });
236
+ return { chat: result.chat, sync: result.hint };
237
+ }));
238
+ app.post("/v0/chats/:chatId/deleteChannel", authenticated(auth, async (request, _reply, userId) => {
239
+ const body = requestBody(request, ["reason"]);
240
+ const result = await repository.deleteChannel({
241
+ actorUserId: userId,
242
+ chatId: pathId(request, "chatId"),
243
+ reason: has(body, "reason")
244
+ ? (nullableTrimmedString(body, "reason", 500) ?? undefined)
245
+ : undefined,
246
+ });
247
+ await publishHints(request, pubsub, [result.hint], {
248
+ userIds: result.memberUserIds,
249
+ });
250
+ return { sync: result.hint };
251
+ }));
252
+ app.post("/v0/chats/:chatId/join", authenticated(auth, async (request, _reply, userId) => {
253
+ emptyBody(request);
254
+ const result = await repository.joinPublicChannel(userId, pathId(request, "chatId"));
255
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
256
+ return { chat: result.chat, sync: result.hint };
257
+ }));
258
+ app.post("/v0/chats/:chatId/leave", authenticated(auth, async (request, _reply, userId) => {
259
+ emptyBody(request);
260
+ const result = await repository.leaveChannel(userId, pathId(request, "chatId"));
261
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
262
+ return { sync: result.hint };
263
+ }));
264
+ app.post("/v0/chats/:chatId/addMember", authenticated(auth, async (request, _reply, userId) => {
265
+ const body = requestBody(request, ["userId", "role"]);
266
+ const addedUserId = idField(body, "userId");
267
+ const result = await repository.addChannelMember({
268
+ actorUserId: userId,
269
+ chatId: pathId(request, "chatId"),
270
+ userId: addedUserId,
271
+ role: optionalEnumField(body, "role", ["owner", "admin", "member"]),
272
+ });
273
+ await publishHints(request, pubsub, [result.hint], { userIds: [addedUserId] });
274
+ return { sync: result.hint };
275
+ }));
276
+ app.post("/v0/chats/:chatId/removeMember", authenticated(auth, async (request, _reply, userId) => {
277
+ const body = requestBody(request, ["userId"]);
278
+ const removedUserId = idField(body, "userId");
279
+ const result = await repository.removeChannelMember({
280
+ actorUserId: userId,
281
+ chatId: pathId(request, "chatId"),
282
+ userId: removedUserId,
283
+ });
284
+ await publishHints(request, pubsub, [result.hint], { userIds: [removedUserId] });
285
+ return { sync: result.hint };
286
+ }));
287
+ app.post("/v0/chats/:chatId/setMemberRole", authenticated(auth, async (request, _reply, userId) => {
288
+ const body = requestBody(request, ["userId", "role"]);
289
+ const targetUserId = idField(body, "userId");
290
+ const result = await repository.setChannelMemberRole({
291
+ actorUserId: userId,
292
+ chatId: pathId(request, "chatId"),
293
+ userId: targetUserId,
294
+ role: enumField(body, "role", ["owner", "admin", "member"]),
295
+ });
296
+ await publishHints(request, pubsub, [result.hint], { userIds: [targetUserId] });
297
+ return { sync: result.hint };
298
+ }));
299
+ app.post("/v0/chats/:chatId/setStar", authenticated(auth, async (request, _reply, userId) => {
300
+ const body = requestBody(request, ["starred"]);
301
+ const result = await repository.setStar(userId, pathId(request, "chatId"), booleanField(body, "starred"));
302
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
303
+ return { sync: result.hint };
304
+ }));
305
+ app.post("/v0/chats/:chatId/markRead", authenticated(auth, async (request, _reply, userId) => {
306
+ const body = requestBody(request, ["messageId"]);
307
+ const result = await repository.markChatRead({
308
+ actorUserId: userId,
309
+ chatId: pathId(request, "chatId"),
310
+ messageId: optionalIdField(body, "messageId"),
311
+ });
312
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
313
+ return { chat: result.chat, sync: result.hint };
314
+ }));
315
+ app.post("/v0/chats/:chatId/updateNotificationPreferences", authenticated(auth, async (request, _reply, userId) => {
316
+ const body = requestBody(request, [
317
+ "notificationLevel",
318
+ "mutedUntil",
319
+ "notifyThreadReplies",
320
+ "showMessagePreviews",
321
+ ]);
322
+ if (Object.keys(body).length === 0)
323
+ throw new InvalidRequest("At least one preference is required");
324
+ const result = await repository.setChatNotificationPreferences({
325
+ actorUserId: userId,
326
+ chatId: pathId(request, "chatId"),
327
+ notificationLevel: optionalEnumField(body, "notificationLevel", [
328
+ "all",
329
+ "mentions",
330
+ "none",
331
+ ]),
332
+ mutedUntil: optionalNullableDateField(body, "mutedUntil"),
333
+ notifyThreadReplies: has(body, "notifyThreadReplies")
334
+ ? booleanField(body, "notifyThreadReplies")
335
+ : undefined,
336
+ showMessagePreviews: has(body, "showMessagePreviews")
337
+ ? booleanField(body, "showMessagePreviews")
338
+ : undefined,
339
+ });
340
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
341
+ return { chat: result.chat, sync: result.hint };
342
+ }));
343
+ app.post("/v0/chats/reorderStarred", authenticated(auth, async (request, _reply, userId) => {
344
+ const body = requestBody(request, ["chatIds"]);
345
+ const result = await repository.reorderStarred(userId, idArrayField(body, "chatIds", 1_000, true));
346
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
347
+ return { sync: result.hint };
348
+ }));
349
+ app.post("/v0/chats/:chatId/sendMessage", authenticated(auth, async (request, reply, userId) => {
350
+ const chatId = pathId(request, "chatId");
351
+ const agentContext = await repository.getDirectAgentChatContext(userId, chatId);
352
+ if (agentContext && !agents)
353
+ return reply.code(503).send({
354
+ error: "agents_unavailable",
355
+ message: "AI agents are not enabled on this Happy (2) server.",
356
+ });
357
+ const body = requestBody(request, [
358
+ "text",
359
+ "attachmentFileIds",
360
+ "quotedMessageId",
361
+ "threadRootMessageId",
362
+ "expiryMode",
363
+ "selfDestructSeconds",
364
+ "afterReadScope",
365
+ "clientMutationId",
366
+ ]);
367
+ const attachmentFileIds = optionalIdArrayField(body, "attachmentFileIds", MAX_ATTACHMENTS);
368
+ const text = messageText(body, attachmentFileIds?.length ?? 0);
369
+ const agentTurn = agentContext && text
370
+ ? await agents.prepareTurn({ actorUserId: userId, chatId })
371
+ : undefined;
372
+ const selfDestructSeconds = optionalPositiveIntegerField(body, "selfDestructSeconds", MAX_SELF_DESTRUCT_SECONDS);
373
+ const result = await repository.sendMessage({
374
+ actorUserId: userId,
375
+ chatId,
376
+ text,
377
+ attachmentFileIds,
378
+ quotedMessageId: optionalIdField(body, "quotedMessageId"),
379
+ threadRootMessageId: optionalIdField(body, "threadRootMessageId"),
380
+ expiryMode: optionalEnumField(body, "expiryMode", [
381
+ "none",
382
+ "after_send",
383
+ "after_read",
384
+ ]) ?? (selfDestructSeconds ? "after_send" : undefined),
385
+ selfDestructSeconds,
386
+ afterReadScope: optionalEnumField(body, "afterReadScope", [
387
+ "any_reader",
388
+ "all_readers",
389
+ ]),
390
+ clientMutationId: optionalTokenField(body, "clientMutationId"),
391
+ agentTurn,
392
+ });
393
+ if (agentTurn)
394
+ agents.startTurn(chatId);
395
+ await publishHints(request, pubsub, [result.hint]);
396
+ return reply.code(201).send({ message: result.message, sync: result.hint });
397
+ }));
398
+ app.post("/v0/messages/:messageId/sendThreadMessage", authenticated(auth, async (request, reply, userId) => {
399
+ const selected = await repository.getMessage(userId, pathId(request, "messageId"));
400
+ const rootMessageId = selected.threadRootMessageId ?? selected.id;
401
+ const body = requestBody(request, [
402
+ "text",
403
+ "attachmentFileIds",
404
+ "quotedMessageId",
405
+ "expiryMode",
406
+ "selfDestructSeconds",
407
+ "afterReadScope",
408
+ "clientMutationId",
409
+ ]);
410
+ const attachmentFileIds = optionalIdArrayField(body, "attachmentFileIds", MAX_ATTACHMENTS);
411
+ const selfDestructSeconds = optionalPositiveIntegerField(body, "selfDestructSeconds", MAX_SELF_DESTRUCT_SECONDS);
412
+ const result = await repository.sendMessage({
413
+ actorUserId: userId,
414
+ chatId: selected.chatId,
415
+ text: messageText(body, attachmentFileIds?.length ?? 0),
416
+ attachmentFileIds,
417
+ quotedMessageId: optionalIdField(body, "quotedMessageId"),
418
+ threadRootMessageId: rootMessageId,
419
+ expiryMode: optionalEnumField(body, "expiryMode", [
420
+ "none",
421
+ "after_send",
422
+ "after_read",
423
+ ]) ?? (selfDestructSeconds ? "after_send" : undefined),
424
+ selfDestructSeconds,
425
+ afterReadScope: optionalEnumField(body, "afterReadScope", [
426
+ "any_reader",
427
+ "all_readers",
428
+ ]),
429
+ clientMutationId: optionalTokenField(body, "clientMutationId"),
430
+ });
431
+ await publishHints(request, pubsub, [result.hint]);
432
+ return reply.code(201).send({ message: result.message, sync: result.hint });
433
+ }));
434
+ app.post("/v0/messages/:messageId/updateThreadSubscription", authenticated(auth, async (request, _reply, userId) => {
435
+ const body = requestBody(request, ["subscribed", "notificationLevel"]);
436
+ const selected = await repository.getMessage(userId, pathId(request, "messageId"));
437
+ const rootMessageId = selected.threadRootMessageId ?? selected.id;
438
+ const result = await repository.setThreadSubscription({
439
+ actorUserId: userId,
440
+ threadRootMessageId: rootMessageId,
441
+ subscribed: booleanField(body, "subscribed"),
442
+ notificationLevel: optionalEnumField(body, "notificationLevel", [
443
+ "all",
444
+ "mentions",
445
+ "none",
446
+ ]),
447
+ });
448
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
449
+ return { sync: result.hint };
450
+ }));
451
+ app.post("/v0/messages/:messageId/markThreadRead", authenticated(auth, async (request, _reply, userId) => {
452
+ const body = requestBody(request, ["throughMessageId"]);
453
+ const selected = await repository.getMessage(userId, pathId(request, "messageId"));
454
+ const rootMessageId = selected.threadRootMessageId ?? selected.id;
455
+ const result = await repository.markThreadRead({
456
+ actorUserId: userId,
457
+ threadRootMessageId: rootMessageId,
458
+ messageId: optionalIdField(body, "throughMessageId"),
459
+ });
460
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
461
+ return { sync: result.hint };
462
+ }));
463
+ app.post("/v0/messages/:messageId/deleteMessage", authenticated(auth, async (request, _reply, userId) => {
464
+ emptyBody(request);
465
+ const result = await repository.deleteMessage(userId, pathId(request, "messageId"));
466
+ await publishHints(request, pubsub, [result.hint]);
467
+ return { message: result.message, sync: result.hint };
468
+ }));
469
+ app.post("/v0/messages/:messageId/editMessage", authenticated(auth, async (request, _reply, userId) => {
470
+ const body = requestBody(request, ["text", "reason", "expectedRevision"]);
471
+ const result = await repository.editMessage({
472
+ actorUserId: userId,
473
+ messageId: pathId(request, "messageId"),
474
+ text: messageText(body, 0),
475
+ reason: has(body, "reason")
476
+ ? (nullableTrimmedString(body, "reason", 500) ?? undefined)
477
+ : undefined,
478
+ expectedRevision: optionalPositiveIntegerField(body, "expectedRevision", Number.MAX_SAFE_INTEGER),
479
+ });
480
+ await publishHints(request, pubsub, [result.hint]);
481
+ return { message: result.message, sync: result.hint };
482
+ }));
483
+ app.get("/v0/messages/:messageId/revisions", authenticated(auth, async (request, _reply, userId) => {
484
+ emptyQuery(request);
485
+ return {
486
+ revisions: await repository.listMessageRevisions(userId, pathId(request, "messageId")),
487
+ };
488
+ }));
489
+ app.post("/v0/messages/:messageId/forwardMessage", authenticated(auth, async (request, reply, userId) => {
490
+ const body = requestBody(request, ["targetChatIds", "clientMutationId"]);
491
+ const result = await repository.forwardMessage({
492
+ actorUserId: userId,
493
+ messageId: pathId(request, "messageId"),
494
+ targetChatIds: idArrayField(body, "targetChatIds", MAX_FORWARD_TARGETS, false),
495
+ clientMutationId: optionalTokenField(body, "clientMutationId"),
496
+ });
497
+ await publishHints(request, pubsub, result.hints);
498
+ return reply.code(201).send({ messages: result.messages, sync: result.hints });
499
+ }));
500
+ app.post("/v0/messages/:messageId/addReaction", authenticated(auth, async (request, _reply, userId) => {
501
+ const reaction = reactionBody(request);
502
+ const result = await repository.setReaction({
503
+ actorUserId: userId,
504
+ messageId: pathId(request, "messageId"),
505
+ ...reaction,
506
+ active: true,
507
+ });
508
+ await publishHints(request, pubsub, [result.hint]);
509
+ return { message: result.message, sync: result.hint };
510
+ }));
511
+ app.post("/v0/messages/:messageId/removeReaction", authenticated(auth, async (request, _reply, userId) => {
512
+ const reaction = reactionBody(request);
513
+ const result = await repository.setReaction({
514
+ actorUserId: userId,
515
+ messageId: pathId(request, "messageId"),
516
+ ...reaction,
517
+ active: false,
518
+ });
519
+ await publishHints(request, pubsub, [result.hint]);
520
+ return { message: result.message, sync: result.hint };
521
+ }));
522
+ app.post("/v0/messages/:messageId/pinMessage", authenticated(auth, async (request, _reply, userId) => {
523
+ emptyBody(request);
524
+ const result = await repository.setMessagePinned({
525
+ actorUserId: userId,
526
+ messageId: pathId(request, "messageId"),
527
+ pinned: true,
528
+ });
529
+ await publishHints(request, pubsub, [result.hint]);
530
+ return { sync: result.hint };
531
+ }));
532
+ app.post("/v0/messages/:messageId/unpinMessage", authenticated(auth, async (request, _reply, userId) => {
533
+ emptyBody(request);
534
+ const result = await repository.setMessagePinned({
535
+ actorUserId: userId,
536
+ messageId: pathId(request, "messageId"),
537
+ pinned: false,
538
+ });
539
+ await publishHints(request, pubsub, [result.hint]);
540
+ return { sync: result.hint };
541
+ }));
542
+ app.get("/v0/chats/:chatId/pins", authenticated(auth, async (request, _reply, userId) => {
543
+ emptyQuery(request);
544
+ return {
545
+ pins: await repository.listChatPins(userId, pathId(request, "chatId")),
546
+ };
547
+ }));
548
+ app.get("/v0/chats/:chatId/bookmarks", authenticated(auth, async (request, _reply, userId) => {
549
+ emptyQuery(request);
550
+ return {
551
+ bookmarks: await repository.listChatBookmarks(userId, pathId(request, "chatId")),
552
+ };
553
+ }));
554
+ app.post("/v0/chats/:chatId/createBookmark", authenticated(auth, async (request, reply, userId) => {
555
+ const body = requestBody(request, [
556
+ "kind",
557
+ "title",
558
+ "url",
559
+ "messageId",
560
+ "fileId",
561
+ "emoji",
562
+ ]);
563
+ const kind = enumField(body, "kind", ["link", "message", "file"]);
564
+ const url = has(body, "url") ? httpUrlField(body, "url") : undefined;
565
+ const messageId = optionalIdField(body, "messageId");
566
+ const fileId = optionalIdField(body, "fileId");
567
+ if ((kind === "link" && (!url || messageId || fileId)) ||
568
+ (kind === "message" && (!messageId || url || fileId)) ||
569
+ (kind === "file" && (!fileId || url || messageId)))
570
+ throw new InvalidRequest("Bookmark target does not match its kind");
571
+ const result = await repository.createChatBookmark({
572
+ actorUserId: userId,
573
+ chatId: pathId(request, "chatId"),
574
+ kind,
575
+ title: trimmedString(body, "title", 200),
576
+ url,
577
+ messageId,
578
+ fileId,
579
+ emoji: has(body, "emoji")
580
+ ? (nullableTrimmedString(body, "emoji", 32) ?? undefined)
581
+ : undefined,
582
+ });
583
+ await publishHints(request, pubsub, [result.hint]);
584
+ return reply.code(201).send({ bookmark: result.bookmark, sync: result.hint });
585
+ }));
586
+ app.post("/v0/chats/:chatId/deleteBookmark", authenticated(auth, async (request, _reply, userId) => {
587
+ const body = requestBody(request, ["bookmarkId"]);
588
+ const result = await repository.deleteChatBookmark({
589
+ actorUserId: userId,
590
+ chatId: pathId(request, "chatId"),
591
+ bookmarkId: idField(body, "bookmarkId"),
592
+ });
593
+ await publishHints(request, pubsub, [result.hint]);
594
+ return { sync: result.hint };
595
+ }));
596
+ app.get("/v0/contacts", authenticated(auth, async (request) => {
597
+ emptyQuery(request);
598
+ const users = await repository.listContacts();
599
+ return {
600
+ users,
601
+ presence: await pubsub.getPresenceSnapshot(users.map((user) => user.id)),
602
+ statuses: await repository.listPresenceSettings(users.map((user) => user.id)),
603
+ };
604
+ }));
605
+ app.get("/v0/directory/users", authenticated(auth, async (request) => {
606
+ emptyQuery(request);
607
+ const users = await repository.listContacts();
608
+ return {
609
+ users,
610
+ presence: await pubsub.getPresenceSnapshot(users.map((user) => user.id)),
611
+ statuses: await repository.listPresenceSettings(users.map((user) => user.id)),
612
+ };
613
+ }));
614
+ app.get("/v0/directory", authenticated(auth, async (request, _reply, userId) => {
615
+ emptyQuery(request);
616
+ const [users, channels, customEmoji, server] = await Promise.all([
617
+ repository.listContacts(),
618
+ repository.listDirectoryChannels(userId),
619
+ repository.listCustomEmoji(),
620
+ repository.getServerProfile(),
621
+ ]);
622
+ return {
623
+ server,
624
+ users,
625
+ channels,
626
+ customEmoji,
627
+ presence: await pubsub.getPresenceSnapshot(users.map((user) => user.id)),
628
+ statuses: await repository.listPresenceSettings(users.map((user) => user.id)),
629
+ };
630
+ }));
631
+ app.get("/v0/presence", authenticated(auth, async (request) => {
632
+ emptyQuery(request);
633
+ const users = await repository.listContacts();
634
+ return {
635
+ presence: await pubsub.getPresenceSnapshot(users.map((user) => user.id)),
636
+ statuses: await repository.listPresenceSettings(users.map((user) => user.id)),
637
+ };
638
+ }));
639
+ app.post("/v0/me/updateStatus", authenticated(auth, async (request, _reply, userId) => {
640
+ const body = requestBody(request, [
641
+ "availability",
642
+ "customStatusText",
643
+ "customStatusEmoji",
644
+ "statusExpiresAt",
645
+ "dndUntil",
646
+ ]);
647
+ if (Object.keys(body).length === 0)
648
+ throw new InvalidRequest("At least one presence field is required");
649
+ const result = await repository.updatePresenceSettings({
650
+ actorUserId: userId,
651
+ availability: optionalEnumField(body, "availability", [
652
+ "automatic",
653
+ "online",
654
+ "away",
655
+ "dnd",
656
+ ]),
657
+ customStatusText: nullableStringUpdate(body, "customStatusText", 200),
658
+ customStatusEmoji: nullableStringUpdate(body, "customStatusEmoji", 32),
659
+ statusExpiresAt: optionalNullableDateField(body, "statusExpiresAt"),
660
+ dndUntil: optionalNullableDateField(body, "dndUntil"),
661
+ });
662
+ await publishHints(request, pubsub, [result.hint], { server: true });
663
+ return { status: result.presence, sync: result.hint };
664
+ }));
665
+ app.get("/v0/me/notificationPreferences", authenticated(auth, async (request, _reply, userId) => {
666
+ emptyQuery(request);
667
+ return { preferences: await repository.getNotificationPreferences(userId) };
668
+ }));
669
+ app.post("/v0/me/updateNotificationPreferences", authenticated(auth, async (request, _reply, userId) => {
670
+ const body = requestBody(request, [
671
+ "directMessages",
672
+ "mentions",
673
+ "threadReplies",
674
+ "reactions",
675
+ "calls",
676
+ "emailNotifications",
677
+ "desktopNotifications",
678
+ "dndStartMinutes",
679
+ "dndEndMinutes",
680
+ "timezone",
681
+ ]);
682
+ if (Object.keys(body).length === 0)
683
+ throw new InvalidRequest("At least one notification preference is required");
684
+ const result = await repository.updateNotificationPreferences({
685
+ actorUserId: userId,
686
+ directMessages: optionalEnumField(body, "directMessages", ["all", "none"]),
687
+ mentions: optionalEnumField(body, "mentions", ["all", "none"]),
688
+ threadReplies: optionalEnumField(body, "threadReplies", [
689
+ "all",
690
+ "mentions",
691
+ "none",
692
+ ]),
693
+ reactions: optionalEnumField(body, "reactions", ["all", "none"]),
694
+ calls: optionalEnumField(body, "calls", ["all", "none"]),
695
+ emailNotifications: has(body, "emailNotifications")
696
+ ? booleanField(body, "emailNotifications")
697
+ : undefined,
698
+ desktopNotifications: has(body, "desktopNotifications")
699
+ ? booleanField(body, "desktopNotifications")
700
+ : undefined,
701
+ dndStartMinutes: optionalNullableMinute(body, "dndStartMinutes"),
702
+ dndEndMinutes: optionalNullableMinute(body, "dndEndMinutes"),
703
+ timezone: nullableStringUpdate(body, "timezone", 100),
704
+ });
705
+ await publishHints(request, pubsub, [result.hint], { userIds: [userId] });
706
+ return { preferences: result.preferences, sync: result.hint };
707
+ }));
708
+ app.get("/v0/directory/channels", authenticated(auth, async (request, _reply, userId) => {
709
+ emptyQuery(request);
710
+ return { channels: await repository.listDirectoryChannels(userId) };
711
+ }));
712
+ app.get("/v0/search", authenticated(auth, async (request, _reply, userId) => {
713
+ const query = requestQuery(request, ["q", "cursor", "limit"]);
714
+ const search = queryString(query, "q", 200);
715
+ if (search.trim().length === 0)
716
+ throw new InvalidRequest("Search query is required");
717
+ return repository.searchPage({
718
+ userId,
719
+ query: search.trim(),
720
+ cursor: optionalQueryString(query, "cursor", 1_024),
721
+ limit: queryLimit(query, "limit", 20, 50),
722
+ });
723
+ }));
724
+ app.get("/v0/files", authenticated(auth, async (request, _reply, userId) => {
725
+ const query = requestQuery(request, ["kind", "before", "limit"]);
726
+ return repository.listFiles({
727
+ userId,
728
+ kind: optionalQueryEnum(query, "kind", ["file", "photo", "video", "gif"]),
729
+ before: optionalQueryString(query, "before", 64),
730
+ limit: queryLimit(query, "limit", 50, 100),
731
+ });
732
+ }));
733
+ app.get("/v0/calls", authenticated(auth, async (request, _reply, userId) => {
734
+ const query = requestQuery(request, ["chatId", "limit"]);
735
+ return {
736
+ calls: await repository.listCalls({
737
+ userId,
738
+ chatId: optionalQueryString(query, "chatId", MAX_ID_LENGTH),
739
+ limit: queryLimit(query, "limit", 50, 100),
740
+ }),
741
+ };
742
+ }));
743
+ app.get("/v0/calls/:callId", authenticated(auth, async (request, _reply, userId) => {
744
+ emptyQuery(request);
745
+ return { call: await repository.getCall(userId, pathId(request, "callId")) };
746
+ }));
747
+ app.post("/v0/chats/:chatId/createCall", authenticated(auth, async (request, reply, userId) => {
748
+ const body = requestBody(request, ["kind", "invitedUserIds"]);
749
+ const result = await repository.createCall({
750
+ actorUserId: userId,
751
+ chatId: pathId(request, "chatId"),
752
+ kind: enumField(body, "kind", ["audio", "video"]),
753
+ invitedUserIds: has(body, "invitedUserIds")
754
+ ? idArrayField(body, "invitedUserIds", 50, false)
755
+ : undefined,
756
+ });
757
+ await publishHints(request, pubsub, [result.hint], {
758
+ userIds: [userId, ...result.invitedUserIds],
759
+ });
760
+ return reply.code(201).send({ call: result.call, sync: result.hint });
761
+ }));
762
+ for (const [path, action] of [
763
+ ["joinCall", "join"],
764
+ ["declineCall", "decline"],
765
+ ["leaveCall", "leave"],
766
+ ])
767
+ app.post(`/v0/calls/:callId/${path}`, authenticated(auth, async (request, _reply, userId) => {
768
+ emptyBody(request);
769
+ const result = await repository.updateCallParticipation({
770
+ actorUserId: userId,
771
+ callId: pathId(request, "callId"),
772
+ action,
773
+ });
774
+ await publishHints(request, pubsub, [result.hint], {
775
+ userIds: result.call.participants.map((participant) => participant.userId),
776
+ });
777
+ return { call: result.call, sync: result.hint };
778
+ }));
779
+ app.post("/v0/calls/:callId/endCall", authenticated(auth, async (request, _reply, userId) => {
780
+ const body = requestBody(request, ["reason"]);
781
+ const result = await repository.endCall({
782
+ actorUserId: userId,
783
+ callId: pathId(request, "callId"),
784
+ reason: has(body, "reason")
785
+ ? (nullableTrimmedString(body, "reason", 200) ?? undefined)
786
+ : undefined,
787
+ });
788
+ await publishHints(request, pubsub, [result.hint], {
789
+ userIds: result.call.participants.map((participant) => participant.userId),
790
+ });
791
+ return { call: result.call, sync: result.hint };
792
+ }));
793
+ app.get("/v0/customEmoji", authenticated(auth, async (request) => {
794
+ emptyQuery(request);
795
+ return { emoji: await repository.listCustomEmoji() };
796
+ }));
797
+ app.post("/v0/customEmoji/createCustomEmoji", authenticated(auth, async (request, reply, userId) => {
798
+ const body = requestBody(request, ["name", "fileId"]);
799
+ const result = await repository.createCustomEmoji({
800
+ actorUserId: userId,
801
+ name: emojiName(body),
802
+ fileId: idField(body, "fileId"),
803
+ });
804
+ await publishHints(request, pubsub, [result.hint], { server: true });
805
+ return reply.code(201).send({ emoji: result.emoji, sync: result.hint });
806
+ }));
807
+ app.post("/v0/customEmoji/:emojiId/deleteCustomEmoji", authenticated(auth, async (request, _reply, userId) => {
808
+ emptyBody(request);
809
+ const result = await repository.deleteCustomEmoji(userId, pathId(request, "emojiId"));
810
+ await publishHints(request, pubsub, [result.hint], { server: true });
811
+ return { sync: result.hint };
812
+ }));
813
+ app.get("/v0/server", authenticated(auth, async (request) => {
814
+ emptyQuery(request);
815
+ return { server: await repository.getServerProfile() };
816
+ }));
817
+ app.get("/v0/admin/users", authenticated(auth, async (request, _reply, userId) => {
818
+ emptyQuery(request);
819
+ return { users: await repository.listAdminUsers(userId) };
820
+ }));
821
+ app.post("/v0/admin/users/:userId/updateUser", authenticated(auth, async (request, _reply, actorUserId) => {
822
+ const body = requestBody(request, ["title", "role"]);
823
+ if (!has(body, "title") && !has(body, "role"))
824
+ throw new InvalidRequest("At least one user field is required");
825
+ const targetUserId = pathId(request, "userId");
826
+ const title = has(body, "title")
827
+ ? body.title === null
828
+ ? null
829
+ : (nullableTrimmedString(body, "title", 200) ?? null)
830
+ : undefined;
831
+ const result = await repository.updateUserAdministration({
832
+ actorUserId,
833
+ userId: targetUserId,
834
+ title,
835
+ role: optionalEnumField(body, "role", ["member", "admin"]),
836
+ });
837
+ await publishHints(request, pubsub, [result.hint], {
838
+ server: true,
839
+ userIds: [targetUserId],
840
+ });
841
+ return { user: result.user, sync: result.hint };
842
+ }));
843
+ app.post("/v0/admin/users/:userId/banUser", authenticated(auth, async (request, _reply, actorUserId) => {
844
+ emptyBody(request);
845
+ const targetUserId = pathId(request, "userId");
846
+ const result = await repository.setUserBanned({
847
+ actorUserId,
848
+ userId: targetUserId,
849
+ banned: true,
850
+ });
851
+ await publishHints(request, pubsub, [result.hint], {
852
+ server: true,
853
+ userIds: [targetUserId],
854
+ });
855
+ return { sync: result.hint };
856
+ }));
857
+ app.post("/v0/admin/users/:userId/unbanUser", authenticated(auth, async (request, _reply, actorUserId) => {
858
+ emptyBody(request);
859
+ const targetUserId = pathId(request, "userId");
860
+ const result = await repository.setUserBanned({
861
+ actorUserId,
862
+ userId: targetUserId,
863
+ banned: false,
864
+ });
865
+ await publishHints(request, pubsub, [result.hint], {
866
+ server: true,
867
+ userIds: [targetUserId],
868
+ });
869
+ return { sync: result.hint };
870
+ }));
871
+ app.post("/v0/admin/users/:userId/deleteUser", authenticated(auth, async (request, _reply, actorUserId) => {
872
+ emptyBody(request);
873
+ const targetUserId = pathId(request, "userId");
874
+ const result = await repository.deleteUser({
875
+ actorUserId,
876
+ userId: targetUserId,
877
+ });
878
+ await publishHints(request, pubsub, [result.hint], {
879
+ server: true,
880
+ userIds: [targetUserId],
881
+ });
882
+ return { sync: result.hint };
883
+ }));
884
+ app.post("/v0/admin/updateServer", authenticated(auth, async (request, _reply, actorUserId) => {
885
+ const body = requestBody(request, [
886
+ "name",
887
+ "title",
888
+ "photoFileId",
889
+ "defaultRetentionMode",
890
+ "defaultRetentionSeconds",
891
+ ]);
892
+ if (Object.keys(body).length === 0)
893
+ throw new InvalidRequest("At least one server field is required");
894
+ const result = await repository.updateServerProfile({
895
+ actorUserId,
896
+ name: has(body, "name") ? trimmedString(body, "name", 100) : undefined,
897
+ title: has(body, "title")
898
+ ? (nullableTrimmedString(body, "title", 200) ?? null)
899
+ : undefined,
900
+ photoFileId: has(body, "photoFileId")
901
+ ? body.photoFileId === null
902
+ ? null
903
+ : id(body.photoFileId, "photoFileId")
904
+ : undefined,
905
+ defaultRetentionMode: optionalEnumField(body, "defaultRetentionMode", [
906
+ "forever",
907
+ "duration",
908
+ ]),
909
+ defaultRetentionSeconds: optionalNullablePositiveIntegerField(body, "defaultRetentionSeconds", MAX_SELF_DESTRUCT_SECONDS * 100),
910
+ });
911
+ await publishHints(request, pubsub, [result.hint], { server: true });
912
+ return { server: result.server, sync: result.hint };
913
+ }));
914
+ app.post("/v0/admin/sendAutomatedMessage", authenticated(auth, async (request, reply, actorUserId) => {
915
+ const body = requestBody(request, [
916
+ "chatId",
917
+ "text",
918
+ "attachmentFileIds",
919
+ "botId",
920
+ "clientMutationId",
921
+ ]);
922
+ const attachmentFileIds = optionalIdArrayField(body, "attachmentFileIds", MAX_ATTACHMENTS);
923
+ const result = await repository.sendAutomatedMessage({
924
+ actorUserId,
925
+ chatId: idField(body, "chatId"),
926
+ text: messageText(body, attachmentFileIds?.length ?? 0),
927
+ attachmentFileIds,
928
+ botId: optionalIdField(body, "botId"),
929
+ clientMutationId: optionalTokenField(body, "clientMutationId"),
930
+ });
931
+ await publishHints(request, pubsub, [result.hint]);
932
+ return reply.code(201).send({ message: result.message, sync: result.hint });
933
+ }));
934
+ }
935
+ function authenticated(auth, handler) {
936
+ return async (request, reply) => {
937
+ const current = await auth.authenticate(request);
938
+ if (!current)
939
+ return reply.code(401).send({ error: "unauthorized" });
940
+ try {
941
+ return await handler(request, reply, current.user.id);
942
+ }
943
+ catch (error) {
944
+ if (error instanceof InvalidRequest)
945
+ return reply.code(400).send({ error: "invalid_request", message: error.message });
946
+ if (error instanceof CollaborationError) {
947
+ const status = collaborationStatus(error.code);
948
+ return reply.code(status).send({ error: error.code, message: error.message });
949
+ }
950
+ throw error;
951
+ }
952
+ };
953
+ }
954
+ function collaborationStatus(code) {
955
+ switch (code) {
956
+ case "invalid":
957
+ return 400;
958
+ case "forbidden":
959
+ return 403;
960
+ case "not_found":
961
+ return 404;
962
+ case "conflict":
963
+ case "future_state":
964
+ case "generation_mismatch":
965
+ return 409;
966
+ }
967
+ }
968
+ async function publishHints(request, pubsub, hints, audience = {}) {
969
+ const publications = [];
970
+ for (const hint of hints) {
971
+ const event = { type: "sync", ...hint };
972
+ const topics = new Set();
973
+ for (const chat of hint.chats)
974
+ topics.add(realtimeTopics.chat(chat.chatId));
975
+ for (const userId of audience.userIds ?? [])
976
+ topics.add(realtimeTopics.user(userId));
977
+ if (audience.server || hint.areas.some((area) => area !== "preferences"))
978
+ topics.add(realtimeTopics.server);
979
+ for (const topic of topics)
980
+ publications.push({ topic, event });
981
+ }
982
+ const results = await Promise.allSettled(publications.map(({ topic, event }) => pubsub.publish(topic, event)));
983
+ for (const result of results) {
984
+ if (result.status === "rejected")
985
+ request.log.warn({ err: result.reason }, "Could not publish realtime sync hint");
986
+ }
987
+ }
988
+ function requestBody(request, allowed) {
989
+ const body = record(request.body, "Request body");
990
+ onlyKeys(body, allowed, "request body");
991
+ return body;
992
+ }
993
+ function agentUsername(body) {
994
+ const username = trimmedString(body, "username", 32).toLowerCase();
995
+ if (!/^[a-z0-9][a-z0-9_.-]{1,31}$/u.test(username))
996
+ throw new InvalidRequest("username must contain 2-32 safe characters");
997
+ return username;
998
+ }
999
+ function emptyBody(request) {
1000
+ if (request.body === undefined || request.body === null)
1001
+ return;
1002
+ const body = record(request.body, "Request body");
1003
+ onlyKeys(body, [], "request body");
1004
+ }
1005
+ function requestQuery(request, allowed) {
1006
+ const query = record(request.query, "Query");
1007
+ onlyKeys(query, allowed, "query");
1008
+ return query;
1009
+ }
1010
+ function emptyQuery(request) {
1011
+ requestQuery(request, []);
1012
+ }
1013
+ function pathId(request, key) {
1014
+ const params = record(request.params, "Path parameters");
1015
+ return id(params[key], key);
1016
+ }
1017
+ function record(value, name) {
1018
+ if (!value || typeof value !== "object" || Array.isArray(value))
1019
+ throw new InvalidRequest(`${name} must be an object`);
1020
+ return value;
1021
+ }
1022
+ function onlyKeys(value, allowed, name) {
1023
+ const unexpected = Object.keys(value).filter((key) => !allowed.includes(key));
1024
+ if (unexpected.length > 0)
1025
+ throw new InvalidRequest(`Unexpected ${name} field: ${unexpected[0]}`);
1026
+ }
1027
+ function requireField(value, key) {
1028
+ if (!has(value, key))
1029
+ throw new InvalidRequest(`${key} is required`);
1030
+ }
1031
+ function has(value, key) {
1032
+ return Object.prototype.hasOwnProperty.call(value, key);
1033
+ }
1034
+ function idField(body, key) {
1035
+ requireField(body, key);
1036
+ return id(body[key], key);
1037
+ }
1038
+ function optionalIdField(body, key) {
1039
+ return has(body, key) ? id(body[key], key) : undefined;
1040
+ }
1041
+ function id(value, name) {
1042
+ if (typeof value !== "string" ||
1043
+ value.length === 0 ||
1044
+ value.length > MAX_ID_LENGTH ||
1045
+ value.trim() !== value ||
1046
+ hasControlCharacters(value))
1047
+ throw new InvalidRequest(`${name} must be a valid identifier`);
1048
+ return value;
1049
+ }
1050
+ function idArrayField(body, key, maximum, allowEmpty) {
1051
+ requireField(body, key);
1052
+ return idArray(body[key], key, maximum, allowEmpty);
1053
+ }
1054
+ function optionalIdArrayField(body, key, maximum) {
1055
+ return has(body, key) ? idArray(body[key], key, maximum, true) : undefined;
1056
+ }
1057
+ function idArray(value, name, maximum, allowEmpty) {
1058
+ if (!Array.isArray(value) || (!allowEmpty && value.length === 0) || value.length > maximum)
1059
+ throw new InvalidRequest(`${name} must contain ${allowEmpty ? "at most" : "between 1 and"} ${maximum} identifiers`);
1060
+ const result = value.map((entry) => id(entry, name));
1061
+ if (new Set(result).size !== result.length)
1062
+ throw new InvalidRequest(`${name} must not contain duplicates`);
1063
+ return result;
1064
+ }
1065
+ function trimmedString(body, key, maximum) {
1066
+ requireField(body, key);
1067
+ const value = body[key];
1068
+ if (typeof value !== "string")
1069
+ throw new InvalidRequest(`${key} must be a string`);
1070
+ const result = value.trim();
1071
+ if (result.length === 0 || result.length > maximum || hasControlCharacters(result))
1072
+ throw new InvalidRequest(`${key} must be between 1 and ${maximum} characters`);
1073
+ return result;
1074
+ }
1075
+ function nullableTrimmedString(body, key, maximum) {
1076
+ if (!has(body, key) || body[key] === null)
1077
+ return undefined;
1078
+ const value = body[key];
1079
+ if (typeof value !== "string")
1080
+ throw new InvalidRequest(`${key} must be a string or null`);
1081
+ const result = value.trim();
1082
+ if (result.length === 0)
1083
+ return undefined;
1084
+ if (result.length > maximum || hasControlCharacters(result))
1085
+ throw new InvalidRequest(`${key} must be at most ${maximum} characters`);
1086
+ return result;
1087
+ }
1088
+ function enumField(body, key, values) {
1089
+ requireField(body, key);
1090
+ const value = body[key];
1091
+ if (typeof value !== "string" || !values.includes(value))
1092
+ throw new InvalidRequest(`${key} must be one of: ${values.join(", ")}`);
1093
+ return value;
1094
+ }
1095
+ function optionalEnumField(body, key, values) {
1096
+ return has(body, key) ? enumField(body, key, values) : undefined;
1097
+ }
1098
+ function booleanField(body, key) {
1099
+ requireField(body, key);
1100
+ if (typeof body[key] !== "boolean")
1101
+ throw new InvalidRequest(`${key} must be a boolean`);
1102
+ return body[key];
1103
+ }
1104
+ function channelSlug(body) {
1105
+ const slug = trimmedString(body, "slug", 64).toLowerCase();
1106
+ if (!/^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/.test(slug) && !/^[a-z0-9]$/.test(slug))
1107
+ throw new InvalidRequest("slug may contain lowercase letters, numbers, and inner hyphens");
1108
+ return slug;
1109
+ }
1110
+ function emojiName(body) {
1111
+ const name = trimmedString(body, "name", 32).toLowerCase();
1112
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(name))
1113
+ throw new InvalidRequest("name may contain lowercase letters, numbers, underscores, and hyphens");
1114
+ return name;
1115
+ }
1116
+ function messageText(body, attachmentCount) {
1117
+ if (!has(body, "text")) {
1118
+ if (attachmentCount > 0)
1119
+ return "";
1120
+ throw new InvalidRequest("A message requires text or an attachment");
1121
+ }
1122
+ if (typeof body.text !== "string")
1123
+ throw new InvalidRequest("text must be a string");
1124
+ if (body.text.length > MAX_MESSAGE_LENGTH)
1125
+ throw new InvalidRequest(`text must be at most ${MAX_MESSAGE_LENGTH} characters`);
1126
+ if (hasControlCharacters(body.text, true))
1127
+ throw new InvalidRequest("text contains unsupported control characters");
1128
+ if (body.text.trim().length === 0 && attachmentCount === 0)
1129
+ throw new InvalidRequest("A message requires text or an attachment");
1130
+ return body.text;
1131
+ }
1132
+ function optionalTokenField(body, key) {
1133
+ if (!has(body, key))
1134
+ return undefined;
1135
+ const value = body[key];
1136
+ if (typeof value !== "string" ||
1137
+ value.length === 0 ||
1138
+ value.length > 128 ||
1139
+ value.trim() !== value ||
1140
+ hasControlCharacters(value))
1141
+ throw new InvalidRequest(`${key} must be a non-empty token of at most 128 characters`);
1142
+ return value;
1143
+ }
1144
+ function optionalPositiveIntegerField(body, key, maximum) {
1145
+ if (!has(body, key))
1146
+ return undefined;
1147
+ const value = body[key];
1148
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1 || value > maximum)
1149
+ throw new InvalidRequest(`${key} must be an integer between 1 and ${maximum}`);
1150
+ return value;
1151
+ }
1152
+ function optionalNullablePositiveIntegerField(body, key, maximum) {
1153
+ if (!has(body, key))
1154
+ return undefined;
1155
+ if (body[key] === null)
1156
+ return null;
1157
+ return optionalPositiveIntegerField(body, key, maximum);
1158
+ }
1159
+ function reactionBody(request) {
1160
+ const body = requestBody(request, ["emoji", "customEmojiId"]);
1161
+ const hasEmoji = has(body, "emoji");
1162
+ const hasCustom = has(body, "customEmojiId");
1163
+ if (hasEmoji === hasCustom)
1164
+ throw new InvalidRequest("Exactly one of emoji or customEmojiId is required");
1165
+ if (hasCustom)
1166
+ return { customEmojiId: id(body.customEmojiId, "customEmojiId") };
1167
+ if (typeof body.emoji !== "string" ||
1168
+ body.emoji.length === 0 ||
1169
+ body.emoji.length > 32 ||
1170
+ body.emoji.trim() !== body.emoji ||
1171
+ hasControlCharacters(body.emoji))
1172
+ throw new InvalidRequest("emoji must be between 1 and 32 characters");
1173
+ return { emoji: body.emoji };
1174
+ }
1175
+ function messagePage(request, allowed) {
1176
+ const query = requestQuery(request, allowed);
1177
+ const beforeSequence = optionalPositiveQueryInteger(query, "beforeSequence");
1178
+ const afterSequence = optionalPositiveQueryInteger(query, "afterSequence");
1179
+ if (beforeSequence !== undefined && afterSequence !== undefined)
1180
+ throw new InvalidRequest("beforeSequence and afterSequence are mutually exclusive");
1181
+ return {
1182
+ beforeSequence,
1183
+ afterSequence,
1184
+ limit: queryLimit(query, "limit", 100, 200),
1185
+ };
1186
+ }
1187
+ function queryLimit(query, key, fallback, maximum) {
1188
+ if (!has(query, key))
1189
+ return fallback;
1190
+ const value = query[key];
1191
+ if (typeof value !== "string" || !/^[1-9]\d*$/.test(value))
1192
+ throw new InvalidRequest(`${key} must be a positive integer`);
1193
+ const parsed = Number(value);
1194
+ if (!Number.isSafeInteger(parsed) || parsed > maximum)
1195
+ throw new InvalidRequest(`${key} must not exceed ${maximum}`);
1196
+ return parsed;
1197
+ }
1198
+ function optionalPositiveQueryInteger(query, key) {
1199
+ if (!has(query, key))
1200
+ return undefined;
1201
+ const value = query[key];
1202
+ if (typeof value !== "string" || !/^[1-9]\d*$/.test(value))
1203
+ throw new InvalidRequest(`${key} must be a positive integer`);
1204
+ const parsed = Number(value);
1205
+ if (!Number.isSafeInteger(parsed))
1206
+ throw new InvalidRequest(`${key} is too large`);
1207
+ return parsed;
1208
+ }
1209
+ function queryString(query, key, maximum) {
1210
+ if (!has(query, key) || typeof query[key] !== "string")
1211
+ throw new InvalidRequest(`${key} is required`);
1212
+ if (query[key].length > maximum || hasControlCharacters(query[key]))
1213
+ throw new InvalidRequest(`${key} must be at most ${maximum} characters`);
1214
+ return query[key];
1215
+ }
1216
+ function optionalQueryString(query, key, maximum) {
1217
+ if (!has(query, key))
1218
+ return undefined;
1219
+ return queryString(query, key, maximum);
1220
+ }
1221
+ function optionalQueryEnum(query, key, values) {
1222
+ if (!has(query, key))
1223
+ return undefined;
1224
+ const value = query[key];
1225
+ if (typeof value !== "string" || !values.includes(value))
1226
+ throw new InvalidRequest(`${key} must be one of: ${values.join(", ")}`);
1227
+ return value;
1228
+ }
1229
+ function optionalQueryBoolean(query, key) {
1230
+ if (!has(query, key))
1231
+ return undefined;
1232
+ if (query[key] !== "true" && query[key] !== "false")
1233
+ throw new InvalidRequest(`${key} must be true or false`);
1234
+ return query[key] === "true";
1235
+ }
1236
+ function optionalNullableDateField(body, key) {
1237
+ if (!has(body, key))
1238
+ return undefined;
1239
+ if (body[key] === null)
1240
+ return null;
1241
+ if (typeof body[key] !== "string" || !Number.isFinite(Date.parse(body[key])))
1242
+ throw new InvalidRequest(`${key} must be an ISO date-time or null`);
1243
+ return new Date(body[key]).toISOString();
1244
+ }
1245
+ function nullableStringUpdate(body, key, maximum) {
1246
+ if (!has(body, key))
1247
+ return undefined;
1248
+ if (body[key] === null)
1249
+ return null;
1250
+ return nullableTrimmedString(body, key, maximum) ?? null;
1251
+ }
1252
+ function optionalNullableMinute(body, key) {
1253
+ if (!has(body, key))
1254
+ return undefined;
1255
+ if (body[key] === null)
1256
+ return null;
1257
+ const value = body[key];
1258
+ if (!Number.isSafeInteger(value) || value < 0 || value >= 1_440)
1259
+ throw new InvalidRequest(`${key} must be an integer from 0 through 1439 or null`);
1260
+ return value;
1261
+ }
1262
+ function httpUrlField(body, key) {
1263
+ const value = trimmedString(body, key, 2_048);
1264
+ let parsed;
1265
+ try {
1266
+ parsed = new URL(value);
1267
+ }
1268
+ catch {
1269
+ throw new InvalidRequest(`${key} must be a valid URL`);
1270
+ }
1271
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
1272
+ throw new InvalidRequest(`${key} must use http or https`);
1273
+ return parsed.toString();
1274
+ }
1275
+ function hasControlCharacters(value, allowLineBreaks = false) {
1276
+ for (const character of value) {
1277
+ const code = character.charCodeAt(0);
1278
+ if (code === 127 || (code < 32 && !(allowLineBreaks && (code === 9 || code === 10))))
1279
+ return true;
1280
+ }
1281
+ return false;
1282
+ }
1283
+ class InvalidRequest extends Error {
1284
+ }
1285
+ //# sourceMappingURL=collaboration.js.map