@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.
Files changed (194) hide show
  1. package/dist/agents/agent-loop.d.ts.map +1 -1
  2. package/dist/agents/agent-loop.js +51 -1
  3. package/dist/agents/agent-loop.js.map +1 -1
  4. package/dist/agents/channels/bundled-channel-metas.d.ts +2 -0
  5. package/dist/agents/channels/bundled-channel-metas.d.ts.map +1 -1
  6. package/dist/agents/channels/bundled-channel-metas.js +11 -0
  7. package/dist/agents/channels/bundled-channel-metas.js.map +1 -1
  8. package/dist/agents/channels/discord/account-config.d.ts +177 -0
  9. package/dist/agents/channels/discord/account-config.d.ts.map +1 -0
  10. package/dist/agents/channels/discord/account-config.js +349 -0
  11. package/dist/agents/channels/discord/account-config.js.map +1 -0
  12. package/dist/agents/channels/discord/adapter.d.ts +79 -0
  13. package/dist/agents/channels/discord/adapter.d.ts.map +1 -0
  14. package/dist/agents/channels/discord/adapter.js +693 -0
  15. package/dist/agents/channels/discord/adapter.js.map +1 -0
  16. package/dist/agents/channels/discord/approval-authorize.d.ts +43 -0
  17. package/dist/agents/channels/discord/approval-authorize.d.ts.map +1 -0
  18. package/dist/agents/channels/discord/approval-authorize.js +71 -0
  19. package/dist/agents/channels/discord/approval-authorize.js.map +1 -0
  20. package/dist/agents/channels/discord/approval-native.d.ts +68 -0
  21. package/dist/agents/channels/discord/approval-native.d.ts.map +1 -0
  22. package/dist/agents/channels/discord/approval-native.js +81 -0
  23. package/dist/agents/channels/discord/approval-native.js.map +1 -0
  24. package/dist/agents/channels/discord/command-menu.d.ts +49 -0
  25. package/dist/agents/channels/discord/command-menu.d.ts.map +1 -0
  26. package/dist/agents/channels/discord/command-menu.js +73 -0
  27. package/dist/agents/channels/discord/command-menu.js.map +1 -0
  28. package/dist/agents/channels/discord/component-blocks.d.ts +108 -0
  29. package/dist/agents/channels/discord/component-blocks.d.ts.map +1 -0
  30. package/dist/agents/channels/discord/component-blocks.js +113 -0
  31. package/dist/agents/channels/discord/component-blocks.js.map +1 -0
  32. package/dist/agents/channels/discord/components.d.ts +175 -0
  33. package/dist/agents/channels/discord/components.d.ts.map +1 -0
  34. package/dist/agents/channels/discord/components.js +220 -0
  35. package/dist/agents/channels/discord/components.js.map +1 -0
  36. package/dist/agents/channels/discord/connection.d.ts +570 -0
  37. package/dist/agents/channels/discord/connection.d.ts.map +1 -0
  38. package/dist/agents/channels/discord/connection.js +1600 -0
  39. package/dist/agents/channels/discord/connection.js.map +1 -0
  40. package/dist/agents/channels/discord/directory-cache.d.ts +47 -0
  41. package/dist/agents/channels/discord/directory-cache.d.ts.map +1 -0
  42. package/dist/agents/channels/discord/directory-cache.js +131 -0
  43. package/dist/agents/channels/discord/directory-cache.js.map +1 -0
  44. package/dist/agents/channels/discord/directory-live.d.ts +61 -0
  45. package/dist/agents/channels/discord/directory-live.d.ts.map +1 -0
  46. package/dist/agents/channels/discord/directory-live.js +140 -0
  47. package/dist/agents/channels/discord/directory-live.js.map +1 -0
  48. package/dist/agents/channels/discord/draft-stream.d.ts +92 -0
  49. package/dist/agents/channels/discord/draft-stream.d.ts.map +1 -0
  50. package/dist/agents/channels/discord/draft-stream.js +213 -0
  51. package/dist/agents/channels/discord/draft-stream.js.map +1 -0
  52. package/dist/agents/channels/discord/format.d.ts +70 -0
  53. package/dist/agents/channels/discord/format.d.ts.map +1 -0
  54. package/dist/agents/channels/discord/format.js +303 -0
  55. package/dist/agents/channels/discord/format.js.map +1 -0
  56. package/dist/agents/channels/discord/guilds.d.ts +25 -0
  57. package/dist/agents/channels/discord/guilds.d.ts.map +1 -0
  58. package/dist/agents/channels/discord/guilds.js +46 -0
  59. package/dist/agents/channels/discord/guilds.js.map +1 -0
  60. package/dist/agents/channels/discord/inbound-extras.d.ts +377 -0
  61. package/dist/agents/channels/discord/inbound-extras.d.ts.map +1 -0
  62. package/dist/agents/channels/discord/inbound-extras.js +589 -0
  63. package/dist/agents/channels/discord/inbound-extras.js.map +1 -0
  64. package/dist/agents/channels/discord/index.d.ts +21 -0
  65. package/dist/agents/channels/discord/index.d.ts.map +1 -0
  66. package/dist/agents/channels/discord/index.js +21 -0
  67. package/dist/agents/channels/discord/index.js.map +1 -0
  68. package/dist/agents/channels/discord/media.d.ts +85 -0
  69. package/dist/agents/channels/discord/media.d.ts.map +1 -0
  70. package/dist/agents/channels/discord/media.js +242 -0
  71. package/dist/agents/channels/discord/media.js.map +1 -0
  72. package/dist/agents/channels/discord/modal-registry.d.ts +89 -0
  73. package/dist/agents/channels/discord/modal-registry.d.ts.map +1 -0
  74. package/dist/agents/channels/discord/modal-registry.js +104 -0
  75. package/dist/agents/channels/discord/modal-registry.js.map +1 -0
  76. package/dist/agents/channels/discord/modals.d.ts +100 -0
  77. package/dist/agents/channels/discord/modals.d.ts.map +1 -0
  78. package/dist/agents/channels/discord/modals.js +124 -0
  79. package/dist/agents/channels/discord/modals.js.map +1 -0
  80. package/dist/agents/channels/discord/module.d.ts +15 -0
  81. package/dist/agents/channels/discord/module.d.ts.map +1 -0
  82. package/dist/agents/channels/discord/module.js +22 -0
  83. package/dist/agents/channels/discord/module.js.map +1 -0
  84. package/dist/agents/channels/discord/permission-audit.d.ts +43 -0
  85. package/dist/agents/channels/discord/permission-audit.d.ts.map +1 -0
  86. package/dist/agents/channels/discord/permission-audit.js +192 -0
  87. package/dist/agents/channels/discord/permission-audit.js.map +1 -0
  88. package/dist/agents/channels/discord/plugin.d.ts +89 -0
  89. package/dist/agents/channels/discord/plugin.d.ts.map +1 -0
  90. package/dist/agents/channels/discord/plugin.js +372 -0
  91. package/dist/agents/channels/discord/plugin.js.map +1 -0
  92. package/dist/agents/channels/discord/probe.d.ts +115 -0
  93. package/dist/agents/channels/discord/probe.d.ts.map +1 -0
  94. package/dist/agents/channels/discord/probe.js +193 -0
  95. package/dist/agents/channels/discord/probe.js.map +1 -0
  96. package/dist/agents/channels/discord/reasoning-lane.d.ts +42 -0
  97. package/dist/agents/channels/discord/reasoning-lane.d.ts.map +1 -0
  98. package/dist/agents/channels/discord/reasoning-lane.js +68 -0
  99. package/dist/agents/channels/discord/reasoning-lane.js.map +1 -0
  100. package/dist/agents/channels/discord/rest-actions.d.ts +346 -0
  101. package/dist/agents/channels/discord/rest-actions.d.ts.map +1 -0
  102. package/dist/agents/channels/discord/rest-actions.js +559 -0
  103. package/dist/agents/channels/discord/rest-actions.js.map +1 -0
  104. package/dist/agents/channels/discord/rest-components.d.ts +122 -0
  105. package/dist/agents/channels/discord/rest-components.d.ts.map +1 -0
  106. package/dist/agents/channels/discord/rest-components.js +243 -0
  107. package/dist/agents/channels/discord/rest-components.js.map +1 -0
  108. package/dist/agents/channels/discord/security-audit.d.ts +29 -0
  109. package/dist/agents/channels/discord/security-audit.d.ts.map +1 -0
  110. package/dist/agents/channels/discord/security-audit.js +94 -0
  111. package/dist/agents/channels/discord/security-audit.js.map +1 -0
  112. package/dist/agents/channels/discord/security-doctor.d.ts +43 -0
  113. package/dist/agents/channels/discord/security-doctor.d.ts.map +1 -0
  114. package/dist/agents/channels/discord/security-doctor.js +83 -0
  115. package/dist/agents/channels/discord/security-doctor.js.map +1 -0
  116. package/dist/agents/channels/discord/status-issues.d.ts +37 -0
  117. package/dist/agents/channels/discord/status-issues.d.ts.map +1 -0
  118. package/dist/agents/channels/discord/status-issues.js +66 -0
  119. package/dist/agents/channels/discord/status-issues.js.map +1 -0
  120. package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts +57 -0
  121. package/dist/agents/channels/discord/subagent-thread-binding-store.d.ts.map +1 -0
  122. package/dist/agents/channels/discord/subagent-thread-binding-store.js +98 -0
  123. package/dist/agents/channels/discord/subagent-thread-binding-store.js.map +1 -0
  124. package/dist/agents/channels/discord/subagent-thread-binding.d.ts +95 -0
  125. package/dist/agents/channels/discord/subagent-thread-binding.d.ts.map +1 -0
  126. package/dist/agents/channels/discord/subagent-thread-binding.js +208 -0
  127. package/dist/agents/channels/discord/subagent-thread-binding.js.map +1 -0
  128. package/dist/agents/channels/discord/system-events.d.ts +31 -0
  129. package/dist/agents/channels/discord/system-events.d.ts.map +1 -0
  130. package/dist/agents/channels/discord/system-events.js +74 -0
  131. package/dist/agents/channels/discord/system-events.js.map +1 -0
  132. package/dist/agents/channels/general-callback.d.ts +12 -0
  133. package/dist/agents/channels/general-callback.d.ts.map +1 -1
  134. package/dist/agents/channels/general-callback.js +18 -0
  135. package/dist/agents/channels/general-callback.js.map +1 -1
  136. package/dist/agents/channels/inbound-pipeline.d.ts.map +1 -1
  137. package/dist/agents/channels/inbound-pipeline.js +70 -10
  138. package/dist/agents/channels/inbound-pipeline.js.map +1 -1
  139. package/dist/agents/channels/sdk.d.ts +2 -0
  140. package/dist/agents/channels/sdk.d.ts.map +1 -1
  141. package/dist/agents/channels/sdk.js +2 -0
  142. package/dist/agents/channels/sdk.js.map +1 -1
  143. package/dist/agents/extensions/modules/index.d.ts.map +1 -1
  144. package/dist/agents/extensions/modules/index.js +5 -0
  145. package/dist/agents/extensions/modules/index.js.map +1 -1
  146. package/dist/agents/extensions/types.d.ts +7 -0
  147. package/dist/agents/extensions/types.d.ts.map +1 -1
  148. package/dist/agents/extensions/types.js.map +1 -1
  149. package/dist/agents/subagent-announce-delivery.d.ts +10 -0
  150. package/dist/agents/subagent-announce-delivery.d.ts.map +1 -1
  151. package/dist/agents/subagent-announce-delivery.js +1 -0
  152. package/dist/agents/subagent-announce-delivery.js.map +1 -1
  153. package/dist/agents/subagent-completion-bridge.d.ts.map +1 -1
  154. package/dist/agents/subagent-completion-bridge.js +81 -0
  155. package/dist/agents/subagent-completion-bridge.js.map +1 -1
  156. package/dist/agents/subagent-spawn.d.ts.map +1 -1
  157. package/dist/agents/subagent-spawn.js +57 -4
  158. package/dist/agents/subagent-spawn.js.map +1 -1
  159. package/dist/agents/tools/cron-tool.d.ts.map +1 -1
  160. package/dist/agents/tools/cron-tool.js +4 -1
  161. package/dist/agents/tools/cron-tool.js.map +1 -1
  162. package/dist/agents/tools/discord-action-tool.d.ts +224 -0
  163. package/dist/agents/tools/discord-action-tool.d.ts.map +1 -0
  164. package/dist/agents/tools/discord-action-tool.js +848 -0
  165. package/dist/agents/tools/discord-action-tool.js.map +1 -0
  166. package/dist/agents/tools/registry.d.ts.map +1 -1
  167. package/dist/agents/tools/registry.js +21 -0
  168. package/dist/agents/tools/registry.js.map +1 -1
  169. package/dist/agents/tools/sessions/index.d.ts +8 -0
  170. package/dist/agents/tools/sessions/index.d.ts.map +1 -1
  171. package/dist/agents/tools/sessions/index.js +15 -3
  172. package/dist/agents/tools/sessions/index.js.map +1 -1
  173. package/dist/buildstamp.json +1 -1
  174. package/dist/cli/commands/channels.d.ts +2 -0
  175. package/dist/cli/commands/channels.d.ts.map +1 -1
  176. package/dist/cli/commands/channels.js +58 -1
  177. package/dist/cli/commands/channels.js.map +1 -1
  178. package/dist/core/auth-bridge.d.ts +1 -0
  179. package/dist/core/auth-bridge.d.ts.map +1 -1
  180. package/dist/core/auth-bridge.js +46 -1
  181. package/dist/core/auth-bridge.js.map +1 -1
  182. package/dist/core/server.d.ts.map +1 -1
  183. package/dist/core/server.js +18 -2
  184. package/dist/core/server.js.map +1 -1
  185. package/dist/cron/isolated-agent/run-executor.d.ts +11 -0
  186. package/dist/cron/isolated-agent/run-executor.d.ts.map +1 -1
  187. package/dist/cron/isolated-agent/run-executor.js +20 -4
  188. package/dist/cron/isolated-agent/run-executor.js.map +1 -1
  189. package/dist/cron/types.d.ts +8 -0
  190. package/dist/cron/types.d.ts.map +1 -1
  191. package/dist/system-prompt/assembler.d.ts.map +1 -1
  192. package/dist/system-prompt/assembler.js +4 -2
  193. package/dist/system-prompt/assembler.js.map +1 -1
  194. 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":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,gFAAgF;AAChF,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC;AAElD,mDAAmD;AACnD,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC,wFAAwF;AACxF,MAAM,CAAC,MAAM,wBAAwB,GAAG;IACvC,KAAK,EAAE,CAAC;IACR,SAAS,EAAE,CAAC;CACH,CAAC;AA+CX,mEAAmE;AACnE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA6B,CAAC;AAEtD,sFAAsF;AACtF,IAAI,QAAQ,GAAG,CAAC,CAAC;AAEjB,gEAAgE;AAChE,IAAI,KAAK,GAAiB,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC;AAE3C,2EAA2E;AAC3E,MAAM,UAAU,kBAAkB;IACjC,QAAQ,IAAI,CAAC,CAAC;IACd,OAAO,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACrE,CAAC;AAED,yDAAyD;AACzD,SAAS,YAAY;IACpB,MAAM,GAAG,GAAG,KAAK,EAAE,CAAC;IACpB,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,QAAQ,EAAE,CAAC;QACpC,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG;YAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACjD,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAA6B;IACjE,YAAY,EAAE,CAAC;IACf,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;IAChC,MAAM,GAAG,GAAG,KAAK,EAAE,CAAC;IACpB,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,uBAAuB,CAAC,CAAC;IACpE,MAAM,KAAK,GAAsB;QAChC,KAAK,EAAE,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,MAAM;QACzC,MAAM;QACN,SAAS,EAAE,GAAG;QACd,SAAS,EAAE,GAAG,GAAG,kBAAkB;KACnC,CAAC;IACF,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS;QAAE,KAAK,CAAC,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC;IACpE,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS;QAAE,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,OAAO,CAAC;IAC3D,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,SAAS,GAAG,GAAG,CAAC,SAAS,CAAC;IACjE,IAAI,GAAG,CAAC,YAAY,KAAK,SAAS;QAAE,KAAK,CAAC,YAAY,GAAG,GAAG,CAAC,YAAY,CAAC;IAC1E,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IACxB,OAAO,EAAE,CAAC;AACX,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,eAAe,CAAC,OAAe;IAC9C,YAAY,EAAE,CAAC;IACf,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACpC,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,EAAE,EAAE,CAAC;QAChC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACzB,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe;IAClD,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,KAAK;QAAE,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACpC,OAAO,KAAK,CAAC;AACd,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,kCAAkC,CAAC,KAAoB;IACtE,QAAQ,CAAC,KAAK,EAAE,CAAC;IACjB,QAAQ,GAAG,CAAC,CAAC;IACb,KAAK,GAAG,KAAK,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;AACrC,CAAC"}