diff --git a/extensions/slack/src/monitor/provider-support.ts b/extensions/slack/src/monitor/provider-support.ts new file mode 100644 index 00000000000..e3b4e9f8143 --- /dev/null +++ b/extensions/slack/src/monitor/provider-support.ts @@ -0,0 +1,244 @@ +import type { SlackChannelResolution } from "../resolve-channels.js"; +import type { SlackUserResolution } from "../resolve-users.js"; +import { formatUnknownError, waitForSlackSocketDisconnect } from "./reconnect-policy.js"; + +type SlackAppConstructor = typeof import("@slack/bolt").App; +type SlackHttpReceiverConstructor = typeof import("@slack/bolt").HTTPReceiver; +type SlackSocketModeReceiverConstructor = typeof import("@slack/bolt").SocketModeReceiver; + +export type SlackBoltResolvedExports = { + App: SlackAppConstructor; + HTTPReceiver: SlackHttpReceiverConstructor; + SocketModeReceiver: SlackSocketModeReceiverConstructor; +}; + +type SlackSocketShutdownClient = { + shuttingDown?: boolean; +}; +type Constructor = abstract new (...args: never[]) => unknown; + +function isConstructorFunction< + // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Constructor guard preserves the requested concrete Slack constructor type. + T extends Constructor, +>(value: unknown): value is T { + return typeof value === "function"; +} + +function resolveSlackBoltModule(value: unknown): SlackBoltResolvedExports | null { + if (!value || typeof value !== "object") { + return null; + } + const app = Reflect.get(value, "App"); + const httpReceiver = Reflect.get(value, "HTTPReceiver"); + const socketModeReceiver = Reflect.get(value, "SocketModeReceiver"); + if ( + !isConstructorFunction(app) || + !isConstructorFunction(httpReceiver) || + !isConstructorFunction(socketModeReceiver) + ) { + return null; + } + return { + App: app, + HTTPReceiver: httpReceiver, + SocketModeReceiver: socketModeReceiver, + }; +} + +export function resolveSlackBoltInterop(params: { + defaultImport: unknown; + namespaceImport: unknown; +}): SlackBoltResolvedExports { + const { defaultImport, namespaceImport } = params; + const nestedDefault = + defaultImport && typeof defaultImport === "object" + ? Reflect.get(defaultImport, "default") + : undefined; + const namespaceDefault = + namespaceImport && typeof namespaceImport === "object" + ? Reflect.get(namespaceImport, "default") + : undefined; + const namespaceReceiver = + namespaceImport && typeof namespaceImport === "object" + ? Reflect.get(namespaceImport, "HTTPReceiver") + : undefined; + const namespaceSocketModeReceiver = + namespaceImport && typeof namespaceImport === "object" + ? Reflect.get(namespaceImport, "SocketModeReceiver") + : undefined; + const directModule = + resolveSlackBoltModule(defaultImport) ?? + resolveSlackBoltModule(nestedDefault) ?? + resolveSlackBoltModule(namespaceDefault) ?? + resolveSlackBoltModule(namespaceImport); + if (directModule) { + return directModule; + } + if ( + isConstructorFunction(defaultImport) && + isConstructorFunction(namespaceReceiver) && + isConstructorFunction(namespaceSocketModeReceiver) + ) { + return { + App: defaultImport, + HTTPReceiver: namespaceReceiver, + SocketModeReceiver: namespaceSocketModeReceiver, + }; + } + throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports"); +} + +export function publishSlackConnectedStatus(setStatus?: (next: Record) => void) { + if (!setStatus) { + return; + } + const now = Date.now(); + setStatus({ + connected: true, + lastConnectedAt: now, + healthState: "healthy", + lastError: null, + }); +} + +export function publishSlackDisconnectedStatus( + setStatus?: (next: Record) => void, + error?: unknown, +) { + if (!setStatus) { + return; + } + const at = Date.now(); + const message = error ? formatUnknownError(error) : undefined; + setStatus({ + connected: false, + healthState: "disconnected", + lastDisconnect: message ? { at, error: message } : { at }, + lastError: message ?? null, + }); +} + +export function createSlackBoltApp(params: { + interop: SlackBoltResolvedExports; + slackMode: "socket" | "http"; + botToken: string; + appToken?: string; + signingSecret?: string; + slackWebhookPath: string; + clientOptions: Record; +}) { + const receiver = + params.slackMode === "socket" + ? new params.interop.SocketModeReceiver({ + appToken: params.appToken ?? "", + autoReconnectEnabled: false, + installerOptions: { + clientOptions: params.clientOptions, + }, + }) + : new params.interop.HTTPReceiver({ + signingSecret: params.signingSecret ?? "", + endpoints: params.slackWebhookPath, + }); + const app = new params.interop.App({ + token: params.botToken, + receiver, + clientOptions: params.clientOptions, + }); + return { app, receiver }; +} + +export function createSlackSocketDisconnectWaiter(app: unknown, abortSignal?: AbortSignal) { + const waiterAbortController = new AbortController(); + const relayAbort = () => waiterAbortController.abort(); + abortSignal?.addEventListener("abort", relayAbort, { once: true }); + return { + promise: waitForSlackSocketDisconnect(app, waiterAbortController.signal), + cancel: () => { + waiterAbortController.abort(); + abortSignal?.removeEventListener("abort", relayAbort); + }, + complete: () => { + abortSignal?.removeEventListener("abort", relayAbort); + }, + }; +} + +export async function startSlackSocketAndWaitForDisconnect(params: { + app: { start: () => unknown }; + abortSignal?: AbortSignal; + onStarted?: () => void; +}) { + const disconnectWaiter = createSlackSocketDisconnectWaiter(params.app, params.abortSignal); + try { + await Promise.resolve(params.app.start()); + if (params.abortSignal?.aborted) { + disconnectWaiter.cancel(); + return null; + } + params.onStarted?.(); + const disconnect = await disconnectWaiter.promise; + disconnectWaiter.complete(); + return disconnect; + } catch (err) { + disconnectWaiter.cancel(); + throw err; + } +} + +export function resolveSlackSocketShutdownClient( + app: unknown, +): SlackSocketShutdownClient | undefined { + if (!app || typeof app !== "object") { + return undefined; + } + const receiver = Reflect.get(app, "receiver"); + if (!receiver || typeof receiver !== "object") { + return undefined; + } + const client = Reflect.get(receiver, "client"); + if (!client || typeof client !== "object") { + return undefined; + } + return client as SlackSocketShutdownClient; +} + +export async function gracefulStopSlackApp(app: { stop: () => unknown }) { + const socketClient = resolveSlackSocketShutdownClient(app); + if (socketClient) { + socketClient.shuttingDown = true; + } + await Promise.resolve(app.stop()).catch(() => undefined); +} + +function formatSlackResolvedLabel(params: { + input: string; + id: string; + name?: string; + extra?: string[]; +}): string { + const extras = params.extra?.filter(Boolean) ?? []; + const suffix = + extras.length > 0 ? ` (id:${params.id}, ${extras.join(", ")})` : ` (id:${params.id})`; + return `${params.input}→${params.name ?? params.id}${suffix}`; +} + +export function formatSlackChannelResolved(entry: SlackChannelResolution): string { + const id = entry.id ?? entry.input; + return formatSlackResolvedLabel({ + input: entry.input, + id, + name: entry.name, + extra: entry.archived ? ["archived"] : [], + }); +} + +export function formatSlackUserResolved(entry: SlackUserResolution): string { + const id = entry.id ?? entry.input; + return formatSlackResolvedLabel({ + input: entry.input, + id, + name: entry.name, + extra: entry.note ? [entry.note] : [], + }); +} diff --git a/extensions/slack/src/monitor/provider.allowlist.test.ts b/extensions/slack/src/monitor/provider.allowlist.test.ts index 2a686f2fcbd..df1e39c9caa 100644 --- a/extensions/slack/src/monitor/provider.allowlist.test.ts +++ b/extensions/slack/src/monitor/provider.allowlist.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; -import { __testing } from "./provider.js"; +import { formatSlackChannelResolved, formatSlackUserResolved } from "./provider-support.js"; describe("slack allowlist log formatting", () => { it("prints channel names alongside ids", () => { expect( - __testing.formatSlackChannelResolved({ + formatSlackChannelResolved({ input: "C0AQXEG6QFJ", resolved: true, id: "C0AQXEG6QFJ", @@ -15,7 +15,7 @@ describe("slack allowlist log formatting", () => { it("prints user names alongside ids", () => { expect( - __testing.formatSlackUserResolved({ + formatSlackUserResolved({ input: "U090HHQ029J", resolved: true, id: "U090HHQ029J", diff --git a/extensions/slack/src/monitor/provider.auth-errors.test.ts b/extensions/slack/src/monitor/provider.auth-errors.test.ts index c37c6c29ef3..9a89dbb017f 100644 --- a/extensions/slack/src/monitor/provider.auth-errors.test.ts +++ b/extensions/slack/src/monitor/provider.auth-errors.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { isNonRecoverableSlackAuthError } from "./provider.js"; +import { isNonRecoverableSlackAuthError } from "./reconnect-policy.js"; describe("isNonRecoverableSlackAuthError", () => { it.each([ diff --git a/extensions/slack/src/monitor/provider.interop.test.ts b/extensions/slack/src/monitor/provider.interop.test.ts index 59754d30978..7d029883445 100644 --- a/extensions/slack/src/monitor/provider.interop.test.ts +++ b/extensions/slack/src/monitor/provider.interop.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { __testing } from "./provider.js"; +import { createSlackBoltApp, resolveSlackBoltInterop } from "./provider-support.js"; describe("resolveSlackBoltInterop", () => { function FakeApp() {} @@ -7,7 +7,7 @@ describe("resolveSlackBoltInterop", () => { function FakeSocketModeReceiver() {} it("uses the default import when it already exposes named exports", () => { - const resolved = __testing.resolveSlackBoltInterop({ + const resolved = resolveSlackBoltInterop({ defaultImport: { App: FakeApp, HTTPReceiver: FakeHTTPReceiver, @@ -24,7 +24,7 @@ describe("resolveSlackBoltInterop", () => { }); it("uses nested default export when the default import is a wrapper object", () => { - const resolved = __testing.resolveSlackBoltInterop({ + const resolved = resolveSlackBoltInterop({ defaultImport: { default: { App: FakeApp, @@ -43,7 +43,7 @@ describe("resolveSlackBoltInterop", () => { }); it("uses the namespace receiver when the default import is the App constructor itself", () => { - const resolved = __testing.resolveSlackBoltInterop({ + const resolved = resolveSlackBoltInterop({ defaultImport: FakeApp, namespaceImport: { HTTPReceiver: FakeHTTPReceiver, @@ -59,7 +59,7 @@ describe("resolveSlackBoltInterop", () => { }); it("uses namespace.default when it exposes named exports", () => { - const resolved = __testing.resolveSlackBoltInterop({ + const resolved = resolveSlackBoltInterop({ defaultImport: undefined, namespaceImport: { default: { @@ -78,7 +78,7 @@ describe("resolveSlackBoltInterop", () => { }); it("falls back to the namespace import when it exposes named exports", () => { - const resolved = __testing.resolveSlackBoltInterop({ + const resolved = resolveSlackBoltInterop({ defaultImport: undefined, namespaceImport: { App: FakeApp, @@ -96,7 +96,7 @@ describe("resolveSlackBoltInterop", () => { it("throws when the module cannot be resolved", () => { expect(() => - __testing.resolveSlackBoltInterop({ + resolveSlackBoltInterop({ defaultImport: null, namespaceImport: {}, }), @@ -131,7 +131,7 @@ describe("createSlackBoltApp", () => { it("uses SocketModeReceiver with OpenClaw-owned reconnects and shared client options", () => { const clientOptions = { teamId: "T1" }; - const { app, receiver } = __testing.createSlackBoltApp({ + const { app, receiver } = createSlackBoltApp({ interop: { App: FakeApp as never, HTTPReceiver: FakeHTTPReceiver as never, @@ -162,7 +162,7 @@ describe("createSlackBoltApp", () => { it("uses HTTPReceiver for webhook mode", () => { const clientOptions = { teamId: "T1" }; - const { app, receiver } = __testing.createSlackBoltApp({ + const { app, receiver } = createSlackBoltApp({ interop: { App: FakeApp as never, HTTPReceiver: FakeHTTPReceiver as never, diff --git a/extensions/slack/src/monitor/provider.reconnect.test.ts b/extensions/slack/src/monitor/provider.reconnect.test.ts index 70e381d3db1..e6d7f78d42a 100644 --- a/extensions/slack/src/monitor/provider.reconnect.test.ts +++ b/extensions/slack/src/monitor/provider.reconnect.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it, vi } from "vitest"; -import { __testing } from "./provider.js"; +import { + gracefulStopSlackApp, + publishSlackConnectedStatus, + publishSlackDisconnectedStatus, + startSlackSocketAndWaitForDisconnect, +} from "./provider-support.js"; +import { waitForSlackSocketDisconnect } from "./reconnect-policy.js"; class FakeEmitter { private listeners = new Map void>>(); @@ -29,7 +35,7 @@ describe("slack socket reconnect helpers", () => { it("marks socket mode healthy without seeding event liveness on connect", () => { const setStatus = vi.fn(); - __testing.publishSlackConnectedStatus(setStatus); + publishSlackConnectedStatus(setStatus); expect(setStatus).toHaveBeenCalledTimes(1); expect(setStatus).toHaveBeenCalledWith( @@ -49,7 +55,7 @@ describe("slack socket reconnect helpers", () => { const setStatus = vi.fn(); const err = new Error("dns down"); - __testing.publishSlackDisconnectedStatus(setStatus, err); + publishSlackDisconnectedStatus(setStatus, err); expect(setStatus).toHaveBeenCalledTimes(1); expect(setStatus).toHaveBeenCalledWith({ @@ -66,7 +72,7 @@ describe("slack socket reconnect helpers", () => { it("marks socket mode disconnected without error when the socket closes cleanly", () => { const setStatus = vi.fn(); - __testing.publishSlackDisconnectedStatus(setStatus); + publishSlackDisconnectedStatus(setStatus); expect(setStatus).toHaveBeenCalledTimes(1); expect(setStatus).toHaveBeenCalledWith({ @@ -83,7 +89,7 @@ describe("slack socket reconnect helpers", () => { const client = new FakeEmitter(); const app = { receiver: { client } }; - const waiter = __testing.waitForSlackSocketDisconnect(app as never); + const waiter = waitForSlackSocketDisconnect(app as never); client.emit("disconnected"); await expect(waiter).resolves.toEqual({ event: "disconnect" }); @@ -94,7 +100,7 @@ describe("slack socket reconnect helpers", () => { const app = { receiver: { client } }; const err = new Error("dns down"); - const waiter = __testing.waitForSlackSocketDisconnect(app as never); + const waiter = waitForSlackSocketDisconnect(app as never); client.emit("error", err); await expect(waiter).resolves.toEqual({ event: "error", error: err }); @@ -111,7 +117,7 @@ describe("slack socket reconnect helpers", () => { const onStarted = vi.fn(); await expect( - __testing.startSlackSocketAndWaitForDisconnect({ + startSlackSocketAndWaitForDisconnect({ app: app as never, onStarted, }), @@ -130,7 +136,7 @@ describe("slack socket reconnect helpers", () => { const err = new Error("status sink failed"); await expect( - __testing.startSlackSocketAndWaitForDisconnect({ + startSlackSocketAndWaitForDisconnect({ app: app as never, onStarted: () => { throw err; @@ -148,7 +154,7 @@ describe("slack socket reconnect helpers", () => { const app = { receiver: { client } }; const err = new Error("invalid_auth"); - const waiter = __testing.waitForSlackSocketDisconnect(app as never); + const waiter = waitForSlackSocketDisconnect(app as never); client.emit("unable_to_socket_mode_start", err); await expect(waiter).resolves.toEqual({ @@ -165,7 +171,7 @@ describe("slack socket reconnect helpers", () => { }), }; - await __testing.gracefulStopSlackApp(app); + await gracefulStopSlackApp(app); expect(app.stop).toHaveBeenCalledTimes(1); expect(app.receiver.client.shuttingDown).toBe(true); diff --git a/extensions/slack/src/monitor/provider.ts b/extensions/slack/src/monitor/provider.ts index f39127fc68a..25d577113f7 100644 --- a/extensions/slack/src/monitor/provider.ts +++ b/extensions/slack/src/monitor/provider.ts @@ -27,8 +27,8 @@ import { resolveSlackWebClientOptions } from "../client.js"; import { isSlackExecApprovalClientEnabled } from "../exec-approvals.js"; import { normalizeSlackWebhookPath, registerSlackHttpHandler } from "../http/index.js"; import { SLACK_TEXT_LIMIT } from "../limits.js"; -import { resolveSlackChannelAllowlist, type SlackChannelResolution } from "../resolve-channels.js"; -import { resolveSlackUserAllowlist, type SlackUserResolution } from "../resolve-users.js"; +import { resolveSlackChannelAllowlist } from "../resolve-channels.js"; +import { resolveSlackUserAllowlist } from "../resolve-users.js"; import { resolveSlackAppToken, resolveSlackBotToken } from "../token.js"; import { normalizeAllowList } from "./allow-list.js"; import { resolveSlackSlashCommandConfig } from "./commands.js"; @@ -42,6 +42,19 @@ import { import { createSlackMonitorContext } from "./context.js"; import { registerSlackMonitorEvents } from "./events.js"; import { createSlackMessageHandler } from "./message-handler.js"; +import { + createSlackBoltApp, + createSlackSocketDisconnectWaiter, + formatSlackChannelResolved, + formatSlackUserResolved, + gracefulStopSlackApp, + publishSlackConnectedStatus, + publishSlackDisconnectedStatus, + resolveSlackBoltInterop, + resolveSlackSocketShutdownClient, + startSlackSocketAndWaitForDisconnect, + type SlackBoltResolvedExports, +} from "./provider-support.js"; import { formatUnknownError, getSocketEmitter, @@ -53,90 +66,6 @@ import { resolveTextChunkLimit } from "./reply.runtime.js"; import { registerSlackMonitorSlashCommands } from "./slash.js"; import type { MonitorSlackOpts } from "./types.js"; -type SlackAppConstructor = typeof import("@slack/bolt").App; -type SlackHttpReceiverConstructor = typeof import("@slack/bolt").HTTPReceiver; -type SlackSocketModeReceiverConstructor = typeof import("@slack/bolt").SocketModeReceiver; -type SlackBoltResolvedExports = { - App: SlackAppConstructor; - HTTPReceiver: SlackHttpReceiverConstructor; - SocketModeReceiver: SlackSocketModeReceiverConstructor; -}; -type SlackSocketShutdownClient = { - shuttingDown?: boolean; -}; -type Constructor = abstract new (...args: never[]) => unknown; - -function isConstructorFunction< - // oxlint-disable-next-line typescript/no-unnecessary-type-parameters -- Constructor guard preserves the requested concrete Slack constructor type. - T extends Constructor, ->(value: unknown): value is T { - return typeof value === "function"; -} - -function resolveSlackBoltModule(value: unknown): SlackBoltResolvedExports | null { - if (!value || typeof value !== "object") { - return null; - } - const app = Reflect.get(value, "App"); - const httpReceiver = Reflect.get(value, "HTTPReceiver"); - const socketModeReceiver = Reflect.get(value, "SocketModeReceiver"); - if ( - !isConstructorFunction(app) || - !isConstructorFunction(httpReceiver) || - !isConstructorFunction(socketModeReceiver) - ) { - return null; - } - return { - App: app, - HTTPReceiver: httpReceiver, - SocketModeReceiver: socketModeReceiver, - }; -} - -function resolveSlackBoltInterop(params: { - defaultImport: unknown; - namespaceImport: unknown; -}): SlackBoltResolvedExports { - const { defaultImport, namespaceImport } = params; - const nestedDefault = - defaultImport && typeof defaultImport === "object" - ? Reflect.get(defaultImport, "default") - : undefined; - const namespaceDefault = - namespaceImport && typeof namespaceImport === "object" - ? Reflect.get(namespaceImport, "default") - : undefined; - const namespaceReceiver = - namespaceImport && typeof namespaceImport === "object" - ? Reflect.get(namespaceImport, "HTTPReceiver") - : undefined; - const namespaceSocketModeReceiver = - namespaceImport && typeof namespaceImport === "object" - ? Reflect.get(namespaceImport, "SocketModeReceiver") - : undefined; - const directModule = - resolveSlackBoltModule(defaultImport) ?? - resolveSlackBoltModule(nestedDefault) ?? - resolveSlackBoltModule(namespaceDefault) ?? - resolveSlackBoltModule(namespaceImport); - if (directModule) { - return directModule; - } - if ( - isConstructorFunction(defaultImport) && - isConstructorFunction(namespaceReceiver) && - isConstructorFunction(namespaceSocketModeReceiver) - ) { - return { - App: defaultImport, - HTTPReceiver: namespaceReceiver, - SocketModeReceiver: namespaceSocketModeReceiver, - }; - } - throw new TypeError("Unable to resolve @slack/bolt App/HTTPReceiver exports"); -} - let slackBoltInterop: SlackBoltResolvedExports | undefined; function getSlackBoltInterop(): SlackBoltResolvedExports { @@ -161,158 +90,6 @@ function parseApiAppIdFromAppToken(raw?: string) { return match?.[1]?.toUpperCase(); } -function publishSlackConnectedStatus(setStatus?: (next: Record) => void) { - if (!setStatus) { - return; - } - const now = Date.now(); - setStatus({ - connected: true, - lastConnectedAt: now, - healthState: "healthy", - lastError: null, - }); -} - -function publishSlackDisconnectedStatus( - setStatus?: (next: Record) => void, - error?: unknown, -) { - if (!setStatus) { - return; - } - const at = Date.now(); - const message = error ? formatUnknownError(error) : undefined; - setStatus({ - connected: false, - healthState: "disconnected", - lastDisconnect: message ? { at, error: message } : { at }, - lastError: message ?? null, - }); -} - -function createSlackBoltApp(params: { - interop: SlackBoltResolvedExports; - slackMode: "socket" | "http"; - botToken: string; - appToken?: string; - signingSecret?: string; - slackWebhookPath: string; - clientOptions: Record; -}) { - const receiver = - params.slackMode === "socket" - ? new params.interop.SocketModeReceiver({ - appToken: params.appToken ?? "", - autoReconnectEnabled: false, - installerOptions: { - clientOptions: params.clientOptions, - }, - }) - : new params.interop.HTTPReceiver({ - signingSecret: params.signingSecret ?? "", - endpoints: params.slackWebhookPath, - }); - const app = new params.interop.App({ - token: params.botToken, - receiver, - clientOptions: params.clientOptions, - }); - return { app, receiver }; -} - -function createSlackSocketDisconnectWaiter(app: unknown, abortSignal?: AbortSignal) { - const waiterAbortController = new AbortController(); - const relayAbort = () => waiterAbortController.abort(); - abortSignal?.addEventListener("abort", relayAbort, { once: true }); - return { - promise: waitForSlackSocketDisconnect(app, waiterAbortController.signal), - cancel: () => { - waiterAbortController.abort(); - abortSignal?.removeEventListener("abort", relayAbort); - }, - complete: () => { - abortSignal?.removeEventListener("abort", relayAbort); - }, - }; -} - -async function startSlackSocketAndWaitForDisconnect(params: { - app: { start: () => unknown }; - abortSignal?: AbortSignal; - onStarted?: () => void; -}) { - const disconnectWaiter = createSlackSocketDisconnectWaiter(params.app, params.abortSignal); - try { - await Promise.resolve(params.app.start()); - if (params.abortSignal?.aborted) { - disconnectWaiter.cancel(); - return null; - } - params.onStarted?.(); - const disconnect = await disconnectWaiter.promise; - disconnectWaiter.complete(); - return disconnect; - } catch (err) { - disconnectWaiter.cancel(); - throw err; - } -} - -function resolveSlackSocketShutdownClient(app: unknown): SlackSocketShutdownClient | undefined { - if (!app || typeof app !== "object") { - return undefined; - } - const receiver = Reflect.get(app, "receiver"); - if (!receiver || typeof receiver !== "object") { - return undefined; - } - const client = Reflect.get(receiver, "client"); - if (!client || typeof client !== "object") { - return undefined; - } - return client as SlackSocketShutdownClient; -} - -async function gracefulStopSlackApp(app: { stop: () => unknown }) { - const socketClient = resolveSlackSocketShutdownClient(app); - if (socketClient) { - socketClient.shuttingDown = true; - } - await Promise.resolve(app.stop()).catch(() => undefined); -} - -function formatSlackResolvedLabel(params: { - input: string; - id: string; - name?: string; - extra?: string[]; -}): string { - const extras = params.extra?.filter(Boolean) ?? []; - const suffix = - extras.length > 0 ? ` (id:${params.id}, ${extras.join(", ")})` : ` (id:${params.id})`; - return `${params.input}→${params.name ?? params.id}${suffix}`; -} - -function formatSlackChannelResolved(entry: SlackChannelResolution): string { - const id = entry.id ?? entry.input; - return formatSlackResolvedLabel({ - input: entry.input, - id, - name: entry.name, - extra: entry.archived ? ["archived"] : [], - }); -} - -function formatSlackUserResolved(entry: SlackUserResolution): string { - const id = entry.id ?? entry.input; - return formatSlackResolvedLabel({ - input: entry.input, - id, - name: entry.name, - extra: entry.note ? [entry.note] : [], - }); -} export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const cfg = opts.config ?? loadConfig(); const runtime: RuntimeEnv = opts.runtime ?? createNonExitingRuntime(); @@ -428,7 +205,9 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { const slackHttpHandler = slackMode === "http" && receiver ? async (req: IncomingMessage, res: ServerResponse) => { - const httpReceiver = receiver as InstanceType; + const httpReceiver = receiver as { + requestListener: (req: IncomingMessage, res: ServerResponse) => unknown; + }; const guard = installRequestBodyLimitGuard(req, res, { maxBytes: SLACK_WEBHOOK_MAX_BODY_BYTES, timeoutMs: SLACK_WEBHOOK_BODY_TIMEOUT_MS,