@spinabot/brigade 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/agents/channels/bundled-channel-metas.d.ts +2 -0
- package/dist/agents/channels/bundled-channel-metas.d.ts.map +1 -1
- package/dist/agents/channels/bundled-channel-metas.js +11 -0
- package/dist/agents/channels/bundled-channel-metas.js.map +1 -1
- package/dist/agents/channels/manager.d.ts.map +1 -1
- package/dist/agents/channels/manager.js +18 -0
- package/dist/agents/channels/manager.js.map +1 -1
- package/dist/agents/channels/sdk.d.ts +2 -0
- package/dist/agents/channels/sdk.d.ts.map +1 -1
- package/dist/agents/channels/sdk.js +2 -0
- package/dist/agents/channels/sdk.js.map +1 -1
- package/dist/agents/channels/slack/account-config.d.ts +172 -0
- package/dist/agents/channels/slack/account-config.d.ts.map +1 -0
- package/dist/agents/channels/slack/account-config.js +353 -0
- package/dist/agents/channels/slack/account-config.js.map +1 -0
- package/dist/agents/channels/slack/account-registry.d.ts +45 -0
- package/dist/agents/channels/slack/account-registry.d.ts.map +1 -0
- package/dist/agents/channels/slack/account-registry.js +58 -0
- package/dist/agents/channels/slack/account-registry.js.map +1 -0
- package/dist/agents/channels/slack/adapter.d.ts +66 -0
- package/dist/agents/channels/slack/adapter.d.ts.map +1 -0
- package/dist/agents/channels/slack/adapter.js +547 -0
- package/dist/agents/channels/slack/adapter.js.map +1 -0
- package/dist/agents/channels/slack/approval-authorize.d.ts +43 -0
- package/dist/agents/channels/slack/approval-authorize.d.ts.map +1 -0
- package/dist/agents/channels/slack/approval-authorize.js +71 -0
- package/dist/agents/channels/slack/approval-authorize.js.map +1 -0
- package/dist/agents/channels/slack/approval-native.d.ts +70 -0
- package/dist/agents/channels/slack/approval-native.d.ts.map +1 -0
- package/dist/agents/channels/slack/approval-native.js +85 -0
- package/dist/agents/channels/slack/approval-native.js.map +1 -0
- package/dist/agents/channels/slack/blocks.d.ts +125 -0
- package/dist/agents/channels/slack/blocks.d.ts.map +1 -0
- package/dist/agents/channels/slack/blocks.js +145 -0
- package/dist/agents/channels/slack/blocks.js.map +1 -0
- package/dist/agents/channels/slack/command-menu.d.ts +44 -0
- package/dist/agents/channels/slack/command-menu.d.ts.map +1 -0
- package/dist/agents/channels/slack/command-menu.js +66 -0
- package/dist/agents/channels/slack/command-menu.js.map +1 -0
- package/dist/agents/channels/slack/connection.d.ts +422 -0
- package/dist/agents/channels/slack/connection.d.ts.map +1 -0
- package/dist/agents/channels/slack/connection.js +1042 -0
- package/dist/agents/channels/slack/connection.js.map +1 -0
- package/dist/agents/channels/slack/directory-live.d.ts +129 -0
- package/dist/agents/channels/slack/directory-live.d.ts.map +1 -0
- package/dist/agents/channels/slack/directory-live.js +148 -0
- package/dist/agents/channels/slack/directory-live.js.map +1 -0
- package/dist/agents/channels/slack/draft-stream.d.ts +93 -0
- package/dist/agents/channels/slack/draft-stream.d.ts.map +1 -0
- package/dist/agents/channels/slack/draft-stream.js +218 -0
- package/dist/agents/channels/slack/draft-stream.js.map +1 -0
- package/dist/agents/channels/slack/format.d.ts +41 -0
- package/dist/agents/channels/slack/format.d.ts.map +1 -0
- package/dist/agents/channels/slack/format.js +271 -0
- package/dist/agents/channels/slack/format.js.map +1 -0
- package/dist/agents/channels/slack/inbound-extras.d.ts +179 -0
- package/dist/agents/channels/slack/inbound-extras.d.ts.map +1 -0
- package/dist/agents/channels/slack/inbound-extras.js +257 -0
- package/dist/agents/channels/slack/inbound-extras.js.map +1 -0
- package/dist/agents/channels/slack/index.d.ts +15 -0
- package/dist/agents/channels/slack/index.d.ts.map +1 -0
- package/dist/agents/channels/slack/index.js +15 -0
- package/dist/agents/channels/slack/index.js.map +1 -0
- package/dist/agents/channels/slack/media.d.ts +90 -0
- package/dist/agents/channels/slack/media.d.ts.map +1 -0
- package/dist/agents/channels/slack/media.js +215 -0
- package/dist/agents/channels/slack/media.js.map +1 -0
- package/dist/agents/channels/slack/module.d.ts +26 -0
- package/dist/agents/channels/slack/module.d.ts.map +1 -0
- package/dist/agents/channels/slack/module.js +67 -0
- package/dist/agents/channels/slack/module.js.map +1 -0
- package/dist/agents/channels/slack/plugin.d.ts +69 -0
- package/dist/agents/channels/slack/plugin.d.ts.map +1 -0
- package/dist/agents/channels/slack/plugin.js +318 -0
- package/dist/agents/channels/slack/plugin.js.map +1 -0
- package/dist/agents/channels/slack/probe.d.ts +72 -0
- package/dist/agents/channels/slack/probe.d.ts.map +1 -0
- package/dist/agents/channels/slack/probe.js +103 -0
- package/dist/agents/channels/slack/probe.js.map +1 -0
- package/dist/agents/channels/slack/proxy-agent.d.ts +30 -0
- package/dist/agents/channels/slack/proxy-agent.d.ts.map +1 -0
- package/dist/agents/channels/slack/proxy-agent.js +44 -0
- package/dist/agents/channels/slack/proxy-agent.js.map +1 -0
- package/dist/agents/channels/slack/reasoning-lane.d.ts +42 -0
- package/dist/agents/channels/slack/reasoning-lane.d.ts.map +1 -0
- package/dist/agents/channels/slack/reasoning-lane.js +68 -0
- package/dist/agents/channels/slack/reasoning-lane.js.map +1 -0
- package/dist/agents/channels/slack/user-directory.d.ts +69 -0
- package/dist/agents/channels/slack/user-directory.d.ts.map +1 -0
- package/dist/agents/channels/slack/user-directory.js +94 -0
- package/dist/agents/channels/slack/user-directory.js.map +1 -0
- package/dist/agents/channels/slack/webhook.d.ts +89 -0
- package/dist/agents/channels/slack/webhook.d.ts.map +1 -0
- package/dist/agents/channels/slack/webhook.js +228 -0
- package/dist/agents/channels/slack/webhook.js.map +1 -0
- package/dist/agents/channels/telegram/adapter.d.ts.map +1 -1
- package/dist/agents/channels/telegram/adapter.js +10 -3
- package/dist/agents/channels/telegram/adapter.js.map +1 -1
- package/dist/agents/channels/telegram/connection.d.ts +10 -0
- package/dist/agents/channels/telegram/connection.d.ts.map +1 -1
- package/dist/agents/channels/telegram/connection.js +161 -5
- package/dist/agents/channels/telegram/connection.js.map +1 -1
- package/dist/agents/channels/telegram/format.d.ts +17 -0
- package/dist/agents/channels/telegram/format.d.ts.map +1 -1
- package/dist/agents/channels/telegram/format.js +53 -1
- package/dist/agents/channels/telegram/format.js.map +1 -1
- package/dist/agents/channels/telegram/inbound-extras.d.ts +17 -1
- package/dist/agents/channels/telegram/inbound-extras.d.ts.map +1 -1
- package/dist/agents/channels/telegram/inbound-extras.js +68 -7
- package/dist/agents/channels/telegram/inbound-extras.js.map +1 -1
- package/dist/agents/channels/telegram/media.d.ts +8 -0
- package/dist/agents/channels/telegram/media.d.ts.map +1 -1
- package/dist/agents/channels/telegram/media.js +30 -2
- package/dist/agents/channels/telegram/media.js.map +1 -1
- package/dist/agents/channels/telegram/webhook.d.ts.map +1 -1
- package/dist/agents/channels/telegram/webhook.js +7 -1
- package/dist/agents/channels/telegram/webhook.js.map +1 -1
- package/dist/agents/extensions/modules/index.d.ts.map +1 -1
- package/dist/agents/extensions/modules/index.js +5 -0
- package/dist/agents/extensions/modules/index.js.map +1 -1
- package/dist/agents/extensions/types.d.ts +11 -0
- package/dist/agents/extensions/types.d.ts.map +1 -1
- package/dist/agents/extensions/types.js.map +1 -1
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/convex-cmd.d.ts +2 -1
- package/dist/cli/commands/convex-cmd.d.ts.map +1 -1
- package/dist/cli/commands/convex-cmd.js +79 -6
- package/dist/cli/commands/convex-cmd.js.map +1 -1
- package/dist/cli/program/build-program.js +1 -1
- package/dist/cli/program/build-program.js.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +24 -5
- package/dist/core/server.js.map +1 -1
- package/package.json +4 -1
- package/scripts/convex-dev.mjs +28 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning-lane.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/slack/reasoning-lane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,8BAAoB,CAAC;AAKlD,MAAM,WAAW,mBAAmB;IACnC,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,mBAAmB,CAKpE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack reasoning-lane split (OPTIONAL, default OFF).
|
|
3
|
+
*
|
|
4
|
+
* Brigade strips `<think>…</think>` reasoning from every channel reply via the
|
|
5
|
+
* shared `sanitizeReplyForChannel` — recipients see only the final answer. For
|
|
6
|
+
* Slack an operator can OPT IN (config `channels.slack.surfaceReasoning: true`)
|
|
7
|
+
* to ALSO receive the reasoning trace as a separate, prefixed message.
|
|
8
|
+
*
|
|
9
|
+
* This module is the pure splitter: given the raw agent reply, it returns
|
|
10
|
+
* `{ reasoningText?, answerText }`. When surfacing is OFF the pipeline never
|
|
11
|
+
* calls this and behavior is byte-identical to today (strip + send answer). When
|
|
12
|
+
* ON, the pipeline sends `reasoningText` first (a `🧠 Reasoning:` block) then the
|
|
13
|
+
* normal sanitized answer.
|
|
14
|
+
*
|
|
15
|
+
* The answer half is computed with the SAME sanitizer the default path uses, so
|
|
16
|
+
* enabling reasoning never changes what the answer message contains — it only
|
|
17
|
+
* ADDS the reasoning message in front.
|
|
18
|
+
*
|
|
19
|
+
* Pure / deterministic / dependency-light (re-uses `sanitizeReplyForChannel`).
|
|
20
|
+
* Slack mirror of `telegram/reasoning-lane.ts`.
|
|
21
|
+
*/
|
|
22
|
+
import { sanitizeReplyForChannel } from "../reply-sanitizer.js";
|
|
23
|
+
/** Prefix on the reasoning message so the recipient knows it's the trace. */
|
|
24
|
+
export const REASONING_PREFIX = "🧠 Reasoning:\n";
|
|
25
|
+
/** Matches a `<think>`/`<thinking>`/`<thought>` open/close tag. */
|
|
26
|
+
const THINK_TAG_RE = /<\s*(\/?)\s*(?:think(?:ing)?|thought)\b[^<>]*>/gi;
|
|
27
|
+
/**
|
|
28
|
+
* Extract the concatenated text INSIDE `<think>…</think>` blocks. Handles
|
|
29
|
+
* multiple blocks and an unclosed trailing block (model truncated mid-thought).
|
|
30
|
+
* Returns "" when there's no reasoning content.
|
|
31
|
+
*/
|
|
32
|
+
export function extractReasoning(text) {
|
|
33
|
+
if (!text)
|
|
34
|
+
return "";
|
|
35
|
+
const parts = [];
|
|
36
|
+
let inThink = false;
|
|
37
|
+
let lastIndex = 0;
|
|
38
|
+
THINK_TAG_RE.lastIndex = 0;
|
|
39
|
+
for (const match of text.matchAll(THINK_TAG_RE)) {
|
|
40
|
+
const idx = match.index ?? 0;
|
|
41
|
+
const isClose = match[1] === "/";
|
|
42
|
+
if (inThink && isClose) {
|
|
43
|
+
parts.push(text.slice(lastIndex, idx));
|
|
44
|
+
inThink = false;
|
|
45
|
+
}
|
|
46
|
+
else if (!inThink && !isClose) {
|
|
47
|
+
inThink = true;
|
|
48
|
+
lastIndex = idx + match[0].length;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Unclosed trailing block — keep what the model emitted.
|
|
52
|
+
if (inThink)
|
|
53
|
+
parts.push(text.slice(lastIndex));
|
|
54
|
+
return parts.join("\n").trim();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Split a raw agent reply into an optional reasoning message + the sanitized
|
|
58
|
+
* answer. The answer is identical to `sanitizeReplyForChannel(raw)`; the
|
|
59
|
+
* reasoning is only populated when a `<think>` block carried content.
|
|
60
|
+
*/
|
|
61
|
+
export function splitSlackReasoning(raw) {
|
|
62
|
+
const answerText = sanitizeReplyForChannel(raw ?? "");
|
|
63
|
+
const reasoning = extractReasoning(raw ?? "");
|
|
64
|
+
if (!reasoning)
|
|
65
|
+
return { answerText };
|
|
66
|
+
return { reasoningText: `${REASONING_PREFIX}${reasoning}`, answerText };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=reasoning-lane.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reasoning-lane.js","sourceRoot":"","sources":["../../../../src/agents/channels/slack/reasoning-lane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,iBAAiB,CAAC;AAElD,mEAAmE;AACnE,MAAM,YAAY,GAAG,kDAAkD,CAAC;AASxE;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IACrB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,YAAY,CAAC,SAAS,GAAG,CAAC,CAAC;IAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QACjD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;QAC7B,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC;QACjC,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC;YACvC,OAAO,GAAG,KAAK,CAAC;QACjB,CAAC;aAAM,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,EAAE,CAAC;YACjC,OAAO,GAAG,IAAI,CAAC;YACf,SAAS,GAAG,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;QACnC,CAAC;IACF,CAAC;IACD,yDAAyD;IACzD,IAAI,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;AAChC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC9C,MAAM,UAAU,GAAG,uBAAuB,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IACtD,MAAM,SAAS,GAAG,gBAAgB,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC;IAC9C,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IACtC,OAAO,EAAE,aAAa,EAAE,GAAG,gBAAgB,GAAG,SAAS,EAAE,EAAE,UAAU,EAAE,CAAC;AACzE,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack user-name directory — a small cached `id → display-name` resolver.
|
|
3
|
+
*
|
|
4
|
+
* Slack message events carry only the sender's user id (`U…` / `W…`); the human
|
|
5
|
+
* name needs a `users.info` call. Doing that synchronously on the inbound hot
|
|
6
|
+
* path would add a network round-trip per message, so this directory resolves
|
|
7
|
+
* names in the BACKGROUND:
|
|
8
|
+
*
|
|
9
|
+
* - `prime(id)` fires a memoized, non-blocking `users.info` and caches the
|
|
10
|
+
* resolved name.
|
|
11
|
+
* - `resolveNameSync(id)` reads whatever is currently cached (no network).
|
|
12
|
+
*
|
|
13
|
+
* So the FIRST message from a never-seen user surfaces the raw id; every message
|
|
14
|
+
* after the prime settles surfaces the name. In a real workspace the bot sees
|
|
15
|
+
* the same people repeatedly, so names appear almost immediately. Names are
|
|
16
|
+
* cached with a TTL; failed / empty lookups are negative-cached briefly so a bad
|
|
17
|
+
* id never hammers the API, and a concurrent burst for one id fires a single
|
|
18
|
+
* request (in-flight de-dupe).
|
|
19
|
+
*
|
|
20
|
+
* Built over the injected `users.info` slice so it unit-tests with a fake — no
|
|
21
|
+
* live workspace, no globals.
|
|
22
|
+
*/
|
|
23
|
+
/** The minimal `users.info` slice the directory drives (a `WebClient` satisfies it). */
|
|
24
|
+
export interface SlackUsersInfoApi {
|
|
25
|
+
users?: {
|
|
26
|
+
info?(args: {
|
|
27
|
+
user: string;
|
|
28
|
+
}): Promise<{
|
|
29
|
+
ok?: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
user?: SlackUserInfoUser;
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The subset of a Slack user object the directory reads for a display name. */
|
|
36
|
+
export interface SlackUserInfoUser {
|
|
37
|
+
id?: string;
|
|
38
|
+
name?: string;
|
|
39
|
+
real_name?: string;
|
|
40
|
+
profile?: {
|
|
41
|
+
display_name?: string;
|
|
42
|
+
real_name?: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export interface SlackUserDirectory {
|
|
46
|
+
/** Read the cached display name for a user id (no network). Undefined when not cached yet. */
|
|
47
|
+
resolveNameSync(id: string | undefined): string | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Fire a memoized background `users.info` so the name is cached for next time.
|
|
50
|
+
* No-op when the id is empty, isn't a user id, is already fresh in the cache,
|
|
51
|
+
* or has a request in flight. Never throws, never blocks.
|
|
52
|
+
*/
|
|
53
|
+
prime(id: string | undefined): void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* A Slack human USER id is `U…` or `W…` (Enterprise Grid). Bot ids (`B…`),
|
|
57
|
+
* channel ids (`C…`/`D…`/`G…`), and empties have no `users.info`, so we never
|
|
58
|
+
* call for them.
|
|
59
|
+
*/
|
|
60
|
+
export declare function isSlackUserId(id: string): boolean;
|
|
61
|
+
export declare function createSlackUserDirectory(args: {
|
|
62
|
+
web: SlackUsersInfoApi | null;
|
|
63
|
+
ttlMs?: number;
|
|
64
|
+
negativeTtlMs?: number;
|
|
65
|
+
/** Injectable clock for deterministic TTL tests (defaults to `Date.now`). */
|
|
66
|
+
nowImpl?: () => number;
|
|
67
|
+
log?: (msg: string, meta?: Record<string, unknown>) => void;
|
|
68
|
+
}): SlackUserDirectory;
|
|
69
|
+
//# sourceMappingURL=user-directory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-directory.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/slack/user-directory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IACjC,KAAK,CAAC,EAAE;QACP,IAAI,CAAC,CAAC,IAAI,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,GAAG,OAAO,CAAC;YAAE,EAAE,CAAC,EAAE,OAAO,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,iBAAiB,CAAA;SAAE,CAAC,CAAC;KACnG,CAAC;CACF;AAED,gFAAgF;AAChF,MAAM,WAAW,iBAAiB;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,WAAW,kBAAkB;IAClC,8FAA8F;IAC9F,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;IAC5D;;;;OAIG;IACH,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;CACpC;AAOD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC9C,GAAG,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC5D,GAAG,kBAAkB,CAyDrB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack user-name directory — a small cached `id → display-name` resolver.
|
|
3
|
+
*
|
|
4
|
+
* Slack message events carry only the sender's user id (`U…` / `W…`); the human
|
|
5
|
+
* name needs a `users.info` call. Doing that synchronously on the inbound hot
|
|
6
|
+
* path would add a network round-trip per message, so this directory resolves
|
|
7
|
+
* names in the BACKGROUND:
|
|
8
|
+
*
|
|
9
|
+
* - `prime(id)` fires a memoized, non-blocking `users.info` and caches the
|
|
10
|
+
* resolved name.
|
|
11
|
+
* - `resolveNameSync(id)` reads whatever is currently cached (no network).
|
|
12
|
+
*
|
|
13
|
+
* So the FIRST message from a never-seen user surfaces the raw id; every message
|
|
14
|
+
* after the prime settles surfaces the name. In a real workspace the bot sees
|
|
15
|
+
* the same people repeatedly, so names appear almost immediately. Names are
|
|
16
|
+
* cached with a TTL; failed / empty lookups are negative-cached briefly so a bad
|
|
17
|
+
* id never hammers the API, and a concurrent burst for one id fires a single
|
|
18
|
+
* request (in-flight de-dupe).
|
|
19
|
+
*
|
|
20
|
+
* Built over the injected `users.info` slice so it unit-tests with a fake — no
|
|
21
|
+
* live workspace, no globals.
|
|
22
|
+
*/
|
|
23
|
+
/** Positive cache lifetime — a resolved name is reused for an hour. */
|
|
24
|
+
const DEFAULT_TTL_MS = 60 * 60 * 1_000;
|
|
25
|
+
/** Negative cache lifetime — a failed / empty lookup is not retried for a minute. */
|
|
26
|
+
const NEGATIVE_TTL_MS = 60 * 1_000;
|
|
27
|
+
/**
|
|
28
|
+
* A Slack human USER id is `U…` or `W…` (Enterprise Grid). Bot ids (`B…`),
|
|
29
|
+
* channel ids (`C…`/`D…`/`G…`), and empties have no `users.info`, so we never
|
|
30
|
+
* call for them.
|
|
31
|
+
*/
|
|
32
|
+
export function isSlackUserId(id) {
|
|
33
|
+
return /^[UW][A-Z0-9]+$/.test(id);
|
|
34
|
+
}
|
|
35
|
+
export function createSlackUserDirectory(args) {
|
|
36
|
+
const ttlMs = args.ttlMs ?? DEFAULT_TTL_MS;
|
|
37
|
+
const negativeTtlMs = args.negativeTtlMs ?? NEGATIVE_TTL_MS;
|
|
38
|
+
const now = args.nowImpl ?? (() => Date.now());
|
|
39
|
+
const cache = new Map();
|
|
40
|
+
const inflight = new Set();
|
|
41
|
+
// Capture a `this`-bound `users.info` caller ONCE (null when the injected web
|
|
42
|
+
// has no such slice — e.g. a test fake), so `prime` stays a safe no-op and the
|
|
43
|
+
// call preserves the WebClient facet as its receiver.
|
|
44
|
+
const usersApi = args.web?.users;
|
|
45
|
+
const fetchUser = usersApi && usersApi.info ? (id) => usersApi.info.call(usersApi, { user: id }) : null;
|
|
46
|
+
const nameFromUser = (u) => {
|
|
47
|
+
if (!u)
|
|
48
|
+
return undefined;
|
|
49
|
+
const candidate = u.profile?.display_name || u.profile?.real_name || u.real_name || u.name;
|
|
50
|
+
const trimmed = typeof candidate === "string" ? candidate.trim() : "";
|
|
51
|
+
return trimmed || undefined;
|
|
52
|
+
};
|
|
53
|
+
const isFresh = (id) => {
|
|
54
|
+
const hit = cache.get(id);
|
|
55
|
+
return hit !== undefined && hit.expiresAt > now();
|
|
56
|
+
};
|
|
57
|
+
const resolveNameSync = (id) => {
|
|
58
|
+
if (!id)
|
|
59
|
+
return undefined;
|
|
60
|
+
const hit = cache.get(id);
|
|
61
|
+
if (hit && hit.expiresAt > now())
|
|
62
|
+
return hit.name;
|
|
63
|
+
return undefined;
|
|
64
|
+
};
|
|
65
|
+
const prime = (id) => {
|
|
66
|
+
if (!id || !fetchUser)
|
|
67
|
+
return;
|
|
68
|
+
if (!isSlackUserId(id))
|
|
69
|
+
return;
|
|
70
|
+
if (inflight.has(id) || isFresh(id))
|
|
71
|
+
return;
|
|
72
|
+
inflight.add(id);
|
|
73
|
+
void (async () => {
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetchUser(id);
|
|
76
|
+
const name = res && res.ok === false ? undefined : nameFromUser(res?.user);
|
|
77
|
+
cache.set(id, { ...(name ? { name } : {}), expiresAt: now() + (name ? ttlMs : negativeTtlMs) });
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// Negative-cache the failure so a transient/bad id isn't retried in a loop.
|
|
81
|
+
cache.set(id, { expiresAt: now() + negativeTtlMs });
|
|
82
|
+
args.log?.("slack users.info failed", {
|
|
83
|
+
user: id,
|
|
84
|
+
error: err instanceof Error ? err.message : String(err),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
inflight.delete(id);
|
|
89
|
+
}
|
|
90
|
+
})();
|
|
91
|
+
};
|
|
92
|
+
return { resolveNameSync, prime };
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=user-directory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-directory.js","sourceRoot":"","sources":["../../../../src/agents/channels/slack/user-directory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Events-API gateway route.
|
|
3
|
+
*
|
|
4
|
+
* In events transport mode (`channels.slack.mode: "events"`) Slack POSTs each
|
|
5
|
+
* event to a public URL instead of Brigade opening a Socket Mode websocket. This
|
|
6
|
+
* module builds the Brigade `HttpRoute` that receives those POSTs:
|
|
7
|
+
*
|
|
8
|
+
* 1. Verify the `X-Slack-Signature` header. Slack signs every request as
|
|
9
|
+
* `v0=` + HMAC-SHA256 of `v0:${timestamp}:${rawBody}` keyed with the app's
|
|
10
|
+
* signing secret. A mismatch (or a stale timestamp outside the replay
|
|
11
|
+
* window) → 401, BEFORE the body is routed, so a forged event can't reach
|
|
12
|
+
* the agent. The signature is computed over the RAW body, so the handler
|
|
13
|
+
* reads the raw bytes first and verifies before `JSON.parse`.
|
|
14
|
+
* 2. Answer the one-time `url_verification` handshake by echoing the
|
|
15
|
+
* `challenge` value (Slack's endpoint-ownership check).
|
|
16
|
+
* 3. For an `event_callback`, hand the inner event to the started Slack
|
|
17
|
+
* adapter's `feedWebhookEvent("event", …)`, which runs it through the SAME
|
|
18
|
+
* normalize + dedupe + dispatch path Socket Mode uses. Interactive
|
|
19
|
+
* (`block_actions`) + slash-command payloads arrive as
|
|
20
|
+
* `application/x-www-form-urlencoded` with a `payload=` field and route via
|
|
21
|
+
* `feedWebhookEvent("interactive" | "slash", …)`.
|
|
22
|
+
* 4. Reply `200` so Slack marks the event delivered.
|
|
23
|
+
*
|
|
24
|
+
* The route is registered with `auth: "none"` because Slack authenticates via
|
|
25
|
+
* the signed request, not Brigade's operator-auth (Slack can't present an
|
|
26
|
+
* operator credential). The signature check IS the auth.
|
|
27
|
+
*
|
|
28
|
+
* Socket mode never registers this route — it's added by the module only when
|
|
29
|
+
* events mode is configured, so the default local-first install exposes no
|
|
30
|
+
* inbound HTTP surface. Slack mirror of `telegram/webhook.ts`.
|
|
31
|
+
*/
|
|
32
|
+
import type { HttpRoute } from "../sdk.js";
|
|
33
|
+
/** Slack's request-signature header. */
|
|
34
|
+
export declare const SLACK_SIGNATURE_HEADER = "x-slack-signature";
|
|
35
|
+
/** Slack's request-timestamp header (replay-window guard). */
|
|
36
|
+
export declare const SLACK_TIMESTAMP_HEADER = "x-slack-request-timestamp";
|
|
37
|
+
/** Constant-time compare of two hex signatures — avoids leaking via timing. */
|
|
38
|
+
export declare function safeEqualSignature(a: string, b: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Verify a Slack request signature over the raw body. Returns false when no
|
|
41
|
+
* secret is configured AND a signature was supplied (a configured Slack app
|
|
42
|
+
* always signs), when the timestamp is missing / stale, or when the computed
|
|
43
|
+
* `v0=` HMAC doesn't match. When `expectedSecret` is empty the check is SKIPPED
|
|
44
|
+
* (returns true) — the operator opted out of verification (not recommended).
|
|
45
|
+
*/
|
|
46
|
+
export declare function verifySlackSignature(args: {
|
|
47
|
+
signingSecret: string;
|
|
48
|
+
signature: string | undefined;
|
|
49
|
+
timestamp: string | undefined;
|
|
50
|
+
rawBody: string;
|
|
51
|
+
nowSeconds?: number;
|
|
52
|
+
}): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Parse the request payload. Slack delivers EVENTS as JSON
|
|
55
|
+
* (`application/json`) and INTERACTIONS / SLASH-COMMANDS as
|
|
56
|
+
* `application/x-www-form-urlencoded` carrying a `payload=` (interactive) or
|
|
57
|
+
* flat form fields (slash). Returns a discriminated shape the handler routes on.
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseSlackBody(rawBody: string, contentType: string): {
|
|
60
|
+
kind: "json";
|
|
61
|
+
data: Record<string, unknown>;
|
|
62
|
+
} | {
|
|
63
|
+
kind: "interactive";
|
|
64
|
+
data: Record<string, unknown>;
|
|
65
|
+
} | {
|
|
66
|
+
kind: "slash";
|
|
67
|
+
data: Record<string, unknown>;
|
|
68
|
+
} | null;
|
|
69
|
+
/** The minimal adapter surface the webhook route drives. */
|
|
70
|
+
export interface SlackWebhookSink {
|
|
71
|
+
/** Feed a parsed Slack payload into the inbound path. */
|
|
72
|
+
feedWebhookEvent(kind: "event" | "interactive" | "slash", payload: unknown): void;
|
|
73
|
+
}
|
|
74
|
+
export interface BuildSlackWebhookRouteArgs {
|
|
75
|
+
/** The gateway route path (e.g. `/slack/events`). */
|
|
76
|
+
path: string;
|
|
77
|
+
/** The configured signing secret (`""` → no signature check). */
|
|
78
|
+
signingSecret: string;
|
|
79
|
+
/** Resolve the started adapter to feed events into (null when not started). */
|
|
80
|
+
resolveSink: () => SlackWebhookSink | null;
|
|
81
|
+
/** Logger (token-redacted upstream). */
|
|
82
|
+
log?: (msg: string, meta?: Record<string, unknown>) => void;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Build the Brigade `HttpRoute` for the Slack Events API. Register it via
|
|
86
|
+
* `b.httpRoute(...)` from the module when events mode is active.
|
|
87
|
+
*/
|
|
88
|
+
export declare function buildSlackWebhookRoute(args: BuildSlackWebhookRouteArgs): HttpRoute;
|
|
89
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/slack/webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,wCAAwC;AACxC,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAC1D,8DAA8D;AAC9D,eAAO,MAAM,sBAAsB,8BAA8B,CAAC;AAQlE,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAOhE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IAC1C,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAYV;AA0BD;;;;;GAKG;AACH,wBAAgB,cAAc,CAC7B,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GACjB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAAG,IAAI,CA0BpK;AAED,4DAA4D;AAC5D,MAAM,WAAW,gBAAgB;IAChC,yDAAyD;IACzD,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,GAAG,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CAClF;AAED,MAAM,WAAW,0BAA0B;IAC1C,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,aAAa,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,WAAW,EAAE,MAAM,gBAAgB,GAAG,IAAI,CAAC;IAC3C,wCAAwC;IACxC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC5D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,0BAA0B,GAAG,SAAS,CAoFlF"}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Events-API gateway route.
|
|
3
|
+
*
|
|
4
|
+
* In events transport mode (`channels.slack.mode: "events"`) Slack POSTs each
|
|
5
|
+
* event to a public URL instead of Brigade opening a Socket Mode websocket. This
|
|
6
|
+
* module builds the Brigade `HttpRoute` that receives those POSTs:
|
|
7
|
+
*
|
|
8
|
+
* 1. Verify the `X-Slack-Signature` header. Slack signs every request as
|
|
9
|
+
* `v0=` + HMAC-SHA256 of `v0:${timestamp}:${rawBody}` keyed with the app's
|
|
10
|
+
* signing secret. A mismatch (or a stale timestamp outside the replay
|
|
11
|
+
* window) → 401, BEFORE the body is routed, so a forged event can't reach
|
|
12
|
+
* the agent. The signature is computed over the RAW body, so the handler
|
|
13
|
+
* reads the raw bytes first and verifies before `JSON.parse`.
|
|
14
|
+
* 2. Answer the one-time `url_verification` handshake by echoing the
|
|
15
|
+
* `challenge` value (Slack's endpoint-ownership check).
|
|
16
|
+
* 3. For an `event_callback`, hand the inner event to the started Slack
|
|
17
|
+
* adapter's `feedWebhookEvent("event", …)`, which runs it through the SAME
|
|
18
|
+
* normalize + dedupe + dispatch path Socket Mode uses. Interactive
|
|
19
|
+
* (`block_actions`) + slash-command payloads arrive as
|
|
20
|
+
* `application/x-www-form-urlencoded` with a `payload=` field and route via
|
|
21
|
+
* `feedWebhookEvent("interactive" | "slash", …)`.
|
|
22
|
+
* 4. Reply `200` so Slack marks the event delivered.
|
|
23
|
+
*
|
|
24
|
+
* The route is registered with `auth: "none"` because Slack authenticates via
|
|
25
|
+
* the signed request, not Brigade's operator-auth (Slack can't present an
|
|
26
|
+
* operator credential). The signature check IS the auth.
|
|
27
|
+
*
|
|
28
|
+
* Socket mode never registers this route — it's added by the module only when
|
|
29
|
+
* events mode is configured, so the default local-first install exposes no
|
|
30
|
+
* inbound HTTP surface. Slack mirror of `telegram/webhook.ts`.
|
|
31
|
+
*/
|
|
32
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
33
|
+
/** Slack's request-signature header. */
|
|
34
|
+
export const SLACK_SIGNATURE_HEADER = "x-slack-signature";
|
|
35
|
+
/** Slack's request-timestamp header (replay-window guard). */
|
|
36
|
+
export const SLACK_TIMESTAMP_HEADER = "x-slack-request-timestamp";
|
|
37
|
+
/** Signature version prefix Slack uses (`v0`). */
|
|
38
|
+
const SLACK_SIG_VERSION = "v0";
|
|
39
|
+
/** Reject a request whose timestamp is older than this (replay protection). */
|
|
40
|
+
const MAX_TIMESTAMP_SKEW_SECONDS = 60 * 5;
|
|
41
|
+
/** Cap on the webhook body (a Slack event is small; 1 MiB is generous). */
|
|
42
|
+
const WEBHOOK_MAX_BODY_BYTES = 1 * 1024 * 1024;
|
|
43
|
+
/** Constant-time compare of two hex signatures — avoids leaking via timing. */
|
|
44
|
+
export function safeEqualSignature(a, b) {
|
|
45
|
+
if (a.length !== b.length)
|
|
46
|
+
return false;
|
|
47
|
+
try {
|
|
48
|
+
return timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Verify a Slack request signature over the raw body. Returns false when no
|
|
56
|
+
* secret is configured AND a signature was supplied (a configured Slack app
|
|
57
|
+
* always signs), when the timestamp is missing / stale, or when the computed
|
|
58
|
+
* `v0=` HMAC doesn't match. When `expectedSecret` is empty the check is SKIPPED
|
|
59
|
+
* (returns true) — the operator opted out of verification (not recommended).
|
|
60
|
+
*/
|
|
61
|
+
export function verifySlackSignature(args) {
|
|
62
|
+
if (!args.signingSecret)
|
|
63
|
+
return true; // no secret configured → no check
|
|
64
|
+
const sig = typeof args.signature === "string" ? args.signature : "";
|
|
65
|
+
const ts = typeof args.timestamp === "string" ? args.timestamp : "";
|
|
66
|
+
if (!sig || !ts)
|
|
67
|
+
return false;
|
|
68
|
+
const tsNum = Number.parseInt(ts, 10);
|
|
69
|
+
if (!Number.isFinite(tsNum))
|
|
70
|
+
return false;
|
|
71
|
+
const now = args.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
72
|
+
if (Math.abs(now - tsNum) > MAX_TIMESTAMP_SKEW_SECONDS)
|
|
73
|
+
return false; // stale → replay guard
|
|
74
|
+
const base = `${SLACK_SIG_VERSION}:${ts}:${args.rawBody}`;
|
|
75
|
+
const computed = `${SLACK_SIG_VERSION}=${createHmac("sha256", args.signingSecret).update(base).digest("hex")}`;
|
|
76
|
+
return safeEqualSignature(computed, sig);
|
|
77
|
+
}
|
|
78
|
+
/** Read a request body up to `maxBytes`, rejecting (→ null) when it overflows. */
|
|
79
|
+
function readBody(req, maxBytes) {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const chunks = [];
|
|
82
|
+
let size = 0;
|
|
83
|
+
let overflowed = false;
|
|
84
|
+
req.on("data", (chunk) => {
|
|
85
|
+
if (overflowed)
|
|
86
|
+
return;
|
|
87
|
+
size += chunk.length;
|
|
88
|
+
if (size > maxBytes) {
|
|
89
|
+
overflowed = true;
|
|
90
|
+
resolve(null);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
chunks.push(chunk);
|
|
94
|
+
});
|
|
95
|
+
req.on("end", () => {
|
|
96
|
+
if (overflowed)
|
|
97
|
+
return;
|
|
98
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
99
|
+
});
|
|
100
|
+
req.on("error", () => resolve(null));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse the request payload. Slack delivers EVENTS as JSON
|
|
105
|
+
* (`application/json`) and INTERACTIONS / SLASH-COMMANDS as
|
|
106
|
+
* `application/x-www-form-urlencoded` carrying a `payload=` (interactive) or
|
|
107
|
+
* flat form fields (slash). Returns a discriminated shape the handler routes on.
|
|
108
|
+
*/
|
|
109
|
+
export function parseSlackBody(rawBody, contentType) {
|
|
110
|
+
const ct = (contentType ?? "").toLowerCase();
|
|
111
|
+
if (ct.includes("application/json")) {
|
|
112
|
+
try {
|
|
113
|
+
return { kind: "json", data: JSON.parse(rawBody) };
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Form-encoded: an interactive payload rides as `payload=<json>`; a slash
|
|
120
|
+
// command is flat form fields (`command=/x&text=…`).
|
|
121
|
+
const params = new URLSearchParams(rawBody);
|
|
122
|
+
const payload = params.get("payload");
|
|
123
|
+
if (payload) {
|
|
124
|
+
try {
|
|
125
|
+
return { kind: "interactive", data: JSON.parse(payload) };
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (params.has("command")) {
|
|
132
|
+
const data = {};
|
|
133
|
+
for (const [k, v] of params.entries())
|
|
134
|
+
data[k] = v;
|
|
135
|
+
return { kind: "slash", data };
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Build the Brigade `HttpRoute` for the Slack Events API. Register it via
|
|
141
|
+
* `b.httpRoute(...)` from the module when events mode is active.
|
|
142
|
+
*/
|
|
143
|
+
export function buildSlackWebhookRoute(args) {
|
|
144
|
+
const handler = async (req, res) => {
|
|
145
|
+
const reply = (status, body, contentType = "application/json") => {
|
|
146
|
+
res.statusCode = status;
|
|
147
|
+
res.setHeader("content-type", contentType);
|
|
148
|
+
res.end(typeof body === "string" ? body : JSON.stringify(body));
|
|
149
|
+
};
|
|
150
|
+
// Only POST carries events.
|
|
151
|
+
if ((req.method ?? "").toUpperCase() !== "POST") {
|
|
152
|
+
reply(405, { ok: false, error: "method not allowed" });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// The gateway dispatcher has ALREADY drained the request stream and buffered
|
|
156
|
+
// it onto `req.body` (see core/server.ts). Re-reading the stream here would
|
|
157
|
+
// hang until the 30s timeout (→ 408) because the `data`/`end` events already
|
|
158
|
+
// fired. Read the pre-buffered body first; only fall back to streaming when
|
|
159
|
+
// the route is exercised outside the gateway (e.g. a direct unit test).
|
|
160
|
+
const pre = req.body;
|
|
161
|
+
const raw = pre ? pre.toString("utf8") : await readBody(req, WEBHOOK_MAX_BODY_BYTES);
|
|
162
|
+
if (raw === null) {
|
|
163
|
+
reply(413, { ok: false, error: "payload too large" });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Signature check FIRST (over the RAW body) — refuse a forged event before
|
|
167
|
+
// parsing / routing.
|
|
168
|
+
const sigHeader = req.headers[SLACK_SIGNATURE_HEADER];
|
|
169
|
+
const tsHeader = req.headers[SLACK_TIMESTAMP_HEADER];
|
|
170
|
+
const signature = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader;
|
|
171
|
+
const timestamp = Array.isArray(tsHeader) ? tsHeader[0] : tsHeader;
|
|
172
|
+
if (!verifySlackSignature({ signingSecret: args.signingSecret, signature, timestamp, rawBody: raw })) {
|
|
173
|
+
args.log?.("slack webhook rejected — bad signature");
|
|
174
|
+
reply(401, { ok: false, error: "unauthorized" });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const contentType = (() => {
|
|
178
|
+
const c = req.headers["content-type"];
|
|
179
|
+
return Array.isArray(c) ? (c[0] ?? "") : (c ?? "");
|
|
180
|
+
})();
|
|
181
|
+
const parsed = parseSlackBody(raw, contentType);
|
|
182
|
+
if (!parsed) {
|
|
183
|
+
reply(400, { ok: false, error: "invalid body" });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// The one-time endpoint-ownership handshake — echo the challenge verbatim.
|
|
187
|
+
if (parsed.kind === "json" && parsed.data["type"] === "url_verification") {
|
|
188
|
+
const challenge = typeof parsed.data["challenge"] === "string" ? parsed.data["challenge"] : "";
|
|
189
|
+
reply(200, { challenge });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const sink = args.resolveSink();
|
|
193
|
+
if (sink) {
|
|
194
|
+
try {
|
|
195
|
+
if (parsed.kind === "json" && parsed.data["type"] === "event_callback") {
|
|
196
|
+
sink.feedWebhookEvent("event", parsed.data);
|
|
197
|
+
}
|
|
198
|
+
else if (parsed.kind === "interactive") {
|
|
199
|
+
sink.feedWebhookEvent("interactive", parsed.data);
|
|
200
|
+
}
|
|
201
|
+
else if (parsed.kind === "slash") {
|
|
202
|
+
sink.feedWebhookEvent("slash", parsed.data);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
args.log?.("slack webhook dispatch threw", { error: err instanceof Error ? err.message : String(err) });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// Always 200 so Slack doesn't retry-storm — a dispatch error is ours to fix,
|
|
210
|
+
// not Slack's to redeliver. A slash command replies empty to clear the spinner.
|
|
211
|
+
if (parsed.kind === "slash") {
|
|
212
|
+
reply(200, "");
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
reply(200, { ok: true });
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
return {
|
|
219
|
+
method: "POST",
|
|
220
|
+
path: args.path,
|
|
221
|
+
auth: "none", // Slack can't present operator-auth; the signed request IS the auth.
|
|
222
|
+
match: "exact",
|
|
223
|
+
maxBodyBytes: WEBHOOK_MAX_BODY_BYTES,
|
|
224
|
+
skipSessionGuard: true,
|
|
225
|
+
handler,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.js","sourceRoot":"","sources":["../../../../src/agents/channels/slack/webhook.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/telegram/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,OAAO,EACN,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EAOxB,KAAK,mBAAmB,EAExB,MAAM,WAAW,CAAC;AAiBnB,OAAO,EAAmB,KAAK,mBAAmB,EAA4B,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAUtJ,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC5C,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACzE;;;;;OAKG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9D;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,4BAAiC,GAAG,cAAc,
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/telegram/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AASH,OAAO,EACN,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EAOxB,KAAK,mBAAmB,EAExB,MAAM,WAAW,CAAC;AAiBnB,OAAO,EAAmB,KAAK,mBAAmB,EAA4B,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAUtJ,mEAAmE;AACnE,MAAM,WAAW,4BAA4B;IAC5C,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,mBAAmB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACzE;;;;;OAKG;IACH,WAAW,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC9D;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,GAAE,4BAAiC,GAAG,cAAc,CAsjB7F;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAItG;AAED,qFAAqF;AACrF,eAAO,MAAM,qBAAqB,EAAE,mBAUnC,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACtD,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtH;;;;OAIG;IACH,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACzC,qFAAqF;IACrF,aAAa,IAAI,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;CACrD;AAgBD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAIpD"}
|
|
@@ -242,6 +242,13 @@ export function createTelegramAdapter(opts = {}) {
|
|
|
242
242
|
// to the FIRST chunk only — quoting every chunk of a long reply would
|
|
243
243
|
// render a chain of quoted messages. Omitted → unquoted send (unchanged).
|
|
244
244
|
const replyToMessageId = opts?.replyToId;
|
|
245
|
+
// Send options honored on every chunk: silent notification + link-preview
|
|
246
|
+
// toggle. Purely additive — undefined keeps the prior behavior byte-for-byte.
|
|
247
|
+
const sendExtras = { threadId };
|
|
248
|
+
if (opts?.silent)
|
|
249
|
+
sendExtras.silent = true;
|
|
250
|
+
if (opts?.linkPreview !== undefined)
|
|
251
|
+
sendExtras.linkPreview = opts.linkPreview;
|
|
245
252
|
// Chunk on the RAW markdown so fences/paragraphs aren't shredded, then
|
|
246
253
|
// convert each chunk to Telegram HTML and send. A chunk whose HTML is
|
|
247
254
|
// empty (syntax-only) or that Telegram rejects with a parse error is
|
|
@@ -255,19 +262,19 @@ export function createTelegramAdapter(opts = {}) {
|
|
|
255
262
|
if (telegramHtmlIsEmpty(html)) {
|
|
256
263
|
// Nothing renderable — send the raw chunk as plain text (if it has any).
|
|
257
264
|
if (chunk.trim().length > 0) {
|
|
258
|
-
await connection.sendText(conversationId, chunk, {
|
|
265
|
+
await connection.sendText(conversationId, chunk, { ...sendExtras, ...replyOpt });
|
|
259
266
|
first = false;
|
|
260
267
|
}
|
|
261
268
|
continue;
|
|
262
269
|
}
|
|
263
270
|
try {
|
|
264
|
-
await connection.sendText(conversationId, html, { html: true,
|
|
271
|
+
await connection.sendText(conversationId, html, { ...sendExtras, html: true, ...replyOpt });
|
|
265
272
|
}
|
|
266
273
|
catch (err) {
|
|
267
274
|
const msg = err instanceof Error ? err.message : String(err);
|
|
268
275
|
if (PARSE_ERROR_RE.test(msg) && chunk.trim().length > 0) {
|
|
269
276
|
// HTML failed to parse — fall back to the plain chunk.
|
|
270
|
-
await connection.sendText(conversationId, chunk, {
|
|
277
|
+
await connection.sendText(conversationId, chunk, { ...sendExtras, ...replyOpt });
|
|
271
278
|
}
|
|
272
279
|
else {
|
|
273
280
|
throw err;
|