@desplega.ai/agent-swarm 2.4.5 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/README.md +4 -2
  2. package/dist/{acp-adapter-2wdkktv6.js → acp-adapter-11gp67nr.js} +8 -6
  3. package/dist/{actions-c40ebcm4.js → actions-8p2kze8j.js} +23 -21
  4. package/dist/{amp-adapter-wb67m95z.js → amp-adapter-z4qvhacj.js} +2 -2
  5. package/dist/{anthropic-messages-g6pqmqk4.js → anthropic-messages-txs190dd.js} +17 -20
  6. package/dist/{app-353vre8g.js → app-0qp0a2gk.js} +3 -3
  7. package/dist/{assistant-sj1njh23.js → assistant-jedd3ktm.js} +24 -22
  8. package/dist/{azure-openai-responses-9dbmj346.js → azure-openai-responses-9kfwhmn7.js} +18 -13
  9. package/dist/{boot-reembed-qdkvxeem.js → boot-reembed-4xwq5tg0.js} +1 -1
  10. package/dist/{boot-reembed-b9c3az3c.js → boot-reembed-n4wq5dd2.js} +1 -1
  11. package/dist/{boot-scrub-sweep-m3wh8eks.js → boot-scrub-sweep-69h90xcq.js} +4 -4
  12. package/dist/{boot-seeds-j2273s54.js → boot-seeds-3vn0m9sg.js} +4 -4
  13. package/dist/{claude-adapter-m7q0a085.js → claude-adapter-221mvad5.js} +8 -6
  14. package/dist/{claude-managed-adapter-5ptb6tzs.js → claude-managed-adapter-gt6pa35d.js} +2 -2
  15. package/dist/{claude-managed-setup-smrqk5j5.js → claude-managed-setup-c3hp36ph.js} +4 -4
  16. package/dist/{claude-sdk-session-38xnz21e.js → claude-sdk-session-8thjsk5e.js} +9012 -8928
  17. package/dist/{cli-b9fcxxev.js → cli-007cnhm1.js} +50 -4
  18. package/dist/{cli-j4rqdf2n.js → cli-03vpq45j.js} +2 -2
  19. package/dist/{cli-dk44dxec.js → cli-0y6f0ckq.js} +1 -1
  20. package/dist/{cli-kpw04va0.js → cli-0z3vjrpm.js} +1 -1
  21. package/dist/{cli-94gwyg7m.js → cli-179xcyky.js} +9 -9
  22. package/dist/{cli-3r8samv3.js → cli-1f0gp9vz.js} +3 -2
  23. package/dist/{cli-4zfqgjc5.js → cli-1pr672hz.js} +2 -2
  24. package/dist/{cli-0bqteszy.js → cli-2nf3b08t.js} +328 -25
  25. package/dist/{cli-43x64hv3.js → cli-36j4jnk3.js} +4 -4
  26. package/dist/{cli-k8g16gz3.js → cli-3qv8naq8.js} +115 -119
  27. package/dist/cli-3zwye9sn.js +1193 -0
  28. package/dist/{cli-60cgh1rv.js → cli-459fk97x.js} +7 -7
  29. package/dist/{cli-v8fjakwf.js → cli-4w24f7z3.js} +1 -1
  30. package/dist/{cli-n72rg53p.js → cli-5kzk3472.js} +812 -418
  31. package/dist/{cli-h7gc0b4r.js → cli-5m84zd2k.js} +55 -18
  32. package/dist/{cli-j99azg3g.js → cli-5rvg1ddk.js} +5 -5
  33. package/dist/{cli-dvkmp9bt.js → cli-6ctwrf1z.js} +20 -10
  34. package/dist/{cli-848hxdwj.js → cli-6rg2jn2y.js} +2 -1
  35. package/dist/{cli-fv5hepyy.js → cli-7bwtc6yx.js} +3 -3
  36. package/dist/{cli-ms85309d.js → cli-7g40g8hq.js} +11 -11
  37. package/dist/{cli-xpa64rek.js → cli-92mqenkc.js} +2362 -159
  38. package/dist/{cli-018r4jq6.js → cli-9ce2xw98.js} +8 -8
  39. package/dist/{cli-1syn2xy1.js → cli-bgdtv62r.js} +32 -18
  40. package/dist/{cli-wxbn3yfx.js → cli-cz5nmdxt.js} +2 -2
  41. package/dist/{cli-wt645v7q.js → cli-dabehxhp.js} +1 -1
  42. package/dist/{cli-vfha21c9.js → cli-e5bxnjps.js} +2 -2
  43. package/dist/{cli-wt0jf9hm.js → cli-fcc5qe3f.js} +553 -193
  44. package/dist/{cli-ftc1g5tj.js → cli-fes51ya2.js} +3 -3
  45. package/dist/{cli-3a45zcgm.js → cli-ftmq4nvk.js} +2 -2
  46. package/dist/{cli-37j04k05.js → cli-fy1jyyym.js} +13 -13
  47. package/dist/{cli-s95dknxh.js → cli-g1cw7xtt.js} +1 -1
  48. package/dist/{cli-74c1wpm0.js → cli-gpv60278.js} +1 -1
  49. package/dist/{cli-pemywhfq.js → cli-ha9rmwgq.js} +2 -1
  50. package/dist/{cli-g35m2xwm.js → cli-j1eghqfm.js} +2 -2
  51. package/dist/{cli-qfxc84gh.js → cli-jpxsrxcb.js} +1 -1
  52. package/dist/{cli-ejs3m1sx.js → cli-kxpfx2xr.js} +1 -1
  53. package/dist/{cli-p14m2cnp.js → cli-mbfj11fq.js} +1 -1
  54. package/dist/{cli-pnp2j5zg.js → cli-mmhj8vd7.js} +2 -2
  55. package/dist/{cli-xf234ws8.js → cli-n4c8e3s5.js} +2 -0
  56. package/dist/{cli-f01h25yt.js → cli-n6kmqngw.js} +2 -2
  57. package/dist/{cli-v0yrfmdh.js → cli-nnvjb17s.js} +3 -3
  58. package/dist/{cli-1dmcq2eh.js → cli-nzfnt2gs.js} +1 -1
  59. package/dist/{cli-625tb7aq.js → cli-p859hcc8.js} +2 -2
  60. package/dist/{cli-smvcw9z5.js → cli-pasyvjmm.js} +1 -1
  61. package/dist/cli-pkaqk57w.js +128 -0
  62. package/dist/{cli-8nyhf7za.js → cli-qqzmjxkf.js} +129 -105
  63. package/dist/{cli-9yqgm9nd.js → cli-r25qpaxt.js} +161 -63
  64. package/dist/{cli-eww120g8.js → cli-rh5smt8t.js} +18 -4
  65. package/dist/{cli-qwkcwjdn.js → cli-rycdaj0a.js} +1 -1
  66. package/dist/{cli-a92r1127.js → cli-s4sntwt4.js} +2 -2
  67. package/dist/{cli-be2161h1.js → cli-segs7hx9.js} +6 -6
  68. package/dist/{cli-36ckw0wx.js → cli-stxdrqs2.js} +2 -2
  69. package/dist/{cli-z7d29rw9.js → cli-sy8f9705.js} +4 -4
  70. package/dist/{cli-1m17dc9d.js → cli-ta8bd7th.js} +2 -2
  71. package/dist/{cli-st8aw1m6.js → cli-we0ra057.js} +4 -4
  72. package/dist/{cli-7c9qs530.js → cli-x21dqn18.js} +5 -5
  73. package/dist/{cli-1sp861s4.js → cli-x6vf4sad.js} +11 -11
  74. package/dist/{cli-d2mb5s8a.js → cli-x77gen2p.js} +680 -440
  75. package/dist/{cli-tw4p90jb.js → cli-xawhpgc9.js} +5 -5
  76. package/dist/{cli-f0g3pgvf.js → cli-xd4eqtr2.js} +2 -2
  77. package/dist/{cli-1f3qzw6y.js → cli-xt768r5m.js} +2 -2
  78. package/dist/{cli-cxxe61f6.js → cli-y4x8ww6s.js} +2 -2
  79. package/dist/{cli-bnes4z6b.js → cli-y9jvxfhk.js} +3 -3
  80. package/dist/{cli-qwhy2qma.js → cli-z63a269p.js} +2 -2
  81. package/dist/{cli-sm8rses9.js → cli-zfdp01hn.js} +8 -5
  82. package/dist/cli.js +29 -27
  83. package/dist/{cloudflare-workers-ai-system-one-ch2wxfcp.js → cloudflare-workers-ai-system-one-43v9qkjk.js} +7 -2
  84. package/dist/{codex-adapter-tn6k5mkg.js → codex-adapter-tcg1q3jk.js} +6 -4
  85. package/dist/{codex-login-m819c41r.js → codex-login-rn7c5gxt.js} +1 -1
  86. package/dist/{codex-session-runner-7vjt7jbw.js → codex-session-runner-xfx4wd9x.js} +7 -5
  87. package/dist/{commands-t7rz9146.js → commands-ew83nj70.js} +2 -2
  88. package/dist/{controller-m2jasma5.js → controller-gnq1k1mf.js} +4 -4
  89. package/dist/{controllers-connect-9ftpex99.js → controllers-connect-tw1dan9r.js} +25 -23
  90. package/dist/{cursor-adapter-mjv79e4z.js → cursor-adapter-yq5xgfcg.js} +1 -1
  91. package/dist/{db-6sfrn9g3.js → db-7gbc53fd.js} +14 -2
  92. package/dist/{db-rn673r06.js → db-t86xrnes.js} +3 -3
  93. package/dist/{devin-adapter-zgqadvnm.js → devin-adapter-4ycy4vrf.js} +1 -1
  94. package/dist/{dispatcher-c155xz0p.js → dispatcher-64bdyhps.js} +22 -20
  95. package/dist/{docker-fkgtr75t.js → docker-nfy2gc8t.js} +2 -2
  96. package/dist/{dsh-adapter-3mmszc1x.js → dsh-adapter-tvhc4k2d.js} +1 -1
  97. package/dist/{e2b-16d6ch39.js → e2b-9gkdhm3c.js} +2 -2
  98. package/dist/{e2b-zn0b4w5w.js → e2b-cjtf8k1z.js} +2 -2
  99. package/dist/{easter-egg-3d-5cgynaja.js → easter-egg-3d-e4qv8dxj.js} +1 -1
  100. package/dist/{event-bus-9vpk8gmq.js → event-bus-3z78ep5n.js} +1 -1
  101. package/dist/{execute-t4e6m3rw.js → execute-0xpz8950.js} +85 -23
  102. package/dist/{external-channel-35k6tkv8.js → external-channel-e9nw24zv.js} +2 -2
  103. package/dist/{google-generative-ai-bmq8gkf3.js → google-generative-ai-mk9vcpe1.js} +15 -15
  104. package/dist/{google-vertex-b1rvp52s.js → google-vertex-q580kz8k.js} +20 -20
  105. package/dist/{grok-adapter-tfrq7tg1.js → grok-adapter-v82srhrx.js} +9 -7
  106. package/dist/handlers-c9ajzhbn.js +41 -0
  107. package/dist/{heartbeat-dyg4npkp.js → heartbeat-2vqs1319.js} +25 -23
  108. package/dist/{hook-x26b31bg.js → hook-01nk13wx.js} +7 -5
  109. package/dist/{hook-drspbwpe.js → hook-jzpheegg.js} +9 -7
  110. package/dist/{http-pk972xyw.js → http-fc1k6ec8.js} +544 -76
  111. package/dist/index-07ef7nkm.js +42 -0
  112. package/dist/{index-g6tbb6ve.js → index-31ceyf42.js} +1 -1
  113. package/dist/index-7yts8n9s.js +43 -0
  114. package/dist/index-q3t40ryv.js +46 -0
  115. package/dist/{index-arytgj5q.js → index-sepja052.js} +1 -1
  116. package/dist/{index-jwxkkex2.js → index-zdycv3ja.js} +13 -11
  117. package/dist/{index-g216q6jr.js → index-zsyx51ap.js} +1 -4
  118. package/dist/{keepalive-7yrng7px.js → keepalive-5nh2rhmr.js} +4 -4
  119. package/dist/lead-aayq64jn.js +79 -0
  120. package/dist/{llama-cpp-classify-vv43gswy.js → llama-cpp-classify-ee7vmd7q.js} +4 -1
  121. package/dist/{maintenance-e6cemdqt.js → maintenance-hjhzdt3k.js} +1 -1
  122. package/dist/{mistral-conversations-1medfwv7.js → mistral-conversations-sd0xf1tn.js} +13 -13
  123. package/dist/{node-de6akczr.js → node-jzjj7r9x.js} +1 -1
  124. package/dist/{oauth-refresh-sweep-yw3xdbcj.js → oauth-refresh-sweep-y3n1knmd.js} +3 -3
  125. package/dist/{onboard-0a9y5yrc.js → onboard-1zq3m7db.js} +2 -2
  126. package/dist/{openai-codex-responses-de9pvs4d.js → openai-codex-responses-79s8xnbh.js} +29 -23
  127. package/dist/{openai-completions-mtqge3nf.js → openai-completions-tdbyts0x.js} +21 -20
  128. package/dist/{openai-decisions-ygk4kkxd.js → openai-decisions-bqhnjyfz.js} +2 -1
  129. package/dist/{openai-responses-dkyng71g.js → openai-responses-dm3t5vqp.js} +26 -17
  130. package/dist/{opencode-adapter-x4tqzhkh.js → opencode-adapter-e56086m1.js} +7 -5
  131. package/dist/{openrouter-images-wvv7camj.js → openrouter-images-ssttr1y4.js} +6 -3
  132. package/dist/{otel-impl-trjpkfb7.js → otel-impl-h9yyza47.js} +2 -2
  133. package/dist/{pi-messages-nhh1pkz2.js → pi-messages-3nf8bq1a.js} +6 -4
  134. package/dist/{pi-mono-adapter-mcfkn9vf.js → pi-mono-adapter-tzps8f38.js} +9 -37
  135. package/dist/{pricing-refresh-d2b5my0y.js → pricing-refresh-nevhat8h.js} +3 -3
  136. package/dist/{rbac-roles-nxh5ahwk.js → rbac-roles-4y6ngvzq.js} +1 -1
  137. package/dist/{rbac-roles-hey3yv9d.js → rbac-roles-exbg8xb6.js} +2 -2
  138. package/dist/{render-v2-rr3p2atg.js → render-v2-cfsh13z3.js} +3 -3
  139. package/dist/{resume-2efkf1s1.js → resume-5s2t0dbs.js} +10 -8
  140. package/dist/{runtime-xe3aqy4x.js → runtime-bt5pkpm1.js} +1 -1
  141. package/dist/{session-access-s2mttxpw.js → session-access-dnbyp3cn.js} +6 -4
  142. package/dist/{setup-vypma5yt.js → setup-1wj1y77j.js} +1 -1
  143. package/dist/{typesafe-system-one-2hgpayzy.js → typesafe-system-one-bre38hvs.js} +4 -2
  144. package/dist/{virtual-modules-4r3zkc20.js → virtual-modules-mnfbh5mr.js} +8 -2
  145. package/dist/{work-objects-rvhckdkp.js → work-objects-7mg6216g.js} +25 -23
  146. package/dist/worker-nwb789mg.js +79 -0
  147. package/dist/{x-y45mthwn.js → x-pbpezr0s.js} +2 -2
  148. package/openapi.json +785 -5
  149. package/package.json +9 -9
  150. package/src/assets/key.ts +17 -0
  151. package/src/azure-devops/handlers.ts +1 -1
  152. package/src/be/db/approvals.ts +18 -0
  153. package/src/be/db/session-moves.ts +164 -0
  154. package/src/be/db/session-tree.ts +384 -0
  155. package/src/be/db/tasks/read.ts +40 -4
  156. package/src/be/db/workflow-runs.ts +96 -0
  157. package/src/be/db.ts +80 -236
  158. package/src/be/onboarding.ts +3 -0
  159. package/src/be/skill-sync.ts +43 -2
  160. package/src/be/swarm-config-guard.ts +1 -0
  161. package/src/bitbucket/api.ts +81 -0
  162. package/src/bitbucket/auth.ts +64 -0
  163. package/src/bitbucket/handlers.ts +434 -0
  164. package/src/bitbucket/index.ts +28 -0
  165. package/src/bitbucket/templates.ts +134 -0
  166. package/src/bitbucket/types.ts +65 -0
  167. package/src/commands/runner.ts +55 -3
  168. package/src/e2b/env.ts +1 -0
  169. package/src/extensions/contract-types.generated.ts +1 -1
  170. package/src/extensions/contract.ts +2 -2
  171. package/src/extensions/post-bridge.ts +1 -0
  172. package/src/github/handlers.ts +6 -6
  173. package/src/gitlab/handlers.ts +5 -5
  174. package/src/http/assets.ts +14 -2
  175. package/src/http/core.ts +12 -0
  176. package/src/http/index.ts +4 -0
  177. package/src/http/onboarding.ts +1 -0
  178. package/src/http/sessions.ts +366 -12
  179. package/src/http/skills.ts +48 -1
  180. package/src/http/tasks.ts +1 -1
  181. package/src/http/users.ts +1 -0
  182. package/src/http/webhooks.ts +141 -0
  183. package/src/integrations/chat-sdk/adapters.ts +19 -3
  184. package/src/integrations/chat-sdk/facts.ts +30 -12
  185. package/src/integrations/chat-sdk/outbound.ts +38 -1
  186. package/src/providers/acp-targets.ts +1 -1
  187. package/src/providers/amp-adapter.ts +1 -1
  188. package/src/providers/pi-mono-adapter.ts +5 -53
  189. package/src/scripts-runtime/types/swarm-extension.d.ts +6 -4
  190. package/src/slack/handlers.test.ts +158 -0
  191. package/src/slack/handlers.ts +19 -28
  192. package/src/tasks/context-key.ts +61 -1
  193. package/src/tests/bitbucket-auth.test.ts +156 -0
  194. package/src/tests/bitbucket-handlers.test.ts +425 -0
  195. package/src/tests/bitbucket-webhook.test.ts +220 -0
  196. package/src/tests/chat-sdk-inbound.test.ts +39 -0
  197. package/src/tests/chat-sdk-telegram-markdown.test.ts +92 -0
  198. package/src/tests/context-key.test.ts +50 -0
  199. package/src/tests/core-auth.test.ts +5 -0
  200. package/src/tests/db-task-read-contract.test.ts +7 -0
  201. package/src/tests/db-task-write-contract.test.ts +6 -0
  202. package/src/tests/docs-index-footprint.test.js +3 -3
  203. package/src/tests/fixtures/bitbucket/pullrequest-comment-created-inline.json +174 -0
  204. package/src/tests/fixtures/bitbucket/pullrequest-comment-created.json +170 -0
  205. package/src/tests/fixtures/bitbucket/pullrequest-created-mention.json +132 -0
  206. package/src/tests/fixtures/bitbucket/pullrequest-created-reviewer.json +140 -0
  207. package/src/tests/fixtures/bitbucket/pullrequest-fulfilled.json +134 -0
  208. package/src/tests/fixtures/bitbucket/pullrequest-rejected.json +133 -0
  209. package/src/tests/generate-mcp-docs.test.ts +101 -0
  210. package/src/tests/gitlab-handlers.test.ts +3 -3
  211. package/src/tests/gitlab-vcs-db.test.ts +4 -4
  212. package/src/tests/integration-disable-flags.test.ts +36 -0
  213. package/src/tests/mcp-tools.test.ts +1 -1
  214. package/src/tests/onboarding.test.ts +12 -0
  215. package/src/tests/pi-codemode.test.ts +8 -2
  216. package/src/tests/preload-env.test.ts +57 -0
  217. package/src/tests/preload.ts +21 -0
  218. package/src/tests/prompt-template-remaining.test.ts +38 -0
  219. package/src/tests/run-root-coverage.test.ts +39 -0
  220. package/src/tests/runner-bitbucket-vcs.test.ts +191 -0
  221. package/src/tests/runner-skills-refresh.test.ts +58 -0
  222. package/src/tests/secret-scrubber.test.ts +15 -0
  223. package/src/tests/sessions-move.test.ts +576 -0
  224. package/src/tests/sessions-tree.test.ts +493 -0
  225. package/src/tests/skill-files-http.test.ts +50 -0
  226. package/src/tests/skills-bundle.test.ts +99 -0
  227. package/src/tests/swarm-config-export-to-env.test.ts +2 -1
  228. package/src/tests/vcs-bitbucket.test.ts +91 -0
  229. package/src/tests/vcs-provider-isolation.test.ts +123 -0
  230. package/src/tests/vcs-provider.test.ts +16 -1
  231. package/src/tests/vcs-tracking.test.ts +3 -3
  232. package/src/tests/workflow-foreach-bodies.test.ts +637 -0
  233. package/src/tests/workflow-foreach-concurrency.test.ts +342 -0
  234. package/src/tests/workflow-foreach-retry.test.ts +537 -0
  235. package/src/tests/workflow-foreach.test.ts +44 -23
  236. package/src/tests/workflow-step-claims.test.ts +6 -6
  237. package/src/tests/workflow-tla-races.test.ts +2 -2
  238. package/src/tools/script-delete.ts +1 -1
  239. package/src/tools/workflows/create-workflow.ts +5 -3
  240. package/src/types.ts +3 -2
  241. package/src/utils/config-env-export.ts +1 -0
  242. package/src/utils/secret-scrubber.ts +4 -0
  243. package/src/utils/skills-refresh.ts +83 -43
  244. package/src/vcs/bitbucket.ts +58 -0
  245. package/src/vcs/index.ts +2 -0
  246. package/src/vcs/types.ts +2 -2
  247. package/src/workflows/approval-resolution.ts +4 -1
  248. package/src/workflows/checkpoint.ts +9 -1
  249. package/src/workflows/constants.ts +3 -0
  250. package/src/workflows/definition.ts +48 -17
  251. package/src/workflows/engine.ts +25 -15
  252. package/src/workflows/executors/foreach.ts +45 -99
  253. package/src/workflows/foreach-body.ts +146 -0
  254. package/src/workflows/foreach-driver.ts +552 -0
  255. package/src/workflows/foreach-join.ts +206 -7
  256. package/src/workflows/node-timeout.ts +21 -0
  257. package/src/workflows/recovery.ts +68 -3
  258. package/src/workflows/resume.ts +221 -43
  259. package/src/workflows/retry-poller.ts +39 -24
  260. package/src/workflows/step-input.ts +59 -0
  261. package/src/workflows/task-step-routing.ts +114 -56
  262. package/templates/skills/slack-interaction/content.md +5 -0
  263. package/templates/skills/work-on-task/SKILL.md +1 -1
  264. package/templates/skills/work-on-task/content.md +1 -1
  265. package/dist/cli-9hstj09h.js +0 -2657
  266. package/dist/cli-yw91rnmw.js +0 -13
  267. package/dist/handlers-7gw64dxw.js +0 -39
  268. package/dist/index-bgdyvcnv.js +0 -40
  269. package/dist/index-gabbcdn2.js +0 -43
  270. package/dist/index-tfb26zhy.js +0 -41
  271. package/dist/lead-7e02z839.js +0 -77
  272. package/dist/worker-5snytpbd.js +0 -77
@@ -79,8 +79,6 @@ const allowedUserIds = (process.env.SLACK_ALLOWED_USER_IDS || "")
79
79
  .map((id) => id.trim())
80
80
  .filter(Boolean);
81
81
 
82
- const filteringEnabled = allowedEmailDomains.length > 0 || allowedUserIds.length > 0;
83
-
84
82
  /**
85
83
  * Configuration for user filtering.
86
84
  */
@@ -89,6 +87,8 @@ export interface UserFilterConfig {
89
87
  allowedUserIds: string[];
90
88
  }
91
89
 
90
+ const envUserFilterConfig: UserFilterConfig = { allowedEmailDomains, allowedUserIds };
91
+
92
92
  /**
93
93
  * Core logic for checking if a user is allowed based on email and/or user ID.
94
94
  * Exported for testing.
@@ -137,40 +137,31 @@ export function checkUserAccess(
137
137
  /**
138
138
  * Check if a user is allowed to interact with the swarm.
139
139
  * Returns true if filtering is disabled, user is in whitelist, or user's email domain is allowed.
140
+ * The allow/deny decision is `checkUserAccess`; this wrapper only adds the lazy email lookup
141
+ * and the deny logs. The config comes from the SLACK_ALLOWED_* env vars, read at module load.
140
142
  */
141
143
  export async function isUserAllowed(client: WebClient, userId: string): Promise<boolean> {
142
- // If no filtering configured, allow all users (backwards compatible)
143
- if (!filteringEnabled) {
144
- return true;
145
- }
146
-
147
- // Check user ID whitelist first (fast path)
148
- if (allowedUserIds.includes(userId)) {
149
- return true;
150
- }
151
-
152
- // No email domains configured and not in user whitelist
153
- if (allowedEmailDomains.length === 0) {
154
- return false;
144
+ const config = envUserFilterConfig;
145
+ // Decide without an email first: filtering off and allowlisted IDs pass, and with no
146
+ // email domains configured an unlisted ID is denied. Only domain checks need a lookup.
147
+ const allowedWithoutEmail = checkUserAccess(userId, null, config);
148
+ if (allowedWithoutEmail || config.allowedEmailDomains.length === 0) {
149
+ return allowedWithoutEmail;
155
150
  }
156
151
 
157
152
  // Check email domain — uses kv-backed enrichment (24h TTL, only success cached).
158
153
  const email = await enrichSlackUserEmail(client, userId);
154
+ const allowed = checkUserAccess(userId, email, config);
159
155
 
160
- if (!email) {
161
- console.log(`[Slack] User ${userId} has no email, denying access`);
162
- return false;
163
- }
164
-
165
- const domain = email.split("@")[1]?.toLowerCase();
166
- if (!domain) {
167
- console.log(`[Slack] User ${userId} has invalid email format, denying access`);
168
- return false;
169
- }
170
-
171
- const allowed = allowedEmailDomains.includes(domain);
172
156
  if (!allowed) {
173
- console.log(`[Slack] User ${userId} email domain "${domain}" not in allowed list`);
157
+ const domain = email?.split("@")[1]?.toLowerCase();
158
+ if (!email) {
159
+ console.log(`[Slack] User ${userId} has no email, denying access`);
160
+ } else if (!domain) {
161
+ console.log(`[Slack] User ${userId} has invalid email format, denying access`);
162
+ } else {
163
+ console.log(`[Slack] User ${userId} email domain "${domain}" not in allowed list`);
164
+ }
174
165
  }
175
166
  return allowed;
176
167
  }
@@ -21,6 +21,10 @@
21
21
  * task:trackers:gitlab:{projectId}:{mr|issue}:{iid}
22
22
  * task:trackers:azure-devops:{repositoryId}:pr:{pullRequestId}
23
23
  * task:trackers:azure-devops:{organization}:workitem:{workItemId}
24
+ * task:trackers:bitbucket:{repoUuid}:pr:{pullRequestId}
25
+ * (repo UUID with Bitbucket's `{}` braces
26
+ * stripped and lowercased, so renames keep
27
+ * task threads)
24
28
  * task:trackers:linear:{issueIdentifier} (e.g. DES-42 — case preserved)
25
29
  * task:trackers:jira:{issueIdentifier} (e.g. PROJ-123 — case preserved)
26
30
  * task:schedule:{scheduleId}
@@ -50,7 +54,13 @@ export type ContextKeyFamily =
50
54
  | "schedule"
51
55
  | "workflow";
52
56
 
53
- export type TrackerProvider = "github" | "gitlab" | "azure-devops" | "linear" | "jira";
57
+ export type TrackerProvider =
58
+ | "github"
59
+ | "gitlab"
60
+ | "azure-devops"
61
+ | "bitbucket"
62
+ | "linear"
63
+ | "jira";
54
64
 
55
65
  export type ParsedContextKey =
56
66
  | { family: "slack"; parts: { channelId: string; threadTs: string } }
@@ -76,6 +86,11 @@ export type ParsedContextKey =
76
86
  subFamily: "azure-devops";
77
87
  parts: { organization: string; kind: "workitem"; workItemId: number };
78
88
  }
89
+ | {
90
+ family: "trackers";
91
+ subFamily: "bitbucket";
92
+ parts: { repoUuid: string; kind: "pr"; pullRequestId: number };
93
+ }
79
94
  | {
80
95
  family: "trackers";
81
96
  subFamily: "linear";
@@ -224,6 +239,34 @@ export function azureDevOpsWorkItemContextKey(input: {
224
239
  return ["task", "trackers", "azure-devops", organization, "workitem", workItemId].join(SEPARATOR);
225
240
  }
226
241
 
242
+ /**
243
+ * Bitbucket Cloud wraps repository UUIDs in braces (`{1a2b...}`). Strip them
244
+ * and lowercase so the key is stable regardless of which payload field the
245
+ * UUID came from.
246
+ */
247
+ export function bitbucketContextKey({
248
+ repoUuid,
249
+ pullRequestId,
250
+ }: {
251
+ repoUuid: string;
252
+ pullRequestId: number | string;
253
+ }): string {
254
+ const rawUuid = typeof repoUuid === "string" ? repoUuid.trim().replace(/^\{|\}$/g, "") : repoUuid;
255
+ const uuid = assertSafePart(rawUuid, "repoUuid").toLowerCase();
256
+ if (/[{}]/.test(uuid)) {
257
+ throw new Error(
258
+ `context-key: bitbucket "repoUuid" has unbalanced braces (got ${JSON.stringify(repoUuid)})`,
259
+ );
260
+ }
261
+ const id = assertSafePart(pullRequestId, "pullRequestId");
262
+ if (!/^\d+$/.test(id)) {
263
+ throw new Error(
264
+ `context-key: bitbucket "pullRequestId" must be a positive integer (got ${JSON.stringify(id)})`,
265
+ );
266
+ }
267
+ return ["task", "trackers", "bitbucket", uuid, "pr", id].join(SEPARATOR);
268
+ }
269
+
227
270
  export function linearContextKey(input: { issueIdentifier: string }): string {
228
271
  const issueIdentifier = assertSafePart(input.issueIdentifier, "issueIdentifier");
229
272
  return ["task", "trackers", "linear", issueIdentifier].join(SEPARATOR);
@@ -401,6 +444,23 @@ export function parseContextKey(key: string): ParsedContextKey {
401
444
  parts: { repositoryId: parts[3] as string, kind: "pr", pullRequestId },
402
445
  };
403
446
  }
447
+ if (subFamily === "bitbucket") {
448
+ if (parts.length !== 6 || parts[4] !== "pr" || !parts[3]) {
449
+ throw new Error(`context-key: malformed bitbucket key: ${JSON.stringify(key)}`);
450
+ }
451
+ const idStr = parts[5] as string;
452
+ const pullRequestId = Number.parseInt(idStr, 10);
453
+ if (!/^\d+$/.test(idStr) || !Number.isFinite(pullRequestId)) {
454
+ throw new Error(
455
+ `context-key: malformed bitbucket pullRequestId "${idStr}": ${JSON.stringify(key)}`,
456
+ );
457
+ }
458
+ return {
459
+ family: "trackers",
460
+ subFamily: "bitbucket",
461
+ parts: { repoUuid: parts[3] as string, kind: "pr", pullRequestId },
462
+ };
463
+ }
404
464
  if (subFamily === "linear") {
405
465
  if (parts.length !== 4) {
406
466
  throw new Error(`context-key: malformed linear key: ${JSON.stringify(key)}`);
@@ -0,0 +1,156 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { createHmac } from "node:crypto";
3
+ import { resetBitbucketBotCache, resolveBitbucketBotAccountId } from "../bitbucket/api";
4
+ import {
5
+ getBitbucketToken,
6
+ initBitbucket,
7
+ isBitbucketEnabled,
8
+ resetBitbucket,
9
+ verifyBitbucketWebhook,
10
+ } from "../bitbucket/auth";
11
+
12
+ const ENV_KEYS = [
13
+ "BITBUCKET_WEBHOOK_SECRET",
14
+ "BITBUCKET_TOKEN",
15
+ "BITBUCKET_DISABLE",
16
+ "BITBUCKET_BOT_ACCOUNT_ID",
17
+ ] as const;
18
+
19
+ function sign(secret: string, body: string): string {
20
+ return `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`;
21
+ }
22
+
23
+ afterEach(() => {
24
+ for (const key of ENV_KEYS) delete process.env[key];
25
+ resetBitbucket();
26
+ resetBitbucketBotCache();
27
+ });
28
+
29
+ describe("isBitbucketEnabled", () => {
30
+ test("disabled without a webhook secret", () => {
31
+ expect(isBitbucketEnabled()).toBe(false);
32
+ });
33
+
34
+ test("enabled with a webhook secret", () => {
35
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
36
+ expect(isBitbucketEnabled()).toBe(true);
37
+ });
38
+
39
+ test("BITBUCKET_DISABLE wins over the secret", () => {
40
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
41
+ process.env.BITBUCKET_DISABLE = "true";
42
+ expect(isBitbucketEnabled()).toBe(false);
43
+ });
44
+ });
45
+
46
+ describe("verifyBitbucketWebhook", () => {
47
+ test("matches Atlassian's documented test vector", () => {
48
+ process.env.BITBUCKET_WEBHOOK_SECRET = "It's a Secret to Everybody";
49
+ initBitbucket();
50
+ expect(
51
+ verifyBitbucketWebhook(
52
+ "Hello World!",
53
+ "sha256=a4771c39fbe90f317c7824e83ddef3caae9cb3d976c214ace1f2937e133263c9",
54
+ ),
55
+ ).toBe(true);
56
+ });
57
+
58
+ test("rejects a signature made with the wrong secret", () => {
59
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
60
+ initBitbucket();
61
+ expect(verifyBitbucketWebhook("{}", sign("other-secret", "{}"))).toBe(false);
62
+ expect(verifyBitbucketWebhook("{}", sign("example-secret", "{}"))).toBe(true);
63
+ });
64
+
65
+ test("rejects a tampered body", () => {
66
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
67
+ initBitbucket();
68
+ expect(verifyBitbucketWebhook('{"a":2}', sign("example-secret", '{"a":1}'))).toBe(false);
69
+ });
70
+
71
+ test("rejects a missing header", () => {
72
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
73
+ initBitbucket();
74
+ expect(verifyBitbucketWebhook("{}", undefined)).toBe(false);
75
+ });
76
+
77
+ test("rejects a bare hex digest without the sha256= prefix", () => {
78
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
79
+ initBitbucket();
80
+ const bare = sign("example-secret", "{}").slice("sha256=".length);
81
+ expect(verifyBitbucketWebhook("{}", bare)).toBe(false);
82
+ });
83
+
84
+ test("rejects everything before init and after reset", () => {
85
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
86
+ const sig = sign("example-secret", "{}");
87
+ expect(verifyBitbucketWebhook("{}", sig)).toBe(false);
88
+ initBitbucket();
89
+ expect(verifyBitbucketWebhook("{}", sig)).toBe(true);
90
+ resetBitbucket();
91
+ expect(verifyBitbucketWebhook("{}", sig)).toBe(false);
92
+ });
93
+ });
94
+
95
+ describe("initBitbucket", () => {
96
+ test("does not initialize when disabled", () => {
97
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
98
+ process.env.BITBUCKET_TOKEN = "example-token";
99
+ process.env.BITBUCKET_DISABLE = "true";
100
+ initBitbucket();
101
+ expect(getBitbucketToken()).toBeNull();
102
+ });
103
+
104
+ test("loads the API token", () => {
105
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
106
+ process.env.BITBUCKET_TOKEN = "example-token";
107
+ initBitbucket();
108
+ expect(getBitbucketToken()).toBe("example-token");
109
+ });
110
+ });
111
+
112
+ describe("resolveBitbucketBotAccountId", () => {
113
+ test("prefers BITBUCKET_BOT_ACCOUNT_ID", async () => {
114
+ process.env.BITBUCKET_BOT_ACCOUNT_ID = "557058:env-bot";
115
+ expect(await resolveBitbucketBotAccountId()).toBe("557058:env-bot");
116
+ });
117
+
118
+ test("returns null without a token", async () => {
119
+ expect(await resolveBitbucketBotAccountId()).toBeNull();
120
+ });
121
+
122
+ test("resolves GET /user with a Bearer token and caches it", async () => {
123
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
124
+ process.env.BITBUCKET_TOKEN = "example-token";
125
+ initBitbucket();
126
+ const calls: Array<{ url: string; auth?: string }> = [];
127
+ const realFetch = globalThis.fetch;
128
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
129
+ const headers = (init?.headers ?? {}) as Record<string, string>;
130
+ calls.push({ url: String(input), auth: headers.Authorization });
131
+ return new Response(JSON.stringify({ account_id: "557058:api-bot" }), { status: 200 });
132
+ }) as typeof fetch;
133
+ try {
134
+ expect(await resolveBitbucketBotAccountId()).toBe("557058:api-bot");
135
+ expect(await resolveBitbucketBotAccountId()).toBe("557058:api-bot");
136
+ } finally {
137
+ globalThis.fetch = realFetch;
138
+ }
139
+ expect(calls).toEqual([
140
+ { url: "https://api.bitbucket.org/2.0/user", auth: "Bearer example-token" },
141
+ ]);
142
+ });
143
+
144
+ test("returns null on a failed lookup", async () => {
145
+ process.env.BITBUCKET_WEBHOOK_SECRET = "example-secret";
146
+ process.env.BITBUCKET_TOKEN = "example-token";
147
+ initBitbucket();
148
+ const realFetch = globalThis.fetch;
149
+ globalThis.fetch = (async () => new Response("nope", { status: 401 })) as typeof fetch;
150
+ try {
151
+ expect(await resolveBitbucketBotAccountId()).toBeNull();
152
+ } finally {
153
+ globalThis.fetch = realFetch;
154
+ }
155
+ });
156
+ });