From 38091a5a2be81fc137b1cc2c5e0a396f5b9d72a0 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 15:44:19 -0800 Subject: [PATCH 01/19] Add slack/socket mode dependency --- packages/adapter-slack/package.json | 1 + packages/adapter-slack/tsup.config.ts | 2 +- pnpm-lock.yaml | 429 ++++---------------------- 3 files changed, 59 insertions(+), 373 deletions(-) diff --git a/packages/adapter-slack/package.json b/packages/adapter-slack/package.json index a2ad97a4..a7cad1e9 100644 --- a/packages/adapter-slack/package.json +++ b/packages/adapter-slack/package.json @@ -25,6 +25,7 @@ }, "dependencies": { "@chat-adapter/shared": "workspace:*", + "@slack/socket-mode": "^2.0.5", "@slack/web-api": "^7.14.0", "chat": "workspace:*" }, diff --git a/packages/adapter-slack/tsup.config.ts b/packages/adapter-slack/tsup.config.ts index a1bb0248..c4bd937c 100644 --- a/packages/adapter-slack/tsup.config.ts +++ b/packages/adapter-slack/tsup.config.ts @@ -6,5 +6,5 @@ export default defineConfig({ dts: true, clean: true, sourcemap: true, - external: ["@slack/web-api"], + external: ["@slack/web-api", "@slack/socket-mode"], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3a1f680..7f33f302 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,16 +45,10 @@ importers: specifier: ^3.1.16 version: 3.1.18 '@streamdown/cjk': - specifier: ^1.0.2 + specifier: ^1.0.1 version: 1.0.2(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.3)(unified@11.0.5) '@streamdown/code': - specifier: ^1.0.3 - version: 1.0.3(react@19.2.3) - '@streamdown/math': - specifier: ^1.0.2 - version: 1.0.2(react@19.2.3) - '@streamdown/mermaid': - specifier: ^1.0.2 + specifier: ^1.0.1 version: 1.0.2(react@19.2.3) '@vercel/analytics': specifier: ^1.6.1 @@ -132,8 +126,8 @@ importers: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) streamdown: - specifier: ^2.3.0 - version: 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^2.1.0 + version: 2.2.0(react@19.2.3) tailwind-merge: specifier: ^3.4.0 version: 3.4.1 @@ -204,9 +198,6 @@ importers: '@chat-adapter/telegram': specifier: workspace:* version: link:../../packages/adapter-telegram - '@chat-adapter/whatsapp': - specifier: workspace:* - version: link:../../packages/adapter-whatsapp ai: specifier: ^6.0.5 version: 6.0.6(zod@4.3.3) @@ -381,9 +372,12 @@ importers: '@chat-adapter/shared': specifier: workspace:* version: link:../adapter-shared + '@slack/socket-mode': + specifier: ^2.0.5 + version: 2.0.5 '@slack/web-api': - specifier: ^7.14.0 - version: 7.14.1 + specifier: ^7.11.0 + version: 7.13.0 chat: specifier: workspace:* version: link:../chat @@ -412,9 +406,6 @@ importers: botbuilder: specifier: ^4.23.1 version: 4.23.3 - botframework-connector: - specifier: ^4.23.3 - version: 4.23.3 chat: specifier: workspace:* version: link:../chat @@ -457,28 +448,6 @@ importers: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) - packages/adapter-whatsapp: - dependencies: - '@chat-adapter/shared': - specifier: workspace:* - version: link:../adapter-shared - chat: - specifier: workspace:* - version: link:../chat - devDependencies: - '@types/node': - specifier: ^25.3.2 - version: 25.3.2 - tsup: - specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) - typescript: - specifier: ^5.7.2 - version: 5.9.3 - vitest: - specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) - packages/chat: dependencies: '@workflow/serde': @@ -496,9 +465,6 @@ importers: remark-stringify: specifier: ^11.0.0 version: 11.0.0 - remend: - specifier: ^1.2.1 - version: 1.2.1 unified: specifier: ^11.0.5 version: 11.0.5 @@ -539,9 +505,6 @@ importers: '@chat-adapter/telegram': specifier: workspace:* version: link:../adapter-telegram - '@chat-adapter/whatsapp': - specifier: workspace:* - version: link:../adapter-whatsapp chat: specifier: workspace:* version: link:../chat @@ -600,34 +563,6 @@ importers: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) - packages/state-pg: - dependencies: - chat: - specifier: workspace:* - version: link:../chat - pg: - specifier: ^8.20.0 - version: 8.20.0 - devDependencies: - '@types/node': - specifier: ^25.3.2 - version: 25.3.2 - '@types/pg': - specifier: ^8.18.0 - version: 8.18.0 - '@vitest/coverage-v8': - specifier: ^4.0.18 - version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) - tsup: - specifier: ^8.3.5 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) - typescript: - specifier: ^5.7.2 - version: 5.9.3 - vitest: - specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) - packages/state-redis: dependencies: chat: @@ -2580,12 +2515,16 @@ packages: resolution: {integrity: sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==} engines: {node: '>= 18', npm: '>= 8.6.0'} - '@slack/types@2.20.0': - resolution: {integrity: sha512-PVF6P6nxzDMrzPC8fSCsnwaI+kF8YfEpxf3MqXmdyjyWTYsZQURpkK7WWUWvP5QpH55pB7zyYL9Qem/xSgc5VA==} + '@slack/socket-mode@2.0.5': + resolution: {integrity: sha512-VaapvmrAifeFLAFaDPfGhEwwunTKsI6bQhYzxRXw7BSujZUae5sANO76WqlVsLXuhVtCVrBWPiS2snAQR2RHJQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/types@2.19.0': + resolution: {integrity: sha512-7+QZ38HGcNh/b/7MpvPG6jnw7mliV6UmrquJLqgdxkzJgQEYUcEztvFWRU49z0x4vthF0ixL5lTK601AXrS8IA==} engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - '@slack/web-api@7.14.1': - resolution: {integrity: sha512-RoygyteJeFswxDPJjUMESn9dldWVMD2xUcHHd9DenVavSfVC6FeVnSdDerOO7m8LLvw4Q132nQM4hX8JiF7dng==} + '@slack/web-api@7.13.0': + resolution: {integrity: sha512-ERcExbWrnkDN8ovoWWe6Wgt/usanj1dWUd18dJLpctUI4mlPS0nKt81Joh8VI+OPbNnY1lIilVt9gdMBD9U2ig==} engines: {node: '>= 18', npm: '>= 8.6.0'} '@standard-schema/spec@1.1.0': @@ -2596,18 +2535,8 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 - '@streamdown/code@1.0.3': - resolution: {integrity: sha512-3Ym5TCLcGhrHY2qBaUVWpqNRtxnZvqh4Y5Qm/pTIKA4AmEWwAAoYjZnxG7mOsvOpWVWiDwETjUtchNL1XzQEAw==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - - '@streamdown/math@1.0.2': - resolution: {integrity: sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g==} - peerDependencies: - react: ^18.0.0 || ^19.0.0 - - '@streamdown/mermaid@1.0.2': - resolution: {integrity: sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw==} + '@streamdown/code@1.0.2': + resolution: {integrity: sha512-QKLS3sC8no5y0YvhGLA+ZjtNhznWU09IvFcjRKgSA35ulckMLw3b5T1ha+o1DaW8BS8l0zceLPFZa3/X9+agWQ==} peerDependencies: react: ^18.0.0 || ^19.0.0 @@ -2888,9 +2817,6 @@ packages: '@types/jsonwebtoken@9.0.6': resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} - '@types/katex@0.16.8': - resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -2906,9 +2832,6 @@ packages: '@types/node@25.3.2': resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} - '@types/pg@8.18.0': - resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==} - '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -3130,9 +3053,6 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} - axios@1.13.6: - resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} - bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -4050,21 +3970,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hast-util-from-dom@5.0.1: - resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} - - hast-util-from-html-isomorphic@2.0.0: - resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} - - hast-util-from-html@2.0.3: - resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} - hast-util-from-parse5@8.0.3: resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} - hast-util-is-element@3.0.0: - resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -4089,9 +3997,6 @@ packages: hast-util-to-string@3.0.1: resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} - hast-util-to-text@4.0.2: - resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} - hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -4560,9 +4465,6 @@ packages: mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} - mdast-util-math@3.0.0: - resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} - mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -4656,9 +4558,6 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - micromark-extension-math@3.1.0: - resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} - micromark-extension-mdx-expression@3.0.1: resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} @@ -4990,40 +4889,6 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pg-cloudflare@1.3.0: - resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} - - pg-connection-string@2.12.0: - resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} - - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-pool@3.13.0: - resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} - peerDependencies: - pg: '>=8.0' - - pg-protocol@1.13.0: - resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - - pg@8.20.0: - resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} - engines: {node: '>= 16.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true - - pgpass@1.0.5: - resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -5085,22 +4950,6 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-bytea@1.0.1: - resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -5240,11 +5089,8 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - rehype-harden@1.1.8: - resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} - - rehype-katex@7.0.1: - resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} + rehype-harden@1.1.7: + resolution: {integrity: sha512-j5DY0YSK2YavvNGV+qBHma15J9m0WZmRe8posT5AtKDS6TNWtMVTo6RiqF8SidfcASYz8f3k2J/1RWmq5zTXUw==} rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} @@ -5278,9 +5124,6 @@ packages: remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} - remark-math@6.0.0: - resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} - remark-mdx@3.1.1: resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} @@ -5296,8 +5139,8 @@ packages: remark@15.0.1: resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - remend@1.2.1: - resolution: {integrity: sha512-4wC12bgXsfKAjF1ewwkNIQz5sqewz/z1xgIgjEMb3r1pEytQ37F0Cm6i+OhbTWEvguJD7lhOUJhK5fSasw9f0w==} + remend@1.2.0: + resolution: {integrity: sha512-NbKrdWweTRuByPYErzQCNpNtsR9M1QQ0hK2UzmnmlSaEqHnkQ5Korlyi8KpdbOJ0rImJfRy4EAY0uDxYnL9Plw==} resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} @@ -5440,10 +5283,6 @@ packages: spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} - split2@4.2.0: - resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} - engines: {node: '>= 10.x'} - spotify-audio-element@1.0.4: resolution: {integrity: sha512-QdKrJPkYCzaNwwz2vN2eDGyoW0KmQFmnwVprB41mpMzj4qujbqr6pegEchQeTn0b5PceKiLoVu0pp2QDpTcWnw==} @@ -5459,11 +5298,10 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - streamdown@2.3.0: - resolution: {integrity: sha512-OqS3by/lt91lSicE8RQP2nTsYI6Q/dQgGP2vcyn9YesCmRHhNjswAuBAZA1z0F4+oBU3II/eV51LqjCqwTb1lw==} + streamdown@2.2.0: + resolution: {integrity: sha512-Y51o1I/sjpAy4Yn7j7R4TbUl9gcUZ7BTrHS+68IhrUBoYpNQZ28z06vww1MBFu4mSwvgF8xQIxIH2b9S9IHDyQ==} peerDependencies: react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -5706,9 +5544,6 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unist-util-find-after@5.0.0: - resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} - unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -5981,10 +5816,6 @@ packages: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - youtube-video-element@1.8.1: resolution: {integrity: sha512-+5UuAGaj+5AnBf39huLVpy/4dLtR0rmJP1TxOHVZ81bac4ZHFpTtQ4Dz2FAn2GPnfXISezvUEaQoAdFW4hH9Xg==} @@ -6755,7 +6586,7 @@ snapshots: unified: 11.0.5 unist-util-position-from-estree: 2.0.0 unist-util-stringify-position: 4.0.0 - unist-util-visit: 5.1.0 + unist-util-visit: 5.0.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color @@ -7941,15 +7772,28 @@ snapshots: dependencies: '@types/node': 25.3.2 - '@slack/types@2.20.0': {} + '@slack/socket-mode@2.0.5': + dependencies: + '@slack/logger': 4.0.0 + '@slack/web-api': 7.13.0 + '@types/node': 25.3.2 + '@types/ws': 8.18.1 + eventemitter3: 5.0.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + + '@slack/types@2.19.0': {} - '@slack/web-api@7.14.1': + '@slack/web-api@7.13.0': dependencies: '@slack/logger': 4.0.0 - '@slack/types': 2.20.0 + '@slack/types': 2.19.0 '@types/node': 25.3.2 '@types/retry': 0.12.0 - axios: 1.13.6 + axios: 1.13.2 eventemitter3: 5.0.1 form-data: 4.0.5 is-electron: 2.2.2 @@ -7967,32 +7811,18 @@ snapshots: react: 19.2.3 remark-cjk-friendly: 1.2.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) remark-cjk-friendly-gfm-strikethrough: 1.2.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) - unist-util-visit: 5.1.0 + unist-util-visit: 5.0.0 transitivePeerDependencies: - '@types/mdast' - micromark - micromark-util-types - unified - '@streamdown/code@1.0.3(react@19.2.3)': + '@streamdown/code@1.0.2(react@19.2.3)': dependencies: react: 19.2.3 shiki: 3.22.0 - '@streamdown/math@1.0.2(react@19.2.3)': - dependencies: - katex: 0.16.28 - react: 19.2.3 - rehype-katex: 7.0.1 - remark-math: 6.0.0 - transitivePeerDependencies: - - supports-color - - '@streamdown/mermaid@1.0.2(react@19.2.3)': - dependencies: - mermaid: 11.12.2 - react: 19.2.3 - '@svta/cml-608@1.0.1': {} '@svta/cml-cmcd@1.0.1(@svta/cml-cta@1.0.1(@svta/cml-structured-field-values@1.0.1(@svta/cml-utils@1.0.1))(@svta/cml-utils@1.0.1))(@svta/cml-structured-field-values@1.0.1(@svta/cml-utils@1.0.1))(@svta/cml-utils@1.0.1)': @@ -8257,8 +8087,6 @@ snapshots: dependencies: '@types/node': 25.3.2 - '@types/katex@0.16.8': {} - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -8273,12 +8101,6 @@ snapshots: dependencies: undici-types: 7.18.2 - '@types/pg@8.18.0': - dependencies: - '@types/node': 25.3.2 - pg-protocol: 1.13.0 - pg-types: 2.2.0 - '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -8462,14 +8284,6 @@ snapshots: transitivePeerDependencies: - debug - axios@1.13.6: - dependencies: - follow-redirects: 1.15.11 - form-data: 4.0.5 - proxy-from-env: 1.1.0 - transitivePeerDependencies: - - debug - bail@2.0.2: {} balanced-match@1.0.2: {} @@ -8560,7 +8374,7 @@ snapshots: '@azure/identity': 4.13.0 '@azure/msal-node': 2.16.3 '@types/jsonwebtoken': 9.0.6 - axios: 1.13.6 + axios: 1.13.2 base64url: 3.0.1 botbuilder-stdlib: 4.23.3-internal botframework-schema: 4.23.3 @@ -9547,28 +9361,6 @@ snapshots: dependencies: function-bind: 1.1.2 - hast-util-from-dom@5.0.1: - dependencies: - '@types/hast': 3.0.4 - hastscript: 9.0.1 - web-namespaces: 2.0.1 - - hast-util-from-html-isomorphic@2.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-from-dom: 5.0.1 - hast-util-from-html: 2.0.3 - unist-util-remove-position: 5.0.0 - - hast-util-from-html@2.0.3: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - hast-util-from-parse5: 8.0.3 - parse5: 7.3.0 - vfile: 6.0.3 - vfile-message: 4.0.3 - hast-util-from-parse5@8.0.3: dependencies: '@types/hast': 3.0.4 @@ -9580,10 +9372,6 @@ snapshots: vfile-location: 5.0.3 web-namespaces: 2.0.1 - hast-util-is-element@3.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -9599,7 +9387,7 @@ snapshots: mdast-util-to-hast: 13.2.1 parse5: 7.3.0 unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 + unist-util-visit: 5.0.0 vfile: 6.0.3 web-namespaces: 2.0.1 zwitch: 2.0.4 @@ -9679,13 +9467,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hast-util-to-text@4.0.2: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - hast-util-is-element: 3.0.0 - unist-util-find-after: 5.0.0 - hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -10160,18 +9941,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-math@3.0.0: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - devlop: 1.1.0 - longest-streak: 3.1.0 - mdast-util-from-markdown: 2.0.2 - mdast-util-to-markdown: 2.1.2 - unist-util-remove-position: 5.0.0 - transitivePeerDependencies: - - supports-color - mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -10235,7 +10004,7 @@ snapshots: micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 + unist-util-visit: 5.0.0 vfile: 6.0.3 mdast-util-to-markdown@2.1.2: @@ -10247,7 +10016,7 @@ snapshots: mdast-util-to-string: 4.0.0 micromark-util-classify-character: 2.0.1 micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.1.0 + unist-util-visit: 5.0.0 zwitch: 2.0.4 mdast-util-to-string@4.0.0: @@ -10402,16 +10171,6 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-math@3.1.0: - dependencies: - '@types/katex': 0.16.8 - devlop: 1.1.0 - katex: 0.16.28 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - micromark-extension-mdx-expression@3.0.1: dependencies: '@types/estree': 1.0.8 @@ -10841,41 +10600,6 @@ snapshots: pathe@2.0.3: {} - pg-cloudflare@1.3.0: - optional: true - - pg-connection-string@2.12.0: {} - - pg-int8@1.0.1: {} - - pg-pool@3.13.0(pg@8.20.0): - dependencies: - pg: 8.20.0 - - pg-protocol@1.13.0: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.1 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - pg@8.20.0: - dependencies: - pg-connection-string: 2.12.0 - pg-pool: 3.13.0(pg@8.20.0) - pg-protocol: 1.13.0 - pg-types: 2.2.0 - pgpass: 1.0.5 - optionalDependencies: - pg-cloudflare: 1.3.0 - - pgpass@1.0.5: - dependencies: - split2: 4.2.0 - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -10930,16 +10654,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postgres-array@2.0.0: {} - - postgres-bytea@1.0.1: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - prettier@2.8.8: {} prop-types@15.8.1: @@ -11151,19 +10865,9 @@ snapshots: dependencies: regex-utilities: 2.3.0 - rehype-harden@1.1.8: + rehype-harden@1.1.7: dependencies: - unist-util-visit: 5.1.0 - - rehype-katex@7.0.1: - dependencies: - '@types/hast': 3.0.4 - '@types/katex': 0.16.8 - hast-util-from-html-isomorphic: 2.0.0 - hast-util-to-text: 4.0.2 - katex: 0.16.28 - unist-util-visit-parents: 6.0.2 - vfile: 6.0.3 + unist-util-visit: 5.0.0 rehype-raw@7.0.0: dependencies: @@ -11215,15 +10919,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-math@6.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-math: 3.0.0 - micromark-extension-math: 3.1.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - remark-mdx@3.1.1: dependencies: mdast-util-mdx: 3.0.0 @@ -11263,7 +10958,7 @@ snapshots: transitivePeerDependencies: - supports-color - remend@1.2.1: {} + remend@1.2.0: {} resolve-from@5.0.0: {} @@ -11445,8 +11140,6 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - split2@4.2.0: {} - spotify-audio-element@1.0.4: {} sprintf-js@1.0.3: {} @@ -11457,24 +11150,23 @@ snapshots: std-env@3.10.0: {} - streamdown@2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + streamdown@2.2.0(react@19.2.3): dependencies: clsx: 2.1.1 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 marked: 17.0.2 react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - rehype-harden: 1.1.8 + rehype-harden: 1.1.7 rehype-raw: 7.0.0 rehype-sanitize: 6.0.0 remark-gfm: 4.0.1 remark-parse: 11.0.0 remark-rehype: 11.1.2 - remend: 1.2.1 + remend: 1.2.0 tailwind-merge: 3.4.1 unified: 11.0.5 - unist-util-visit: 5.1.0 + unist-util-visit: 5.0.0 unist-util-visit-parents: 6.0.2 transitivePeerDependencies: - supports-color @@ -11695,11 +11387,6 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unist-util-find-after@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -11715,7 +11402,7 @@ snapshots: unist-util-remove-position@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-visit: 5.1.0 + unist-util-visit: 5.0.0 unist-util-stringify-position@4.0.0: dependencies: @@ -11929,8 +11616,6 @@ snapshots: dependencies: sax: 1.4.4 - xtend@4.0.2: {} - youtube-video-element@1.8.1: {} zod@3.25.76: {} From e2887af31e981efbbca1006d7ec2c84b87fb9ba0 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 15:44:45 -0800 Subject: [PATCH 02/19] Update config types and SlackAdapter class --- packages/adapter-slack/src/index.ts | 35 ++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index ebc419d4..5ec0456c 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -9,6 +9,7 @@ import { toBuffer, ValidationError, } from "@chat-adapter/shared"; +import { SocketModeClient } from "@slack/socket-mode"; import { WebClient } from "@slack/web-api"; import type { ActionEvent, @@ -91,7 +92,11 @@ function findNextMention(text: string): number { const SLACK_MESSAGE_URL_PATTERN = /^https?:\/\/[^/]+\.slack\.com\/archives\/([A-Z0-9]+)\/p(\d+)(?:\?.*)?$/; +export type SlackAdapterMode = "webhook" | "socket"; + export interface SlackAdapterConfig { + /** App-level token (xapp-...). Required for socket mode. */ + appToken?: string; /** Bot token (xoxb-...). Required for single-workspace mode. Omit for multi-workspace. */ botToken?: string; /** Bot user ID (will be fetched if not provided) */ @@ -112,6 +117,8 @@ export interface SlackAdapterConfig { installationKeyPrefix?: string; /** Logger instance for error reporting. Defaults to ConsoleLogger. */ logger?: Logger; + /** Connection mode: "webhook" (default) or "socket" */ + mode?: SlackAdapterMode; /** Signing secret for webhook verification. Defaults to SLACK_SIGNING_SECRET env var. */ signingSecret?: string; /** Override bot username (optional) */ @@ -375,6 +382,11 @@ export class SlackAdapter implements Adapter { private static CHANNEL_CACHE_TTL_MS = 8 * 24 * 60 * 60 * 1000; // 8 days private static REVERSE_INDEX_TTL_MS = 8 * 24 * 60 * 60 * 1000; // 8 days + // Socket mode support + private readonly appToken: string | undefined; + private readonly mode: SlackAdapterMode; + private socketClient: SocketModeClient | null = null; + // Multi-workspace support private readonly clientId: string | undefined; private readonly clientSecret: string | undefined; @@ -394,13 +406,17 @@ export class SlackAdapter implements Adapter { return this._botUserId || undefined; } + get isSocketMode(): boolean { + return this.mode === "socket"; + } + constructor(config: SlackAdapterConfig = {}) { const signingSecret = config.signingSecret ?? process.env.SLACK_SIGNING_SECRET; - if (!signingSecret) { + if (!signingSecret && (config.mode ?? "webhook") === "webhook") { throw new ValidationError( "slack", - "signingSecret is required. Set SLACK_SIGNING_SECRET or provide it in config." + "signingSecret is required for webhook mode. Set SLACK_SIGNING_SECRET or provide it in config." ); } @@ -419,12 +435,15 @@ export class SlackAdapter implements Adapter { config.botToken ?? (zeroConfig ? process.env.SLACK_BOT_TOKEN : undefined); this.client = new WebClient(botToken); - this.signingSecret = signingSecret; + this.signingSecret = signingSecret ?? ""; this.defaultBotToken = botToken; this.logger = config.logger ?? new ConsoleLogger("info").child("slack"); this.userName = config.userName || "bot"; this._botUserId = config.botUserId || null; + this.appToken = config.appToken; + this.mode = config.mode ?? "webhook"; + this.clientId = config.clientId ?? (zeroConfig ? process.env.SLACK_CLIENT_ID : undefined); this.clientSecret = @@ -493,6 +512,10 @@ export class SlackAdapter implements Adapter { if (!this.defaultBotToken) { this.logger.info("Slack adapter initialized in multi-workspace mode"); } + + if (this.mode === "socket") { + await this.startSocketMode(); + } } // =========================================================================== @@ -812,6 +835,12 @@ export class SlackAdapter implements Adapter { request: Request, options?: WebhookOptions ): Promise { + if (this.mode === "socket") { + return new Response("Webhooks are disabled in socket mode", { + status: 405, + }); + } + const body = await request.text(); this.logger.debug("Slack webhook raw body", { body }); From ee31b623a4a8e5f5e4488ab095ed83bc3d87fe15 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 15:45:11 -0800 Subject: [PATCH 03/19] Add socket mode methods, extract interactive dispatch --- packages/adapter-slack/src/index.ts | 95 +++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 5ec0456c..a8f7a43a 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -986,6 +986,17 @@ export class SlackAdapter implements Adapter { return new Response("Invalid payload JSON", { status: 400 }); } + return this.dispatchInteractivePayload(payload, options); + } + + /** + * Dispatch a pre-parsed interactive payload to the correct handler. + * Used by both webhook and socket mode paths. + */ + private dispatchInteractivePayload( + payload: SlackInteractivePayload, + options?: WebhookOptions + ): Response | Promise { switch (payload.type) { case "block_actions": this.handleBlockActions(payload, options); @@ -1264,6 +1275,90 @@ export class SlackAdapter implements Adapter { return modal; } + // =========================================================================== + // Socket Mode + // =========================================================================== + + /** + * Start Socket Mode connection. + * Creates a SocketModeClient, registers event handlers, and connects. + */ + private async startSocketMode(): Promise { + if (!this.appToken) { + throw new ValidationError( + "slack", + "appToken is required for socket mode. Set SLACK_APP_TOKEN or provide it in config." + ); + } + + this.socketClient = new SocketModeClient({ appToken: this.appToken }); + + this.socketClient.on("slack_event", async ({ ack, body, retry_num }) => { + // Immediately ack to prevent retries + await ack(); + + // Skip retries + if (retry_num && retry_num > 0) { + this.logger.debug("Skipping socket mode retry", { retry_num }); + return; + } + + this.routeSocketEvent(body); + }); + + await this.socketClient.start(); + this.logger.info("Slack socket mode connected"); + } + + /** + * Route a socket mode event to the appropriate handler. + */ + private routeSocketEvent( + body: Record + ): void { + const type = body.type as string; + + switch (type) { + case "event_callback": + this.processEventPayload(body as unknown as SlackWebhookPayload); + break; + + case "slash_commands": { + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(body)) { + if (typeof value === "string") { + params.set(key, value); + } + } + this.handleSlashCommand(params); + break; + } + + case "interactive": { + const payload = body.payload as SlackInteractivePayload | undefined; + if (payload) { + this.dispatchInteractivePayload(payload); + } + break; + } + + default: + this.logger.debug("Unhandled socket mode event type", { type }); + } + } + + /** + * Disconnect the socket mode client. + * No-op if not connected. + */ + async disconnect(): Promise { + if (this.socketClient) { + await this.socketClient.disconnect(); + this.socketClient = null; + this.logger.info("Slack socket mode disconnected"); + } + } + private verifySignature( body: string, timestamp: string | null, From ebec0d66fc6486a079f9a28b9683da204a8aaca7 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 15:45:31 -0800 Subject: [PATCH 04/19] Update createSlackAdapter factory function --- packages/adapter-slack/src/index.ts | 58 ++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index a8f7a43a..85304cae 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -3980,8 +3980,62 @@ export class SlackAdapter implements Adapter { } } -export function createSlackAdapter(config?: SlackAdapterConfig): SlackAdapter { - return new SlackAdapter(config ?? {}); +export function createSlackAdapter( + config?: Partial +): SlackAdapter { + const mode = config?.mode ?? "webhook"; + const appToken = config?.appToken ?? process.env.SLACK_APP_TOKEN; + + if (mode === "socket") { + if (!appToken) { + throw new ValidationError( + "slack", + "appToken is required for socket mode. Set SLACK_APP_TOKEN or provide it in config." + ); + } + if (config?.clientId || config?.clientSecret) { + throw new ValidationError( + "slack", + "Multi-workspace (clientId/clientSecret) is not supported in socket mode." + ); + } + } + + const signingSecret = + config?.signingSecret ?? process.env.SLACK_SIGNING_SECRET; + if (mode === "webhook" && !signingSecret) { + throw new ValidationError( + "slack", + "signingSecret is required. Set SLACK_SIGNING_SECRET or provide it in config." + ); + } + + // Auth fields (botToken, clientId, clientSecret) are modal: botToken's + // presence selects single-workspace mode, its absence selects multi-workspace + // (per-team token lookup via installations). Only fall back to env vars + // in zero-config mode (no config provided at all). + const zeroConfig = !config; + + const resolved: SlackAdapterConfig = { + appToken, + mode, + signingSecret, + botToken: + config?.botToken ?? + (zeroConfig ? process.env.SLACK_BOT_TOKEN : undefined), + clientId: + config?.clientId ?? + (zeroConfig ? process.env.SLACK_CLIENT_ID : undefined), + clientSecret: + config?.clientSecret ?? + (zeroConfig ? process.env.SLACK_CLIENT_SECRET : undefined), + encryptionKey: config?.encryptionKey ?? process.env.SLACK_ENCRYPTION_KEY, + installationKeyPrefix: config?.installationKeyPrefix, + logger: config?.logger ?? new ConsoleLogger("info").child("slack"), + userName: config?.userName, + botUserId: config?.botUserId, + }; + return new SlackAdapter(resolved); } // Re-export card converter for advanced use From 349eef8a04743a4c9b7050e9cdb6c36337ee597f Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 15:48:22 -0800 Subject: [PATCH 05/19] Write tests for socket mode --- packages/adapter-slack/src/index.test.ts | 305 +++++++++++++++++++++++ 1 file changed, 305 insertions(+) diff --git a/packages/adapter-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts index 91cd0989..a3578572 100644 --- a/packages/adapter-slack/src/index.test.ts +++ b/packages/adapter-slack/src/index.test.ts @@ -16,6 +16,25 @@ import { createSlackAdapter, SlackAdapter } from "./index"; const FILE_ID_PATTERN = /^file-/; +// Mock @slack/socket-mode +const mockSocketStart = vi.fn().mockResolvedValue({}); +const mockSocketDisconnect = vi.fn().mockResolvedValue(undefined); +const mockSocketOn = vi.fn(); + +vi.mock("@slack/socket-mode", () => { + return { + SocketModeClient: class MockSocketModeClient { + start = mockSocketStart; + disconnect = mockSocketDisconnect; + on = mockSocketOn; + constructor(_opts: Record) { + MockSocketModeClient.lastOpts = _opts; + } + static lastOpts: Record = {}; + }, + }; +}); + const mockLogger: Logger = { debug: vi.fn(), info: vi.fn(), @@ -4982,3 +5001,289 @@ describe("reverse user lookup", () => { }); }); }); + +// ============================================================================ +// Socket Mode Tests +// ============================================================================ + +describe("socket mode - factory validation", () => { + it("throws without appToken in socket mode", () => { + expect(() => + createSlackAdapter({ + mode: "socket", + botToken: "xoxb-test-token", + logger: mockLogger, + }) + ).toThrow(ValidationError); + }); + + it("creates adapter with appToken in socket mode", () => { + const adapter = createSlackAdapter({ + mode: "socket", + appToken: "xapp-test-token", + botToken: "xoxb-test-token", + logger: mockLogger, + }); + expect(adapter).toBeInstanceOf(SlackAdapter); + expect(adapter.isSocketMode).toBe(true); + }); + + it("does not require signingSecret in socket mode", () => { + const adapter = createSlackAdapter({ + mode: "socket", + appToken: "xapp-test-token", + botToken: "xoxb-test-token", + logger: mockLogger, + }); + expect(adapter).toBeInstanceOf(SlackAdapter); + }); + + it("rejects multi-workspace config in socket mode", () => { + expect(() => + createSlackAdapter({ + mode: "socket", + appToken: "xapp-test-token", + botToken: "xoxb-test-token", + clientId: "client-id", + clientSecret: "client-secret", + logger: mockLogger, + }) + ).toThrow(ValidationError); + }); + + it("isSocketMode returns false for webhook mode", () => { + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: "test-secret", + logger: mockLogger, + }); + expect(adapter.isSocketMode).toBe(false); + }); +}); + +describe("socket mode - handleWebhook", () => { + it("returns 405 in socket mode", async () => { + const adapter = createSlackAdapter({ + mode: "socket", + appToken: "xapp-test-token", + botToken: "xoxb-test-token", + logger: mockLogger, + }); + + const request = new Request("https://example.com/webhook", { + method: "POST", + body: "{}", + }); + const response = await adapter.handleWebhook(request); + expect(response.status).toBe(405); + }); +}); + +describe("socket mode - initialize", () => { + it("creates SocketModeClient and starts on initialize", async () => { + const { SocketModeClient: MockedClient } = await import( + "@slack/socket-mode" + ); + + mockSocketStart.mockClear(); + mockSocketOn.mockClear(); + + const state = createMockState(); + const adapter = createSlackAdapter({ + mode: "socket", + appToken: "xapp-test-token", + botToken: "xoxb-test-token", + logger: mockLogger, + }); + + await adapter.initialize(createMockChatInstance(state)); + + expect( + (MockedClient as unknown as { lastOpts: Record }) + .lastOpts + ).toEqual({ + appToken: "xapp-test-token", + }); + expect(mockSocketOn).toHaveBeenCalledWith( + "slack_event", + expect.any(Function) + ); + expect(mockSocketStart).toHaveBeenCalled(); + }); +}); + +describe("socket mode - routeSocketEvent", () => { + async function createSocketAdapter() { + mockSocketStart.mockClear(); + mockSocketOn.mockClear(); + mockSocketDisconnect.mockClear(); + + const state = createMockState(); + const chatInstance = createMockChatInstance(state); + const adapter = createSlackAdapter({ + mode: "socket", + appToken: "xapp-test-token", + botToken: "xoxb-test-token", + logger: mockLogger, + }); + + await adapter.initialize(chatInstance); + + // Extract the slack_event handler registered via on() + const slackEventHandler = mockSocketOn.mock.calls.find( + (call: unknown[]) => call[0] === "slack_event" + )?.[1] as (args: { + ack: () => Promise; + body: Record; + retry_num?: number; + }) => Promise; + + return { adapter, chatInstance, slackEventHandler }; + } + + it("dispatches event_callback to processMessage", async () => { + const { chatInstance, slackEventHandler } = await createSocketAdapter(); + + await slackEventHandler({ + ack: vi.fn().mockResolvedValue(undefined), + body: { + type: "event_callback", + event: { + type: "message", + channel: "C123", + ts: "1234567890.123456", + text: "hello from socket", + user: "U_USER", + }, + }, + }); + + expect(chatInstance.processMessage).toHaveBeenCalled(); + }); + + it("dispatches slash_commands to processSlashCommand", async () => { + const { chatInstance, slackEventHandler } = await createSocketAdapter(); + + await slackEventHandler({ + ack: vi.fn().mockResolvedValue(undefined), + body: { + type: "slash_commands", + command: "/test", + text: "arg1", + user_id: "U_USER", + channel_id: "C123", + }, + }); + + // handleSlashCommand is async (user lookup), wait for it to complete + await vi.waitFor(() => { + expect(chatInstance.processSlashCommand).toHaveBeenCalled(); + }); + }); + + it("dispatches interactive payloads to processAction", async () => { + const { chatInstance, slackEventHandler } = await createSocketAdapter(); + + await slackEventHandler({ + ack: vi.fn().mockResolvedValue(undefined), + body: { + type: "interactive", + payload: { + type: "block_actions", + actions: [ + { + type: "button", + action_id: "test_action", + value: "clicked", + }, + ], + channel: { id: "C123", name: "test" }, + container: { + type: "message", + message_ts: "1234567890.123456", + channel_id: "C123", + }, + message: { ts: "1234567890.123456" }, + trigger_id: "trigger123", + user: { id: "U_USER", username: "testuser" }, + }, + }, + }); + + expect(chatInstance.processAction).toHaveBeenCalled(); + }); + + it("skips retries", async () => { + const { chatInstance, slackEventHandler } = await createSocketAdapter(); + + await slackEventHandler({ + ack: vi.fn().mockResolvedValue(undefined), + body: { + type: "event_callback", + event: { + type: "message", + channel: "C123", + ts: "1234567890.123456", + text: "retried", + user: "U_USER", + }, + }, + retry_num: 1, + }); + + expect(chatInstance.processMessage).not.toHaveBeenCalled(); + }); + + it("acks immediately", async () => { + const { slackEventHandler } = await createSocketAdapter(); + const ack = vi.fn().mockResolvedValue(undefined); + + await slackEventHandler({ + ack, + body: { + type: "event_callback", + event: { + type: "message", + channel: "C123", + ts: "1234567890.123456", + text: "test", + user: "U_USER", + }, + }, + }); + + expect(ack).toHaveBeenCalled(); + }); +}); + +describe("socket mode - disconnect", () => { + it("calls socketClient.disconnect()", async () => { + mockSocketStart.mockClear(); + mockSocketOn.mockClear(); + mockSocketDisconnect.mockClear(); + + const state = createMockState(); + const adapter = createSlackAdapter({ + mode: "socket", + appToken: "xapp-test-token", + botToken: "xoxb-test-token", + logger: mockLogger, + }); + + await adapter.initialize(createMockChatInstance(state)); + await adapter.disconnect(); + + expect(mockSocketDisconnect).toHaveBeenCalled(); + }); + + it("is a no-op when not connected", async () => { + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: "test-secret", + logger: mockLogger, + }); + + // Should not throw + await adapter.disconnect(); + }); +}); From e64e36c197c0f5a4798c5c50f67c079f3e786691 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 15:49:13 -0800 Subject: [PATCH 06/19] Create slack-socket-mode.md --- .changeset/slack-socket-mode.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slack-socket-mode.md diff --git a/.changeset/slack-socket-mode.md b/.changeset/slack-socket-mode.md new file mode 100644 index 00000000..9815a63e --- /dev/null +++ b/.changeset/slack-socket-mode.md @@ -0,0 +1,5 @@ +--- +"@chat-adapter/slack": minor +--- + +Add Socket Mode support for environments behind firewalls that can't expose public HTTP endpoints From 816f971329874e02cc5aef66ca2baf99b35ea1a8 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 15:52:35 -0800 Subject: [PATCH 07/19] Run fix --- packages/adapter-slack/src/index.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 85304cae..d7ed3c66 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -1313,9 +1313,7 @@ export class SlackAdapter implements Adapter { /** * Route a socket mode event to the appropriate handler. */ - private routeSocketEvent( - body: Record - ): void { + private routeSocketEvent(body: Record): void { const type = body.type as string; switch (type) { From 4795773078706db049161b884d8ccf5f3426329f Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 15:56:58 -0800 Subject: [PATCH 08/19] Fix polynomial regex issues --- packages/adapter-slack/src/markdown.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/adapter-slack/src/markdown.ts b/packages/adapter-slack/src/markdown.ts index 6c2bc087..b465da0b 100644 --- a/packages/adapter-slack/src/markdown.ts +++ b/packages/adapter-slack/src/markdown.ts @@ -86,10 +86,13 @@ export class SlackFormatConverter extends BaseFormatConverter { markdown = markdown.replace(/<#([A-Z0-9_]+)>/g, "#$1"); // Links: -> [text](url) - markdown = markdown.replace(/<(https?:\/\/[^|>]+)\|([^>]+)>/g, "[$2]($1)"); + markdown = markdown.replace( + /<(https?:\/\/[^|<>]+)\|([^<>]+)>/g, + "[$2]($1)" + ); // Bare links: -> url - markdown = markdown.replace(/<(https?:\/\/[^>]+)>/g, "$1"); + markdown = markdown.replace(/<(https?:\/\/[^<>]+)>/g, "$1"); // Bold: *text* -> **text** (but be careful with emphasis) // This is tricky because Slack uses * for bold, not emphasis From a1d34bbb57fad54086ac8ee0a04b9de0da03f87a Mon Sep 17 00:00:00 2001 From: Vercel Date: Tue, 3 Mar 2026 00:04:45 +0000 Subject: [PATCH 09/19] Fix: Floating promises in `routeSocketEvent` for slash commands and interactive payloads can cause unhandled promise rejections that crash the Node.js process. This commit fixes the issue reported at packages/adapter-slack/src/index.ts:1152 **Bug Analysis:** In `routeSocketEvent` (line 1150), which is a synchronous `void` method, two async operations produce floating promises: 1. `this.handleSlashCommand(params)` (line 1165) - `handleSlashCommand` is `async` and always returns a `Promise`. It calls `await this.lookupUser(userId)` which internally calls `await this.chat.getState().get()` (before the try/catch around the API call), and `this.chat.processSlashCommand()`. Any of these could throw. 2. `this.dispatchInteractivePayload(payload)` (line 1172) - Returns `Response | Promise`. When the payload type is `view_submission`, it delegates to `async handleViewSubmission()`, which calls `await this.chat.processModalSubmit()` and accesses `payload.view.state.values` (which could throw on malformed payloads). Since `routeSocketEvent` is synchronous (`void` return type) and called from a sync context within the socket mode event handler (after `await ack()` has already completed), these returned promises are fire-and-forget. If any reject, it triggers an unhandled promise rejection, which in Node.js 15+ terminates the process by default. In contrast, in the webhook code path (`handleWebhook`), these same methods are always `return`-ed from async functions, so their promises are properly chained to the caller. **Fix:** Added `.catch()` handlers to both floating promises: 1. For `handleSlashCommand`: Added `.catch()` that logs the error via `this.logger.error`. 2. For `dispatchInteractivePayload`: Since it returns `Response | Promise` (only a Promise for `view_submission`), used `instanceof Promise` to conditionally attach a `.catch()` handler only when the result is a Promise. This approach was chosen over making `routeSocketEvent` async because: (a) it doesn't change the method signature, (b) the caller doesn't need to await it (the ack has already been sent), and (c) errors are logged rather than silently swallowed. Co-authored-by: Vercel Co-authored-by: haydenbleasel --- packages/adapter-slack/src/index.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index d7ed3c66..25cadd07 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -1328,14 +1328,26 @@ export class SlackAdapter implements Adapter { params.set(key, value); } } - this.handleSlashCommand(params); + this.handleSlashCommand(params).catch((error) => { + this.logger.error("Error handling slash command via socket mode", { + error, + }); + }); break; } case "interactive": { const payload = body.payload as SlackInteractivePayload | undefined; if (payload) { - this.dispatchInteractivePayload(payload); + const result = this.dispatchInteractivePayload(payload); + if (result instanceof Promise) { + result.catch((error) => { + this.logger.error( + "Error handling interactive payload via socket mode", + { error } + ); + }); + } } break; } From e4af0ae051cd86b4878dcda324a7ff3e1ba721b3 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 16:51:34 -0800 Subject: [PATCH 10/19] Add socket mode forwarding support to Slack adapter - Export SlackForwardedSocketEvent type - Add x-slack-socket-token check at top of handleWebhook() for forwarded events - Update routeSocketEvent() to accept WebhookOptions and use waitUntil - Add startSocketModeListener(), runSocketModeListener(), forwardSocketEvent() Co-Authored-By: Claude Opus 4.6 --- packages/adapter-slack/src/index.ts | 233 ++++++++++++++++++++++++++-- 1 file changed, 219 insertions(+), 14 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 25cadd07..471c9282 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -94,6 +94,13 @@ const SLACK_MESSAGE_URL_PATTERN = export type SlackAdapterMode = "webhook" | "socket"; +/** Envelope for events forwarded from a socket mode listener via HTTP POST */ +export interface SlackForwardedSocketEvent { + body: Record; + timestamp: number; + type: "socket_event"; +} + export interface SlackAdapterConfig { /** App-level token (xapp-...). Required for socket mode. */ appToken?: string; @@ -835,6 +842,24 @@ export class SlackAdapter implements Adapter { request: Request, options?: WebhookOptions ): Promise { + // Check for forwarded socket mode events (from external socket listener) + const socketToken = request.headers.get("x-slack-socket-token"); + if (socketToken) { + if (!this.appToken || socketToken !== this.appToken) { + this.logger.warn("Invalid socket forwarding token"); + return new Response("Invalid socket token", { status: 401 }); + } + this.logger.info("Slack forwarded socket event received"); + try { + const body = await request.text(); + const event = JSON.parse(body) as SlackForwardedSocketEvent; + this.routeSocketEvent(event.body, options); + return new Response("ok", { status: 200 }); + } catch { + return new Response("Invalid JSON", { status: 400 }); + } + } + if (this.mode === "socket") { return new Response("Webhooks are disabled in socket mode", { status: 405, @@ -1313,12 +1338,28 @@ export class SlackAdapter implements Adapter { /** * Route a socket mode event to the appropriate handler. */ - private routeSocketEvent(body: Record): void { + private routeSocketEvent( + body: Record, + options?: WebhookOptions + ): void { const type = body.type as string; + const wrapAsync = (promise: Promise): void => { + if (options?.waitUntil) { + options.waitUntil(promise); + } else { + promise.catch((error) => { + this.logger.error("Error in socket mode async handler", { error }); + }); + } + }; + switch (type) { case "event_callback": - this.processEventPayload(body as unknown as SlackWebhookPayload); + this.processEventPayload( + body as unknown as SlackWebhookPayload, + options + ); break; case "slash_commands": { @@ -1328,25 +1369,16 @@ export class SlackAdapter implements Adapter { params.set(key, value); } } - this.handleSlashCommand(params).catch((error) => { - this.logger.error("Error handling slash command via socket mode", { - error, - }); - }); + wrapAsync(this.handleSlashCommand(params, options)); break; } case "interactive": { const payload = body.payload as SlackInteractivePayload | undefined; if (payload) { - const result = this.dispatchInteractivePayload(payload); + const result = this.dispatchInteractivePayload(payload, options); if (result instanceof Promise) { - result.catch((error) => { - this.logger.error( - "Error handling interactive payload via socket mode", - { error } - ); - }); + wrapAsync(result); } } break; @@ -1357,6 +1389,179 @@ export class SlackAdapter implements Adapter { } } + /** + * Start a transient Socket Mode listener for serverless environments. + * The listener maintains a WebSocket for `durationMs`, acks events, and + * forwards them via HTTP POST to the webhook endpoint (or processes directly). + * + * @param options - Webhook options with waitUntil function + * @param durationMs - How long to keep listening (default: 180000ms = 3 minutes) + * @param abortSignal - Optional signal to stop the listener early + * @param webhookUrl - URL to forward socket events to (required for forwarding mode) + */ + async startSocketModeListener( + options: WebhookOptions, + durationMs = 180000, + abortSignal?: AbortSignal, + webhookUrl?: string + ): Promise { + if (!this.appToken) { + return new Response("appToken is required for socket mode listener", { + status: 500, + }); + } + + if (!options.waitUntil) { + return new Response("waitUntil not provided", { status: 500 }); + } + + this.logger.info("Starting Slack socket mode listener", { + durationMs, + webhookUrl: webhookUrl ? "configured" : "not configured", + }); + + const listenerPromise = this.runSocketModeListener( + durationMs, + abortSignal, + webhookUrl, + options + ); + + options.waitUntil(listenerPromise); + + return new Response( + JSON.stringify({ + status: "listening", + durationMs, + message: `Socket mode listener started, will run for ${durationMs / 1000} seconds`, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + } + ); + } + + /** + * Run the socket mode listener for a specified duration. + */ + private async runSocketModeListener( + durationMs: number, + abortSignal?: AbortSignal, + webhookUrl?: string, + options?: WebhookOptions + ): Promise { + // appToken is guaranteed to exist — callers check before invoking + const appToken = this.appToken as string; + const client = new SocketModeClient({ appToken }); + let isShuttingDown = false; + + client.on("slack_event", async ({ ack, body, retry_num }) => { + if (isShuttingDown) { + return; + } + + await ack(); + + if (retry_num && retry_num > 0) { + this.logger.debug("Skipping socket mode retry", { retry_num }); + return; + } + + if (webhookUrl) { + await this.forwardSocketEvent(webhookUrl, { + type: "socket_event", + body: body as Record, + timestamp: Date.now(), + }); + } else { + this.routeSocketEvent(body as Record, options); + } + }); + + try { + await client.start(); + this.logger.info("Slack socket mode listener connected"); + + await new Promise((resolve) => { + const timeout = setTimeout(resolve, durationMs); + + if (abortSignal) { + if (abortSignal.aborted) { + clearTimeout(timeout); + resolve(); + return; + } + abortSignal.addEventListener( + "abort", + () => { + this.logger.info( + "Slack socket mode listener received abort signal" + ); + clearTimeout(timeout); + resolve(); + }, + { once: true } + ); + } + }); + + this.logger.info( + "Slack socket mode listener duration elapsed, disconnecting" + ); + } catch (error) { + this.logger.error("Slack socket mode listener error", { + error: String(error), + }); + } finally { + isShuttingDown = true; + await client.disconnect(); + this.logger.info("Slack socket mode listener stopped"); + } + } + + /** + * Forward a socket mode event to the webhook endpoint. + */ + private async forwardSocketEvent( + webhookUrl: string, + event: SlackForwardedSocketEvent + ): Promise { + try { + this.logger.debug("Forwarding socket event to webhook", { + type: (event.body.type as string) || "unknown", + webhookUrl, + }); + + const response = await fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-slack-socket-token": this.appToken as string, + }, + body: JSON.stringify(event), + }); + + if (response.ok) { + this.logger.debug("Socket event forwarded successfully", { + type: (event.body.type as string) || "unknown", + }); + } else { + const errorText = await response.text(); + this.logger.error("Failed to forward socket event", { + type: (event.body.type as string) || "unknown", + status: response.status, + error: errorText, + }); + } + } catch (error) { + this.logger.error("Error forwarding socket event", { + type: (event.body.type as string) || "unknown", + error: String(error), + }); + } + } + /** * Disconnect the socket mode client. * No-op if not connected. From d84af36d68a0ec44f5ec5fb938634e941cab1f1b Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 16:51:38 -0800 Subject: [PATCH 11/19] Add tests for socket mode forwarding - Forwarded event accepted/rejected based on appToken - Bypasses signature verification for forwarded events - Options passthrough to handlers - startSocketModeListener returns 200/500 appropriately Co-Authored-By: Claude Opus 4.6 --- packages/adapter-slack/src/index.test.ts | 289 +++++++++++++++++++++++ 1 file changed, 289 insertions(+) diff --git a/packages/adapter-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts index a3578572..afc63325 100644 --- a/packages/adapter-slack/src/index.test.ts +++ b/packages/adapter-slack/src/index.test.ts @@ -5287,3 +5287,292 @@ describe("socket mode - disconnect", () => { await adapter.disconnect(); }); }); + +// ============================================================================ +// Socket Mode Forwarding Tests +// ============================================================================ + +describe("socket mode forwarding - handleWebhook", () => { + const secret = "test-signing-secret"; + const appToken = "xapp-forwarding-token"; + + it("accepts forwarded event with valid appToken", async () => { + const state = createMockState(); + const chatInstance = createMockChatInstance(state); + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + appToken, + logger: mockLogger, + }); + await adapter.initialize(chatInstance); + + const body = JSON.stringify({ + type: "socket_event", + body: { + type: "event_callback", + event: { + type: "message", + user: "U123", + channel: "C456", + text: "forwarded message", + ts: "1234567890.123456", + }, + }, + timestamp: Date.now(), + }); + + const request = new Request("https://example.com/webhook", { + method: "POST", + headers: { + "content-type": "application/json", + "x-slack-socket-token": appToken, + }, + body, + }); + + const response = await adapter.handleWebhook(request); + expect(response.status).toBe(200); + expect(chatInstance.processMessage).toHaveBeenCalled(); + }); + + it("rejects forwarded event with invalid token", async () => { + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + appToken, + logger: mockLogger, + }); + + const body = JSON.stringify({ + type: "socket_event", + body: { type: "event_callback" }, + timestamp: Date.now(), + }); + + const request = new Request("https://example.com/webhook", { + method: "POST", + headers: { + "content-type": "application/json", + "x-slack-socket-token": "wrong-token", + }, + body, + }); + + const response = await adapter.handleWebhook(request); + expect(response.status).toBe(401); + }); + + it("rejects forwarded event when no appToken configured", async () => { + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + logger: mockLogger, + }); + + const body = JSON.stringify({ + type: "socket_event", + body: { type: "event_callback" }, + timestamp: Date.now(), + }); + + const request = new Request("https://example.com/webhook", { + method: "POST", + headers: { + "content-type": "application/json", + "x-slack-socket-token": "any-token", + }, + body, + }); + + const response = await adapter.handleWebhook(request); + expect(response.status).toBe(401); + }); + + it("bypasses signature verification for forwarded events", async () => { + const state = createMockState(); + const chatInstance = createMockChatInstance(state); + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + appToken, + logger: mockLogger, + }); + await adapter.initialize(chatInstance); + + // No x-slack-request-timestamp or x-slack-signature headers + const body = JSON.stringify({ + type: "socket_event", + body: { + type: "event_callback", + event: { + type: "message", + user: "U123", + channel: "C456", + text: "no sig needed", + ts: "1234567890.123456", + }, + }, + timestamp: Date.now(), + }); + + const request = new Request("https://example.com/webhook", { + method: "POST", + headers: { + "content-type": "application/json", + "x-slack-socket-token": appToken, + }, + body, + }); + + const response = await adapter.handleWebhook(request); + expect(response.status).toBe(200); + }); + + it("passes options through to handlers for forwarded events", async () => { + const state = createMockState(); + const chatInstance = createMockChatInstance(state); + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + appToken, + logger: mockLogger, + }); + await adapter.initialize(chatInstance); + + const waitUntil = vi.fn(); + const body = JSON.stringify({ + type: "socket_event", + body: { + type: "event_callback", + event: { + type: "message", + user: "U123", + channel: "C456", + text: "with options", + ts: "1234567890.123456", + }, + }, + timestamp: Date.now(), + }); + + const request = new Request("https://example.com/webhook", { + method: "POST", + headers: { + "content-type": "application/json", + "x-slack-socket-token": appToken, + }, + body, + }); + + const response = await adapter.handleWebhook(request, { waitUntil }); + expect(response.status).toBe(200); + // processMessage receives the options + expect(chatInstance.processMessage).toHaveBeenCalledWith( + adapter, + expect.any(String), + expect.any(Function), + { waitUntil } + ); + }); +}); + +describe("startSocketModeListener", () => { + const secret = "test-signing-secret"; + + it("returns 200 with valid config", async () => { + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + appToken: "xapp-test-token", + logger: mockLogger, + }); + + const waitUntil = vi.fn(); + const response = await adapter.startSocketModeListener({ waitUntil }, 1000); + + expect(response.status).toBe(200); + const body = await response.json(); + expect(body.status).toBe("listening"); + expect(waitUntil).toHaveBeenCalled(); + }); + + it("returns 500 without appToken", async () => { + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + logger: mockLogger, + }); + + const response = await adapter.startSocketModeListener( + { waitUntil: vi.fn() }, + 1000 + ); + + expect(response.status).toBe(500); + expect(await response.text()).toContain("appToken"); + }); + + it("returns 500 without waitUntil", async () => { + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + appToken: "xapp-test-token", + logger: mockLogger, + }); + + const response = await adapter.startSocketModeListener({}, 1000); + + expect(response.status).toBe(500); + expect(await response.text()).toContain("waitUntil"); + }); +}); + +describe("routeSocketEvent with options", () => { + async function createSocketAdapterWithOptions() { + mockSocketStart.mockClear(); + mockSocketOn.mockClear(); + mockSocketDisconnect.mockClear(); + + const state = createMockState(); + const chatInstance = createMockChatInstance(state); + const adapter = createSlackAdapter({ + mode: "socket", + appToken: "xapp-test-token", + botToken: "xoxb-test-token", + logger: mockLogger, + }); + + await adapter.initialize(chatInstance); + + const slackEventHandler = mockSocketOn.mock.calls.find( + (call: unknown[]) => call[0] === "slack_event" + )?.[1] as (args: { + ack: () => Promise; + body: Record; + retry_num?: number; + }) => Promise; + + return { adapter, chatInstance, slackEventHandler }; + } + + it("dispatches slash_commands with waitUntil wrapping", async () => { + const { chatInstance, slackEventHandler } = + await createSocketAdapterWithOptions(); + + await slackEventHandler({ + ack: vi.fn().mockResolvedValue(undefined), + body: { + type: "slash_commands", + command: "/test", + text: "arg1", + user_id: "U_USER", + channel_id: "C123", + }, + }); + + await vi.waitFor(() => { + expect(chatInstance.processSlashCommand).toHaveBeenCalled(); + }); + }); +}); From 62339d51dbd36de24bee7869aed5c2c56a86c54d Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 16:51:43 -0800 Subject: [PATCH 12/19] Add socket mode cron route and vercel config - New /api/slack/socket-mode route using createPersistentListener - Mirrors Discord gateway pattern (CRON_SECRET auth, Redis coordination) - Cron runs every 9 min, listener duration 10 min Co-Authored-By: Claude Opus 4.6 --- .../src/app/api/slack/socket-mode/route.ts | 92 +++++++++++++++++++ examples/nextjs-chat/vercel.json | 4 + 2 files changed, 96 insertions(+) create mode 100644 examples/nextjs-chat/src/app/api/slack/socket-mode/route.ts diff --git a/examples/nextjs-chat/src/app/api/slack/socket-mode/route.ts b/examples/nextjs-chat/src/app/api/slack/socket-mode/route.ts new file mode 100644 index 00000000..9963e6f1 --- /dev/null +++ b/examples/nextjs-chat/src/app/api/slack/socket-mode/route.ts @@ -0,0 +1,92 @@ +import { after } from "next/server"; +import { bot } from "@/lib/bot"; +import { createPersistentListener } from "@/lib/persistent-listener"; + +export const maxDuration = 800; + +// Default listener duration: 10 minutes +const DEFAULT_DURATION_MS = 600 * 1000; + +/** + * Persistent listener for Slack Socket Mode. + * Handles cross-instance coordination via Redis pub/sub. + */ +const slackSocketMode = createPersistentListener({ + name: "slack-socket-mode", + redisUrl: process.env.REDIS_URL, + defaultDurationMs: DEFAULT_DURATION_MS, + maxDurationMs: DEFAULT_DURATION_MS, +}); + +/** + * Start the Slack Socket Mode WebSocket listener. + * + * This endpoint is invoked by a Vercel cron job every 9 minutes to maintain + * continuous Socket Mode connectivity. Events are acked immediately and + * forwarded via HTTP POST to the existing webhook endpoint. + * + * Security: Requires CRON_SECRET validation. + * + * Usage: GET /api/slack/socket-mode + * Optional query param: ?duration=600000 (milliseconds, max 600000) + */ +export async function GET(request: Request): Promise { + const cronSecret = process.env.CRON_SECRET; + if (!cronSecret) { + console.error("[slack-socket-mode] CRON_SECRET not configured"); + return new Response("CRON_SECRET not configured", { status: 500 }); + } + const authHeader = request.headers.get("authorization"); + if (authHeader !== `Bearer ${cronSecret}`) { + console.log("[slack-socket-mode] Unauthorized: invalid CRON_SECRET"); + return new Response("Unauthorized", { status: 401 }); + } + + await bot.initialize(); + + const slack = bot.getAdapter("slack"); + if (!slack) { + console.log("[slack-socket-mode] Slack adapter not configured"); + return new Response("Slack adapter not configured", { status: 404 }); + } + + // Construct webhook URL for forwarding socket events + const baseUrl = + process.env.VERCEL_PROJECT_PRODUCTION_URL || + process.env.VERCEL_URL || + process.env.NEXT_PUBLIC_BASE_URL; + let webhookUrl: string | undefined; + if (baseUrl) { + const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET; + const queryParam = bypassSecret + ? `?x-vercel-protection-bypass=${bypassSecret}` + : ""; + webhookUrl = `https://${baseUrl}/api/webhooks/slack${queryParam}`; + } + + return slackSocketMode.start(request, { + afterTask: (task) => after(() => task), + run: async ({ abortSignal, durationMs, listenerId }) => { + console.log( + `[slack-socket-mode] Starting Socket Mode listener: ${listenerId}`, + { + webhookUrl: webhookUrl ? "configured" : "not configured", + durationMs, + } + ); + + const response = await slack.startSocketModeListener( + { waitUntil: (task: Promise) => after(() => task) }, + durationMs, + abortSignal, + webhookUrl + ); + + console.log( + `[slack-socket-mode] Socket Mode listener ${listenerId} completed with status: ${response.status}` + ); + + return response; + }, + }); +} diff --git a/examples/nextjs-chat/vercel.json b/examples/nextjs-chat/vercel.json index 92a6885a..ea74bb9d 100644 --- a/examples/nextjs-chat/vercel.json +++ b/examples/nextjs-chat/vercel.json @@ -4,6 +4,10 @@ { "path": "/api/discord/gateway", "schedule": "*/9 * * * *" + }, + { + "path": "/api/slack/socket-mode", + "schedule": "*/9 * * * *" } ] } From d98462b1fb710e48b3646b91c6f222b27acacb6d Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 17:00:43 -0800 Subject: [PATCH 13/19] Fix signingSecret defaulting to empty string in socket mode Make signingSecret optional (string | undefined) instead of falling back to "". verifySignature now returns false when no secret is configured, preventing HMAC with an empty key from silently passing. Co-Authored-By: Claude Opus 4.6 --- packages/adapter-slack/src/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 471c9282..14d32ee2 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -378,7 +378,7 @@ export class SlackAdapter implements Adapter { readonly userName: string; private readonly client: WebClient; - private readonly signingSecret: string; + private readonly signingSecret: string | undefined; private readonly defaultBotToken: string | undefined; private chat: ChatInstance | null = null; private readonly logger: Logger; @@ -442,7 +442,7 @@ export class SlackAdapter implements Adapter { config.botToken ?? (zeroConfig ? process.env.SLACK_BOT_TOKEN : undefined); this.client = new WebClient(botToken); - this.signingSecret = signingSecret ?? ""; + this.signingSecret = signingSecret; this.defaultBotToken = botToken; this.logger = config.logger ?? new ConsoleLogger("info").child("slack"); this.userName = config.userName || "bot"; @@ -1579,7 +1579,7 @@ export class SlackAdapter implements Adapter { timestamp: string | null, signature: string | null ): boolean { - if (!(timestamp && signature)) { + if (!(timestamp && signature && this.signingSecret)) { return false; } From 62f83b517b22bb69eaec5bf740d6efa7f0d21903 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 17:01:44 -0800 Subject: [PATCH 14/19] Wrap event_callback in try-catch in routeSocketEvent Sync errors from processEventPayload were silently dropped in socket mode. Wrap with try-catch for parity with slash_commands and interactive cases. Co-Authored-By: Claude Opus 4.6 --- packages/adapter-slack/src/index.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 14d32ee2..092db6c5 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -1356,10 +1356,16 @@ export class SlackAdapter implements Adapter { switch (type) { case "event_callback": - this.processEventPayload( - body as unknown as SlackWebhookPayload, - options - ); + try { + this.processEventPayload( + body as unknown as SlackWebhookPayload, + options + ); + } catch (error) { + this.logger.error("Error processing socket mode event_callback", { + error, + }); + } break; case "slash_commands": { From c82ea9dbd2e50215c8b96204870e3c6f3c063323 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 17:03:43 -0800 Subject: [PATCH 15/19] Use dedicated socketForwardingSecret for forwarding auth Stop using the Slack app-level token (xapp-...) as the bearer token for HTTP forwarding. Adds socketForwardingSecret config option (auto-detected from SLACK_SOCKET_FORWARDING_SECRET) with fallback to appToken for backwards compatibility. Co-Authored-By: Claude Opus 4.6 --- packages/adapter-slack/src/index.test.ts | 69 ++++++++++++++++++++++++ packages/adapter-slack/src/index.ts | 12 ++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/adapter-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts index afc63325..827b529d 100644 --- a/packages/adapter-slack/src/index.test.ts +++ b/packages/adapter-slack/src/index.test.ts @@ -5389,6 +5389,75 @@ describe("socket mode forwarding - handleWebhook", () => { expect(response.status).toBe(401); }); + it("accepts forwarded event with dedicated socketForwardingSecret", async () => { + const forwardingSecret = "my-forwarding-secret"; + const state = createMockState(); + const chatInstance = createMockChatInstance(state); + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + appToken, + socketForwardingSecret: forwardingSecret, + logger: mockLogger, + }); + await adapter.initialize(chatInstance); + + const body = JSON.stringify({ + type: "socket_event", + body: { + type: "event_callback", + event: { + type: "message", + user: "U123", + channel: "C456", + text: "forwarded with dedicated secret", + ts: "1234567890.123456", + }, + }, + timestamp: Date.now(), + }); + + const request = new Request("https://example.com/webhook", { + method: "POST", + headers: { + "content-type": "application/json", + "x-slack-socket-token": forwardingSecret, + }, + body, + }); + + const response = await adapter.handleWebhook(request); + expect(response.status).toBe(200); + }); + + it("rejects forwarded event with appToken when socketForwardingSecret is set", async () => { + const adapter = createSlackAdapter({ + botToken: "xoxb-test-token", + signingSecret: secret, + appToken, + socketForwardingSecret: "my-forwarding-secret", + logger: mockLogger, + }); + + const body = JSON.stringify({ + type: "socket_event", + body: { type: "event_callback" }, + timestamp: Date.now(), + }); + + const request = new Request("https://example.com/webhook", { + method: "POST", + headers: { + "content-type": "application/json", + "x-slack-socket-token": appToken, + }, + body, + }); + + const response = await adapter.handleWebhook(request); + expect(response.status).toBe(401); + }); + it("bypasses signature verification for forwarded events", async () => { const state = createMockState(); const chatInstance = createMockChatInstance(state); diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 092db6c5..c635dbf1 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -128,6 +128,8 @@ export interface SlackAdapterConfig { mode?: SlackAdapterMode; /** Signing secret for webhook verification. Defaults to SLACK_SIGNING_SECRET env var. */ signingSecret?: string; + /** Shared secret for authenticating forwarded socket mode events. Auto-detected from SLACK_SOCKET_FORWARDING_SECRET. Falls back to appToken if not set. */ + socketForwardingSecret?: string; /** Override bot username (optional) */ userName?: string; } @@ -392,6 +394,7 @@ export class SlackAdapter implements Adapter { // Socket mode support private readonly appToken: string | undefined; private readonly mode: SlackAdapterMode; + private readonly socketForwardingSecret: string | undefined; private socketClient: SocketModeClient | null = null; // Multi-workspace support @@ -450,6 +453,8 @@ export class SlackAdapter implements Adapter { this.appToken = config.appToken; this.mode = config.mode ?? "webhook"; + this.socketForwardingSecret = + config.socketForwardingSecret ?? config.appToken; this.clientId = config.clientId ?? (zeroConfig ? process.env.SLACK_CLIENT_ID : undefined); @@ -845,7 +850,7 @@ export class SlackAdapter implements Adapter { // Check for forwarded socket mode events (from external socket listener) const socketToken = request.headers.get("x-slack-socket-token"); if (socketToken) { - if (!this.appToken || socketToken !== this.appToken) { + if (!this.socketForwardingSecret || socketToken !== this.socketForwardingSecret) { this.logger.warn("Invalid socket forwarding token"); return new Response("Invalid socket token", { status: 401 }); } @@ -1543,7 +1548,7 @@ export class SlackAdapter implements Adapter { method: "POST", headers: { "Content-Type": "application/json", - "x-slack-socket-token": this.appToken as string, + "x-slack-socket-token": this.socketForwardingSecret as string, }, body: JSON.stringify(event), }); @@ -4253,6 +4258,9 @@ export function createSlackAdapter( encryptionKey: config?.encryptionKey ?? process.env.SLACK_ENCRYPTION_KEY, installationKeyPrefix: config?.installationKeyPrefix, logger: config?.logger ?? new ConsoleLogger("info").child("slack"), + socketForwardingSecret: + config?.socketForwardingSecret ?? + process.env.SLACK_SOCKET_FORWARDING_SECRET, userName: config?.userName, botUserId: config?.botUserId, }; From d001452a636ff2bff65f705214cbdcef8cb72eb5 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 17:04:15 -0800 Subject: [PATCH 16/19] Replace double cast with type guard for socket event body Validate body.event exists and construct a properly typed SlackWebhookPayload instead of using `as unknown as`. Co-Authored-By: Claude Opus 4.6 --- packages/adapter-slack/src/index.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index c635dbf1..ff2fe2d1 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -1360,18 +1360,29 @@ export class SlackAdapter implements Adapter { }; switch (type) { - case "event_callback": + case "event_callback": { + if (!body.event || typeof body.event !== "object") { + this.logger.warn("Socket mode event_callback missing event field", { + body, + }); + break; + } + const payload: SlackWebhookPayload = { + type: body.type as string, + event: body.event as SlackWebhookPayload["event"], + team_id: body.team_id as string | undefined, + event_id: body.event_id as string | undefined, + event_time: body.event_time as number | undefined, + }; try { - this.processEventPayload( - body as unknown as SlackWebhookPayload, - options - ); + this.processEventPayload(payload, options); } catch (error) { this.logger.error("Error processing socket mode event_callback", { error, }); } break; + } case "slash_commands": { const params = new URLSearchParams(); From 2a9133767c8f490aa5765950d15e23e72d4fbdb6 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 17:04:37 -0800 Subject: [PATCH 17/19] Internalize SlackForwardedSocketEvent type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove export — only used internally by the forwarding mechanism. Co-Authored-By: Claude Opus 4.6 --- packages/adapter-slack/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index ff2fe2d1..636f1e24 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -95,7 +95,7 @@ const SLACK_MESSAGE_URL_PATTERN = export type SlackAdapterMode = "webhook" | "socket"; /** Envelope for events forwarded from a socket mode listener via HTTP POST */ -export interface SlackForwardedSocketEvent { +interface SlackForwardedSocketEvent { body: Record; timestamp: number; type: "socket_event"; From 9d8581c490a3351ce8fe27205a9b731e7f411f26 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 2 Mar 2026 17:05:45 -0800 Subject: [PATCH 18/19] Fix formatting in socketForwardingSecret check Co-Authored-By: Claude Opus 4.6 --- packages/adapter-slack/src/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 636f1e24..73f8e4c9 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -850,7 +850,10 @@ export class SlackAdapter implements Adapter { // Check for forwarded socket mode events (from external socket listener) const socketToken = request.headers.get("x-slack-socket-token"); if (socketToken) { - if (!this.socketForwardingSecret || socketToken !== this.socketForwardingSecret) { + if ( + !this.socketForwardingSecret || + socketToken !== this.socketForwardingSecret + ) { this.logger.warn("Invalid socket forwarding token"); return new Response("Invalid socket token", { status: 401 }); } From e8a3a97370298ba76767cc66f8480a04ede3342d Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Mon, 16 Mar 2026 13:28:16 -0700 Subject: [PATCH 19/19] Add socket mode documentation to Slack adapter README Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/adapter-slack/README.md | 89 ++++++- pnpm-lock.yaml | 419 ++++++++++++++++++++++++++++--- 2 files changed, 468 insertions(+), 40 deletions(-) diff --git a/packages/adapter-slack/README.md b/packages/adapter-slack/README.md index 13971cf9..f40634a3 100644 --- a/packages/adapter-slack/README.md +++ b/packages/adapter-slack/README.md @@ -98,6 +98,85 @@ openssl rand -base64 32 When `encryptionKey` is set, `setInstallation()` encrypts the token before storing and `getInstallation()` decrypts it transparently. +## Socket mode + +For environments behind firewalls that can't expose public HTTP endpoints, the adapter supports [Slack Socket Mode](https://api.slack.com/apis/socket-mode). Instead of receiving webhooks, the adapter connects to Slack over a WebSocket. + +```typescript +import { Chat } from "chat"; +import { createSlackAdapter } from "@chat-adapter/slack"; + +const bot = new Chat({ + userName: "mybot", + adapters: { + slack: createSlackAdapter({ + mode: "socket", + appToken: process.env.SLACK_APP_TOKEN!, + botToken: process.env.SLACK_BOT_TOKEN!, + }), + }, +}); +``` + +### Slack app setup for socket mode + +1. Go to your app's settings at [api.slack.com/apps](https://api.slack.com/apps) +2. Navigate to **Socket Mode** and enable it +3. Generate an **App-Level Token** with the `connections:write` scope — this is your `SLACK_APP_TOKEN` (`xapp-...`) +4. Event subscriptions and interactivity still need to be configured, but no public request URL is required + +> Socket mode is not compatible with multi-workspace OAuth (`clientId`/`clientSecret`). It's designed for single-workspace deployments. + +### Socket mode on serverless (Vercel) + +Socket mode requires a persistent WebSocket connection, which doesn't fit the request/response model of serverless functions. The adapter provides a forwarding mechanism to bridge this gap: + +1. A cron job periodically starts a transient socket listener +2. The listener connects via WebSocket, acks events immediately, and forwards them as HTTP requests to your webhook endpoint +3. Your existing webhook route processes the forwarded events normally + +```typescript +// api/slack/socket-mode/route.ts +import { bot, slackAdapter } from "@/lib/bot"; + +export const maxDuration = 800; + +export async function GET(request: Request) { + // Verify cron secret + const authHeader = request.headers.get("authorization"); + if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { + return new Response("Unauthorized", { status: 401 }); + } + + await bot.initialize(); + + const webhookUrl = `${process.env.VERCEL_URL}/api/webhooks/slack`; + + await slackAdapter.startSocketModeListener( + { forwardTo: webhookUrl }, + 600_000 // 10 minutes + ); + + return new Response("OK"); +} +``` + +Schedule the cron job to run every 9 minutes (overlapping with the 10-minute listener duration) to maintain continuous coverage: + +```json +// vercel.json +{ + "crons": [ + { + "path": "/api/slack/socket-mode", + "schedule": "*/9 * * * *" + } + ] +} +``` + +Forwarded events are authenticated using the `socketForwardingSecret` config option (defaults to `SLACK_SOCKET_FORWARDING_SECRET` env var, falling back to `appToken`). + ## Slack app setup ### 1. Create a Slack app from manifest @@ -184,19 +263,25 @@ All options are auto-detected from environment variables when not provided. You |--------|----------|-------------| | `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` | | `signingSecret` | No* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` | +| `mode` | No | Connection mode: `"webhook"` (default) or `"socket"` | +| `appToken` | No** | App-level token (`xapp-...`) for socket mode. Auto-detected from `SLACK_APP_TOKEN` | +| `socketForwardingSecret` | No | Shared secret for authenticating forwarded socket events. Auto-detected from `SLACK_SOCKET_FORWARDING_SECRET`, falls back to `appToken` | | `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` | | `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` | | `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` | | `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` | | `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) | -*`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var. +*`signingSecret` is required for webhook mode — either via config or `SLACK_SIGNING_SECRET` env var. +**`appToken` is required for socket mode — either via config or `SLACK_APP_TOKEN` env var. ## Environment variables ```bash SLACK_BOT_TOKEN=xoxb-... # Single-workspace only -SLACK_SIGNING_SECRET=... +SLACK_SIGNING_SECRET=... # Required for webhook mode +SLACK_APP_TOKEN=xapp-... # Required for socket mode +SLACK_SOCKET_FORWARDING_SECRET=... # Optional, for socket event forwarding auth SLACK_CLIENT_ID=... # Multi-workspace only SLACK_CLIENT_SECRET=... # Multi-workspace only SLACK_ENCRYPTION_KEY=... # Optional, for token encryption diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f33f302..b430132a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,10 +45,16 @@ importers: specifier: ^3.1.16 version: 3.1.18 '@streamdown/cjk': - specifier: ^1.0.1 + specifier: ^1.0.2 version: 1.0.2(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(react@19.2.3)(unified@11.0.5) '@streamdown/code': - specifier: ^1.0.1 + specifier: ^1.0.3 + version: 1.1.0(react@19.2.3) + '@streamdown/math': + specifier: ^1.0.2 + version: 1.0.2(react@19.2.3) + '@streamdown/mermaid': + specifier: ^1.0.2 version: 1.0.2(react@19.2.3) '@vercel/analytics': specifier: ^1.6.1 @@ -126,8 +132,8 @@ importers: specifier: ^2.0.7 version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) streamdown: - specifier: ^2.1.0 - version: 2.2.0(react@19.2.3) + specifier: ^2.3.0 + version: 2.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tailwind-merge: specifier: ^3.4.0 version: 3.4.1 @@ -198,6 +204,9 @@ importers: '@chat-adapter/telegram': specifier: workspace:* version: link:../../packages/adapter-telegram + '@chat-adapter/whatsapp': + specifier: workspace:* + version: link:../../packages/adapter-whatsapp ai: specifier: ^6.0.5 version: 6.0.6(zod@4.3.3) @@ -376,8 +385,8 @@ importers: specifier: ^2.0.5 version: 2.0.5 '@slack/web-api': - specifier: ^7.11.0 - version: 7.13.0 + specifier: ^7.14.0 + version: 7.15.0 chat: specifier: workspace:* version: link:../chat @@ -406,6 +415,9 @@ importers: botbuilder: specifier: ^4.23.1 version: 4.23.3 + botframework-connector: + specifier: ^4.23.3 + version: 4.23.3 chat: specifier: workspace:* version: link:../chat @@ -448,6 +460,28 @@ importers: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + packages/adapter-whatsapp: + dependencies: + '@chat-adapter/shared': + specifier: workspace:* + version: link:../adapter-shared + chat: + specifier: workspace:* + version: link:../chat + devDependencies: + '@types/node': + specifier: ^25.3.2 + version: 25.3.2 + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + packages/chat: dependencies: '@workflow/serde': @@ -465,6 +499,9 @@ importers: remark-stringify: specifier: ^11.0.0 version: 11.0.0 + remend: + specifier: ^1.2.1 + version: 1.2.2 unified: specifier: ^11.0.5 version: 11.0.5 @@ -505,6 +542,9 @@ importers: '@chat-adapter/telegram': specifier: workspace:* version: link:../adapter-telegram + '@chat-adapter/whatsapp': + specifier: workspace:* + version: link:../adapter-whatsapp chat: specifier: workspace:* version: link:../chat @@ -563,6 +603,34 @@ importers: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + packages/state-pg: + dependencies: + chat: + specifier: workspace:* + version: link:../chat + pg: + specifier: ^8.20.0 + version: 8.20.0 + devDependencies: + '@types/node': + specifier: ^25.3.2 + version: 25.3.2 + '@types/pg': + specifier: ^8.18.0 + version: 8.18.0 + '@vitest/coverage-v8': + specifier: ^4.0.18 + version: 4.0.18(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + packages/state-redis: dependencies: chat: @@ -2515,16 +2583,20 @@ packages: resolution: {integrity: sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==} engines: {node: '>= 18', npm: '>= 8.6.0'} + '@slack/logger@4.0.1': + resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + '@slack/socket-mode@2.0.5': resolution: {integrity: sha512-VaapvmrAifeFLAFaDPfGhEwwunTKsI6bQhYzxRXw7BSujZUae5sANO76WqlVsLXuhVtCVrBWPiS2snAQR2RHJQ==} engines: {node: '>= 18', npm: '>= 8.6.0'} - '@slack/types@2.19.0': - resolution: {integrity: sha512-7+QZ38HGcNh/b/7MpvPG6jnw7mliV6UmrquJLqgdxkzJgQEYUcEztvFWRU49z0x4vthF0ixL5lTK601AXrS8IA==} + '@slack/types@2.20.1': + resolution: {integrity: sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==} engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} - '@slack/web-api@7.13.0': - resolution: {integrity: sha512-ERcExbWrnkDN8ovoWWe6Wgt/usanj1dWUd18dJLpctUI4mlPS0nKt81Joh8VI+OPbNnY1lIilVt9gdMBD9U2ig==} + '@slack/web-api@7.15.0': + resolution: {integrity: sha512-va7zYIt3QHG1x9M/jqXXRPFMoOVlVSSRHC5YH+DzKYsrz5xUKOA3lR4THsu/Zxha9N1jOndbKFKLtr0WOPW1Vw==} engines: {node: '>= 18', npm: '>= 8.6.0'} '@standard-schema/spec@1.1.0': @@ -2535,8 +2607,18 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 - '@streamdown/code@1.0.2': - resolution: {integrity: sha512-QKLS3sC8no5y0YvhGLA+ZjtNhznWU09IvFcjRKgSA35ulckMLw3b5T1ha+o1DaW8BS8l0zceLPFZa3/X9+agWQ==} + '@streamdown/code@1.1.0': + resolution: {integrity: sha512-swypCjtE6vv01bnEtPeaw2ew9cbL2nbsLc06HAIK3K6nYXj5WDA8VLR6GEiwdh7HLIPt5dGze+PJ0eJVkqesug==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + '@streamdown/math@1.0.2': + resolution: {integrity: sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + '@streamdown/mermaid@1.0.2': + resolution: {integrity: sha512-Fr/4sBWnAeSnxM3PcrV/+DiZe5oPMq9gOkUIAH7ZauJeuwrZ/DVzD4g0zlav6AH0axh2m/sOfrfLtY5aLT7niw==} peerDependencies: react: ^18.0.0 || ^19.0.0 @@ -2817,6 +2899,9 @@ packages: '@types/jsonwebtoken@9.0.6': resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -2832,6 +2917,9 @@ packages: '@types/node@25.3.2': resolution: {integrity: sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==} + '@types/pg@8.18.0': + resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -3053,6 +3141,9 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axios@1.13.6: + resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -3970,9 +4061,21 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-from-dom@5.0.1: + resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} + + hast-util-from-html-isomorphic@2.0.0: + resolution: {integrity: sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + hast-util-from-parse5@8.0.3: resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} @@ -3997,6 +4100,9 @@ packages: hast-util-to-string@3.0.1: resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -4465,6 +4571,9 @@ packages: mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -4558,6 +4667,9 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + micromark-extension-mdx-expression@3.0.1: resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} @@ -4889,6 +5001,40 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.12.0: + resolution: {integrity: sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.13.0: + resolution: {integrity: sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.20.0: + resolution: {integrity: sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4950,6 +5096,22 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -5089,8 +5251,11 @@ packages: regex@6.1.0: resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - rehype-harden@1.1.7: - resolution: {integrity: sha512-j5DY0YSK2YavvNGV+qBHma15J9m0WZmRe8posT5AtKDS6TNWtMVTo6RiqF8SidfcASYz8f3k2J/1RWmq5zTXUw==} + rehype-harden@1.1.8: + resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} + + rehype-katex@7.0.1: + resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==} rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} @@ -5124,6 +5289,9 @@ packages: remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + remark-mdx@3.1.1: resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} @@ -5139,8 +5307,8 @@ packages: remark@15.0.1: resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} - remend@1.2.0: - resolution: {integrity: sha512-NbKrdWweTRuByPYErzQCNpNtsR9M1QQ0hK2UzmnmlSaEqHnkQ5Korlyi8KpdbOJ0rImJfRy4EAY0uDxYnL9Plw==} + remend@1.2.2: + resolution: {integrity: sha512-4ZJgIB9EG9fQE41mOJCRHMmnxDTKHWawQoJWZyUbZuj680wVyogu2ihnj8Edqm7vh2mo/TWHyEZpn2kqeDvS7w==} resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} @@ -5283,6 +5451,10 @@ packages: spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + spotify-audio-element@1.0.4: resolution: {integrity: sha512-QdKrJPkYCzaNwwz2vN2eDGyoW0KmQFmnwVprB41mpMzj4qujbqr6pegEchQeTn0b5PceKiLoVu0pp2QDpTcWnw==} @@ -5298,10 +5470,11 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - streamdown@2.2.0: - resolution: {integrity: sha512-Y51o1I/sjpAy4Yn7j7R4TbUl9gcUZ7BTrHS+68IhrUBoYpNQZ28z06vww1MBFu4mSwvgF8xQIxIH2b9S9IHDyQ==} + streamdown@2.4.0: + resolution: {integrity: sha512-fRk4HEYNznRLmxoVeT8wsGBwHF6/Yrdey6k+ZrE1Qtp4NyKwm7G/6e2Iw8penY4yLx31TlAHWT5Bsg1weZ9FZg==} peerDependencies: react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -5544,6 +5717,9 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -5816,6 +5992,10 @@ packages: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + youtube-video-element@1.8.1: resolution: {integrity: sha512-+5UuAGaj+5AnBf39huLVpy/4dLtR0rmJP1TxOHVZ81bac4ZHFpTtQ4Dz2FAn2GPnfXISezvUEaQoAdFW4hH9Xg==} @@ -6586,7 +6766,7 @@ snapshots: unified: 11.0.5 unist-util-position-from-estree: 2.0.0 unist-util-stringify-position: 4.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 transitivePeerDependencies: - supports-color @@ -7772,10 +7952,14 @@ snapshots: dependencies: '@types/node': 25.3.2 + '@slack/logger@4.0.1': + dependencies: + '@types/node': 25.3.2 + '@slack/socket-mode@2.0.5': dependencies: '@slack/logger': 4.0.0 - '@slack/web-api': 7.13.0 + '@slack/web-api': 7.15.0 '@types/node': 25.3.2 '@types/ws': 8.18.1 eventemitter3: 5.0.1 @@ -7785,15 +7969,15 @@ snapshots: - debug - utf-8-validate - '@slack/types@2.19.0': {} + '@slack/types@2.20.1': {} - '@slack/web-api@7.13.0': + '@slack/web-api@7.15.0': dependencies: - '@slack/logger': 4.0.0 - '@slack/types': 2.19.0 + '@slack/logger': 4.0.1 + '@slack/types': 2.20.1 '@types/node': 25.3.2 '@types/retry': 0.12.0 - axios: 1.13.2 + axios: 1.13.6 eventemitter3: 5.0.1 form-data: 4.0.5 is-electron: 2.2.2 @@ -7811,18 +7995,32 @@ snapshots: react: 19.2.3 remark-cjk-friendly: 1.2.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) remark-cjk-friendly-gfm-strikethrough: 1.2.3(@types/mdast@4.0.4)(micromark-util-types@2.0.2)(micromark@4.0.2)(unified@11.0.5) - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 transitivePeerDependencies: - '@types/mdast' - micromark - micromark-util-types - unified - '@streamdown/code@1.0.2(react@19.2.3)': + '@streamdown/code@1.1.0(react@19.2.3)': dependencies: react: 19.2.3 shiki: 3.22.0 + '@streamdown/math@1.0.2(react@19.2.3)': + dependencies: + katex: 0.16.28 + react: 19.2.3 + rehype-katex: 7.0.1 + remark-math: 6.0.0 + transitivePeerDependencies: + - supports-color + + '@streamdown/mermaid@1.0.2(react@19.2.3)': + dependencies: + mermaid: 11.12.2 + react: 19.2.3 + '@svta/cml-608@1.0.1': {} '@svta/cml-cmcd@1.0.1(@svta/cml-cta@1.0.1(@svta/cml-structured-field-values@1.0.1(@svta/cml-utils@1.0.1))(@svta/cml-utils@1.0.1))(@svta/cml-structured-field-values@1.0.1(@svta/cml-utils@1.0.1))(@svta/cml-utils@1.0.1)': @@ -8087,6 +8285,8 @@ snapshots: dependencies: '@types/node': 25.3.2 + '@types/katex@0.16.8': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -8101,6 +8301,12 @@ snapshots: dependencies: undici-types: 7.18.2 + '@types/pg@8.18.0': + dependencies: + '@types/node': 25.3.2 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -8284,6 +8490,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.13.6: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -9361,6 +9575,28 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-from-dom@5.0.1: + dependencies: + '@types/hast': 3.0.4 + hastscript: 9.0.1 + web-namespaces: 2.0.1 + + hast-util-from-html-isomorphic@2.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-dom: 5.0.1 + hast-util-from-html: 2.0.3 + unist-util-remove-position: 5.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.3 + parse5: 7.3.0 + vfile: 6.0.3 + vfile-message: 4.0.3 + hast-util-from-parse5@8.0.3: dependencies: '@types/hast': 3.0.4 @@ -9372,6 +9608,10 @@ snapshots: vfile-location: 5.0.3 web-namespaces: 2.0.1 + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-parse-selector@4.0.0: dependencies: '@types/hast': 3.0.4 @@ -9387,7 +9627,7 @@ snapshots: mdast-util-to-hast: 13.2.1 parse5: 7.3.0 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 web-namespaces: 2.0.1 zwitch: 2.0.4 @@ -9467,6 +9707,13 @@ snapshots: dependencies: '@types/hast': 3.0.4 + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -9941,6 +10188,18 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -10004,7 +10263,7 @@ snapshots: micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 unist-util-position: 5.0.0 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 vfile: 6.0.3 mdast-util-to-markdown@2.1.2: @@ -10016,7 +10275,7 @@ snapshots: mdast-util-to-string: 4.0.0 micromark-util-classify-character: 2.0.1 micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 zwitch: 2.0.4 mdast-util-to-string@4.0.0: @@ -10171,6 +10430,16 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.28 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + micromark-extension-mdx-expression@3.0.1: dependencies: '@types/estree': 1.0.8 @@ -10600,6 +10869,41 @@ snapshots: pathe@2.0.3: {} + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.12.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.13.0(pg@8.20.0): + dependencies: + pg: 8.20.0 + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.20.0: + dependencies: + pg-connection-string: 2.12.0 + pg-pool: 3.13.0(pg@8.20.0) + pg-protocol: 1.13.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -10654,6 +10958,16 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prettier@2.8.8: {} prop-types@15.8.1: @@ -10865,9 +11179,19 @@ snapshots: dependencies: regex-utilities: 2.3.0 - rehype-harden@1.1.7: + rehype-harden@1.1.8: dependencies: - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 + + rehype-katex@7.0.1: + dependencies: + '@types/hast': 3.0.4 + '@types/katex': 0.16.8 + hast-util-from-html-isomorphic: 2.0.0 + hast-util-to-text: 4.0.2 + katex: 0.16.28 + unist-util-visit-parents: 6.0.2 + vfile: 6.0.3 rehype-raw@7.0.0: dependencies: @@ -10919,6 +11243,15 @@ snapshots: transitivePeerDependencies: - supports-color + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-mdx@3.1.1: dependencies: mdast-util-mdx: 3.0.0 @@ -10958,7 +11291,7 @@ snapshots: transitivePeerDependencies: - supports-color - remend@1.2.0: {} + remend@1.2.2: {} resolve-from@5.0.0: {} @@ -11140,6 +11473,8 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + split2@4.2.0: {} + spotify-audio-element@1.0.4: {} sprintf-js@1.0.3: {} @@ -11150,23 +11485,24 @@ snapshots: std-env@3.10.0: {} - streamdown@2.2.0(react@19.2.3): + streamdown@2.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: clsx: 2.1.1 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 marked: 17.0.2 react: 19.2.3 - rehype-harden: 1.1.7 + react-dom: 19.2.3(react@19.2.3) + rehype-harden: 1.1.8 rehype-raw: 7.0.0 rehype-sanitize: 6.0.0 remark-gfm: 4.0.1 remark-parse: 11.0.0 remark-rehype: 11.1.2 - remend: 1.2.0 + remend: 1.2.2 tailwind-merge: 3.4.1 unified: 11.0.5 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 transitivePeerDependencies: - supports-color @@ -11387,6 +11723,11 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -11402,7 +11743,7 @@ snapshots: unist-util-remove-position@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-visit: 5.0.0 + unist-util-visit: 5.1.0 unist-util-stringify-position@4.0.0: dependencies: @@ -11616,6 +11957,8 @@ snapshots: dependencies: sax: 1.4.4 + xtend@4.0.2: {} + youtube-video-element@1.8.1: {} zod@3.25.76: {}