feat(slack): add HTTP receiver webhook mode (#1143) - thanks @jdrhyne

Co-authored-by: Jonathan Rhyne <jdrhyne@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-18 15:01:04 +00:00
parent e9a08dc507
commit 4726580c7e
11 changed files with 342 additions and 24 deletions

View File

@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { validateConfigObject } from "./config.js";
describe("Slack HTTP mode config", () => {
it("accepts HTTP mode when signing secret is configured", () => {
const res = validateConfigObject({
channels: {
slack: {
mode: "http",
signingSecret: "secret",
},
},
});
expect(res.ok).toBe(true);
});
it("rejects HTTP mode without signing secret", () => {
const res = validateConfigObject({
channels: {
slack: {
mode: "http",
},
},
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues[0]?.path).toBe("channels.slack.signingSecret");
}
});
it("accepts account HTTP mode when base signing secret is set", () => {
const res = validateConfigObject({
channels: {
slack: {
signingSecret: "secret",
accounts: {
ops: {
mode: "http",
},
},
},
},
});
expect(res.ok).toBe(true);
});
it("rejects account HTTP mode without signing secret", () => {
const res = validateConfigObject({
channels: {
slack: {
accounts: {
ops: {
mode: "http",
},
},
},
},
});
expect(res.ok).toBe(false);
if (!res.ok) {
expect(res.issues[0]?.path).toBe("channels.slack.accounts.ops.signingSecret");
}
});
});

View File

@@ -70,6 +70,12 @@ export type SlackThreadConfig = {
export type SlackAccountConfig = {
/** Optional display name for this account (used in CLI/UI lists). */
name?: string;
/** Slack connection mode (socket|http). Default: socket. */
mode?: "socket" | "http";
/** Slack signing secret (required for HTTP mode). */
signingSecret?: string;
/** Slack Events API webhook path (default: /slack/events). */
webhookPath?: string;
/** Optional provider capability tags used for agent/runtime guidance. */
capabilities?: string[];
/** Override native command registration for Slack (bool or "auto"). */

View File

@@ -258,6 +258,9 @@ export const SlackThreadSchema = z.object({
export const SlackAccountSchema = z.object({
name: z.string().optional(),
mode: z.enum(["socket", "http"]).optional(),
signingSecret: z.string().optional(),
webhookPath: z.string().optional(),
capabilities: z.array(z.string()).optional(),
enabled: z.boolean().optional(),
commands: ProviderCommandsSchema,
@@ -305,7 +308,35 @@ export const SlackAccountSchema = z.object({
});
export const SlackConfigSchema = SlackAccountSchema.extend({
mode: z.enum(["socket", "http"]).optional().default("socket"),
signingSecret: z.string().optional(),
webhookPath: z.string().optional().default("/slack/events"),
accounts: z.record(z.string(), SlackAccountSchema.optional()).optional(),
}).superRefine((value, ctx) => {
const baseMode = value.mode ?? "socket";
if (baseMode === "http" && !value.signingSecret) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'channels.slack.mode="http" requires channels.slack.signingSecret',
path: ["signingSecret"],
});
}
if (!value.accounts) return;
for (const [accountId, account] of Object.entries(value.accounts)) {
if (!account) continue;
if (account.enabled === false) continue;
const accountMode = account.mode ?? baseMode;
if (accountMode !== "http") continue;
const accountSecret = account.signingSecret ?? value.signingSecret;
if (!accountSecret) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message:
'channels.slack.accounts.*.mode="http" requires channels.slack.signingSecret or channels.slack.accounts.*.signingSecret',
path: ["accounts", accountId, "signingSecret"],
});
}
}
});
export const SignalAccountSchemaBase = z.object({