import { type RetryOptions, type WebClientOptions, WebClient } from "@slack/web-api"; import { HttpsProxyAgent } from "https-proxy-agent"; import { resolveEnvHttpProxyUrl } from "openclaw/plugin-sdk/infra-runtime"; export const SLACK_DEFAULT_RETRY_OPTIONS: RetryOptions = { retries: 2, factor: 2, minTimeout: 500, maxTimeout: 3000, randomize: true, }; export const SLACK_WRITE_RETRY_OPTIONS: RetryOptions = { retries: 0, }; /** * Check whether a hostname is excluded from proxying by `NO_PROXY` / `no_proxy`. * Supports comma-separated entries with optional leading dots (e.g. `.slack.com`). */ function isHostExcludedByNoProxy(hostname: string, env: NodeJS.ProcessEnv = process.env): boolean { const raw = env.no_proxy ?? env.NO_PROXY; if (!raw) { return false; } const entries = raw .split(/[,\s]+/) .map((e) => e.trim().toLowerCase()) .filter(Boolean); const lower = hostname.toLowerCase(); for (const entry of entries) { if (entry === "*") { return true; } // Strip optional wildcard/leading dot so `*.slack.com` and `.slack.com` // match both `slack.com` (apex) and Slack subdomains. const bare = entry.startsWith("*.") ? entry.slice(2) : entry.startsWith(".") ? entry.slice(1) : entry; if (lower === bare || lower.endsWith(`.${bare}`)) { return true; } } return false; } /** * Build an HTTPS proxy agent from env vars (HTTPS_PROXY, HTTP_PROXY, etc.) * for use as the `agent` option in Slack WebClient and Socket Mode connections. * * When set, this agent is forwarded through @slack/bolt → @slack/socket-mode → * SlackWebSocket as the `httpAgent`, which the `ws` library uses to tunnel the * WebSocket upgrade request through the proxy. This fixes Socket Mode in * environments where outbound traffic must go through an HTTP CONNECT proxy. * * Respects `NO_PROXY` / `no_proxy` — if `*.slack.com` (or a matching pattern) * appears in the exclusion list, returns `undefined` so the connection is direct. * * Returns `undefined` when no proxy env var is configured or when Slack hosts * are excluded by `NO_PROXY`. */ function resolveSlackProxyAgent(): HttpsProxyAgent | undefined { const proxyUrl = resolveEnvHttpProxyUrl("https"); if (!proxyUrl) { return undefined; } // Slack Socket Mode connects to these hosts; skip proxy if excluded. if (isHostExcludedByNoProxy("slack.com")) { return undefined; } try { return new HttpsProxyAgent(proxyUrl); } catch { // Malformed proxy URL — degrade gracefully to direct connection. return undefined; } } export function resolveSlackWebClientOptions(options: WebClientOptions = {}): WebClientOptions { return { ...options, agent: options.agent ?? resolveSlackProxyAgent(), retryConfig: options.retryConfig ?? SLACK_DEFAULT_RETRY_OPTIONS, }; } export function resolveSlackWriteClientOptions(options: WebClientOptions = {}): WebClientOptions { return { ...options, agent: options.agent ?? resolveSlackProxyAgent(), retryConfig: options.retryConfig ?? SLACK_WRITE_RETRY_OPTIONS, }; } export function createSlackWebClient(token: string, options: WebClientOptions = {}) { return new WebClient(token, resolveSlackWebClientOptions(options)); } export function createSlackWriteClient(token: string, options: WebClientOptions = {}) { return new WebClient(token, resolveSlackWriteClientOptions(options)); }