@spinabot/brigade 1.6.0 → 1.7.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/dist/agents/agent-loop.d.ts.map +1 -1
- package/dist/agents/agent-loop.js +51 -1
- package/dist/agents/agent-loop.js.map +1 -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/discord/account-config.d.ts +177 -0
- package/dist/agents/channels/discord/account-config.d.ts.map +1 -0
- package/dist/agents/channels/discord/account-config.js +349 -0
- package/dist/agents/channels/discord/account-config.js.map +1 -0
- package/dist/agents/channels/discord/adapter.d.ts +79 -0
- package/dist/agents/channels/discord/adapter.d.ts.map +1 -0
- package/dist/agents/channels/discord/adapter.js +693 -0
- package/dist/agents/channels/discord/adapter.js.map +1 -0
- package/dist/agents/channels/discord/approval-authorize.d.ts +43 -0
- package/dist/agents/channels/discord/approval-authorize.d.ts.map +1 -0
- package/dist/agents/channels/discord/approval-authorize.js +71 -0
- package/dist/agents/channels/discord/approval-authorize.js.map +1 -0
- package/dist/agents/channels/discord/approval-native.d.ts +68 -0
- package/dist/agents/channels/discord/approval-native.d.ts.map +1 -0
- package/dist/agents/channels/discord/approval-native.js +81 -0
- package/dist/agents/channels/discord/approval-native.js.map +1 -0
- package/dist/agents/channels/discord/command-menu.d.ts +49 -0
- package/dist/agents/channels/discord/command-menu.d.ts.map +1 -0
- package/dist/agents/channels/discord/command-menu.js +73 -0
- package/dist/agents/channels/discord/command-menu.js.map +1 -0
- package/dist/agents/channels/discord/component-blocks.d.ts +108 -0
- package/dist/agents/channels/discord/component-blocks.d.ts.map +1 -0
- package/dist/agents/channels/discord/component-blocks.js +113 -0
- package/dist/agents/channels/discord/component-blocks.js.map +1 -0
- package/dist/agents/channels/discord/components.d.ts +175 -0
- package/dist/agents/channels/discord/components.d.ts.map +1 -0
- package/dist/agents/channels/discord/components.js +220 -0
- package/dist/agents/channels/discord/components.js.map +1 -0
- package/dist/agents/channels/discord/connection.d.ts +570 -0
- package/dist/agents/channels/discord/connection.d.ts.map +1 -0
- package/dist/agents/channels/discord/connection.js +1600 -0
- package/dist/agents/channels/discord/connection.js.map +1 -0
- package/dist/agents/channels/discord/directory-cache.d.ts +47 -0
- package/dist/agents/channels/discord/directory-cache.d.ts.map +1 -0
- package/dist/agents/channels/discord/directory-cache.js +131 -0
- package/dist/agents/channels/discord/directory-cache.js.map +1 -0
- package/dist/agents/channels/discord/directory-live.d.ts +61 -0
- package/dist/agents/channels/discord/directory-live.d.ts.map +1 -0
- package/dist/agents/channels/discord/directory-live.js +140 -0
- package/dist/agents/channels/discord/directory-live.js.map +1 -0
- package/dist/agents/channels/discord/draft-stream.d.ts +92 -0
- package/dist/agents/channels/discord/draft-stream.d.ts.map +1 -0
- package/dist/agents/channels/discord/draft-stream.js +213 -0
- package/dist/agents/channels/discord/draft-stream.js.map +1 -0
- package/dist/agents/channels/discord/format.d.ts +70 -0
- package/dist/agents/channels/discord/format.d.ts.map +1 -0
- package/dist/agents/channels/discord/format.js +303 -0
- package/dist/agents/channels/discord/format.js.map +1 -0
- package/dist/agents/channels/discord/guilds.d.ts +25 -0
- package/dist/agents/channels/discord/guilds.d.ts.map +1 -0
- package/dist/agents/channels/discord/guilds.js +46 -0
- package/dist/agents/channels/discord/guilds.js.map +1 -0
- package/dist/agents/channels/discord/inbound-extras.d.ts +377 -0
- package/dist/agents/channels/discord/inbound-extras.d.ts.map +1 -0
- package/dist/agents/channels/discord/inbound-extras.js +589 -0
- package/dist/agents/channels/discord/inbound-extras.js.map +1 -0
- package/dist/agents/channels/discord/index.d.ts +21 -0
- package/dist/agents/channels/discord/index.d.ts.map +1 -0
- package/dist/agents/channels/discord/index.js +21 -0
- package/dist/agents/channels/discord/index.js.map +1 -0
- package/dist/agents/channels/discord/media.d.ts +85 -0
- package/dist/agents/channels/discord/media.d.ts.map +1 -0
- package/dist/agents/channels/discord/media.js +242 -0
- package/dist/agents/channels/discord/media.js.map +1 -0
- package/dist/agents/channels/discord/modal-registry.d.ts +89 -0
- package/dist/agents/channels/discord/modal-registry.d.ts.map +1 -0
- package/dist/agents/channels/discord/modal-registry.js +104 -0
- package/dist/agents/channels/discord/modal-registry.js.map +1 -0
- package/dist/agents/channels/discord/modals.d.ts +100 -0
- package/dist/agents/channels/discord/modals.d.ts.map +1 -0
- package/dist/agents/channels/discord/modals.js +124 -0
- package/dist/agents/channels/discord/modals.js.map +1 -0
- package/dist/agents/channels/discord/module.d.ts +15 -0
- package/dist/agents/channels/discord/module.d.ts.map +1 -0
- package/dist/agents/channels/discord/module.js +22 -0
- package/dist/agents/channels/discord/module.js.map +1 -0
- package/dist/agents/channels/discord/permission-audit.d.ts +43 -0
- package/dist/agents/channels/discord/permission-audit.d.ts.map +1 -0
- package/dist/agents/channels/discord/permission-audit.js +192 -0
- package/dist/agents/channels/discord/permission-audit.js.map +1 -0
- package/dist/agents/channels/discord/plugin.d.ts +89 -0
- package/dist/agents/channels/discord/plugin.d.ts.map +1 -0
- package/dist/agents/channels/discord/plugin.js +372 -0
- package/dist/agents/channels/discord/plugin.js.map +1 -0
- package/dist/agents/channels/discord/probe.d.ts +115 -0
- package/dist/agents/channels/discord/probe.d.ts.map +1 -0
- package/dist/agents/channels/discord/probe.js +193 -0
- package/dist/agents/channels/discord/probe.js.map +1 -0
- package/dist/agents/channels/discord/reasoning-lane.d.ts +42 -0
- package/dist/agents/channels/discord/reasoning-lane.d.ts.map +1 -0
- package/dist/agents/channels/discord/reasoning-lane.js +68 -0
- package/dist/agents/channels/discord/reasoning-lane.js.map +1 -0
- package/dist/agents/channels/discord/rest-actions.d.ts +346 -0
- package/dist/agents/channels/discord/rest-actions.d.ts.map +1 -0
- package/dist/agents/channels/discord/rest-actions.js +559 -0
- package/dist/agents/channels/discord/rest-actions.js.map +1 -0
- package/dist/agents/channels/discord/rest-components.d.ts +122 -0
- package/dist/agents/channels/discord/rest-components.d.ts.map +1 -0
- package/dist/agents/channels/discord/rest-components.js +243 -0
- package/dist/agents/channels/discord/rest-components.js.map +1 -0
- package/dist/agents/channels/discord/security-audit.d.ts +29 -0
- package/dist/agents/channels/discord/security-audit.d.ts.map +1 -0
- package/dist/agents/channels/discord/security-audit.js +94 -0
- package/dist/agents/channels/discord/security-audit.js.map +1 -0
- package/dist/agents/channels/discord/security-doctor.d.ts +43 -0
- package/dist/agents/channels/discord/security-doctor.d.ts.map +1 -0
- package/dist/agents/channels/discord/security-doctor.js +83 -0
- package/dist/agents/channels/discord/security-doctor.js.map +1 -0
- package/dist/agents/channels/discord/status-issues.d.ts +37 -0
- package/dist/agents/channels/discord/status-issues.d.ts.map +1 -0
- package/dist/agents/channels/discord/status-issues.js +66 -0
- package/dist/agents/channels/discord/status-issues.js.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts +57 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.js +98 -0
- package/dist/agents/channels/discord/subagent-thread-binding-store.js.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding.d.ts +95 -0
- package/dist/agents/channels/discord/subagent-thread-binding.d.ts.map +1 -0
- package/dist/agents/channels/discord/subagent-thread-binding.js +208 -0
- package/dist/agents/channels/discord/subagent-thread-binding.js.map +1 -0
- package/dist/agents/channels/discord/system-events.d.ts +31 -0
- package/dist/agents/channels/discord/system-events.d.ts.map +1 -0
- package/dist/agents/channels/discord/system-events.js +74 -0
- package/dist/agents/channels/discord/system-events.js.map +1 -0
- package/dist/agents/channels/general-callback.d.ts +12 -0
- package/dist/agents/channels/general-callback.d.ts.map +1 -1
- package/dist/agents/channels/general-callback.js +18 -0
- package/dist/agents/channels/general-callback.js.map +1 -1
- package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
- package/dist/agents/channels/inbound-pipeline.js +70 -10
- package/dist/agents/channels/inbound-pipeline.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/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 +7 -0
- package/dist/agents/extensions/types.d.ts.map +1 -1
- package/dist/agents/extensions/types.js.map +1 -1
- package/dist/agents/subagent-announce-delivery.d.ts +10 -0
- package/dist/agents/subagent-announce-delivery.d.ts.map +1 -1
- package/dist/agents/subagent-announce-delivery.js +1 -0
- package/dist/agents/subagent-announce-delivery.js.map +1 -1
- package/dist/agents/subagent-completion-bridge.d.ts.map +1 -1
- package/dist/agents/subagent-completion-bridge.js +81 -0
- package/dist/agents/subagent-completion-bridge.js.map +1 -1
- package/dist/agents/subagent-spawn.d.ts.map +1 -1
- package/dist/agents/subagent-spawn.js +57 -4
- package/dist/agents/subagent-spawn.js.map +1 -1
- package/dist/agents/tools/cron-tool.d.ts.map +1 -1
- package/dist/agents/tools/cron-tool.js +4 -1
- package/dist/agents/tools/cron-tool.js.map +1 -1
- package/dist/agents/tools/discord-action-tool.d.ts +224 -0
- package/dist/agents/tools/discord-action-tool.d.ts.map +1 -0
- package/dist/agents/tools/discord-action-tool.js +848 -0
- package/dist/agents/tools/discord-action-tool.js.map +1 -0
- package/dist/agents/tools/registry.d.ts.map +1 -1
- package/dist/agents/tools/registry.js +21 -0
- package/dist/agents/tools/registry.js.map +1 -1
- package/dist/agents/tools/sessions/index.d.ts +8 -0
- package/dist/agents/tools/sessions/index.d.ts.map +1 -1
- package/dist/agents/tools/sessions/index.js +15 -3
- package/dist/agents/tools/sessions/index.js.map +1 -1
- package/dist/buildstamp.json +1 -1
- package/dist/cli/commands/channels.d.ts +2 -0
- package/dist/cli/commands/channels.d.ts.map +1 -1
- package/dist/cli/commands/channels.js +58 -1
- package/dist/cli/commands/channels.js.map +1 -1
- package/dist/core/auth-bridge.d.ts +1 -0
- package/dist/core/auth-bridge.d.ts.map +1 -1
- package/dist/core/auth-bridge.js +46 -1
- package/dist/core/auth-bridge.js.map +1 -1
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +18 -2
- package/dist/core/server.js.map +1 -1
- package/dist/cron/isolated-agent/run-executor.d.ts +11 -0
- package/dist/cron/isolated-agent/run-executor.d.ts.map +1 -1
- package/dist/cron/isolated-agent/run-executor.js +20 -4
- package/dist/cron/isolated-agent/run-executor.js.map +1 -1
- package/dist/cron/types.d.ts +8 -0
- package/dist/cron/types.d.ts.map +1 -1
- package/dist/system-prompt/assembler.d.ts.map +1 -1
- package/dist/system-prompt/assembler.js +4 -2
- package/dist/system-prompt/assembler.js.map +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Discord channel — public surface. */
|
|
2
|
+
export { createDiscordAdapter, buildReactionNote, DISCORD_CAPABILITIES, } from "./adapter.js";
|
|
3
|
+
export { listDiscordAccountIds, resolveDiscordAccount, resolveDiscordAutoThread, resolveDiscordBotToken, resolveDiscordPresence, resolveDiscordProxyUrl, discordChannelEnabled, discordLiveStreamEnabled, discordReactionNotifications, discordStreamThrottleMs, discordSurfaceReasoning, discordThreadIdleTtlMs, maskProxyUrl, stripBotPrefix, DISCORD_BOT_TOKEN_ENV_VAR, DISCORD_CHANNEL_ID, DISCORD_DEFAULT_ACCOUNT_ID, } from "./account-config.js";
|
|
4
|
+
export { buildDiscordApprovalMessage, buildDiscordApprovalText, parseDiscordApprovalAction, } from "./approval-native.js";
|
|
5
|
+
export { resolveDiscordApprover } from "./approval-authorize.js";
|
|
6
|
+
export { buildDiscordApprovalRows, buildDiscordButtonRows, sanitizeDiscordCustomId, DISCORD_BUTTON_STYLE, DISCORD_BUTTONS_PER_ROW, DISCORD_CUSTOM_ID_MAX_CHARS, DISCORD_MAX_ROWS, } from "./components.js";
|
|
7
|
+
export { buildDiscordCommandManifest, normalizeDiscordCommandName, } from "./command-menu.js";
|
|
8
|
+
export { connectDiscord, discordBackoffDelay, isDiscordUnauthorized, redactDiscordToken, sanitizeThreadName, } from "./connection.js";
|
|
9
|
+
export { downloadDiscordAttachment, buildDiscordAttachment, isAllowedDiscordAttachmentUrl, } from "./media.js";
|
|
10
|
+
export { markdownToDiscord, discordTextIsEmpty, DISCORD_MESSAGE_LIMIT } from "./format.js";
|
|
11
|
+
export { createDiscordPlugin } from "./plugin.js";
|
|
12
|
+
export { probeDiscord, decodeMessageContentIntent, decodePrivilegedIntents, MESSAGE_CONTENT_DISABLED_WARNING, } from "./probe.js";
|
|
13
|
+
export { listDiscordGuilds } from "./guilds.js";
|
|
14
|
+
export { listDiscordDirectoryPeers, listDiscordDirectoryGroups, } from "./directory-live.js";
|
|
15
|
+
export { auditDiscordChannelPermissions, } from "./permission-audit.js";
|
|
16
|
+
export { collectDiscordStatusIssues } from "./status-issues.js";
|
|
17
|
+
export { collectDiscordSecurityAuditFindings } from "./security-audit.js";
|
|
18
|
+
export { isDiscordMutableAllowEntry, scanDiscordNumericIdHazards, } from "./security-doctor.js";
|
|
19
|
+
export { collectConfiguredDiscordChannelIds } from "./plugin.js";
|
|
20
|
+
export { discordModule } from "./module.js";
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AAExC,OAAO,EACN,oBAAoB,EACpB,iBAAiB,EACjB,oBAAoB,GAGpB,MAAM,cAAc,CAAC;AACtB,OAAO,EACN,qBAAqB,EACrB,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,4BAA4B,EAC5B,uBAAuB,EACvB,uBAAuB,EACvB,sBAAsB,EACtB,YAAY,EACZ,cAAc,EACd,yBAAyB,EACzB,kBAAkB,EAClB,0BAA0B,GAQ1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACN,2BAA2B,EAC3B,wBAAwB,EACxB,0BAA0B,GAE1B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AACjE,OAAO,EACN,wBAAwB,EACxB,sBAAsB,EACtB,uBAAuB,EACvB,oBAAoB,EACpB,uBAAuB,EACvB,2BAA2B,EAC3B,gBAAgB,GAGhB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,2BAA2B,EAC3B,2BAA2B,GAE3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACN,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,GAKlB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACN,yBAAyB,EACzB,sBAAsB,EACtB,6BAA6B,GAE7B,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAC3F,OAAO,EAAE,mBAAmB,EAAoD,MAAM,aAAa,CAAC;AACpG,OAAO,EACN,YAAY,EACZ,0BAA0B,EAC1B,uBAAuB,EACvB,gCAAgC,GAMhC,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,iBAAiB,EAA4B,MAAM,aAAa,CAAC;AAC1E,OAAO,EACN,yBAAyB,EACzB,0BAA0B,GAG1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACN,8BAA8B,GAG9B,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,0BAA0B,EAA6B,MAAM,oBAAoB,CAAC;AAC3F,OAAO,EAAE,mCAAmC,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EACN,0BAA0B,EAC1B,2BAA2B,GAE3B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,kCAAkC,EAA8B,MAAM,aAAa,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord media helpers — inbound download + outbound upload construction.
|
|
3
|
+
*
|
|
4
|
+
* INBOUND: a Discord message attachment carries a public CDN `url`
|
|
5
|
+
* (cdn.discordapp.com / media.discordapp.net). We GET that URL (NO auth header —
|
|
6
|
+
* Discord attachment URLs are public, unlike Slack's `url_private`) and save the
|
|
7
|
+
* bytes under `~/.brigade/channels/discord/media/<YYYY-MM-DD>/<id>.<ext>` so the
|
|
8
|
+
* agent can `read` the attachment by path. In convex mode the cache relocates to
|
|
9
|
+
* the OS cache dir (never under ~/.brigade, to respect the strict-zero guard).
|
|
10
|
+
*
|
|
11
|
+
* OUTBOUND: `buildDiscordAttachment` validates a local path through Brigade's
|
|
12
|
+
* outbound media-path guard (so a prompt-injected "send ~/.ssh/id_rsa" can't
|
|
13
|
+
* exfiltrate a secret), then returns the `{ path, name }` the connection wraps
|
|
14
|
+
* in a discord.js `AttachmentBuilder`. The builder itself lives in the
|
|
15
|
+
* connection (which imports discord.js); this module stays dependency-light +
|
|
16
|
+
* unit-testable.
|
|
17
|
+
*
|
|
18
|
+
* SSRF GUARD: even though Discord URLs are public, we REQUIRE https + a Discord
|
|
19
|
+
* CDN host before fetching. Without this a prompt-injected / spoofed message
|
|
20
|
+
* could carry `http://169.254.169.254/…` (cloud metadata) or any attacker host
|
|
21
|
+
* and Brigade would fetch it — a classic SSRF. Mirrors `slack/media.ts`.
|
|
22
|
+
*/
|
|
23
|
+
import { type InboundMediaAttachment, type OutboundMedia } from "../sdk.js";
|
|
24
|
+
import { type DiscordAttachmentLike } from "./inbound-extras.js";
|
|
25
|
+
/**
|
|
26
|
+
* Defensive ceiling on an inbound attachment download. Discord's own per-file
|
|
27
|
+
* limit is generous (and tiered by server boost); we cap at 50 MB so a huge
|
|
28
|
+
* upload can't blow out memory. Anything larger is skipped (the message still
|
|
29
|
+
* reaches the agent without the attachment).
|
|
30
|
+
*/
|
|
31
|
+
declare const MAX_BYTES: number;
|
|
32
|
+
/**
|
|
33
|
+
* True when `rawUrl` is an https URL whose host is a Discord CDN host (or a
|
|
34
|
+
* subdomain of one). Anything else (non-https, a non-Discord host, or an
|
|
35
|
+
* unparseable URL) returns false so the caller refuses to fetch.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isAllowedDiscordAttachmentUrl(rawUrl: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Bounded retry for the transient attachment fetch. Discord's CDN occasionally
|
|
40
|
+
* blips on a 5xx or a network reset; one or two quick retries turn a dropped
|
|
41
|
+
* attachment into a delivered one. The caller still wraps the whole thing in a
|
|
42
|
+
* try/catch that degrades to `null`, so an exhausted retry never breaks message
|
|
43
|
+
* delivery. Mirrors `slack/media.ts`'s `withSlackRetry`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function withDiscordRetry<T>(fn: () => Promise<T>, attempts?: number): Promise<T>;
|
|
46
|
+
export interface DownloadDiscordAttachmentArgs {
|
|
47
|
+
/** The Discord attachment object (from the message's `attachments`). */
|
|
48
|
+
attachment: DiscordAttachmentLike;
|
|
49
|
+
/** Injectable fetch (defaults to global fetch) — lets tests stub the download. */
|
|
50
|
+
fetchImpl?: typeof fetch;
|
|
51
|
+
/** Logger so a failed download logs without crashing the inbound flow. */
|
|
52
|
+
log?: (msg: string, meta?: Record<string, unknown>) => void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Download one inbound Discord attachment to disk and return its normalized
|
|
56
|
+
* descriptor, or `null` when it couldn't be fetched (no url / too big / network
|
|
57
|
+
* error / non-Discord host). Never throws — a download glitch must not break
|
|
58
|
+
* message delivery.
|
|
59
|
+
*/
|
|
60
|
+
export declare function downloadDiscordAttachment(args: DownloadDiscordAttachmentArgs): Promise<InboundMediaAttachment | null>;
|
|
61
|
+
/** The validated outbound attachment shape the connection wraps in an `AttachmentBuilder`. */
|
|
62
|
+
export interface DiscordOutboundAttachment {
|
|
63
|
+
/** Local filesystem path (guard-validated). */
|
|
64
|
+
path: string;
|
|
65
|
+
/** Display filename. */
|
|
66
|
+
name: string;
|
|
67
|
+
/** Optional caption — sent as the message content alongside the file. */
|
|
68
|
+
caption?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Validate + shape a local file for outbound upload. Runs the path through
|
|
72
|
+
* Brigade's outbound media-path guard and throws a clear operator-facing error
|
|
73
|
+
* when the guard refuses it (the `send_media` tool surfaces it). Returns the
|
|
74
|
+
* `{ path, name, caption? }` the connection turns into a discord.js
|
|
75
|
+
* `AttachmentBuilder`.
|
|
76
|
+
*
|
|
77
|
+
* Filename precedence: an explicit `media.fileName` wins; otherwise the path's
|
|
78
|
+
* basename. When the chosen name has NO extension we append one inferred from
|
|
79
|
+
* `media.kind` (image→png, video→mp4, audio→mp3, voice→ogg) so Discord detects
|
|
80
|
+
* the file type instead of treating it as an opaque blob (an extensionless
|
|
81
|
+
* `image` upload otherwise renders as a generic file, not an inline preview).
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildDiscordAttachment(media: OutboundMedia): DiscordOutboundAttachment;
|
|
84
|
+
export { MAX_BYTES as DISCORD_MEDIA_MAX_BYTES };
|
|
85
|
+
//# sourceMappingURL=media.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/media.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAUH,OAAO,EAA6B,KAAK,sBAAsB,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AACvG,OAAO,EAAgC,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAI/F;;;;;GAKG;AACH,QAAA,MAAM,SAAS,QAAmB,CAAC;AAUnC;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAUrE;AAuCD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,QAAQ,SAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAWxF;AAED,MAAM,WAAW,6BAA6B;IAC7C,wEAAwE;IACxE,UAAU,EAAE,qBAAqB,CAAC;IAClC,kFAAkF;IAClF,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,0EAA0E;IAC1E,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC5D;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,6BAA6B,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CA4D3H;AAED,8FAA8F;AAC9F,MAAM,WAAW,yBAAyB;IACzC,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAoBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,aAAa,GAAG,yBAAyB,CAiBtF;AAED,OAAO,EAAE,SAAS,IAAI,uBAAuB,EAAE,CAAC"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord media helpers — inbound download + outbound upload construction.
|
|
3
|
+
*
|
|
4
|
+
* INBOUND: a Discord message attachment carries a public CDN `url`
|
|
5
|
+
* (cdn.discordapp.com / media.discordapp.net). We GET that URL (NO auth header —
|
|
6
|
+
* Discord attachment URLs are public, unlike Slack's `url_private`) and save the
|
|
7
|
+
* bytes under `~/.brigade/channels/discord/media/<YYYY-MM-DD>/<id>.<ext>` so the
|
|
8
|
+
* agent can `read` the attachment by path. In convex mode the cache relocates to
|
|
9
|
+
* the OS cache dir (never under ~/.brigade, to respect the strict-zero guard).
|
|
10
|
+
*
|
|
11
|
+
* OUTBOUND: `buildDiscordAttachment` validates a local path through Brigade's
|
|
12
|
+
* outbound media-path guard (so a prompt-injected "send ~/.ssh/id_rsa" can't
|
|
13
|
+
* exfiltrate a secret), then returns the `{ path, name }` the connection wraps
|
|
14
|
+
* in a discord.js `AttachmentBuilder`. The builder itself lives in the
|
|
15
|
+
* connection (which imports discord.js); this module stays dependency-light +
|
|
16
|
+
* unit-testable.
|
|
17
|
+
*
|
|
18
|
+
* SSRF GUARD: even though Discord URLs are public, we REQUIRE https + a Discord
|
|
19
|
+
* CDN host before fetching. Without this a prompt-injected / spoofed message
|
|
20
|
+
* could carry `http://169.254.169.254/…` (cloud metadata) or any attacker host
|
|
21
|
+
* and Brigade would fetch it — a classic SSRF. Mirrors `slack/media.ts`.
|
|
22
|
+
*/
|
|
23
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import { resolveChannelStateDir, resolveOsCacheDir } from "../../../config/paths.js";
|
|
26
|
+
import { tryGetRuntimeContext } from "../../../storage/runtime-context.js";
|
|
27
|
+
// Channel SDK barrel — the outbound-media exfil guard + the contract types. All
|
|
28
|
+
// contract types come from the channel SDK barrel so the channel is built
|
|
29
|
+
// entirely on `../sdk.js`.
|
|
30
|
+
import { validateOutboundMediaPath } from "../sdk.js";
|
|
31
|
+
import { resolveDiscordAttachmentKind } from "./inbound-extras.js";
|
|
32
|
+
const CHANNEL_ID = "discord";
|
|
33
|
+
/**
|
|
34
|
+
* Defensive ceiling on an inbound attachment download. Discord's own per-file
|
|
35
|
+
* limit is generous (and tiered by server boost); we cap at 50 MB so a huge
|
|
36
|
+
* upload can't blow out memory. Anything larger is skipped (the message still
|
|
37
|
+
* reaches the agent without the attachment).
|
|
38
|
+
*/
|
|
39
|
+
const MAX_BYTES = 50 * 1024 * 1024;
|
|
40
|
+
/**
|
|
41
|
+
* Discord's attachment-CDN hosts. An inbound attachment `url` only ever points
|
|
42
|
+
* at one of these — before fetching we REQUIRE https + a Discord host so a
|
|
43
|
+
* spoofed message can't make Brigade fetch an arbitrary internal URL (SSRF).
|
|
44
|
+
* Subdomains of these hosts are allowed.
|
|
45
|
+
*/
|
|
46
|
+
const DISCORD_CDN_HOSTS = ["cdn.discordapp.com", "media.discordapp.net", "cdn.discord.com"];
|
|
47
|
+
/**
|
|
48
|
+
* True when `rawUrl` is an https URL whose host is a Discord CDN host (or a
|
|
49
|
+
* subdomain of one). Anything else (non-https, a non-Discord host, or an
|
|
50
|
+
* unparseable URL) returns false so the caller refuses to fetch.
|
|
51
|
+
*/
|
|
52
|
+
export function isAllowedDiscordAttachmentUrl(rawUrl) {
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = new URL(rawUrl);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (parsed.protocol !== "https:")
|
|
61
|
+
return false;
|
|
62
|
+
const host = parsed.hostname.toLowerCase();
|
|
63
|
+
return DISCORD_CDN_HOSTS.some((h) => host === h || host.endsWith(`.${h}`));
|
|
64
|
+
}
|
|
65
|
+
/** YYYY-MM-DD (UTC) bucket — stable filename grouping for grep / review. */
|
|
66
|
+
function dayBucket() {
|
|
67
|
+
const d = new Date();
|
|
68
|
+
const pad = (x) => String(x).padStart(2, "0");
|
|
69
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
|
|
70
|
+
}
|
|
71
|
+
/** Derive a file extension from a Discord attachment (filename / content-type / kind). */
|
|
72
|
+
function extFromAttachment(att, kind) {
|
|
73
|
+
const name = (att.name ?? "").toLowerCase();
|
|
74
|
+
const fromName = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1).replace(/[^a-z0-9]/g, "") : "";
|
|
75
|
+
if (fromName && /^[a-z0-9]+$/.test(fromName))
|
|
76
|
+
return fromName;
|
|
77
|
+
const mime = (att.contentType ?? "").toLowerCase();
|
|
78
|
+
const fromMime = mime.includes("/") ? mime.slice(mime.lastIndexOf("/") + 1).split(";")[0]?.replace(/[^a-z0-9]/g, "") : "";
|
|
79
|
+
if (fromMime && /^[a-z0-9]+$/.test(fromMime))
|
|
80
|
+
return fromMime;
|
|
81
|
+
// Sensible default by kind when nothing carried an extension.
|
|
82
|
+
switch (kind) {
|
|
83
|
+
case "image":
|
|
84
|
+
return "png";
|
|
85
|
+
case "video":
|
|
86
|
+
return "mp4";
|
|
87
|
+
case "voice":
|
|
88
|
+
return "ogg";
|
|
89
|
+
case "audio":
|
|
90
|
+
return "mp3";
|
|
91
|
+
default:
|
|
92
|
+
return "bin";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Where downloaded media lands — OS cache in convex mode, channel-state dir otherwise. */
|
|
96
|
+
function mediaBaseDir() {
|
|
97
|
+
return tryGetRuntimeContext()?.mode === "convex"
|
|
98
|
+
? path.join(resolveOsCacheDir(), "channels", CHANNEL_ID)
|
|
99
|
+
: resolveChannelStateDir(CHANNEL_ID);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Bounded retry for the transient attachment fetch. Discord's CDN occasionally
|
|
103
|
+
* blips on a 5xx or a network reset; one or two quick retries turn a dropped
|
|
104
|
+
* attachment into a delivered one. The caller still wraps the whole thing in a
|
|
105
|
+
* try/catch that degrades to `null`, so an exhausted retry never breaks message
|
|
106
|
+
* delivery. Mirrors `slack/media.ts`'s `withSlackRetry`.
|
|
107
|
+
*/
|
|
108
|
+
export async function withDiscordRetry(fn, attempts = 3) {
|
|
109
|
+
let lastErr;
|
|
110
|
+
for (let i = 0; i < attempts; i++) {
|
|
111
|
+
try {
|
|
112
|
+
return await fn();
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
lastErr = err;
|
|
116
|
+
if (i < attempts - 1)
|
|
117
|
+
await new Promise((r) => setTimeout(r, 200 * 2 ** i));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
throw lastErr;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Download one inbound Discord attachment to disk and return its normalized
|
|
124
|
+
* descriptor, or `null` when it couldn't be fetched (no url / too big / network
|
|
125
|
+
* error / non-Discord host). Never throws — a download glitch must not break
|
|
126
|
+
* message delivery.
|
|
127
|
+
*/
|
|
128
|
+
export async function downloadDiscordAttachment(args) {
|
|
129
|
+
const { attachment, log } = args;
|
|
130
|
+
const doFetch = args.fetchImpl ?? fetch;
|
|
131
|
+
const url = attachment.url || attachment.proxyURL;
|
|
132
|
+
if (!url) {
|
|
133
|
+
log?.("discord attachment skipped — no url", { id: attachment.id });
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
// SSRF guard: REFUSE any url that isn't https on a Discord CDN host — a spoofed
|
|
137
|
+
// message pointing at `http://169.254.169.254/…` (or any attacker host) must
|
|
138
|
+
// never be fetched. Checked BEFORE the fetch.
|
|
139
|
+
if (!isAllowedDiscordAttachmentUrl(url)) {
|
|
140
|
+
log?.("discord attachment skipped — url is not an allowed Discord CDN host (SSRF guard)", { id: attachment.id });
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
if (typeof attachment.size === "number" && attachment.size > MAX_BYTES) {
|
|
144
|
+
log?.("discord attachment skipped — exceeds size cap", { id: attachment.id, bytes: attachment.size, cap: MAX_BYTES });
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const kind = resolveDiscordAttachmentKind(attachment);
|
|
148
|
+
try {
|
|
149
|
+
const res = await withDiscordRetry(async () => {
|
|
150
|
+
// `redirect: "manual"` so a cross-origin 30x can't carry the request off to
|
|
151
|
+
// a non-Discord host. A redirect surfaces as a non-ok response and falls
|
|
152
|
+
// through to the `!r.ok` handler below.
|
|
153
|
+
const r = await doFetch(url, { redirect: "manual" });
|
|
154
|
+
// Retry 5xx (transient CDN blip); a 4xx falls through to the !ok handler.
|
|
155
|
+
if (!r.ok && r.status >= 500)
|
|
156
|
+
throw new Error(`discord attachment fetch failed (${r.status})`);
|
|
157
|
+
return r;
|
|
158
|
+
});
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
log?.("discord attachment download failed", { id: attachment.id, status: res.status });
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
164
|
+
if (buf.length === 0)
|
|
165
|
+
return null;
|
|
166
|
+
if (buf.length > MAX_BYTES) {
|
|
167
|
+
log?.("discord attachment skipped — exceeds size cap", { id: attachment.id, bytes: buf.length, cap: MAX_BYTES });
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const dir = path.join(mediaBaseDir(), "media", dayBucket());
|
|
171
|
+
mkdirSync(dir, { recursive: true });
|
|
172
|
+
// attachment.id is stable; use it as the filename so the same media resolves
|
|
173
|
+
// idempotently. Fall back to a timestamp.
|
|
174
|
+
const baseName = (attachment.id || `discord_${Date.now()}`).replace(/[^A-Za-z0-9_-]/g, "_");
|
|
175
|
+
const dest = path.join(dir, `${baseName}.${extFromAttachment(attachment, kind)}`);
|
|
176
|
+
writeFileSync(dest, buf, { mode: 0o600 });
|
|
177
|
+
return {
|
|
178
|
+
kind,
|
|
179
|
+
path: dest,
|
|
180
|
+
...(attachment.contentType ? { mimeType: attachment.contentType } : {}),
|
|
181
|
+
...(attachment.name ? { fileName: attachment.name } : {}),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
log?.("discord attachment download failed", {
|
|
186
|
+
id: attachment.id,
|
|
187
|
+
error: err instanceof Error ? err.message : String(err),
|
|
188
|
+
});
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Default outbound file extension by media kind (used when the path has none). */
|
|
193
|
+
function outboundExtForKind(kind) {
|
|
194
|
+
switch (kind) {
|
|
195
|
+
case "image":
|
|
196
|
+
return "png";
|
|
197
|
+
case "video":
|
|
198
|
+
return "mp4";
|
|
199
|
+
case "voice":
|
|
200
|
+
return "ogg";
|
|
201
|
+
case "audio":
|
|
202
|
+
return "mp3";
|
|
203
|
+
default:
|
|
204
|
+
// document / sticker / anything else: leave extensionless (Discord falls
|
|
205
|
+
// back to the content the byte-sniff detects).
|
|
206
|
+
return "";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Validate + shape a local file for outbound upload. Runs the path through
|
|
211
|
+
* Brigade's outbound media-path guard and throws a clear operator-facing error
|
|
212
|
+
* when the guard refuses it (the `send_media` tool surfaces it). Returns the
|
|
213
|
+
* `{ path, name, caption? }` the connection turns into a discord.js
|
|
214
|
+
* `AttachmentBuilder`.
|
|
215
|
+
*
|
|
216
|
+
* Filename precedence: an explicit `media.fileName` wins; otherwise the path's
|
|
217
|
+
* basename. When the chosen name has NO extension we append one inferred from
|
|
218
|
+
* `media.kind` (image→png, video→mp4, audio→mp3, voice→ogg) so Discord detects
|
|
219
|
+
* the file type instead of treating it as an opaque blob (an extensionless
|
|
220
|
+
* `image` upload otherwise renders as a generic file, not an inline preview).
|
|
221
|
+
*/
|
|
222
|
+
export function buildDiscordAttachment(media) {
|
|
223
|
+
const verdict = validateOutboundMediaPath(media.path);
|
|
224
|
+
if (!verdict.ok) {
|
|
225
|
+
throw new Error(`Discord: ${verdict.reason ?? "refusing to attach this file"}`);
|
|
226
|
+
}
|
|
227
|
+
let name = media.fileName || path.basename(media.path) || "file";
|
|
228
|
+
// No extension on the resolved name → infer one from the media kind so Discord
|
|
229
|
+
// type-detects the attachment. `path.extname` returns "" when there's no dot.
|
|
230
|
+
if (!path.extname(name)) {
|
|
231
|
+
const ext = outboundExtForKind(media.kind);
|
|
232
|
+
if (ext)
|
|
233
|
+
name = `${name}.${ext}`;
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
path: media.path,
|
|
237
|
+
name,
|
|
238
|
+
...(media.caption ? { caption: media.caption } : {}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
export { MAX_BYTES as DISCORD_MEDIA_MAX_BYTES };
|
|
242
|
+
//# sourceMappingURL=media.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/media.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory registry of pending Discord modal (form) definitions (Fix 3b).
|
|
3
|
+
*
|
|
4
|
+
* A Discord modal carries up to five text-input fields, each with a label,
|
|
5
|
+
* style, placeholder, and required flag. A modal is OPENED by pressing a
|
|
6
|
+
* "modal-trigger" button; on submit Discord delivers the field values keyed by
|
|
7
|
+
* each field's custom_id. The full field definition is far larger than the
|
|
8
|
+
* 100-char `custom_id` budget a button/modal can carry, so it CANNOT ride inline
|
|
9
|
+
* — hence this small side registry. The trigger button + the modal both carry
|
|
10
|
+
* only a short `modal:<modalId>` marker; the heavy definition lives here, keyed
|
|
11
|
+
* by that id.
|
|
12
|
+
*
|
|
13
|
+
* Entries expire after {@link MODAL_ENTRY_TTL_MS} so a never-submitted form
|
|
14
|
+
* doesn't leak. `consume` removes the entry (a modal is single-use by default);
|
|
15
|
+
* `get` peeks without removing (for the trigger → showModal lookup). A
|
|
16
|
+
* reset hook is exported for tests.
|
|
17
|
+
*
|
|
18
|
+
* Pure module-level state — no I/O.
|
|
19
|
+
*/
|
|
20
|
+
/** A modal lives for 30 minutes before it's reaped (the form was abandoned). */
|
|
21
|
+
export declare const MODAL_ENTRY_TTL_MS: number;
|
|
22
|
+
/** Discord supports at most 5 inputs per modal. */
|
|
23
|
+
export declare const DISCORD_MODAL_FIELD_MAX = 5;
|
|
24
|
+
/** discord.js `TextInputStyle` values (Short = single line, Paragraph = multi-line). */
|
|
25
|
+
export declare const DISCORD_TEXT_INPUT_STYLE: {
|
|
26
|
+
readonly short: 1;
|
|
27
|
+
readonly paragraph: 2;
|
|
28
|
+
};
|
|
29
|
+
/** A single text-input field definition for a modal. */
|
|
30
|
+
export interface DiscordModalField {
|
|
31
|
+
/** Field custom_id — Discord echoes this back keying the submitted value. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** Label shown above the input. */
|
|
34
|
+
label: string;
|
|
35
|
+
/** `short` (single line) or `paragraph` (multi-line). Defaults to `short`. */
|
|
36
|
+
style?: "short" | "paragraph";
|
|
37
|
+
/** Whether the field must be filled before submit (default true). */
|
|
38
|
+
required?: boolean;
|
|
39
|
+
/** Placeholder hint shown in the empty input. */
|
|
40
|
+
placeholder?: string;
|
|
41
|
+
}
|
|
42
|
+
/** A registered modal definition (the heavy form spec the custom_id can't carry). */
|
|
43
|
+
export interface DiscordModalEntry {
|
|
44
|
+
/** Modal heading shown above the form. */
|
|
45
|
+
title: string;
|
|
46
|
+
/** Text-input fields (1..5). */
|
|
47
|
+
fields: DiscordModalField[];
|
|
48
|
+
/** Session the form belongs to (so a submit routes back to the right turn). */
|
|
49
|
+
sessionKey?: string;
|
|
50
|
+
/** Agent that attached the form. */
|
|
51
|
+
agentId?: string;
|
|
52
|
+
/** Account namespace. */
|
|
53
|
+
accountId?: string;
|
|
54
|
+
/** Optional allowlist of user ids permitted to submit. */
|
|
55
|
+
allowedUsers?: string[];
|
|
56
|
+
/** Epoch ms the entry was registered. */
|
|
57
|
+
createdAt: number;
|
|
58
|
+
/** Epoch ms the entry expires. */
|
|
59
|
+
expiresAt: number;
|
|
60
|
+
}
|
|
61
|
+
/** What a caller supplies when registering a modal (timestamps are filled in). */
|
|
62
|
+
export interface DiscordModalRegistration {
|
|
63
|
+
/** Modal heading (defaults to "Form" when empty). */
|
|
64
|
+
title?: string;
|
|
65
|
+
fields: DiscordModalField[];
|
|
66
|
+
sessionKey?: string;
|
|
67
|
+
agentId?: string;
|
|
68
|
+
accountId?: string;
|
|
69
|
+
allowedUsers?: string[];
|
|
70
|
+
}
|
|
71
|
+
/** Mint a short, unique modal id (fits the `modal:<id>` marker budget). */
|
|
72
|
+
export declare function nextDiscordModalId(): string;
|
|
73
|
+
/**
|
|
74
|
+
* Register a modal definition, returning the modal id the trigger button carries.
|
|
75
|
+
* The entry expires after {@link MODAL_ENTRY_TTL_MS}. Fields beyond the Discord
|
|
76
|
+
* 5-input cap are dropped.
|
|
77
|
+
*/
|
|
78
|
+
export declare function registerDiscordModal(reg: DiscordModalRegistration): string;
|
|
79
|
+
/** Peek at a registered modal WITHOUT removing it (the trigger → showModal lookup). */
|
|
80
|
+
export declare function getDiscordModal(modalId: string): DiscordModalEntry | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Consume a registered modal — return it AND remove it (a modal is single-use, so
|
|
83
|
+
* a second submit of the same id degrades gracefully to `undefined`). Returns
|
|
84
|
+
* `undefined` for a missing / expired id.
|
|
85
|
+
*/
|
|
86
|
+
export declare function consumeDiscordModal(modalId: string): DiscordModalEntry | undefined;
|
|
87
|
+
/** TEST-ONLY: clear the registry + reset the clock + id counter. */
|
|
88
|
+
export declare function __resetDiscordModalRegistryForTest(clock?: () => number): void;
|
|
89
|
+
//# sourceMappingURL=modal-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modal-registry.d.ts","sourceRoot":"","sources":["../../../../src/agents/channels/discord/modal-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,gFAAgF;AAChF,eAAO,MAAM,kBAAkB,QAAkB,CAAC;AAElD,mDAAmD;AACnD,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,wFAAwF;AACxF,eAAO,MAAM,wBAAwB;;;CAG3B,CAAC;AAEX,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IACjC,6EAA6E;IAC7E,EAAE,EAAE,MAAM,CAAC;IACX,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;IAC9B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qFAAqF;AACrF,MAAM,WAAW,iBAAiB;IACjC,0CAA0C;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gCAAgC;IAChC,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,kCAAkC;IAClC,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,kFAAkF;AAClF,MAAM,WAAW,wBAAwB;IACxC,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAWD,2EAA2E;AAC3E,wBAAgB,kBAAkB,IAAI,MAAM,CAG3C;AAUD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,wBAAwB,GAAG,MAAM,CAiB1E;AAED,uFAAuF;AACvF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAS9E;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAIlF;AAED,oEAAoE;AACpE,wBAAgB,kCAAkC,CAAC,KAAK,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAI7E"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory registry of pending Discord modal (form) definitions (Fix 3b).
|
|
3
|
+
*
|
|
4
|
+
* A Discord modal carries up to five text-input fields, each with a label,
|
|
5
|
+
* style, placeholder, and required flag. A modal is OPENED by pressing a
|
|
6
|
+
* "modal-trigger" button; on submit Discord delivers the field values keyed by
|
|
7
|
+
* each field's custom_id. The full field definition is far larger than the
|
|
8
|
+
* 100-char `custom_id` budget a button/modal can carry, so it CANNOT ride inline
|
|
9
|
+
* — hence this small side registry. The trigger button + the modal both carry
|
|
10
|
+
* only a short `modal:<modalId>` marker; the heavy definition lives here, keyed
|
|
11
|
+
* by that id.
|
|
12
|
+
*
|
|
13
|
+
* Entries expire after {@link MODAL_ENTRY_TTL_MS} so a never-submitted form
|
|
14
|
+
* doesn't leak. `consume` removes the entry (a modal is single-use by default);
|
|
15
|
+
* `get` peeks without removing (for the trigger → showModal lookup). A
|
|
16
|
+
* reset hook is exported for tests.
|
|
17
|
+
*
|
|
18
|
+
* Pure module-level state — no I/O.
|
|
19
|
+
*/
|
|
20
|
+
/** A modal lives for 30 minutes before it's reaped (the form was abandoned). */
|
|
21
|
+
export const MODAL_ENTRY_TTL_MS = 30 * 60 * 1_000;
|
|
22
|
+
/** Discord supports at most 5 inputs per modal. */
|
|
23
|
+
export const DISCORD_MODAL_FIELD_MAX = 5;
|
|
24
|
+
/** discord.js `TextInputStyle` values (Short = single line, Paragraph = multi-line). */
|
|
25
|
+
export const DISCORD_TEXT_INPUT_STYLE = {
|
|
26
|
+
short: 1,
|
|
27
|
+
paragraph: 2,
|
|
28
|
+
};
|
|
29
|
+
/** modalId → entry. Insertion-ordered; pruned lazily on access. */
|
|
30
|
+
const registry = new Map();
|
|
31
|
+
/** Monotonic counter feeding the generated modal ids (collision-free per process). */
|
|
32
|
+
let modalSeq = 0;
|
|
33
|
+
/** A clock seam so tests can drive expiry deterministically. */
|
|
34
|
+
let nowFn = () => Date.now();
|
|
35
|
+
/** Mint a short, unique modal id (fits the `modal:<id>` marker budget). */
|
|
36
|
+
export function nextDiscordModalId() {
|
|
37
|
+
modalSeq += 1;
|
|
38
|
+
return `m${modalSeq.toString(36)}${nowFn().toString(36).slice(-4)}`;
|
|
39
|
+
}
|
|
40
|
+
/** Drop every entry whose `expiresAt` is in the past. */
|
|
41
|
+
function pruneExpired() {
|
|
42
|
+
const now = nowFn();
|
|
43
|
+
for (const [id, entry] of registry) {
|
|
44
|
+
if (entry.expiresAt <= now)
|
|
45
|
+
registry.delete(id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Register a modal definition, returning the modal id the trigger button carries.
|
|
50
|
+
* The entry expires after {@link MODAL_ENTRY_TTL_MS}. Fields beyond the Discord
|
|
51
|
+
* 5-input cap are dropped.
|
|
52
|
+
*/
|
|
53
|
+
export function registerDiscordModal(reg) {
|
|
54
|
+
pruneExpired();
|
|
55
|
+
const id = nextDiscordModalId();
|
|
56
|
+
const now = nowFn();
|
|
57
|
+
const fields = (reg.fields ?? []).slice(0, DISCORD_MODAL_FIELD_MAX);
|
|
58
|
+
const entry = {
|
|
59
|
+
title: (reg.title ?? "").trim() || "Form",
|
|
60
|
+
fields,
|
|
61
|
+
createdAt: now,
|
|
62
|
+
expiresAt: now + MODAL_ENTRY_TTL_MS,
|
|
63
|
+
};
|
|
64
|
+
if (reg.sessionKey !== undefined)
|
|
65
|
+
entry.sessionKey = reg.sessionKey;
|
|
66
|
+
if (reg.agentId !== undefined)
|
|
67
|
+
entry.agentId = reg.agentId;
|
|
68
|
+
if (reg.accountId !== undefined)
|
|
69
|
+
entry.accountId = reg.accountId;
|
|
70
|
+
if (reg.allowedUsers !== undefined)
|
|
71
|
+
entry.allowedUsers = reg.allowedUsers;
|
|
72
|
+
registry.set(id, entry);
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
/** Peek at a registered modal WITHOUT removing it (the trigger → showModal lookup). */
|
|
76
|
+
export function getDiscordModal(modalId) {
|
|
77
|
+
pruneExpired();
|
|
78
|
+
const entry = registry.get(modalId);
|
|
79
|
+
if (!entry)
|
|
80
|
+
return undefined;
|
|
81
|
+
if (entry.expiresAt <= nowFn()) {
|
|
82
|
+
registry.delete(modalId);
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return entry;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Consume a registered modal — return it AND remove it (a modal is single-use, so
|
|
89
|
+
* a second submit of the same id degrades gracefully to `undefined`). Returns
|
|
90
|
+
* `undefined` for a missing / expired id.
|
|
91
|
+
*/
|
|
92
|
+
export function consumeDiscordModal(modalId) {
|
|
93
|
+
const entry = getDiscordModal(modalId);
|
|
94
|
+
if (entry)
|
|
95
|
+
registry.delete(modalId);
|
|
96
|
+
return entry;
|
|
97
|
+
}
|
|
98
|
+
/** TEST-ONLY: clear the registry + reset the clock + id counter. */
|
|
99
|
+
export function __resetDiscordModalRegistryForTest(clock) {
|
|
100
|
+
registry.clear();
|
|
101
|
+
modalSeq = 0;
|
|
102
|
+
nowFn = clock ?? (() => Date.now());
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=modal-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modal-registry.js","sourceRoot":"","sources":["../../../../src/agents/channels/discord/modal-registry.ts"],"names":[],"mappings":"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"}
|