@mastra/discord 1.3.0-alpha.0 → 1.3.0-alpha.1

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/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#dec","#enc","#fromRecord","#toRecord","#config","#configured","#hasSuppliedCredentials","#mastra","#storeGeneration","#storeIsFallback","#pendingMigration","#store","#initPromise","#adapters","#autoInitialize","#handleWebhook","#doInitialize","#getStore","#activateInstallation","#resolveAppConfig","#apiBaseUrl","#enqueueWebhookTask","#registerCommands","#activateGuildNow","#activationChains","#reconcileInflight","#reconcilePendingInstallation","#getOrCreateAdapter","#activateFromInteraction","#resolveAgent","#createAgentChannels","createDiscordAdapter","#suppliedPartialAppConfig","#suppliedAppConfig","#storeResolution","#resolveStore","#resolveStorage","#migrateFallback"],"sources":["../src/types.ts","../src/discord-client.ts","../src/commands.ts","../src/crypto.ts","../src/install-store.ts","../src/reconcile.ts","../src/discord-provider.ts"],"sourcesContent":["import type {\n ChannelAdapterConfig,\n ChannelConfig,\n ChannelHandlers,\n StreamingConfig,\n WaitUntilFn,\n} from '@mastra/core/channels';\nimport type { ChannelsStorage } from '@mastra/core/storage';\nimport type { DiscordAdapterConfig } from '@chat-adapter/discord';\n\n/**\n * Default Discord REST base — **including** the `/api/v10` version segment. The\n * client concatenates this with the request path verbatim, so an override must\n * carry its own version segment (`https://example.test/api/v10`); passing a bare\n * origin produces unversioned request URLs.\n */\nexport const DISCORD_API_BASE_URL = 'https://discord.com/api/v10';\n\n/** Base for the OAuth2 authorize (bot-invite) URL. */\nexport const DISCORD_OAUTH_AUTHORIZE_URL = 'https://discord.com/oauth2/authorize';\n\n/**\n * Named Discord permission bits (a subset). Discord permissions are a 53+ bit\n * field, so they are represented as `bigint` and serialized to a decimal string\n * in the invite URL.\n *\n * @see https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags\n */\nexport const DISCORD_PERMISSIONS = {\n ADD_REACTIONS: 1n << 6n,\n VIEW_CHANNEL: 1n << 10n,\n SEND_MESSAGES: 1n << 11n,\n EMBED_LINKS: 1n << 14n,\n ATTACH_FILES: 1n << 15n,\n READ_MESSAGE_HISTORY: 1n << 16n,\n USE_APPLICATION_COMMANDS: 1n << 31n,\n SEND_MESSAGES_IN_THREADS: 1n << 38n,\n} as const;\n\n/**\n * Default permissions requested in the bot-invite URL: enough for a chat agent\n * to read and reply (in channels and threads), post embeds/files, add reactions,\n * and see recent history. Kept intentionally minimal — no moderation or manage\n * permissions. Callers can override via {@link DiscordConnectOptions.permissions}\n * or {@link DiscordProviderConfig.permissions}.\n */\nexport const DEFAULT_INVITE_PERMISSIONS: bigint =\n DISCORD_PERMISSIONS.VIEW_CHANNEL |\n DISCORD_PERMISSIONS.SEND_MESSAGES |\n DISCORD_PERMISSIONS.SEND_MESSAGES_IN_THREADS |\n DISCORD_PERMISSIONS.EMBED_LINKS |\n DISCORD_PERMISSIONS.ATTACH_FILES |\n DISCORD_PERMISSIONS.READ_MESSAGE_HISTORY |\n DISCORD_PERMISSIONS.ADD_REACTIONS |\n DISCORD_PERMISSIONS.USE_APPLICATION_COMMANDS;\n\n/** OAuth2 scopes requested by the bot-invite URL. */\nexport const DEFAULT_INVITE_SCOPES = ['bot', 'applications.commands'] as const;\n\n/**\n * A Discord application command as it goes over the wire (bulk-overwrite\n * registration). Only the `CHAT_INPUT` (slash) shape is modeled here.\n * @see https://discord.com/developers/docs/interactions/application-commands\n */\nexport interface DiscordCommand {\n /** 1-32 chars, lowercase `[a-z0-9_-]` (Discord also allows a leading `-`/`_`). */\n name: string;\n /** 1-100 chars. */\n description: string;\n}\n\n/** Command input accepted by the provider — a bare name or `{ name, description }`. */\nexport type DiscordCommandInput = string | { name: string; description?: string };\n\n/**\n * App-level credentials for a single Discord application. Discord has **no\n * programmatic app creation** — these come from the Developer Portal and are\n * stored **once** (one app, many guilds). `botToken` is the only secret;\n * `publicKey` and `applicationId` are public.\n */\nexport interface DiscordAppConfig {\n /** Bot token (`Authorization: Bot <token>`) — the control-plane credential. Secret. */\n botToken: string;\n /** Ed25519 public key used by the adapter to verify interaction signatures. */\n publicKey: string;\n /** The application (client) id — used as `client_id` in the invite URL. */\n applicationId: string;\n}\n\n/**\n * Configuration for {@link DiscordProvider}.\n *\n * Discord is **one app, many guilds**: a single application's credentials are\n * reused across every guild the bot is invited to. There is no per-install\n * token — \"installing\" is a bot invite (an OAuth2 authorize URL), and tenancy\n * is keyed by `guildId` per interaction.\n */\nexport interface DiscordProviderConfig {\n /**\n * App credentials (bot token / public key / application id). May be omitted\n * here and provided via the `DISCORD_BOT_TOKEN` / `DISCORD_PUBLIC_KEY` /\n * `DISCORD_APPLICATION_ID` env vars, or later via {@link DiscordProvider.configure}.\n * A bot token alone is enough: `publicKey` and `applicationId` are resolved\n * from `GET /applications/@me` when omitted. Whichever source resolves first\n * is persisted **once** to channels storage.\n */\n app?: Partial<DiscordAppConfig>;\n /**\n * Public HTTPS base URL for the interactions endpoint (the URL set once in the\n * Developer Portal). May be omitted and auto-detected from the Mastra server.\n */\n baseUrl?: string;\n /**\n * Persistence for the app config + per-agent installs. Defaults to Mastra's\n * channels storage when attached to a Mastra instance with storage, and falls\n * back to an in-memory store otherwise (dev/test — not persisted across restarts).\n */\n storage?: ChannelsStorage;\n /**\n * Override the Discord REST origin (e.g. a test mock). Defaults to\n * {@link DISCORD_API_BASE_URL}.\n */\n apiBaseUrl?: string;\n /**\n * Passphrase for encrypting the stored `botToken` at rest (AES-256-GCM).\n * Defaults to the `MASTRA_ENCRYPTION_KEY` env var. When unset, the token is\n * stored in plaintext (fine for the in-memory dev store; set a key for any\n * persistent backend).\n */\n encryptionKey?: string;\n /**\n * Permissions bitfield requested in the invite URL. Overrides\n * {@link DEFAULT_INVITE_PERMISSIONS}. Accepts a `bigint` or a decimal string.\n */\n permissions?: bigint | string;\n /**\n * Default slash commands registered for every connected agent (a per-agent\n * list can override via {@link DiscordConnectOptions.commands}). Defaults to\n * the conventional `/help` seed.\n */\n commands?: DiscordCommandInput[];\n /**\n * Where slash commands are registered:\n * - `'guild'` (default) — per-guild (`PUT …/guilds/{id}/commands`), updates\n * instantly; registered on first-seen guild, keyed by a per-guild hash.\n * - `'global'` — app-wide (`PUT …/commands`), eventually consistent; registered\n * once regardless of guild.\n *\n * @default 'guild'\n */\n commandScope?: 'guild' | 'global';\n /**\n * Keep the serverless invocation alive while the agent stream runs after the\n * interaction is acked (Vercel/AWS Lambda). See `ChannelConfig.waitUntil`.\n */\n waitUntil?: WaitUntilFn;\n /**\n * Start the Gateway WebSocket (core-owned) so the bot receives DMs, @mentions,\n * and reactions in addition to slash commands. Set `false` for interactions-\n * only serverless deployments. Forwarded to the adapter entry as `gateway`.\n *\n * @default true\n */\n gateway?: boolean;\n /**\n * Role IDs (in addition to direct user mentions) that trigger mention\n * handlers. Forwarded to the adapter (`DISCORD_MENTION_ROLE_IDS` env fallback).\n */\n mentionRoleIds?: string[];\n /**\n * Return interaction response flags (e.g. ephemeral, flag 64) for the initial\n * deferred slash-command response. Flags are locked at defer time, so this\n * fires on the deferred ACK, not the followup. Forwarded to the adapter.\n */\n interactionFlags?: DiscordAdapterConfig['interactionFlags'];\n /** Logger forwarded to the underlying `DiscordAdapter` for internal error reporting. */\n logger?: DiscordAdapterConfig['logger'];\n /**\n * Stream agent text to Discord as it generates, via the adapter's post-and-edit\n * (`editMessage`) loop. Discord has no native token streaming, so this\n * chunk-edits the interaction followup (throttle via `updateIntervalMs`).\n *\n * @default true\n */\n streaming?: StreamingConfig;\n /**\n * Keep a typing indicator alive during generation. Set `false` to disable.\n *\n * @default true\n */\n typingStatus?: boolean;\n\n // ---------------------------------------------------------------------------\n // AgentChannels passthrough — a curated subset of `ChannelConfig` /\n // `ChannelAdapterConfig` forwarded to every agent connected via this provider,\n // mirroring `@mastra/slack` / `@mastra/telegram`. All optional.\n // ---------------------------------------------------------------------------\n\n /** Override built-in event handlers. Forwarded to `AgentChannels`. */\n handlers?: ChannelHandlers;\n /** Which media types to send inline to the model. See `ChannelConfig.inlineMedia`. */\n inlineMedia?: ChannelConfig['inlineMedia'];\n /** Promote URLs in message text to file parts. See `ChannelConfig.inlineLinks`. */\n inlineLinks?: ChannelConfig['inlineLinks'];\n /** State adapter for deduplication, locking, and subscriptions. See `ChannelConfig.state`. */\n state?: ChannelConfig['state'];\n /** Fetch recent thread messages when the agent joins mid-conversation. See `ChannelConfig.threadContext`. */\n threadContext?: ChannelConfig['threadContext'];\n /** Additional options passed directly to the Chat SDK. See `ChannelConfig.chatOptions`. */\n chatOptions?: ChannelConfig['chatOptions'];\n /** Resolve the memory `resourceId` before a channel thread is created. See `ChannelConfig.resolveResourceId`. */\n resolveResourceId?: ChannelConfig['resolveResourceId'];\n /** Resolve `waitUntil` from the request's Hono `Context`. See `ChannelConfig.resolveWaitUntil`. */\n resolveWaitUntil?: ChannelConfig['resolveWaitUntil'];\n /** CORS configuration for the generated interactions route. */\n cors?: ChannelAdapterConfig['cors'];\n /** Override how errors are rendered in Discord messages. See `ChannelAdapterConfig.formatError`. */\n formatError?: ChannelAdapterConfig['formatError'];\n /**\n * How tool calls are rendered in the reply. Discord has native embeds +\n * action-row buttons, so this defaults to `'cards'` (unlike Telegram's\n * `'text'`). See `ChannelAdapterConfig.toolDisplay`.\n *\n * @default 'cards'\n */\n toolDisplay?: ChannelAdapterConfig['toolDisplay'];\n /**\n * Whether to expose channel reaction tools (`add_reaction`/`remove_reaction`)\n * to the agent. Set `false` for models without function calling. See `ChannelConfig.tools`.\n *\n * @default true\n */\n tools?: ChannelConfig['tools'];\n /** Called after an agent successfully connects and the installation is persisted. */\n onInstall?: (installation: DiscordInstallation) => void | Promise<void>;\n}\n\n/** Options accepted by {@link DiscordProvider.connect}. */\nexport interface DiscordConnectOptions {\n /**\n * The guild to install into. When the bot is **already** a member of this\n * guild, connect binds immediately (`{ type: 'immediate' }`) and registers its\n * commands; otherwise the operator is sent the invite URL and the guild is\n * confirmed lazily off the first inbound interaction's authoritative `guild_id`.\n */\n guildId?: string;\n /** Display name for this installation. Defaults to the application's name. */\n name?: string;\n /**\n * Slash commands to register for this agent. Overrides\n * {@link DiscordProviderConfig.commands}. Defaults to the `/help` seed.\n */\n commands?: DiscordCommandInput[];\n /** Permissions bitfield override for this invite (bigint or decimal string). */\n permissions?: bigint | string;\n}\n\n/**\n * A per-agent Discord installation. Unlike Telegram (one token = one row),\n * Discord installs are keyed by agent and track the set of `guildIds` the bot is\n * live in. **No secrets live here** — the bot token lives once in the app config\n * ({@link DiscordAppConfig}); this row only holds routing + per-guild command\n * versions. Persisted through {@link DiscordInstallStore}.\n */\nexport interface DiscordInstallation {\n /** Stable installation id. */\n id: string;\n /** The agent this installation is bound to. */\n agentId: string;\n /**\n * Opaque id embedded in the interactions route path\n * (`/discord/events/:webhookId`). Used to resolve the install on inbound POSTs.\n */\n webhookId: string;\n /**\n * `pending` — invite issued, no guild confirmed yet.\n * `active` — the bot is live in at least one guild (or bound to a known guild).\n */\n status: 'active' | 'pending';\n /** Guilds this agent's bot is confirmed live in (tenancy keys). */\n guildIds: string[];\n /** Display name (application name or an operator-supplied override). */\n displayName?: string;\n /** Normalized slash commands to register for this agent (resolved at connect). */\n commands?: DiscordCommand[];\n /**\n * Registered-command version hash, keyed by `guildId` (guild scope) or the\n * literal `'global'` (global scope). Used to skip re-`PUT`ting commands when\n * unchanged (200/day/guild rate limit).\n */\n commandVersions?: Record<string, string>;\n /** When the installation was created. */\n installedAt: Date;\n /**\n * The bot's guild ids at the moment a pending invite was issued. Lets\n * {@link DiscordProvider.reconcileInstallation} confirm the install after the\n * operator completes the invite in another tab: exactly one guild appearing\n * since the snapshot is the authorized guild, so the install activates\n * without waiting for a first interaction. `undefined` when the snapshot\n * couldn't be taken (or the bot is in too many guilds to page) — the install\n * then activates only via the first interaction, as before. Cleared on\n * activation.\n */\n guildSnapshot?: string[];\n /**\n * When the invite flow that owns {@link guildSnapshot} was last (re)started.\n * The snapshot is only trusted for a bounded claim window after this moment\n * (see `SNAPSHOT_TTL_MS` in `reconcile.ts`) — an abandoned Connect click must\n * not claim a guild the bot joins days later. Set on every invite-flow\n * `connect()`, including when the fetch that should have produced\n * {@link guildSnapshot} failed. Cleared on activation.\n */\n snapshotAt?: Date;\n /**\n * The Discord application {@link guildSnapshot} was taken against. A baseline\n * from app A says nothing about app B's membership — after `configure()`\n * swaps the bot token, every guild B was already in would look newly\n * authorized. Reconciliation and snapshot reuse require it to match the\n * current app. Cleared on activation.\n */\n snapshotApplicationId?: string;\n /**\n * The guild `connect()` explicitly targeted (`options.guildId`) when the\n * pending invite was issued, if any. Reconciliation only auto-activates this\n * install on that guild — a different guild appearing in the bot's\n * membership contradicts the caller's intent and waits for an authoritative\n * first interaction instead. Cleared on activation.\n */\n targetGuildId?: string;\n}\n","import {\n DEFAULT_INVITE_PERMISSIONS,\n DEFAULT_INVITE_SCOPES,\n DISCORD_API_BASE_URL,\n DISCORD_OAUTH_AUTHORIZE_URL,\n} from './types';\nimport type { DiscordCommand } from './types';\n\n/** `CHAT_INPUT` (slash) application command type. */\nconst APPLICATION_COMMAND_TYPE_CHAT_INPUT = 1;\n\n/** Per-request ceiling for control-plane calls, so a hung API can't stall the provider. */\nconst REQUEST_TIMEOUT_MS = 10_000;\n\n/**\n * Discord snowflake pattern (17–20 digits). Every id Discord issues — application,\n * guild, channel, user — is a snowflake. Validate before interpolating any id\n * into a REST path so unverified callers can't smuggle path traversal (`..`) or\n * other non-snowflake values that resolve to a different endpoint (e.g., a\n * `guildId` of `..` on `/applications/{id}/guilds/{guildId}/commands` collapses\n * to the global-commands endpoint and would overwrite an app's global commands).\n *\n * @see https://discord.com/developers/docs/reference#snowflakes\n */\nconst DISCORD_SNOWFLAKE = /^\\d{17,20}$/;\n\nfunction assertSnowflake(value: string, label: string): void {\n if (!DISCORD_SNOWFLAKE.test(value)) {\n throw new Error(`Invalid Discord ${label}: ${JSON.stringify(value)}`);\n }\n}\n\n/**\n * The subset of Discord's application object the control plane reads back from\n * `GET /applications/@me`.\n * @see https://discord.com/developers/docs/resources/application#application-object\n */\nexport interface DiscordApplication {\n /** The application (client) id. */\n id: string;\n /** The application's name. */\n name: string;\n /**\n * The Ed25519 public key used to verify interaction signatures. Discord\n * includes it on every application object, so a bot token alone is enough to\n * resolve the interaction-verification key.\n */\n verify_key: string;\n}\n\n/** Minimal Discord REST error envelope. */\ninterface DiscordErrorBody {\n message?: string;\n code?: number;\n}\n\n/**\n * Call the Discord REST API with Bot-token auth. `method` is required; supplying\n * `body` adds the JSON `content-type` header and serializes the payload. Returns\n * the raw {@link Response} so callers can classify the status themselves rather\n * than having every non-2xx collapse into a thrown error.\n *\n * This is the **control plane only** — validating the app and reading guild\n * membership. Sending replies uses the interaction token inside the adapter,\n * never this bot-token path.\n */\nexport async function discordRequest(\n botToken: string,\n method: string,\n path: string,\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n body?: unknown,\n): Promise<Response> {\n const init: RequestInit = {\n method,\n headers: {\n authorization: `Bot ${botToken}`,\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n };\n try {\n // Bound every control-plane call: without a timeout a hung Discord API\n // blocks connect()/disconnect()/#registerCommands() indefinitely, since no\n // call site in discord-provider.ts imposes one of its own.\n return await fetch(`${apiBaseUrl}${path}`, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });\n } catch (cause) {\n throw new Error(`Discord ${method} ${path} request failed`, { cause });\n }\n}\n\n/** Read a Discord error description from a non-2xx response, best-effort. */\nasync function describeError(response: Response): Promise<string> {\n const body = (await response.json().catch(() => null)) as DiscordErrorBody | null;\n return body?.message ?? `HTTP ${response.status}`;\n}\n\n/**\n * Validate the app's bot token and resolve the application identity via\n * `GET /applications/@me` (Bot auth). Throws if the token is rejected.\n *\n * @see https://discord.com/developers/docs/resources/application#get-current-application\n */\nexport async function validateApp(\n botToken: string,\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<DiscordApplication> {\n const response = await discordRequest(botToken, 'GET', '/applications/@me', apiBaseUrl);\n if (!response.ok) {\n throw new Error(`Discord rejected the bot token: ${await describeError(response)}`);\n }\n const app = (await response.json()) as DiscordApplication;\n if (!app?.id) {\n throw new Error('Discord /applications/@me returned no application id');\n }\n return app;\n}\n\n/**\n * Whether the bot is already a member of a guild. `GET /guilds/{id}` (Bot auth)\n * returns `200` only when the bot is in the guild. Absence is `404` (`10004\n * Unknown Guild`) — Discord standardized on 404 rather than 403 so the response\n * can't confirm a guild exists to a caller that lacks access — with legacy\n * deployments still answering `403` (`50001 Missing Access`).\n *\n * Any **other** failure (`401`, `429`, `5xx`) is a transient or auth problem,\n * not evidence of absence, and throws. Collapsing those into `false` would send\n * a caller whose bot *is* in the guild down the invite path on a rate limit.\n *\n * @see https://discord.com/developers/docs/resources/guild#get-guild\n */\nexport async function guildHealthCheck(\n botToken: string,\n guildId: string,\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<boolean> {\n assertSnowflake(guildId, 'guild id');\n const response = await discordRequest(botToken, 'GET', `/guilds/${guildId}`, apiBaseUrl);\n if (response.ok) {\n // Drain the body so the connection can be reused (undici keep-alive).\n await response.body?.cancel().catch(() => {});\n return true;\n }\n if (response.status === 404 || response.status === 403) {\n await response.body?.cancel().catch(() => {});\n return false;\n }\n throw new Error(`Discord guild lookup failed for \"${guildId}\": ${await describeError(response)}`);\n}\n\n/** Guilds-per-page for `GET /users/@me/guilds` (Discord's documented maximum). */\nconst GUILD_PAGE_LIMIT = 200;\n\n/**\n * Pages we're willing to walk when listing the bot's guilds. The listing feeds\n * the pending-install snapshot diff, whose multiple-new-guilds guard keeps an\n * *incomplete* listing safe (reconcile just stays pending) — so a hard cap is\n * preferable to an unbounded walk on a bot in thousands of guilds.\n */\nconst GUILD_PAGE_MAX_PAGES = 10;\n\n/**\n * The ids of every guild the bot is a member of, via `GET /users/@me/guilds`\n * (Bot auth), following `after`-cursor pagination. Returns `null` instead of a\n * partial listing when the bot is in more guilds than {@link GUILD_PAGE_MAX_PAGES}\n * covers — callers diffing snapshots must not mistake a truncated page for the\n * full membership. Throws on request failure.\n *\n * @see https://discord.com/developers/docs/resources/user#get-current-user-guilds\n */\nexport async function listBotGuildIds(\n botToken: string,\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<string[] | null> {\n const ids: string[] = [];\n let after: string | undefined;\n for (let page = 0; page < GUILD_PAGE_MAX_PAGES; page++) {\n const query = `?limit=${GUILD_PAGE_LIMIT}${after ? `&after=${after}` : ''}`;\n const response = await discordRequest(botToken, 'GET', `/users/@me/guilds${query}`, apiBaseUrl);\n if (!response.ok) {\n throw new Error(`Discord guild listing failed: ${await describeError(response)}`);\n }\n const batch = (await response.json()) as Array<{ id: string }>;\n for (const guild of batch) ids.push(guild.id);\n if (batch.length < GUILD_PAGE_LIMIT) return ids;\n after = batch[batch.length - 1]!.id;\n }\n return null; // more guilds than we page — signal \"unknown\", not a partial list\n}\n\n/** Map normalized commands to the Discord bulk-overwrite `CHAT_INPUT` payload. */\nfunction toCommandPayload(commands: readonly DiscordCommand[]) {\n return commands.map(c => ({\n name: c.name,\n description: c.description,\n type: APPLICATION_COMMAND_TYPE_CHAT_INPUT,\n }));\n}\n\n/**\n * Longest `Retry-After` we will wait out inline before giving up. Registration\n * is best-effort at the call site, so a long bucket is better surfaced as an\n * error than held open.\n */\nconst MAX_RETRY_AFTER_MS = 5_000;\n\n/**\n * `PUT` a bulk command overwrite, retrying **once** if Discord rate-limits it.\n *\n * Discord returns the wait in the `retry_after` body field (seconds, may be\n * fractional) and the `Retry-After` header. The value is dynamic, so it is read\n * from the response rather than assumed.\n *\n * @see https://discord.com/developers/docs/topics/rate-limits\n */\nasync function bulkOverwriteCommands(\n botToken: string,\n path: string,\n commands: readonly DiscordCommand[],\n apiBaseUrl: string,\n scopeLabel: string,\n): Promise<void> {\n const payload = toCommandPayload(commands);\n for (let attempt = 0; attempt < 2; attempt++) {\n const response = await discordRequest(botToken, 'PUT', path, apiBaseUrl, payload);\n if (response.ok) {\n await response.body?.cancel().catch(() => {});\n return;\n }\n if (response.status === 429 && attempt === 0) {\n const body = (await response.json().catch(() => null)) as { retry_after?: number; message?: string } | null;\n const headerSeconds = Number(response.headers.get('retry-after'));\n const seconds = body?.retry_after ?? (Number.isFinite(headerSeconds) ? headerSeconds : 0);\n const waitMs = Math.ceil(seconds * 1000);\n if (waitMs > 0 && waitMs <= MAX_RETRY_AFTER_MS) {\n await new Promise(resolve => setTimeout(resolve, waitMs));\n continue;\n }\n throw new Error(\n `Discord ${scopeLabel} command registration was rate-limited; retry after ${seconds}s: ${body?.message ?? `HTTP ${response.status}`}`,\n );\n }\n throw new Error(`Discord ${scopeLabel} command registration failed: ${await describeError(response)}`);\n }\n}\n\n/**\n * Bulk-overwrite a **guild's** slash commands (`PUT …/guilds/{guildId}/commands`).\n * Guild-scoped commands update **instantly**. Register on first-seen guild;\n * callers skip the call when the command hash is unchanged (200 creates/day/guild).\n *\n * @see https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands\n */\nexport async function registerGuildCommands(\n botToken: string,\n applicationId: string,\n guildId: string,\n commands: readonly DiscordCommand[],\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<void> {\n assertSnowflake(applicationId, 'application id');\n assertSnowflake(guildId, 'guild id');\n const path = `/applications/${applicationId}/guilds/${guildId}/commands`;\n await bulkOverwriteCommands(botToken, path, commands, apiBaseUrl, 'guild');\n}\n\n/**\n * Bulk-overwrite the app's **global** slash commands (`PUT …/commands`).\n * Eventually consistent (propagation is not instant — don't quote a fixed\n * number). Opt-in via `commandScope: 'global'`.\n *\n * @see https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands\n */\nexport async function registerGlobalCommands(\n botToken: string,\n applicationId: string,\n commands: readonly DiscordCommand[],\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<void> {\n assertSnowflake(applicationId, 'application id');\n const path = `/applications/${applicationId}/commands`;\n await bulkOverwriteCommands(botToken, path, commands, apiBaseUrl, 'global');\n}\n\n/** Inputs for {@link buildInviteUrl}. */\nexport interface BuildInviteUrlOptions {\n /** The application (client) id — becomes `client_id`. */\n applicationId: string;\n /** Permissions bitfield (bigint or decimal string). Defaults to {@link DEFAULT_INVITE_PERMISSIONS}. */\n permissions?: bigint | string;\n /** OAuth2 scopes. Defaults to {@link DEFAULT_INVITE_SCOPES} (`bot applications.commands`). */\n scopes?: readonly string[];\n /**\n * Preselect this guild in the authorize screen and lock the picker, so the\n * operator can't authorize a different guild than the one `connect()` recorded.\n */\n guildId?: string;\n}\n\n/**\n * Build the OAuth2 bot-invite URL. This *is* the Discord \"install\" flow: the\n * operator opens it, picks a guild, and authorizes — there is no token exchange.\n * `scope=bot applications.commands` + a `permissions` bitfield.\n *\n * @see https://discord.com/developers/docs/topics/oauth2#bot-authorization-flow\n */\nexport function buildInviteUrl(options: BuildInviteUrlOptions): string {\n const permissions = (options.permissions ?? DEFAULT_INVITE_PERMISSIONS).toString();\n const scopes = options.scopes ?? DEFAULT_INVITE_SCOPES;\n const url = new URL(DISCORD_OAUTH_AUTHORIZE_URL);\n url.searchParams.set('client_id', options.applicationId);\n url.searchParams.set('scope', scopes.join(' '));\n url.searchParams.set('permissions', permissions);\n if (options.guildId) {\n // Preselect and lock the picker so the authorized guild matches the one the\n // install was recorded against.\n url.searchParams.set('guild_id', options.guildId);\n url.searchParams.set('disable_guild_select', 'true');\n }\n return url.toString();\n}\n","import { createHash } from 'node:crypto';\nimport type { DiscordCommand, DiscordCommandInput } from './types';\n\n/**\n * Conventional command seed registered when a connect provides none. Discord\n * surfaces `/help` in the command picker; the built-in DM/mention handling\n * covers everything else, so the seed is intentionally tiny.\n * @see https://discord.com/developers/docs/interactions/application-commands\n */\nexport const DEFAULT_COMMANDS: readonly DiscordCommandInput[] = [\n { name: 'help', description: 'Show what this bot can do' },\n];\n\n/**\n * Map user-supplied commands to Discord `CHAT_INPUT` command shapes, enforcing\n * the API constraints: `name` is lowercased, stripped of a leading slash,\n * reduced to `[a-z0-9_-]`, and clamped to 1-32 chars; `description` defaults to\n * `Run /<name>` and is clamped to 1-100 chars. Empty or duplicate names are\n * dropped.\n *\n * @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming\n */\nexport function normalizeCommands(raw: readonly DiscordCommandInput[] | undefined): DiscordCommand[] {\n if (!raw) return [];\n const seen = new Set<string>();\n const commands: DiscordCommand[] = [];\n for (const item of raw) {\n const input = typeof item === 'string' ? { name: item } : item;\n const name = input.name\n .replace(/^\\//, '')\n .toLowerCase()\n .replace(/[^a-z0-9_-]/g, '')\n .slice(0, 32);\n if (!name || seen.has(name)) continue;\n seen.add(name);\n const description = (input.description?.trim() || `Run /${name}`).slice(0, 100);\n commands.push({ name, description });\n }\n return commands;\n}\n\n/**\n * Stable content hash of a normalized command list, used to skip re-`PUT`ting a\n * guild/global command set that hasn't changed (Discord allows only 200\n * application-command creates per day, per guild). Order-independent: commands\n * are sorted by name before hashing.\n */\nexport function hashCommands(commands: readonly DiscordCommand[]): string {\n const canonical = [...commands]\n .sort((a, b) => a.name.localeCompare(b.name))\n .map(c => ({ name: c.name, description: c.description }));\n return createHash('sha256').update(JSON.stringify(canonical)).digest('hex').slice(0, 16);\n}\n","import { createCipheriv, createDecipheriv, hkdfSync } from 'node:crypto';\n\n/**\n * Opt-in AES-256-GCM encryption for the app's bot token at rest, with\n * HKDF-SHA256 key derivation (mirrors `@mastra/telegram` / `@mastra/slack`'s\n * `crypto.ts`). Each value gets a fresh random 16-byte salt + 12-byte IV; the\n * salt travels in the ciphertext, so the same passphrase never derives the same\n * key twice. The algorithm prefix lets plaintext and encrypted values coexist\n * during migration, so {@link decrypt} can no-op on plaintext.\n *\n * Format: `aes-256-gcm-hkdf:base64(salt):base64(iv):base64(authTag):base64(ciphertext)`\n */\nconst ALGO_PREFIX = 'aes-256-gcm-hkdf';\nconst HKDF_INFO = 'mastra-discord-encryption';\n\nfunction deriveKey(passphrase: string, salt: Buffer): Buffer {\n return Buffer.from(hkdfSync('sha256', passphrase, salt, HKDF_INFO, 32));\n}\n\n/** Whether a stored value was produced by {@link encrypt}. */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(`${ALGO_PREFIX}:`);\n}\n\n/** Encrypt a UTF-8 string with a per-value random salt + IV. */\nexport function encrypt(plaintext: string, passphrase: string): string {\n const salt = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(16)));\n const iv = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(12)));\n const cipher = createCipheriv('aes-256-gcm', deriveKey(passphrase, salt), iv);\n const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n return `${ALGO_PREFIX}:${salt.toString('base64')}:${iv.toString('base64')}:${tag.toString('base64')}:${enc.toString('base64')}`;\n}\n\n/** Decrypt a value from {@link encrypt}. Plaintext (unprefixed) is returned unchanged. */\nexport function decrypt(value: string, passphrase: string): string {\n if (!isEncrypted(value)) return value;\n const [, saltB64, ivB64, tagB64, ctB64] = value.split(':');\n if (!saltB64 || !ivB64 || !tagB64 || ctB64 === undefined) {\n throw new Error('Invalid ciphertext payload');\n }\n try {\n // Setup is inside the guard: base64 decoding never throws, it just yields\n // the wrong number of bytes, and setAuthTag rejects a wrong-length tag\n // outright (\"Invalid authentication tag length\"). A corrupt salt or iv still\n // surfaces at final() instead, but both paths belong to the same failure.\n const decipher = createDecipheriv(\n 'aes-256-gcm',\n deriveKey(passphrase, Buffer.from(saltB64, 'base64')),\n Buffer.from(ivB64, 'base64'),\n );\n decipher.setAuthTag(Buffer.from(tagB64, 'base64'));\n return Buffer.concat([decipher.update(Buffer.from(ctB64, 'base64')), decipher.final()]).toString('utf8');\n } catch (cause) {\n // GCM authentication failed. Node's own messages (\"Unsupported state or\n // unable to authenticate data\", \"Invalid initialization vector\") give an\n // operator nothing to act on, and this runs on the credential-load path —\n // name both causes. A wrong key is the common one, but a corrupt stored\n // value fails identically, and an operator who assumes the key would rotate\n // a perfectly good one and lose the rest of the secrets.\n throw new Error(\n 'Failed to decrypt a stored Discord secret — either the configured encryption key does not match the one it was encrypted with, or the stored value is corrupt. Check `encryptionKey` on DiscordProvider or MASTRA_ENCRYPTION_KEY.',\n { cause },\n );\n }\n}\n","import type { ChannelInstallationInfo } from '@mastra/core/channels';\nimport type { ChannelInstallation, ChannelsStorage } from '@mastra/core/storage';\nimport type { DiscordAppConfig, DiscordCommand, DiscordInstallation } from './types';\nimport { decrypt, encrypt, isEncrypted } from './crypto';\n\n/** Platform identifier used for every stored record, config, and route. */\nexport const PLATFORM = 'discord';\n\n/** Per-agent fields serialized into a {@link ChannelInstallation.data} blob (no secrets). */\ninterface DiscordInstallationData {\n guildIds?: string[];\n displayName?: string;\n commands?: DiscordCommand[];\n commandVersions?: Record<string, string>;\n /** Pending-invite reconcile baseline — see {@link DiscordInstallation.guildSnapshot}. */\n guildSnapshot?: string[];\n /** ISO timestamp of the invite flow that owns the baseline — see {@link DiscordInstallation.snapshotAt}. */\n snapshotAt?: string;\n /** Application the baseline was taken against — see {@link DiscordInstallation.snapshotApplicationId}. */\n snapshotApplicationId?: string;\n /** Explicit invite target — see {@link DiscordInstallation.targetGuildId}. */\n targetGuildId?: string;\n}\n\n/** App-config fields serialized into the platform `ChannelConfig.data` blob. */\ninterface DiscordAppConfigData {\n /** Bot token — the only secret; AES-256-GCM encrypted when an `encryptionKey` is set. */\n botToken?: string;\n /** Ed25519 public key (public — stored plaintext). */\n publicKey?: string;\n /** Application (client) id (public — stored plaintext). */\n applicationId?: string;\n}\n\n/**\n * Two-tier persistence for Discord, layered over the platform-agnostic\n * `ChannelsStorage` (the same store `@mastra/slack` / `@mastra/telegram` use).\n *\n * - **App tier** (`saveConfig`/`getConfig`) — the one application's\n * `botToken` + `publicKey` + `applicationId`, stored **once** (one app, many\n * guilds). `botToken` is AES-256-GCM encrypted at rest when an `encryptionKey`\n * is supplied.\n * - **Install tier** (`getInstallationByAgent`/`…ByWebhookId`/`saveInstallation`)\n * — one row per agent, keyed by agent, tracking the `guildIds` the bot is live\n * in. Secrets are **not** duplicated per row.\n */\nexport class DiscordInstallStore {\n constructor(\n private readonly storage: ChannelsStorage,\n private readonly encryptionKey?: string,\n ) {}\n\n // --- App tier (stored once) -----------------------------------------------\n\n /** The stored app config, if any (bot token decrypted). */\n async getAppConfig(): Promise<DiscordAppConfig | null> {\n const config = await this.storage.getConfig(PLATFORM);\n if (!config) return null;\n const data = (config.data ?? {}) as DiscordAppConfigData;\n if (!data.botToken || !data.publicKey || !data.applicationId) return null;\n return {\n botToken: this.#dec(data.botToken)!,\n publicKey: data.publicKey,\n applicationId: data.applicationId,\n };\n }\n\n /** Persist the app config once (bot token encrypted at rest when keyed). */\n async saveAppConfig(app: DiscordAppConfig): Promise<void> {\n const data: DiscordAppConfigData = {\n botToken: this.#enc(app.botToken),\n publicKey: app.publicKey,\n applicationId: app.applicationId,\n };\n await this.storage.saveConfig({ platform: PLATFORM, data: data as Record<string, unknown>, updatedAt: new Date() });\n }\n\n /** Remove the stored app config. */\n async deleteAppConfig(): Promise<void> {\n await this.storage.deleteConfig(PLATFORM);\n }\n\n // --- Install tier (one row per agent) -------------------------------------\n\n /** The installation for an agent, if any. */\n async getByAgent(agentId: string): Promise<DiscordInstallation | null> {\n const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);\n return record ? this.#fromRecord(record) : null;\n }\n\n /** Look up an installation by the routing id in its interactions-route path. */\n async getByWebhookId(webhookId: string): Promise<DiscordInstallation | null> {\n const record = await this.storage.getInstallationByWebhookId(webhookId);\n return record && record.platform === PLATFORM ? this.#fromRecord(record) : null;\n }\n\n /**\n * Find the installation that owns a guild (tenancy lookup by `guildId`). Used\n * when the routing key is a guild rather than a webhook id.\n *\n * `guildIds` is per-agent and nothing enforces exclusivity, so two agents can\n * legitimately be installed into the same guild. Returning the first match\n * would make routing depend on storage row order — the same interaction could\n * reach a different agent on the next lookup. Instead the **oldest** install\n * wins (ties broken by id), which is stable across restarts and storage\n * backends, and the ambiguity is logged once so an operator can see it.\n *\n * `ChannelsStorage` has no query-by-data-field, so this necessarily lists the\n * platform's installations; it is not on the interactions hot path, which\n * routes by `webhookId`.\n */\n async getByGuildId(guildId: string): Promise<DiscordInstallation | null> {\n const records = await this.storage.listInstallations(PLATFORM);\n const matches = records.filter(r => ((r.data as DiscordInstallationData)?.guildIds ?? []).includes(guildId));\n if (matches.length === 0) return null;\n if (matches.length > 1) {\n console.warn(\n `[Discord] Guild \"${guildId}\" is claimed by ${matches.length} installations (${matches\n .map(r => r.agentId)\n .join(', ')}). Routing to the oldest; disconnect the others to remove the ambiguity.`,\n );\n }\n const winner = matches.reduce((oldest, r) => {\n const a = r.createdAt?.getTime() ?? 0;\n const b = oldest.createdAt?.getTime() ?? 0;\n if (a !== b) return a < b ? r : oldest;\n return r.id < oldest.id ? r : oldest;\n });\n return this.#fromRecord(winner);\n }\n\n /** Insert or replace an installation. */\n async save(installation: DiscordInstallation): Promise<void> {\n await this.storage.saveInstallation(this.#toRecord(installation));\n }\n\n /** All Discord installations (active and pending). */\n async list(): Promise<DiscordInstallation[]> {\n const records = await this.storage.listInstallations(PLATFORM);\n return records.map(r => this.#fromRecord(r));\n }\n\n /** Remove an agent's installation, if present. */\n async deleteByAgent(agentId: string): Promise<void> {\n const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);\n if (record) await this.storage.deleteInstallation(record.id);\n }\n\n #enc(value: string | undefined): string | undefined {\n return value && this.encryptionKey ? encrypt(value, this.encryptionKey) : value;\n }\n\n #dec(value: string | undefined): string | undefined {\n if (!value) return value;\n if (!this.encryptionKey) {\n // A deployment that stored an encrypted token and later lost its key would\n // otherwise hand the ciphertext to Discord as a bot token — surfacing as\n // opaque 401s instead of the configuration error it actually is.\n if (isEncrypted(value)) {\n throw new Error(\n 'The stored Discord bot token is encrypted at rest, but no encryption key is configured. Set `encryptionKey` on DiscordProvider or MASTRA_ENCRYPTION_KEY.',\n );\n }\n return value;\n }\n return decrypt(value, this.encryptionKey);\n }\n\n #toRecord(install: DiscordInstallation): ChannelInstallation {\n const data: DiscordInstallationData = {\n guildIds: install.guildIds,\n displayName: install.displayName,\n commands: install.commands,\n commandVersions: install.commandVersions,\n guildSnapshot: install.guildSnapshot,\n snapshotAt: install.snapshotAt?.toISOString(),\n snapshotApplicationId: install.snapshotApplicationId,\n targetGuildId: install.targetGuildId,\n };\n return {\n id: install.id,\n platform: PLATFORM,\n agentId: install.agentId,\n status: install.status,\n webhookId: install.webhookId,\n data: data as Record<string, unknown>,\n createdAt: install.installedAt,\n updatedAt: new Date(),\n };\n }\n\n #fromRecord(record: ChannelInstallation): DiscordInstallation {\n const data = (record.data ?? {}) as DiscordInstallationData;\n return {\n id: record.id,\n agentId: record.agentId,\n webhookId: record.webhookId ?? '',\n status: record.status === 'active' ? 'active' : 'pending',\n guildIds: data.guildIds ?? [],\n displayName: data.displayName,\n commands: data.commands,\n commandVersions: data.commandVersions,\n installedAt: record.createdAt,\n guildSnapshot: data.guildSnapshot,\n snapshotAt: data.snapshotAt ? new Date(data.snapshotAt) : undefined,\n snapshotApplicationId: data.snapshotApplicationId,\n targetGuildId: data.targetGuildId,\n };\n }\n}\n\n/** Project an installation to its public, secret-free info for the editor UI. */\nexport function toInstallationInfo(install: DiscordInstallation): ChannelInstallationInfo {\n return {\n id: install.id,\n platform: PLATFORM,\n agentId: install.agentId,\n status: install.status,\n displayName: install.displayName,\n installedAt: install.installedAt,\n };\n}\n","import type { DiscordInstallation } from './types';\n\n/**\n * How long a pending install's {@link DiscordInstallation.guildSnapshot} stays\n * eligible for reconciliation after the invite flow (re)started. Past this\n * window the baseline is stale evidence: the operator has plainly abandoned\n * the Connect flow, and a guild the bot joins days later (for any reason) must\n * not be attributed to it. An expired install still activates normally via its\n * first interaction.\n */\nexport const SNAPSHOT_TTL_MS = 30 * 60 * 1000;\n\n/** One activation the reconcile pass decided on (see {@link planReconcile}). */\nexport interface ReconcileActivation {\n installationId: string;\n agentId: string;\n webhookId: string;\n /** The single new, uncontested guild attributed to this install's invite. */\n guildId: string;\n}\n\n/**\n * Whether an install's invite flow is still *live*: started against the\n * current application and within {@link SNAPSHOT_TTL_MS}. Deliberately\n * independent of whether the baseline fetch succeeded — a live flow with a\n * missing baseline still poisons attribution (see {@link planReconcile}),\n * while rows from before these fields existed (no `snapshotAt`) are treated\n * as not live and fall back to first-interaction activation.\n */\nexport function isInviteFlowLive(\n installation: Pick<DiscordInstallation, 'status' | 'snapshotAt' | 'snapshotApplicationId'>,\n applicationId: string,\n now: Date,\n): boolean {\n return (\n installation.status === 'pending' &&\n installation.snapshotApplicationId === applicationId &&\n installation.snapshotAt != null &&\n now.getTime() - installation.snapshotAt.getTime() <= SNAPSHOT_TTL_MS\n );\n}\n\n/**\n * Whether a pending install can reconcile at all: a live invite flow (see\n * {@link isInviteFlowLive}) *with* a membership baseline to diff against.\n * Used as a cheap pre-check before the membership fetch.\n */\nexport function hasReconcilableSnapshot(installation: DiscordInstallation, applicationId: string, now: Date): boolean {\n return installation.guildSnapshot != null && isInviteFlowLive(installation, applicationId, now);\n}\n\n/**\n * Decide which pending installs the completed-invite reconcile pass may\n * activate, and on which guild. Pure claim analysis over plain data — all\n * store reads, membership fetches, and activations happen in the caller\n * ({@link DiscordProvider.reconcileInstallation}).\n *\n * Rules, in order:\n * - Only installs with a live invite flow ({@link isInviteFlowLive}) take part.\n * Expired/foreign-app baselines are ignored (first interaction remains their\n * activation path), as are legacy rows without snapshot metadata.\n * - A live flow whose baseline fetch failed at connect time poisons the whole\n * pass: any new guild might be ITS invite landing, so nothing is attributed.\n * - A guild any install already lists is owned and never claimed — it can\n * still look \"new\" relative to a stale baseline.\n * - An install with an explicit {@link DiscordInstallation.targetGuildId}\n * claims *only* that guild. The narrowing happens before claims are tallied,\n * so a targeted install never contests a guild it would refuse anyway — but\n * its target, if new, still blocks other installs from taking it.\n * - An install activates only on exactly one new guild, claimed by no other\n * live install. Zero or multiple candidates, or a contested guild, stay\n * pending and retry on the next pass.\n */\nexport function planReconcile(input: {\n installations: DiscordInstallation[];\n /** The bot's complete current guild membership. */\n currentGuildIds: string[];\n /** The application the membership was fetched for. */\n applicationId: string;\n now: Date;\n}): ReconcileActivation[] {\n const { installations, currentGuildIds, applicationId, now } = input;\n\n const live = installations.filter(i => isInviteFlowLive(i, applicationId, now));\n const claimants = live.filter(i => i.guildSnapshot != null);\n if (claimants.length === 0) return [];\n if (claimants.length !== live.length) return []; // live flow without a baseline — unattributable\n\n const owned = new Set(installations.flatMap(i => i.guildIds));\n\n const diffs = claimants.map(installation => {\n const snapshot = new Set(installation.guildSnapshot);\n let newGuilds = currentGuildIds.filter(id => !snapshot.has(id) && !owned.has(id));\n if (installation.targetGuildId != null) {\n newGuilds = newGuilds.includes(installation.targetGuildId) ? [installation.targetGuildId] : [];\n }\n return { installation, newGuilds };\n });\n\n const claims = new Map<string, number>();\n for (const { newGuilds } of diffs) {\n for (const id of newGuilds) claims.set(id, (claims.get(id) ?? 0) + 1);\n }\n\n const activations: ReconcileActivation[] = [];\n for (const { installation, newGuilds } of diffs) {\n const guildId = newGuilds.length === 1 ? newGuilds[0] : undefined;\n if (guildId == null || claims.get(guildId)! > 1) continue; // ambiguous — wait for an interaction\n activations.push({\n installationId: installation.id,\n agentId: installation.agentId,\n webhookId: installation.webhookId,\n guildId,\n });\n }\n return activations;\n}\n","import { AgentChannels, resolveWaitUntil } from '@mastra/core/channels';\nimport type {\n ChannelAdapterConfig,\n ChannelConnectResult,\n ChannelInstallationInfo,\n ChannelPlatformInfo,\n ChannelProvider,\n StreamingConfig,\n} from '@mastra/core/channels';\nimport type { Agent } from '@mastra/core/agent';\nimport type { Mastra } from '@mastra/core/mastra';\nimport type { ApiRoute, ApiRouteHandler } from '@mastra/core/server';\nimport { InMemoryChannelsStorage } from '@mastra/core/storage';\nimport type { ChannelsStorage } from '@mastra/core/storage';\nimport { createDiscordAdapter } from '@chat-adapter/discord';\nimport type { DiscordAdapter } from '@chat-adapter/discord';\nimport {\n buildInviteUrl,\n guildHealthCheck,\n listBotGuildIds,\n registerGlobalCommands,\n registerGuildCommands,\n validateApp,\n} from './discord-client';\nimport type { DiscordApplication } from './discord-client';\nimport { DEFAULT_COMMANDS, hashCommands, normalizeCommands } from './commands';\nimport { DiscordInstallStore, PLATFORM, toInstallationInfo } from './install-store';\nimport { SNAPSHOT_TTL_MS, hasReconcilableSnapshot, planReconcile } from './reconcile';\nimport { DEFAULT_INVITE_PERMISSIONS, DEFAULT_INVITE_SCOPES, DISCORD_API_BASE_URL } from './types';\nimport type { DiscordAppConfig, DiscordConnectOptions, DiscordInstallation, DiscordProviderConfig } from './types';\n\n/**\n * Resolve the per-adapter config the provider applies to the Discord entry in\n * `AgentChannels.adapters`. Discord has native embeds + action-row buttons, so\n * `toolDisplay` defaults to `'cards'` (unlike Telegram's `'text'`); `streaming`\n * post-and-edits the interaction followup; `gateway` (default `true`) makes core\n * own the DM/mention Gateway reconnection loop.\n */\nexport function resolveDiscordAdapterConfig(\n config: Pick<DiscordProviderConfig, 'streaming' | 'typingStatus' | 'toolDisplay' | 'gateway'>,\n): {\n streaming: StreamingConfig;\n typingStatus: boolean;\n toolDisplay: ChannelAdapterConfig['toolDisplay'];\n gateway: boolean;\n} {\n return {\n streaming: config.streaming ?? true,\n typingStatus: config.typingStatus ?? true,\n toolDisplay: config.toolDisplay ?? 'cards',\n gateway: config.gateway ?? true,\n };\n}\n\n/**\n * Discord channel provider for Mastra — a {@link ChannelProvider} over\n * `@chat-adapter/discord`. The adapter handles the protocol (Ed25519 request\n * verification, PING/PONG, deferrals, embeds/buttons, the Gateway bridge,\n * post-and-edit streaming); this provider adds the lifecycle layer.\n *\n * **One app, many guilds.** Discord has no programmatic app creation, so a\n * single application's credentials (bot token / public key / application id) are\n * stored **once** and reused across every guild. \"Installing\" is a bot invite —\n * an OAuth2 authorize URL — not a token exchange, so `connect()` returns the\n * `oauth` variant. A guild is confirmed either eagerly (the bot is already a\n * member) or lazily off the first inbound interaction's authoritative `guild_id`\n * (see {@link activateGuild}).\n *\n * **Gateway is core's job.** The mounted interactions route only ever receives\n * HTTP Interactions (PING, slash commands, buttons). DMs / @mentions / reactions\n * arrive over the Gateway WebSocket, whose reconnection loop core owns: setting\n * `gateway: true` (default) on the adapter entry is enough — the wrapper never\n * calls `startGatewayListener` or runs a reconnect loop.\n *\n * Implemented (issues `mastra-discord-13x.2` + `.3`): the app-config +\n * guild-keyed install store, `connect()`/`disconnect()`, OAuth2 invite-URL\n * generation, the interactions route (raw-body delegation), and adapter/Gateway\n * wiring. Command registration lands in `mastra-discord-13x.4`.\n */\nexport class DiscordProvider implements ChannelProvider {\n readonly id = PLATFORM;\n\n #config: DiscordProviderConfig;\n #mastra?: Mastra;\n #store?: DiscordInstallStore;\n /** Live adapters, keyed by installation id (one per agent; all share the app's bot token). */\n #adapters = new Map<string, DiscordAdapter>();\n /** Cached sync view of whether the app is configured (for {@link getInfo}). */\n #configured = false;\n #initPromise: Promise<void> | null = null;\n /**\n * Whether {@link #store} was built on the in-memory fallback rather than real\n * storage — i.e. it was resolved before a `Mastra` instance was available.\n */\n #storeIsFallback = false;\n /**\n * A fallback store dropped by {@link __attach}, held until the next store\n * access so its contents can be carried into the real storage. See\n * {@link #migrateFallback}.\n */\n #pendingMigration?: DiscordInstallStore;\n /**\n * The single in-flight store resolution. Concurrent callers share it rather\n * than each running {@link #resolveStore}, so the fallback migration can't be\n * raced: without this, a second caller reads the already-claimed\n * {@link #pendingMigration} as absent, skips the migration, and caches a store\n * over storage the first caller has not finished populating.\n */\n #storeResolution?: Promise<DiscordInstallStore | undefined>;\n /**\n * Bumped by every {@link __attach}. A resolution that started before an attach\n * was built against the previous storage target, so it must not cache its\n * result; it reports itself superseded and the caller resolves again.\n */\n #storeGeneration = 0;\n\n constructor(config: DiscordProviderConfig = {}) {\n this.#config = config;\n // The app is \"configured\" as soon as credentials are resolvable (config/env),\n // even before the first connect persists them. A bot token alone counts —\n // the rest is backfilled from `GET /applications/@me`.\n this.#configured = this.#hasSuppliedCredentials();\n }\n\n /**\n * Called by Mastra when this channel is registered.\n * @internal\n */\n __attach(mastra: Mastra): void {\n const isNewInstance = this.#mastra != null && this.#mastra !== mastra;\n // Any attach can change the storage target, so a resolution already in\n // flight is now stale — including the first attach, where the guard below\n // can't see it: #store is still undefined and #storeIsFallback still false\n // while that resolution runs, so it would sail past and pin the provider to\n // the in-memory fallback *after* real storage became reachable.\n this.#storeGeneration++;\n // Drop the cached store when it belongs to a superseded Mastra, and when a\n // public method (connect(), initialize(), …) ran before registration and\n // pinned the provider to the in-memory fallback — otherwise installs keep\n // going to memory and are lost on restart even though real storage is\n // configured. Whatever that fallback holds was written before real storage\n // was reachable, so hand it to the next store access to carry across rather\n // than discarding it: a pre-registration connect() would otherwise return an\n // installation that no longer exists by the time its route is hit.\n if (isNewInstance || this.#storeIsFallback) {\n if (this.#storeIsFallback) this.#pendingMigration = this.#store;\n this.#initPromise = null;\n this.#store = undefined;\n this.#storeIsFallback = false;\n }\n if (isNewInstance) {\n this.#adapters.clear();\n // The store-derived half of #configured belongs to the old instance; only\n // credentials supplied via config/env survive a re-attach. Without this,\n // getInfo() can report a stale isConfigured before initialize() re-runs.\n this.#configured = this.#hasSuppliedCredentials();\n }\n this.#mastra = mastra;\n }\n\n /**\n * The interactions route: a single POST endpoint keyed by an opaque\n * `webhookId`. The handler passes the **RAW** request bytes straight to\n * `adapter.handleWebhook` — Ed25519 verification + PING/PONG live in the\n * adapter, and any middleware that parsed/re-serialized the body would break\n * every signature. `requiresAuth: false` (Discord authenticates via Ed25519,\n * not a bearer token). Auto-initializes on first hit (mirrors `@mastra/slack`).\n */\n getRoutes(): ApiRoute[] {\n const self = this;\n const withInit = (handler: ApiRouteHandler) => {\n return async ({ mastra }: { mastra: Mastra }): Promise<ApiRouteHandler> => {\n self.#mastra = mastra;\n await self.#autoInitialize();\n return handler.bind(self);\n };\n };\n return [\n {\n path: `/${PLATFORM}/events/:webhookId`,\n method: 'POST',\n requiresAuth: false,\n createHandler: withInit(this.#handleWebhook),\n },\n ];\n }\n\n /** Discovery metadata for the editor UI. */\n getInfo(): ChannelPlatformInfo {\n return {\n id: this.id,\n name: 'Discord',\n isConfigured: this.#configured,\n connectOptionsSchema: {\n type: 'object',\n properties: {\n guildId: {\n type: 'string',\n description:\n 'Guild (server) to install into. If the bot is already a member, connect binds immediately; otherwise you receive an invite URL.',\n },\n name: {\n type: 'string',\n description: \"Display name for the installation (defaults to the application's name).\",\n },\n commands: {\n type: 'array',\n items: { type: 'string' },\n description: 'Slash command names to register for this agent.',\n },\n },\n },\n };\n }\n\n /**\n * Restore state from storage: mark the provider configured when an app config\n * or any active installation exists, and rebuild an adapter + `AgentChannels`\n * per active install so its Gateway loop (core-owned) starts. Idempotent.\n */\n async initialize(): Promise<void> {\n if (this.#initPromise) return this.#initPromise;\n this.#initPromise = this.#doInitialize();\n try {\n await this.#initPromise;\n } catch (err) {\n this.#initPromise = null;\n throw err;\n }\n }\n\n async #doInitialize(): Promise<void> {\n const store = await this.#getStore();\n const active = (await store.list()).filter(i => i.status === 'active');\n const hasApp = (await store.getAppConfig()) != null || this.#hasSuppliedCredentials();\n this.#configured = hasApp || active.length > 0;\n for (const installation of active) {\n try {\n await this.#activateInstallation(installation);\n } catch (err) {\n console.error(`[Discord] Failed to restore installation \"${installation.id}\":`, err);\n }\n }\n }\n\n /**\n * Provide or clear the app credentials at runtime. An object merges/overrides\n * `botToken` / `publicKey` / `applicationId` (persisted on the next `connect`);\n * `null` clears the stored app config. Supplying a **different bot token**\n * replaces the app config instead of merging: the token identifies the\n * Discord application, so any `publicKey`/`applicationId` from the previous\n * application must not survive the switch.\n */\n async configure(credentials: Partial<DiscordAppConfig> | null): Promise<void> {\n if (credentials === null) {\n const store = await this.#getStore();\n await store.deleteAppConfig();\n this.#config = { ...this.#config, app: undefined };\n this.#configured = false;\n // Live adapters still hold the app credentials that were just revoked.\n this.#adapters.clear();\n this.#initPromise = null;\n return;\n }\n const previous = this.#config.app;\n this.#config = { ...this.#config, app: { ...previous, ...credentials } };\n this.#configured = this.#hasSuppliedCredentials() || this.#configured;\n\n // An adapter captures botToken / publicKey / applicationId at construction,\n // so cached ones keep verifying Ed25519 against the *old* public key and\n // authenticating with the *old* bot token. Rebuild them when any changes.\n const changed = (['botToken', 'publicKey', 'applicationId'] as const).some(\n k => credentials[k] !== undefined && credentials[k] !== previous?.[k],\n );\n if (!changed) return;\n\n const store = await this.#getStore();\n const stored = await store.getAppConfig();\n\n // A different bot token means a *different Discord application*. Merging\n // would carry the previous application's publicKey/applicationId into the\n // new config, and its Ed25519 key would keep verifying inbound webhooks —\n // letting the old application's owner forge interactions against the new\n // one. Replace: keep only what this call supplied and drop the stored\n // config so #resolveAppConfig re-derives identity from the new token.\n const tokenSupersedes = (base: Partial<DiscordAppConfig> | null | undefined) =>\n credentials.botToken !== undefined && base?.botToken !== undefined && base.botToken !== credentials.botToken;\n if (tokenSupersedes(previous) || tokenSupersedes(stored)) {\n this.#config = { ...this.#config, app: { ...credentials } };\n await store.deleteAppConfig();\n } else if (stored) {\n // Persist the rotation. Every credential consumer — #resolveAppConfig,\n // #activateInstallation, #handleWebhook — reads the *stored* app config,\n // so updating only #config.app would leave adapters rebuilding from the\n // old credentials and Ed25519 still verifying against the superseded key.\n const merged = { ...stored, ...credentials };\n if (merged.botToken && merged.publicKey && merged.applicationId) {\n await store.saveAppConfig(merged);\n }\n }\n\n const wasInitialized = this.#initPromise !== null;\n this.#adapters.clear();\n this.#initPromise = null;\n if (wasInitialized) await this.initialize();\n }\n\n /**\n * Connect an agent to Discord.\n *\n * - Ensures the app config exists (stored, or supplied via provider config /\n * `DISCORD_*` env and persisted once, after validation). Throws if none —\n * Discord apps are created in the Developer Portal, not programmatically.\n * - Validates the bot token via `GET /applications/@me`.\n * - If `options.guildId` is given **and the bot is already in that guild**,\n * binds immediately (`{ type: 'immediate' }`).\n * - Otherwise persists a pending install and returns the OAuth2 bot-invite URL\n * (`{ type: 'oauth', authorizationUrl }`); the guild activates lazily on the\n * first interaction.\n */\n async connect(agentId: string, options: DiscordConnectOptions = {}): Promise<ChannelConnectResult> {\n const store = await this.#getStore();\n const existing = await store.getByAgent(agentId);\n if (existing?.status === 'active') {\n throw new Error(\n `Agent \"${agentId}\" is already connected to Discord. Disconnect first to reconnect. ` +\n 'To add another server, invite the bot with the existing URL — new guilds activate on first use.',\n );\n }\n\n const { app, persisted, identity: resolvedIdentity } = await this.#resolveAppConfig(store);\n // Validate the token before persisting supplied credentials — an invalid\n // token must leave no app config and no install behind. Skip the second\n // call when #resolveAppConfig already validated it while backfilling.\n const identity = resolvedIdentity ?? (await validateApp(app.botToken, this.#apiBaseUrl()));\n if (!persisted) await store.saveAppConfig(app);\n\n const installationId = existing?.id ?? globalThis.crypto.randomUUID();\n const webhookId = existing?.webhookId ?? globalThis.crypto.randomUUID();\n const displayName = options.name ?? identity.name;\n const installedAt = existing?.installedAt ?? new Date();\n const commands = normalizeCommands(options.commands ?? this.#config.commands ?? DEFAULT_COMMANDS);\n\n // Eager bind: the bot is already a member of the target guild. The save\n // runs inside the per-webhook chain with a fresh re-read — between the\n // read at the top of connect() and this point, a first interaction or a\n // reconcile can have activated the pending row, and an unserialized save\n // would clobber its confirmed guildIds.\n if (options.guildId && (await guildHealthCheck(app.botToken, options.guildId, this.#apiBaseUrl()))) {\n const guildId = options.guildId;\n return await this.#enqueueWebhookTask(webhookId, async () => {\n const fresh = existing ? await store.getByWebhookId(webhookId) : null;\n if (fresh?.status === 'active') {\n // Activated while we validated — the agent is connected; nothing to redo.\n return { type: 'immediate' as const, installationId: fresh.id };\n }\n const installation: DiscordInstallation = {\n id: installationId,\n agentId,\n webhookId,\n status: 'active',\n guildIds: [guildId],\n displayName,\n commands: commands.length ? commands : undefined,\n commandVersions: fresh?.commandVersions ?? existing?.commandVersions,\n installedAt,\n };\n await store.save(installation);\n await this.#registerCommands(app, installation, guildId);\n await this.#activateInstallation(installation);\n this.#configured = true;\n await this.#config.onInstall?.(installation);\n return { type: 'immediate' as const, installationId };\n });\n }\n\n // Invite flow: persist pending, hand back the OAuth2 bot-invite URL.\n // Snapshot the bot's current guild membership first so the install can be\n // reconciled once the operator finishes the invite in another tab (see\n // reconcileInstallation) — without it, activation waits for the first\n // inbound interaction. Best-effort: a failed listing (or a bot in more\n // guilds than we page) just falls back to lazy activation. The save runs\n // inside the per-webhook chain with a fresh re-read so a racing\n // first-interaction/reconcile activation can't be flipped back to pending.\n const outcome = await this.#enqueueWebhookTask(\n webhookId,\n async (): Promise<ChannelConnectResult | DiscordInstallation> => {\n const fresh = existing ? await store.getByWebhookId(webhookId) : null;\n if (fresh?.status === 'active') {\n // The previous invite completed while this connect was validating.\n return { type: 'immediate', installationId: fresh.id };\n }\n const base = fresh ?? existing;\n\n // A re-connect while pending keeps the ORIGINAL baseline — the\n // operator may have already completed the previous invite (the bot\n // joined between the two connect calls), and a fresh snapshot would\n // include that guild and hide it from the reconcile diff forever —\n // but only while it's trustworthy: same application, within the\n // claim window. An expired or foreign-app baseline is replaced.\n const now = new Date();\n const reusable =\n base?.guildSnapshot != null &&\n base.snapshotApplicationId === app.applicationId &&\n base.snapshotAt != null &&\n now.getTime() - base.snapshotAt.getTime() <= SNAPSHOT_TTL_MS;\n let guildSnapshot: string[] | undefined = reusable ? base!.guildSnapshot : undefined;\n if (!guildSnapshot) {\n try {\n guildSnapshot = (await listBotGuildIds(app.botToken, this.#apiBaseUrl())) ?? undefined;\n } catch (err) {\n console.warn('[Discord] Could not snapshot guild membership for pending install:', err);\n }\n }\n const pending: DiscordInstallation = {\n id: installationId,\n agentId,\n webhookId,\n status: 'pending',\n guildIds: base?.guildIds ?? [],\n displayName,\n commands: commands.length ? commands : undefined,\n commandVersions: base?.commandVersions,\n installedAt,\n guildSnapshot,\n // Stamp the flow even when the snapshot fetch failed: a live flow\n // without a baseline must poison reconcile attribution (any new\n // guild might be THIS invite landing) until the window expires.\n snapshotAt: now,\n snapshotApplicationId: app.applicationId,\n // Remember an explicit target so reconciliation never auto-activates\n // this install on some other guild the bot happens to join. A\n // re-connect without a target keeps the original intent.\n targetGuildId: options.guildId ?? base?.targetGuildId,\n };\n await store.save(pending);\n return pending;\n },\n );\n if ('type' in outcome) return outcome; // activated concurrently\n const pending = outcome;\n // Global commands don't need a guild — register once now (best-effort).\n // Guild commands wait for the first-seen guild (lazy activation).\n await this.#registerCommands(app, pending);\n this.#configured = true;\n const authorizationUrl = buildInviteUrl({\n applicationId: app.applicationId,\n permissions: options.permissions ?? this.#config.permissions ?? DEFAULT_INVITE_PERMISSIONS,\n scopes: DEFAULT_INVITE_SCOPES,\n // Preselect the requested guild so the authorized one matches the install.\n ...(options.guildId !== undefined ? { guildId: options.guildId } : {}),\n });\n return { type: 'oauth', authorizationUrl, installationId };\n }\n\n /**\n * Confirm a guild off an inbound interaction's authoritative `guild_id` and\n * mark the installation active (lazy activation). Called by the interactions\n * route on first use. Returns the updated installation, or `null` if the\n * `webhookId` is unknown.\n */\n async activateGuild(webhookId: string, guildId: string): Promise<DiscordInstallation | null> {\n return this.#enqueueWebhookTask(webhookId, () => this.#activateGuildNow(webhookId, guildId));\n }\n\n #activationChains = new Map<string, Promise<unknown>>();\n\n /**\n * Serialize every mutation of one webhook's installation row — activations,\n * pending-row saves, runtime bring-up, disconnects — so each task's read\n * happens *after* the previous task's save. Without the chain, two\n * concurrent first-interactions can lose a guildId (both read the same row,\n * second save overwrites the first) or fire onInstall twice; a connect's\n * pending save can clobber a row a racing interaction just activated; and a\n * disconnect can land mid-activation and be resurrected by its post-HTTP\n * persistence. The chain entry is dropped once the last queued task settles\n * so keys don't accumulate forever.\n */\n async #enqueueWebhookTask<T>(webhookId: string, task: () => Promise<T>): Promise<T> {\n const prev = this.#activationChains.get(webhookId) ?? Promise.resolve();\n const next = prev.catch(() => {}).then(task);\n this.#activationChains.set(webhookId, next);\n try {\n return await next;\n } finally {\n // Only clear our own entry — a newer task may have replaced it.\n if (this.#activationChains.get(webhookId) === next) this.#activationChains.delete(webhookId);\n }\n }\n\n async #activateGuildNow(webhookId: string, guildId: string): Promise<DiscordInstallation | null> {\n const store = await this.#getStore();\n const installation = await store.getByWebhookId(webhookId);\n if (!installation) return null;\n\n const wasActive = installation.status === 'active';\n const known = installation.guildIds.includes(guildId);\n if (wasActive && known) return installation; // nothing to persist\n\n if (!known) installation.guildIds.push(guildId);\n installation.status = 'active';\n delete installation.guildSnapshot; // the reconcile baseline is spent once a guild is confirmed\n delete installation.snapshotAt;\n delete installation.snapshotApplicationId;\n delete installation.targetGuildId;\n await store.save(installation);\n // Register this newly-seen guild's commands (best-effort, hash-skipped).\n const app = await store.getAppConfig();\n if (app) await this.#registerCommands(app, installation, guildId);\n this.#configured = true;\n if (!wasActive) await this.#config.onInstall?.(installation);\n return installation;\n }\n\n /**\n * Disconnect an agent from Discord: remove its installation row and drop the\n * adapter entry.\n *\n * **Limitation:** the Gateway loop is owned by core (it calls\n * `startGatewayListener` itself; there is no `stopGatewayListener`), so\n * disconnect cannot kill an in-flight gateway window — it lapses at the next\n * duration boundary. Contrast Telegram's clean `stopPolling()`.\n */\n async disconnect(agentId: string): Promise<void> {\n const store = await this.#getStore();\n const existing = await store.getByAgent(agentId);\n if (!existing) {\n throw new Error(`No Discord installation found for agent \"${agentId}\"`);\n }\n // Serialize the delete behind any in-flight activation for this webhook.\n // An activation's post-HTTP persistence (the command-version save in\n // #registerCommands) would otherwise land AFTER the delete and resurrect\n // the disconnected row.\n await this.#enqueueWebhookTask(existing.webhookId, async () => {\n this.#adapters.delete(existing.id);\n await store.deleteByAgent(agentId);\n });\n this.#configured = (await store.getAppConfig()) != null || (await store.list()).some(i => i.status === 'active');\n }\n\n /**\n * List installations (public info only — no secrets). A pure read: pending\n * installs are confirmed through the write-authorized\n * {@link reconcileInstallation} path, never as a listing side effect.\n */\n async listInstallations(): Promise<ChannelInstallationInfo[]> {\n const store = await this.#getStore();\n const installations = await store.list();\n return installations.map(toInstallationInfo);\n }\n\n /**\n * Activate an agent's pending install if its invite has since been completed.\n *\n * The invite URL carries no `redirect_uri` (the app would have to allowlist\n * every operator origin), so nothing calls back when the operator authorizes\n * the bot — and a pending install has no adapter, so no interaction or\n * gateway event can reach it either. Instead, each pending install carries a\n * {@link DiscordInstallation.guildSnapshot} taken when the invite was issued;\n * this diffs it against the bot's current membership and activates the\n * install when exactly one new, uncontested guild has appeared (the full\n * claim rules live in {@link planReconcile}). The claim analysis spans every\n * install — other pending invites contest the diff — but only **this**\n * agent's install can activate: the caller holds write access for this agent\n * alone. Anything ambiguous stays pending and retries on the next call, with\n * the first interaction as the authoritative fallback. Best-effort: failures\n * log and resolve rather than throw.\n *\n * Called by the server's reconcile route (write-authorized); Studio invokes\n * it on window focus while a connect is in flight. Returns the (possibly\n * just-activated) installation's public info, or `null` when the agent has\n * no installation.\n *\n * Limitation: the bot-invite URL is per-app, not per-install, so when the\n * operator invites the bot to a new server *for an already-active install*\n * (as `connect()`'s already-connected error suggests) while an invite for\n * another agent is outstanding, nothing distinguishes the two intents — the\n * new guild can be attributed to the pending install. Reconciliation is only\n * reliable while a single invite flow is in progress.\n */\n async reconcileInstallation(agentId: string): Promise<ChannelInstallationInfo | null> {\n const store = await this.#getStore();\n const existing = await store.getByAgent(agentId);\n if (!existing) return null;\n if (existing.status === 'pending') {\n // Single-flight per agent: concurrent reconciles (e.g. rapid focus\n // events) share one pass instead of racing duplicate membership fetches.\n const inflight = this.#reconcileInflight.get(agentId);\n if (inflight) {\n await inflight;\n } else {\n const run = this.#reconcilePendingInstallation(store, agentId).finally(() => {\n this.#reconcileInflight.delete(agentId);\n });\n this.#reconcileInflight.set(agentId, run);\n await run;\n }\n }\n const fresh = await store.getByAgent(agentId);\n return fresh ? toInstallationInfo(fresh) : null;\n }\n\n #reconcileInflight = new Map<string, Promise<void>>();\n\n async #reconcilePendingInstallation(store: DiscordInstallStore, agentId: string): Promise<void> {\n try {\n const app = await store.getAppConfig();\n if (!app) return;\n // Cheap pre-check: skip the membership fetch when this agent's install\n // can't reconcile anyway (no baseline, expired window, foreign app).\n const target = await store.getByAgent(agentId);\n if (!target || target.status !== 'pending') return;\n if (!hasReconcilableSnapshot(target, app.applicationId, new Date())) return;\n\n const current = await listBotGuildIds(app.botToken, this.#apiBaseUrl());\n if (!current) return; // membership unknown (too many guilds to page) — stay pending\n\n // Read the claimant inventory AFTER the slow membership fetch. A\n // connect() landing during the fetch must be counted — computed against\n // a pre-fetch list, its invite's guild would look uncontested and could\n // be attributed to this older pending install.\n const all = await store.list();\n const activation = planReconcile({\n installations: all,\n currentGuildIds: current,\n applicationId: app.applicationId,\n now: new Date(),\n }).find(a => a.agentId === agentId);\n if (!activation) return;\n\n // Activate the row AND bring the runtime up inside one chain step, so a\n // racing first interaction (whose webhook handler also builds the\n // adapter) serializes behind the whole bring-up instead of just the row\n // update — two unserialized bring-ups would start two gateway loops.\n await this.#enqueueWebhookTask(activation.webhookId, async () => {\n const activated = await this.#activateGuildNow(activation.webhookId, activation.guildId);\n if (!activated || this.#adapters.has(activated.id)) return; // runtime already up\n try {\n await this.#activateInstallation(activated);\n } catch (err) {\n // The guild IS confirmed (row saved active) but the runtime didn't\n // come up. Drop the half-built adapter so the first webhook hit —\n // or a provider restart — rebuilds it cleanly.\n this.#adapters.delete(activated.id);\n throw err;\n }\n });\n } catch (err) {\n console.warn(`[Discord] Pending-install reconcile for agent \"${agentId}\" failed (will retry):`, err);\n }\n }\n\n /** The full installation for an agent (no secrets live on it), or `null`. */\n async getInstallation(agentId: string): Promise<DiscordInstallation | null> {\n const store = await this.#getStore();\n return (await store.getByAgent(agentId)) ?? null;\n }\n\n /** Whether the Discord app is configured (credentials resolvable). */\n isConfigured(): boolean {\n return this.#configured;\n }\n\n /** The live `DiscordAdapter` for an installation id, if one is active. */\n getAdapter(installationId: string): DiscordAdapter | undefined {\n return this.#adapters.get(installationId);\n }\n\n // ===========================================================================\n // Webhook handling\n // ===========================================================================\n\n async #handleWebhook(c: {\n req: { param: (k: string) => string | undefined; header: (k: string) => string | undefined; raw: Request };\n json: (body: unknown, status?: number) => Response;\n }): Promise<Response> {\n const webhookId = c.req.param('webhookId');\n if (!webhookId) return c.json({ error: 'Missing webhookId' }, 400);\n\n const store = await this.#getStore();\n const installation = await store.getByWebhookId(webhookId);\n if (!installation) return c.json({ error: 'Unknown webhook' }, 404);\n\n const app = await store.getAppConfig();\n if (!app) return c.json({ error: 'Discord app is not configured' }, 503);\n\n const adapter = this.#getOrCreateAdapter(installation, app);\n const waitUntil = this.#config.waitUntil ?? resolveWaitUntil(c as never);\n\n // Clone the raw bytes FIRST so the adapter still sees an untouched request\n // (Ed25519 footgun). We only schedule guild activation off this clone\n // *after* the adapter has verified the signature — a 2xx response is the\n // adapter's proof of a valid Ed25519 signature; 401/4xx means forged and we\n // must not mutate storage, fire `onInstall`, or hit Discord's API.\n const activationBody = c.req.raw.clone();\n const scheduleActivation = (response: Response): Response => {\n if (response.ok) {\n const activation = this.#activateFromInteraction(webhookId, activationBody);\n if (waitUntil) waitUntil(activation);\n else void activation.catch(() => {});\n }\n return response;\n };\n\n const agent = this.#resolveAgent(installation.agentId);\n if (!agent || !this.#mastra) {\n // No agent wired (endpoint-verification PING, or the agent was removed).\n // Delegate straight to the adapter so Ed25519 + PING/PONG still work.\n try {\n return scheduleActivation(await adapter.handleWebhook(c.req.raw, waitUntil ? { waitUntil } : undefined));\n } catch (err) {\n console.error('[Discord] adapter.handleWebhook error:', err);\n return c.json({ error: 'Internal error' }, 500);\n }\n }\n\n let channels = agent.getChannels();\n if (!channels || channels.adapters[PLATFORM] !== adapter) {\n channels = this.#createAgentChannels(agent, adapter);\n await channels.initialize(this.#mastra);\n }\n\n try {\n return scheduleActivation(\n await channels.handleWebhookEvent(PLATFORM, c.req.raw, waitUntil ? { waitUntil } : undefined),\n );\n } catch (err) {\n console.error('[Discord] Error delegating to AgentChannels:', err);\n return c.json({ error: 'Internal error' }, 500);\n }\n }\n\n /** Extract the guild from an interaction body (a clone) and activate it. Best-effort. */\n async #activateFromInteraction(webhookId: string, request: Request): Promise<void> {\n try {\n const body = (await request.json()) as { guild_id?: string };\n // PING (type 1) and DMs have no guild_id — nothing to activate.\n if (body?.guild_id) await this.activateGuild(webhookId, body.guild_id);\n } catch {\n // Unreadable/non-JSON body, or no guild — skip.\n }\n }\n\n // ===========================================================================\n // Internals\n // ===========================================================================\n\n #apiBaseUrl(): string {\n return this.#config.apiBaseUrl ?? DISCORD_API_BASE_URL;\n }\n\n /**\n * Register an installation's slash commands (best-effort — a failure logs and\n * doesn't block connect). Skips the `PUT` when the command hash is unchanged\n * for the scope key, so re-activation of a known guild costs no API call\n * (200 creates/day/guild). `guildId` is required for guild scope; global scope\n * ignores it and registers once under the `'global'` key.\n */\n async #registerCommands(app: DiscordAppConfig, installation: DiscordInstallation, guildId?: string): Promise<void> {\n const commands = installation.commands ?? [];\n if (!commands.length) return;\n const scope = this.#config.commandScope ?? 'guild';\n const key = scope === 'global' ? 'global' : guildId;\n if (!key) return; // guild scope with no guild yet — defer to lazy activation\n\n const hash = hashCommands(commands);\n const versions = installation.commandVersions ?? {};\n if (versions[key] === hash) return; // unchanged — skip (rate-limit aware)\n\n try {\n if (scope === 'global') {\n await registerGlobalCommands(app.botToken, app.applicationId, commands, this.#apiBaseUrl());\n } else {\n await registerGuildCommands(app.botToken, app.applicationId, guildId!, commands, this.#apiBaseUrl());\n }\n installation.commandVersions = { ...versions, [key]: hash };\n const store = await this.#getStore();\n // The PUT above is a long await — the installation may have been\n // disconnected meanwhile. Persist the version onto the LIVE row only;\n // saving the closed-over object would resurrect a deleted installation.\n const fresh = await store.getByWebhookId(installation.webhookId);\n if (!fresh) return;\n fresh.commandVersions = { ...fresh.commandVersions, [key]: hash };\n await store.save(fresh);\n } catch (err) {\n console.warn(`[Discord] command registration failed (${scope}${guildId ? `, guild ${guildId}` : ''}):`, err);\n }\n }\n\n /** Build (once) the adapter for an installation from the shared app config. */\n #getOrCreateAdapter(installation: DiscordInstallation, app: DiscordAppConfig): DiscordAdapter {\n const existing = this.#adapters.get(installation.id);\n if (existing) return existing;\n const adapter = createDiscordAdapter({\n botToken: app.botToken,\n publicKey: app.publicKey,\n applicationId: app.applicationId,\n apiUrl: this.#apiBaseUrl(),\n userName: installation.displayName,\n ...(this.#config.mentionRoleIds !== undefined ? { mentionRoleIds: this.#config.mentionRoleIds } : {}),\n ...(this.#config.interactionFlags !== undefined ? { interactionFlags: this.#config.interactionFlags } : {}),\n ...(this.#config.logger !== undefined ? { logger: this.#config.logger } : {}),\n });\n this.#adapters.set(installation.id, adapter);\n return adapter;\n }\n\n /** Rebuild the adapter and inject AgentChannels for an active installation. */\n async #activateInstallation(installation: DiscordInstallation): Promise<void> {\n const store = await this.#getStore();\n const app = await store.getAppConfig();\n if (!app) return;\n const agent = this.#resolveAgent(installation.agentId);\n const adapter = this.#getOrCreateAdapter(installation, app);\n if (agent && this.#mastra) {\n const channels = this.#createAgentChannels(agent, adapter);\n await channels.initialize(this.#mastra);\n }\n }\n\n /**\n * Create AgentChannels for an agent with the Discord adapter, preserving any\n * adapters/config the agent author already configured (mirrors `@mastra/slack`\n * / `@mastra/telegram`). `gateway: true` (default) makes core start the Gateway\n * reconnection loop for DMs/mentions.\n */\n #createAgentChannels(agent: Agent, adapter: DiscordAdapter): AgentChannels {\n const existing = agent.getChannels();\n const existingConfig = existing?.channelConfig;\n const cfg = this.#config;\n const entry = {\n adapter,\n ...resolveDiscordAdapterConfig(cfg),\n ...(cfg.cors !== undefined ? { cors: cfg.cors } : {}),\n ...(cfg.formatError !== undefined ? { formatError: cfg.formatError } : {}),\n } as ChannelAdapterConfig;\n const channels = new AgentChannels({\n ...existingConfig,\n adapters: { ...existingConfig?.adapters, [PLATFORM]: entry },\n userName: agent.name,\n handlers: cfg.handlers ?? existingConfig?.handlers,\n inlineMedia: cfg.inlineMedia ?? existingConfig?.inlineMedia,\n inlineLinks: cfg.inlineLinks ?? existingConfig?.inlineLinks,\n state: cfg.state ?? existingConfig?.state,\n threadContext: cfg.threadContext ?? existingConfig?.threadContext,\n chatOptions: cfg.chatOptions ?? existingConfig?.chatOptions,\n tools: cfg.tools ?? existingConfig?.tools,\n resolveResourceId: cfg.resolveResourceId ?? existingConfig?.resolveResourceId,\n waitUntil: cfg.waitUntil ?? existingConfig?.waitUntil,\n resolveWaitUntil: cfg.resolveWaitUntil ?? existingConfig?.resolveWaitUntil,\n });\n agent.setChannels(channels);\n return channels;\n }\n\n async #autoInitialize(): Promise<void> {\n if (!this.#mastra) return;\n await this.initialize();\n }\n\n #resolveAgent(agentId: string): Agent | undefined {\n try {\n return this.#mastra?.getAgentById(agentId) as Agent | undefined;\n } catch {\n return undefined;\n }\n }\n\n /**\n * App credentials from provider config or `DISCORD_*` env.\n *\n * A config-supplied bot token defines the application identity, so\n * `publicKey` / `applicationId` never fall back to the environment in that\n * case — stale `DISCORD_PUBLIC_KEY` / `DISCORD_APPLICATION_ID` from a\n * *different* application would otherwise complete the config and keep that\n * application's Ed25519 key trusted for webhook verification (bypassing the\n * `GET /applications/@me` backfill). Env fallback for those fields applies\n * only when the bot token itself comes from the environment.\n */\n #suppliedPartialAppConfig(): Partial<DiscordAppConfig> {\n const a = this.#config.app ?? {};\n if (a.botToken != null) {\n return { botToken: a.botToken, publicKey: a.publicKey, applicationId: a.applicationId };\n }\n return {\n botToken: process.env.DISCORD_BOT_TOKEN,\n publicKey: a.publicKey ?? process.env.DISCORD_PUBLIC_KEY,\n applicationId: a.applicationId ?? process.env.DISCORD_APPLICATION_ID,\n };\n }\n\n /** App credentials from provider config or `DISCORD_*` env, or `null` if incomplete. */\n #suppliedAppConfig(): DiscordAppConfig | null {\n const { botToken, publicKey, applicationId } = this.#suppliedPartialAppConfig();\n if (botToken && publicKey && applicationId) return { botToken, publicKey, applicationId };\n return null;\n }\n\n /**\n * Whether credentials are resolvable without storage. A bot token alone is\n * enough: `applicationId` and `publicKey` are backfilled from\n * `GET /applications/@me` by {@link #resolveAppConfig}.\n */\n #hasSuppliedCredentials(): boolean {\n return this.#suppliedPartialAppConfig().botToken != null;\n }\n\n /**\n * Resolve the app config: prefer the stored one, else the supplied credentials\n * (config / `DISCORD_*` env). A supplied `botToken` without `publicKey` /\n * `applicationId` is completed from `GET /applications/@me` — the application\n * object carries both the id and the Ed25519 `verify_key`. `persisted` says\n * whether it's already in storage, so the caller can persist supplied\n * credentials **once**, after validation. `identity` is set when the token was\n * already validated against `/applications/@me` here, so callers can skip a\n * second validation call. Throws if no credentials are available.\n */\n async #resolveAppConfig(\n store: DiscordInstallStore,\n ): Promise<{ app: DiscordAppConfig; persisted: boolean; identity?: DiscordApplication }> {\n const stored = await store.getAppConfig();\n if (stored) return { app: stored, persisted: true };\n const supplied = this.#suppliedAppConfig();\n if (supplied) return { app: supplied, persisted: false };\n const partial = this.#suppliedPartialAppConfig();\n if (partial.botToken) {\n const identity = await validateApp(partial.botToken, this.#apiBaseUrl());\n const publicKey = partial.publicKey ?? identity.verify_key;\n if (!publicKey) {\n throw new Error('Discord /applications/@me returned no verify_key; provide publicKey explicitly.');\n }\n return {\n app: { botToken: partial.botToken, publicKey, applicationId: partial.applicationId ?? identity.id },\n persisted: false,\n identity,\n };\n }\n throw new Error(\n 'Discord app is not configured. Provide at least a botToken via the provider `app` option, ' +\n 'the DISCORD_BOT_TOKEN env var, or configure() — publicKey and applicationId are resolved from Discord ' +\n 'automatically when omitted. Discord applications are created in the Developer Portal, not programmatically.',\n );\n }\n\n /**\n * The single entry point to the store. Every caller either gets the cached\n * store or joins the one in-flight resolution — nobody resolves in parallel,\n * so a caller that arrives mid-migration waits for it to finish instead of\n * building a second store over storage that isn't populated yet.\n */\n async #getStore(): Promise<DiscordInstallStore> {\n // Loop rather than await once: an `__attach` landing mid-resolution\n // supersedes it, and we then resolve again against the new storage target.\n for (;;) {\n if (this.#store) return this.#store;\n // Created synchronously, so two callers can't both start a resolution.\n const inFlight = (this.#storeResolution ??= this.#resolveStore(this.#storeGeneration));\n let resolved: DiscordInstallStore | undefined;\n try {\n resolved = await inFlight;\n } finally {\n // Only retract our own attempt: a superseding attempt may have replaced\n // it already, and clearing that would strand a later caller's promise.\n if (this.#storeResolution === inFlight) this.#storeResolution = undefined;\n }\n if (resolved) return resolved;\n }\n }\n\n /**\n * One resolution attempt, run under {@link #getStore}'s in-flight guard — so\n * the {@link #pendingMigration} claim below cannot be double-taken, and the\n * migration completes before any caller sees the store.\n *\n * Returns `undefined` when `__attach` superseded this attempt while it ran:\n * the storage target changed underneath it, so caching would pin the provider\n * to a store built for the previous one.\n */\n async #resolveStore(generation: number): Promise<DiscordInstallStore | undefined> {\n const encryptionKey = this.#config.encryptionKey ?? process.env.MASTRA_ENCRYPTION_KEY;\n const { storage, isFallback } = await this.#resolveStorage();\n // Superseded before anything was claimed — nothing to hand back.\n if (generation !== this.#storeGeneration) return undefined;\n const pending = this.#pendingMigration;\n this.#pendingMigration = undefined;\n // Storage is still unavailable, so a new fallback would be an empty one —\n // keep the store we already have instead of dropping its contents.\n if (pending && isFallback) {\n this.#storeIsFallback = true;\n this.#store = pending;\n return pending;\n }\n const store = new DiscordInstallStore(storage, encryptionKey);\n if (pending) await this.#migrateFallback(pending, store);\n if (generation !== this.#storeGeneration) {\n // Superseded while migrating. Re-queue the fallback so the next attempt\n // carries it into the new target too — #migrateFallback keeps whatever is\n // already persisted, so replaying it is harmless.\n if (pending && !this.#pendingMigration) this.#pendingMigration = pending;\n return undefined;\n }\n this.#storeIsFallback = isFallback;\n this.#store = store;\n return store;\n }\n\n /**\n * Copy what a pre-registration store wrote into the real storage that has\n * since become available. Anything already persisted wins — the real store is\n * the durable record, and a stale in-memory row must not overwrite it. Both\n * stores share an encryption key, so this round-trips through plaintext.\n *\n * Best-effort: a failure here must not take down `__attach`'s caller or the\n * request that triggered the resolution. The provider still works against the\n * real storage; only the pre-registration writes are missing, which is the\n * behaviour before this migration existed.\n */\n async #migrateFallback(from: DiscordInstallStore, to: DiscordInstallStore): Promise<void> {\n try {\n const app = await from.getAppConfig();\n if (app && !(await to.getAppConfig())) await to.saveAppConfig(app);\n for (const install of await from.list()) {\n if (await to.getByAgent(install.agentId)) continue;\n await to.save(install);\n }\n } catch (error) {\n console.warn(\n '[Discord] Failed to carry pre-registration installations into Mastra storage; ' +\n 'connect() again to re-create them.',\n error,\n );\n }\n }\n\n /**\n * Resolve the backing storage, reporting whether it is the in-memory fallback\n * so {@link __attach} can re-resolve a store that was built before Mastra was\n * available, carrying its contents across.\n */\n async #resolveStorage(): Promise<{ storage: ChannelsStorage; isFallback: boolean }> {\n if (this.#config.storage) return { storage: this.#config.storage, isFallback: false };\n const mastraStore = this.#mastra?.getStorage();\n if (mastraStore) {\n try {\n await mastraStore.init();\n const channels = await mastraStore.getStore('channels');\n if (channels) return { storage: channels, isFallback: false };\n } catch {\n // Fall through to the in-memory store below.\n }\n }\n // No persistent storage available — fall back to in-memory. Installs won't\n // survive a restart; pass `storage` or configure Mastra storage in prod.\n return { storage: new InMemoryChannelsStorage(), isFallback: true };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#dec","#enc","#fromRecord","#toRecord","#config","#configured","#hasSuppliedCredentials","#mastra","#storeGeneration","#storeIsFallback","#pendingMigration","#store","#initPromise","#stopAllGatewayLoops","#adapters","#autoInitialize","#handleWebhook","#doInitialize","#getStore","#activateInstallation","#resolveAppConfig","#apiBaseUrl","#enqueueWebhookTask","#registerCommands","#activateGuildNow","#activationChains","#stopGatewayLoop","#reconcileInflight","#reconcilePendingInstallation","#getOrCreateAdapter","#activateFromInteraction","#resolveAgent","#createAgentChannels","#startGatewayLoop","createDiscordAdapter","#gatewayLoops","#suppliedPartialAppConfig","#suppliedAppConfig","#storeResolution","#resolveStore","#resolveStorage","#migrateFallback"],"sources":["../src/types.ts","../src/discord-client.ts","../src/commands.ts","../src/gateway-loop.ts","../src/crypto.ts","../src/install-store.ts","../src/reconcile.ts","../src/discord-provider.ts"],"sourcesContent":["import type {\n ChannelAdapterConfig,\n ChannelConfig,\n ChannelHandlers,\n StreamingConfig,\n WaitUntilFn,\n} from '@mastra/core/channels';\nimport type { ChannelsStorage } from '@mastra/core/storage';\nimport type { DiscordAdapterConfig } from '@chat-adapter/discord';\n\n/**\n * Default Discord REST base — **including** the `/api/v10` version segment. The\n * client concatenates this with the request path verbatim, so an override must\n * carry its own version segment (`https://example.test/api/v10`); passing a bare\n * origin produces unversioned request URLs.\n */\nexport const DISCORD_API_BASE_URL = 'https://discord.com/api/v10';\n\n/** Base for the OAuth2 authorize (bot-invite) URL. */\nexport const DISCORD_OAUTH_AUTHORIZE_URL = 'https://discord.com/oauth2/authorize';\n\n/**\n * Named Discord permission bits (a subset). Discord permissions are a 53+ bit\n * field, so they are represented as `bigint` and serialized to a decimal string\n * in the invite URL.\n *\n * @see https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags\n */\nexport const DISCORD_PERMISSIONS = {\n ADD_REACTIONS: 1n << 6n,\n VIEW_CHANNEL: 1n << 10n,\n SEND_MESSAGES: 1n << 11n,\n EMBED_LINKS: 1n << 14n,\n ATTACH_FILES: 1n << 15n,\n READ_MESSAGE_HISTORY: 1n << 16n,\n USE_APPLICATION_COMMANDS: 1n << 31n,\n SEND_MESSAGES_IN_THREADS: 1n << 38n,\n} as const;\n\n/**\n * Default permissions requested in the bot-invite URL: enough for a chat agent\n * to read and reply (in channels and threads), post embeds/files, add reactions,\n * and see recent history. Kept intentionally minimal — no moderation or manage\n * permissions. Callers can override via {@link DiscordConnectOptions.permissions}\n * or {@link DiscordProviderConfig.permissions}.\n */\nexport const DEFAULT_INVITE_PERMISSIONS: bigint =\n DISCORD_PERMISSIONS.VIEW_CHANNEL |\n DISCORD_PERMISSIONS.SEND_MESSAGES |\n DISCORD_PERMISSIONS.SEND_MESSAGES_IN_THREADS |\n DISCORD_PERMISSIONS.EMBED_LINKS |\n DISCORD_PERMISSIONS.ATTACH_FILES |\n DISCORD_PERMISSIONS.READ_MESSAGE_HISTORY |\n DISCORD_PERMISSIONS.ADD_REACTIONS |\n DISCORD_PERMISSIONS.USE_APPLICATION_COMMANDS;\n\n/** OAuth2 scopes requested by the bot-invite URL. */\nexport const DEFAULT_INVITE_SCOPES = ['bot', 'applications.commands'] as const;\n\n/**\n * A Discord application command as it goes over the wire (bulk-overwrite\n * registration). Only the `CHAT_INPUT` (slash) shape is modeled here.\n * @see https://discord.com/developers/docs/interactions/application-commands\n */\nexport interface DiscordCommand {\n /** 1-32 chars, lowercase `[a-z0-9_-]` (Discord also allows a leading `-`/`_`). */\n name: string;\n /** 1-100 chars. */\n description: string;\n}\n\n/** Command input accepted by the provider — a bare name or `{ name, description }`. */\nexport type DiscordCommandInput = string | { name: string; description?: string };\n\n/**\n * App-level credentials for a single Discord application. Discord has **no\n * programmatic app creation** — these come from the Developer Portal and are\n * stored **once** (one app, many guilds). `botToken` is the only secret;\n * `publicKey` and `applicationId` are public.\n */\nexport interface DiscordAppConfig {\n /** Bot token (`Authorization: Bot <token>`) — the control-plane credential. Secret. */\n botToken: string;\n /** Ed25519 public key used by the adapter to verify interaction signatures. */\n publicKey: string;\n /** The application (client) id — used as `client_id` in the invite URL. */\n applicationId: string;\n}\n\n/**\n * Configuration for {@link DiscordProvider}.\n *\n * Discord is **one app, many guilds**: a single application's credentials are\n * reused across every guild the bot is invited to. There is no per-install\n * token — \"installing\" is a bot invite (an OAuth2 authorize URL), and tenancy\n * is keyed by `guildId` per interaction.\n */\nexport interface DiscordProviderConfig {\n /**\n * App credentials (bot token / public key / application id). May be omitted\n * here and provided via the `DISCORD_BOT_TOKEN` / `DISCORD_PUBLIC_KEY` /\n * `DISCORD_APPLICATION_ID` env vars, or later via {@link DiscordProvider.configure}.\n * A bot token alone is enough: `publicKey` and `applicationId` are resolved\n * from `GET /applications/@me` when omitted. Whichever source resolves first\n * is persisted **once** to channels storage.\n */\n app?: Partial<DiscordAppConfig>;\n /**\n * Public HTTPS base URL for the interactions endpoint (the URL set once in the\n * Developer Portal). May be omitted and auto-detected from the Mastra server.\n */\n baseUrl?: string;\n /**\n * Persistence for the app config + per-agent installs. Defaults to Mastra's\n * channels storage when attached to a Mastra instance with storage, and falls\n * back to an in-memory store otherwise (dev/test — not persisted across restarts).\n */\n storage?: ChannelsStorage;\n /**\n * Override the Discord REST origin (e.g. a test mock). Defaults to\n * {@link DISCORD_API_BASE_URL}.\n */\n apiBaseUrl?: string;\n /**\n * Passphrase for encrypting the stored `botToken` at rest (AES-256-GCM).\n * Defaults to the `MASTRA_ENCRYPTION_KEY` env var. When unset, the token is\n * stored in plaintext (fine for the in-memory dev store; set a key for any\n * persistent backend).\n */\n encryptionKey?: string;\n /**\n * Permissions bitfield requested in the invite URL. Overrides\n * {@link DEFAULT_INVITE_PERMISSIONS}. Accepts a `bigint` or a decimal string.\n */\n permissions?: bigint | string;\n /**\n * Default slash commands registered for every connected agent (a per-agent\n * list can override via {@link DiscordConnectOptions.commands}). Defaults to\n * the conventional `/help` seed.\n */\n commands?: DiscordCommandInput[];\n /**\n * Where slash commands are registered:\n * - `'guild'` (default) — per-guild (`PUT …/guilds/{id}/commands`), updates\n * instantly; registered on first-seen guild, keyed by a per-guild hash.\n * - `'global'` — app-wide (`PUT …/commands`), eventually consistent; registered\n * once regardless of guild.\n *\n * @default 'guild'\n */\n commandScope?: 'guild' | 'global';\n /**\n * Keep the serverless invocation alive while the agent stream runs after the\n * interaction is acked (Vercel/AWS Lambda). See `ChannelConfig.waitUntil`.\n */\n waitUntil?: WaitUntilFn;\n /**\n * Start the Gateway WebSocket (provider-owned reconnect loop with backoff)\n * so the bot receives DMs, @mentions, and reactions in addition to slash\n * commands. Set `false` for interactions-only serverless deployments.\n *\n * @default true\n */\n gateway?: boolean;\n /**\n * Role IDs (in addition to direct user mentions) that trigger mention\n * handlers. Forwarded to the adapter (`DISCORD_MENTION_ROLE_IDS` env fallback).\n */\n mentionRoleIds?: string[];\n /**\n * Return interaction response flags (e.g. ephemeral, flag 64) for the initial\n * deferred slash-command response. Flags are locked at defer time, so this\n * fires on the deferred ACK, not the followup. Forwarded to the adapter.\n */\n interactionFlags?: DiscordAdapterConfig['interactionFlags'];\n /** Logger forwarded to the underlying `DiscordAdapter` for internal error reporting. */\n logger?: DiscordAdapterConfig['logger'];\n /**\n * Stream agent text to Discord as it generates, via the adapter's post-and-edit\n * (`editMessage`) loop. Discord has no native token streaming, so this\n * chunk-edits the interaction followup (throttle via `updateIntervalMs`).\n *\n * @default true\n */\n streaming?: StreamingConfig;\n /**\n * Keep a typing indicator alive during generation. Set `false` to disable.\n *\n * @default true\n */\n typingStatus?: boolean;\n\n // ---------------------------------------------------------------------------\n // AgentChannels passthrough — a curated subset of `ChannelConfig` /\n // `ChannelAdapterConfig` forwarded to every agent connected via this provider,\n // mirroring `@mastra/slack` / `@mastra/telegram`. All optional.\n // ---------------------------------------------------------------------------\n\n /** Override built-in event handlers. Forwarded to `AgentChannels`. */\n handlers?: ChannelHandlers;\n /** Which media types to send inline to the model. See `ChannelConfig.inlineMedia`. */\n inlineMedia?: ChannelConfig['inlineMedia'];\n /** Promote URLs in message text to file parts. See `ChannelConfig.inlineLinks`. */\n inlineLinks?: ChannelConfig['inlineLinks'];\n /** State adapter for deduplication, locking, and subscriptions. See `ChannelConfig.state`. */\n state?: ChannelConfig['state'];\n /** Fetch recent thread messages when the agent joins mid-conversation. See `ChannelConfig.threadContext`. */\n threadContext?: ChannelConfig['threadContext'];\n /** Additional options passed directly to the Chat SDK. See `ChannelConfig.chatOptions`. */\n chatOptions?: ChannelConfig['chatOptions'];\n /** Resolve the memory `resourceId` before a channel thread is created. See `ChannelConfig.resolveResourceId`. */\n resolveResourceId?: ChannelConfig['resolveResourceId'];\n /** Resolve `waitUntil` from the request's Hono `Context`. See `ChannelConfig.resolveWaitUntil`. */\n resolveWaitUntil?: ChannelConfig['resolveWaitUntil'];\n /** CORS configuration for the generated interactions route. */\n cors?: ChannelAdapterConfig['cors'];\n /** Override how errors are rendered in Discord messages. See `ChannelAdapterConfig.formatError`. */\n formatError?: ChannelAdapterConfig['formatError'];\n /**\n * How tool calls are rendered in the reply. Discord has native embeds +\n * action-row buttons, so this defaults to `'cards'` (unlike Telegram's\n * `'text'`). See `ChannelAdapterConfig.toolDisplay`.\n *\n * @default 'cards'\n */\n toolDisplay?: ChannelAdapterConfig['toolDisplay'];\n /**\n * Whether to expose channel reaction tools (`add_reaction`/`remove_reaction`)\n * to the agent. Set `false` for models without function calling. See `ChannelConfig.tools`.\n *\n * @default true\n */\n tools?: ChannelConfig['tools'];\n /** Called after an agent successfully connects and the installation is persisted. */\n onInstall?: (installation: DiscordInstallation) => void | Promise<void>;\n}\n\n/** Options accepted by {@link DiscordProvider.connect}. */\nexport interface DiscordConnectOptions {\n /**\n * The guild to install into. When the bot is **already** a member of this\n * guild, connect binds immediately (`{ type: 'immediate' }`) and registers its\n * commands; otherwise the operator is sent the invite URL and the guild is\n * confirmed lazily off the first inbound interaction's authoritative `guild_id`.\n */\n guildId?: string;\n /** Display name for this installation. Defaults to the application's name. */\n name?: string;\n /**\n * Slash commands to register for this agent. Overrides\n * {@link DiscordProviderConfig.commands}. Defaults to the `/help` seed.\n */\n commands?: DiscordCommandInput[];\n /** Permissions bitfield override for this invite (bigint or decimal string). */\n permissions?: bigint | string;\n}\n\n/**\n * A per-agent Discord installation. Unlike Telegram (one token = one row),\n * Discord installs are keyed by agent and track the set of `guildIds` the bot is\n * live in. **No secrets live here** — the bot token lives once in the app config\n * ({@link DiscordAppConfig}); this row only holds routing + per-guild command\n * versions. Persisted through {@link DiscordInstallStore}.\n */\nexport interface DiscordInstallation {\n /** Stable installation id. */\n id: string;\n /** The agent this installation is bound to. */\n agentId: string;\n /**\n * Opaque id embedded in the interactions route path\n * (`/discord/events/:webhookId`). Used to resolve the install on inbound POSTs.\n */\n webhookId: string;\n /**\n * `pending` — invite issued, no guild confirmed yet.\n * `active` — the bot is live in at least one guild (or bound to a known guild).\n */\n status: 'active' | 'pending';\n /** Guilds this agent's bot is confirmed live in (tenancy keys). */\n guildIds: string[];\n /** Display name (application name or an operator-supplied override). */\n displayName?: string;\n /** Normalized slash commands to register for this agent (resolved at connect). */\n commands?: DiscordCommand[];\n /**\n * Registered-command version hash, keyed by `guildId` (guild scope) or the\n * literal `'global'` (global scope). Used to skip re-`PUT`ting commands when\n * unchanged (200/day/guild rate limit).\n */\n commandVersions?: Record<string, string>;\n /** When the installation was created. */\n installedAt: Date;\n /**\n * The bot's guild ids at the moment a pending invite was issued. Lets\n * {@link DiscordProvider.reconcileInstallation} confirm the install after the\n * operator completes the invite in another tab: exactly one guild appearing\n * since the snapshot is the authorized guild, so the install activates\n * without waiting for a first interaction. `undefined` when the snapshot\n * couldn't be taken (or the bot is in too many guilds to page) — the install\n * then activates only via the first interaction, as before. Cleared on\n * activation.\n */\n guildSnapshot?: string[];\n /**\n * When the invite flow that owns {@link guildSnapshot} was last (re)started.\n * The snapshot is only trusted for a bounded claim window after this moment\n * (see `SNAPSHOT_TTL_MS` in `reconcile.ts`) — an abandoned Connect click must\n * not claim a guild the bot joins days later. Set on every invite-flow\n * `connect()`, including when the fetch that should have produced\n * {@link guildSnapshot} failed. Cleared on activation.\n */\n snapshotAt?: Date;\n /**\n * The Discord application {@link guildSnapshot} was taken against. A baseline\n * from app A says nothing about app B's membership — after `configure()`\n * swaps the bot token, every guild B was already in would look newly\n * authorized. Reconciliation and snapshot reuse require it to match the\n * current app. Cleared on activation.\n */\n snapshotApplicationId?: string;\n /**\n * The guild `connect()` explicitly targeted (`options.guildId`) when the\n * pending invite was issued, if any. Reconciliation only auto-activates this\n * install on that guild — a different guild appearing in the bot's\n * membership contradicts the caller's intent and waits for an authoritative\n * first interaction instead. Cleared on activation.\n */\n targetGuildId?: string;\n}\n","import {\n DEFAULT_INVITE_PERMISSIONS,\n DEFAULT_INVITE_SCOPES,\n DISCORD_API_BASE_URL,\n DISCORD_OAUTH_AUTHORIZE_URL,\n} from './types';\nimport type { DiscordCommand } from './types';\n\n/** `CHAT_INPUT` (slash) application command type. */\nconst APPLICATION_COMMAND_TYPE_CHAT_INPUT = 1;\n\n/** Per-request ceiling for control-plane calls, so a hung API can't stall the provider. */\nconst REQUEST_TIMEOUT_MS = 10_000;\n\n/**\n * Discord snowflake pattern (17–20 digits). Every id Discord issues — application,\n * guild, channel, user — is a snowflake. Validate before interpolating any id\n * into a REST path so unverified callers can't smuggle path traversal (`..`) or\n * other non-snowflake values that resolve to a different endpoint (e.g., a\n * `guildId` of `..` on `/applications/{id}/guilds/{guildId}/commands` collapses\n * to the global-commands endpoint and would overwrite an app's global commands).\n *\n * @see https://discord.com/developers/docs/reference#snowflakes\n */\nconst DISCORD_SNOWFLAKE = /^\\d{17,20}$/;\n\nfunction assertSnowflake(value: string, label: string): void {\n if (!DISCORD_SNOWFLAKE.test(value)) {\n throw new Error(`Invalid Discord ${label}: ${JSON.stringify(value)}`);\n }\n}\n\n/**\n * The subset of Discord's application object the control plane reads back from\n * `GET /applications/@me`.\n * @see https://discord.com/developers/docs/resources/application#application-object\n */\nexport interface DiscordApplication {\n /** The application (client) id. */\n id: string;\n /** The application's name. */\n name: string;\n /**\n * The Ed25519 public key used to verify interaction signatures. Discord\n * includes it on every application object, so a bot token alone is enough to\n * resolve the interaction-verification key.\n */\n verify_key: string;\n}\n\n/** Minimal Discord REST error envelope. */\ninterface DiscordErrorBody {\n message?: string;\n code?: number;\n}\n\n/**\n * Call the Discord REST API with Bot-token auth. `method` is required; supplying\n * `body` adds the JSON `content-type` header and serializes the payload. Returns\n * the raw {@link Response} so callers can classify the status themselves rather\n * than having every non-2xx collapse into a thrown error.\n *\n * This is the **control plane only** — validating the app and reading guild\n * membership. Sending replies uses the interaction token inside the adapter,\n * never this bot-token path.\n */\nexport async function discordRequest(\n botToken: string,\n method: string,\n path: string,\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n body?: unknown,\n): Promise<Response> {\n const init: RequestInit = {\n method,\n headers: {\n authorization: `Bot ${botToken}`,\n ...(body !== undefined ? { 'content-type': 'application/json' } : {}),\n },\n ...(body !== undefined ? { body: JSON.stringify(body) } : {}),\n };\n try {\n // Bound every control-plane call: without a timeout a hung Discord API\n // blocks connect()/disconnect()/#registerCommands() indefinitely, since no\n // call site in discord-provider.ts imposes one of its own.\n return await fetch(`${apiBaseUrl}${path}`, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });\n } catch (cause) {\n throw new Error(`Discord ${method} ${path} request failed`, { cause });\n }\n}\n\n/** Read a Discord error description from a non-2xx response, best-effort. */\nasync function describeError(response: Response): Promise<string> {\n const body = (await response.json().catch(() => null)) as DiscordErrorBody | null;\n return body?.message ?? `HTTP ${response.status}`;\n}\n\n/**\n * Validate the app's bot token and resolve the application identity via\n * `GET /applications/@me` (Bot auth). Throws if the token is rejected.\n *\n * @see https://discord.com/developers/docs/resources/application#get-current-application\n */\nexport async function validateApp(\n botToken: string,\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<DiscordApplication> {\n const response = await discordRequest(botToken, 'GET', '/applications/@me', apiBaseUrl);\n if (!response.ok) {\n throw new Error(`Discord rejected the bot token: ${await describeError(response)}`);\n }\n const app = (await response.json()) as DiscordApplication;\n if (!app?.id) {\n throw new Error('Discord /applications/@me returned no application id');\n }\n return app;\n}\n\n/**\n * Whether the bot is already a member of a guild. `GET /guilds/{id}` (Bot auth)\n * returns `200` only when the bot is in the guild. Absence is `404` (`10004\n * Unknown Guild`) — Discord standardized on 404 rather than 403 so the response\n * can't confirm a guild exists to a caller that lacks access — with legacy\n * deployments still answering `403` (`50001 Missing Access`).\n *\n * Any **other** failure (`401`, `429`, `5xx`) is a transient or auth problem,\n * not evidence of absence, and throws. Collapsing those into `false` would send\n * a caller whose bot *is* in the guild down the invite path on a rate limit.\n *\n * @see https://discord.com/developers/docs/resources/guild#get-guild\n */\nexport async function guildHealthCheck(\n botToken: string,\n guildId: string,\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<boolean> {\n assertSnowflake(guildId, 'guild id');\n const response = await discordRequest(botToken, 'GET', `/guilds/${guildId}`, apiBaseUrl);\n if (response.ok) {\n // Drain the body so the connection can be reused (undici keep-alive).\n await response.body?.cancel().catch(() => {});\n return true;\n }\n if (response.status === 404 || response.status === 403) {\n await response.body?.cancel().catch(() => {});\n return false;\n }\n throw new Error(`Discord guild lookup failed for \"${guildId}\": ${await describeError(response)}`);\n}\n\n/** Guilds-per-page for `GET /users/@me/guilds` (Discord's documented maximum). */\nconst GUILD_PAGE_LIMIT = 200;\n\n/**\n * Pages we're willing to walk when listing the bot's guilds. The listing feeds\n * the pending-install snapshot diff, whose multiple-new-guilds guard keeps an\n * *incomplete* listing safe (reconcile just stays pending) — so a hard cap is\n * preferable to an unbounded walk on a bot in thousands of guilds.\n */\nconst GUILD_PAGE_MAX_PAGES = 10;\n\n/**\n * The ids of every guild the bot is a member of, via `GET /users/@me/guilds`\n * (Bot auth), following `after`-cursor pagination. Returns `null` instead of a\n * partial listing when the bot is in more guilds than {@link GUILD_PAGE_MAX_PAGES}\n * covers — callers diffing snapshots must not mistake a truncated page for the\n * full membership. Throws on request failure.\n *\n * @see https://discord.com/developers/docs/resources/user#get-current-user-guilds\n */\nexport async function listBotGuildIds(\n botToken: string,\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<string[] | null> {\n const ids: string[] = [];\n let after: string | undefined;\n for (let page = 0; page < GUILD_PAGE_MAX_PAGES; page++) {\n const query = `?limit=${GUILD_PAGE_LIMIT}${after ? `&after=${after}` : ''}`;\n const response = await discordRequest(botToken, 'GET', `/users/@me/guilds${query}`, apiBaseUrl);\n if (!response.ok) {\n throw new Error(`Discord guild listing failed: ${await describeError(response)}`);\n }\n const batch = (await response.json()) as Array<{ id: string }>;\n for (const guild of batch) ids.push(guild.id);\n if (batch.length < GUILD_PAGE_LIMIT) return ids;\n after = batch[batch.length - 1]!.id;\n }\n return null; // more guilds than we page — signal \"unknown\", not a partial list\n}\n\n/** Map normalized commands to the Discord bulk-overwrite `CHAT_INPUT` payload. */\nfunction toCommandPayload(commands: readonly DiscordCommand[]) {\n return commands.map(c => ({\n name: c.name,\n description: c.description,\n type: APPLICATION_COMMAND_TYPE_CHAT_INPUT,\n }));\n}\n\n/**\n * Longest `Retry-After` we will wait out inline before giving up. Registration\n * is best-effort at the call site, so a long bucket is better surfaced as an\n * error than held open.\n */\nconst MAX_RETRY_AFTER_MS = 5_000;\n\n/**\n * `PUT` a bulk command overwrite, retrying **once** if Discord rate-limits it.\n *\n * Discord returns the wait in the `retry_after` body field (seconds, may be\n * fractional) and the `Retry-After` header. The value is dynamic, so it is read\n * from the response rather than assumed.\n *\n * @see https://discord.com/developers/docs/topics/rate-limits\n */\nasync function bulkOverwriteCommands(\n botToken: string,\n path: string,\n commands: readonly DiscordCommand[],\n apiBaseUrl: string,\n scopeLabel: string,\n): Promise<void> {\n const payload = toCommandPayload(commands);\n for (let attempt = 0; attempt < 2; attempt++) {\n const response = await discordRequest(botToken, 'PUT', path, apiBaseUrl, payload);\n if (response.ok) {\n await response.body?.cancel().catch(() => {});\n return;\n }\n if (response.status === 429 && attempt === 0) {\n const body = (await response.json().catch(() => null)) as { retry_after?: number; message?: string } | null;\n const headerSeconds = Number(response.headers.get('retry-after'));\n const seconds = body?.retry_after ?? (Number.isFinite(headerSeconds) ? headerSeconds : 0);\n const waitMs = Math.ceil(seconds * 1000);\n if (waitMs > 0 && waitMs <= MAX_RETRY_AFTER_MS) {\n await new Promise(resolve => setTimeout(resolve, waitMs));\n continue;\n }\n throw new Error(\n `Discord ${scopeLabel} command registration was rate-limited; retry after ${seconds}s: ${body?.message ?? `HTTP ${response.status}`}`,\n );\n }\n throw new Error(`Discord ${scopeLabel} command registration failed: ${await describeError(response)}`);\n }\n}\n\n/**\n * Bulk-overwrite a **guild's** slash commands (`PUT …/guilds/{guildId}/commands`).\n * Guild-scoped commands update **instantly**. Register on first-seen guild;\n * callers skip the call when the command hash is unchanged (200 creates/day/guild).\n *\n * @see https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-guild-application-commands\n */\nexport async function registerGuildCommands(\n botToken: string,\n applicationId: string,\n guildId: string,\n commands: readonly DiscordCommand[],\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<void> {\n assertSnowflake(applicationId, 'application id');\n assertSnowflake(guildId, 'guild id');\n const path = `/applications/${applicationId}/guilds/${guildId}/commands`;\n await bulkOverwriteCommands(botToken, path, commands, apiBaseUrl, 'guild');\n}\n\n/**\n * Bulk-overwrite the app's **global** slash commands (`PUT …/commands`).\n * Eventually consistent (propagation is not instant — don't quote a fixed\n * number). Opt-in via `commandScope: 'global'`.\n *\n * @see https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands\n */\nexport async function registerGlobalCommands(\n botToken: string,\n applicationId: string,\n commands: readonly DiscordCommand[],\n apiBaseUrl: string = DISCORD_API_BASE_URL,\n): Promise<void> {\n assertSnowflake(applicationId, 'application id');\n const path = `/applications/${applicationId}/commands`;\n await bulkOverwriteCommands(botToken, path, commands, apiBaseUrl, 'global');\n}\n\n/** Inputs for {@link buildInviteUrl}. */\nexport interface BuildInviteUrlOptions {\n /** The application (client) id — becomes `client_id`. */\n applicationId: string;\n /** Permissions bitfield (bigint or decimal string). Defaults to {@link DEFAULT_INVITE_PERMISSIONS}. */\n permissions?: bigint | string;\n /** OAuth2 scopes. Defaults to {@link DEFAULT_INVITE_SCOPES} (`bot applications.commands`). */\n scopes?: readonly string[];\n /**\n * Preselect this guild in the authorize screen and lock the picker, so the\n * operator can't authorize a different guild than the one `connect()` recorded.\n */\n guildId?: string;\n}\n\n/**\n * Build the OAuth2 bot-invite URL. This *is* the Discord \"install\" flow: the\n * operator opens it, picks a guild, and authorizes — there is no token exchange.\n * `scope=bot applications.commands` + a `permissions` bitfield.\n *\n * @see https://discord.com/developers/docs/topics/oauth2#bot-authorization-flow\n */\nexport function buildInviteUrl(options: BuildInviteUrlOptions): string {\n const permissions = (options.permissions ?? DEFAULT_INVITE_PERMISSIONS).toString();\n const scopes = options.scopes ?? DEFAULT_INVITE_SCOPES;\n const url = new URL(DISCORD_OAUTH_AUTHORIZE_URL);\n url.searchParams.set('client_id', options.applicationId);\n url.searchParams.set('scope', scopes.join(' '));\n url.searchParams.set('permissions', permissions);\n if (options.guildId) {\n // Preselect and lock the picker so the authorized guild matches the one the\n // install was recorded against.\n url.searchParams.set('guild_id', options.guildId);\n url.searchParams.set('disable_guild_select', 'true');\n }\n return url.toString();\n}\n","import { createHash } from 'node:crypto';\nimport type { DiscordCommand, DiscordCommandInput } from './types';\n\n/**\n * Conventional command seed registered when a connect provides none. Discord\n * surfaces `/help` in the command picker; the built-in DM/mention handling\n * covers everything else, so the seed is intentionally tiny.\n * @see https://discord.com/developers/docs/interactions/application-commands\n */\nexport const DEFAULT_COMMANDS: readonly DiscordCommandInput[] = [\n { name: 'help', description: 'Show what this bot can do' },\n];\n\n/**\n * Map user-supplied commands to Discord `CHAT_INPUT` command shapes, enforcing\n * the API constraints: `name` is lowercased, stripped of a leading slash,\n * reduced to `[a-z0-9_-]`, and clamped to 1-32 chars; `description` defaults to\n * `Run /<name>` and is clamped to 1-100 chars. Empty or duplicate names are\n * dropped.\n *\n * @see https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-naming\n */\nexport function normalizeCommands(raw: readonly DiscordCommandInput[] | undefined): DiscordCommand[] {\n if (!raw) return [];\n const seen = new Set<string>();\n const commands: DiscordCommand[] = [];\n for (const item of raw) {\n const input = typeof item === 'string' ? { name: item } : item;\n const name = input.name\n .replace(/^\\//, '')\n .toLowerCase()\n .replace(/[^a-z0-9_-]/g, '')\n .slice(0, 32);\n if (!name || seen.has(name)) continue;\n seen.add(name);\n const description = (input.description?.trim() || `Run /${name}`).slice(0, 100);\n commands.push({ name, description });\n }\n return commands;\n}\n\n/**\n * Stable content hash of a normalized command list, used to skip re-`PUT`ting a\n * guild/global command set that hasn't changed (Discord allows only 200\n * application-command creates per day, per guild). Order-independent: commands\n * are sorted by name before hashing.\n */\nexport function hashCommands(commands: readonly DiscordCommand[]): string {\n const canonical = [...commands]\n .sort((a, b) => a.name.localeCompare(b.name))\n .map(c => ({ name: c.name, description: c.description }));\n return createHash('sha256').update(JSON.stringify(canonical)).digest('hex').slice(0, 16);\n}\n","/**\n * Provider-owned Discord Gateway reconnection loop.\n *\n * Core's `AgentChannels` has a built-in gateway loop, but the provider opts out\n * of it (`gateway: false` on the adapter entry) and runs this one instead,\n * because the core loop has two failure modes that combine into a connect\n * storm — severe enough that Discord's abuse tripwire (>1000 connects in a\n * short window) force-resets the bot token:\n *\n * - `@chat-adapter/discord` **swallows `client.login()` errors**: a session\n * that fails IDENTIFY (invalid/reset token, the MessageContent privileged\n * intent not enabled on the app) resolves \"cleanly\" within milliseconds.\n * - The core loop reconnects **immediately** on a clean session end and keeps\n * **no abort handle**, so a failing session becomes a zero-delay IDENTIFY\n * loop that nothing can stop, and every credential rotation leaks another\n * immortal loop still hammering with the stale token.\n *\n * This loop defends against both: a session that ends before\n * {@link HEALTHY_SESSION_MS} is treated as a failed connect and backed off\n * exponentially; a REST token check distinguishes a revoked token (park the\n * loop until `configure()` delivers new credentials and restarts it) from a\n * transient failure (keep backing off); and the `AbortSignal` lets the\n * provider kill the loop on disconnect, credential rotation, or re-attach.\n * This mirrors how `@mastra/telegram` owns its polling loop (`stopPolling()`\n * on reconfigure/disconnect, capped backoff on errors).\n */\n\n/** How long each Gateway session runs before a scheduled reconnect. */\nexport const GATEWAY_SESSION_MS = 24 * 60 * 60 * 1000;\n/**\n * A session that ends sooner than this is treated as a failed connect. The\n * adapter resolves login failures as if the session ended cleanly, so elapsed\n * time is the only signal that distinguishes \"ran fine until the duration\n * elapsed / Discord closed us\" from \"IDENTIFY was rejected instantly\".\n */\nexport const HEALTHY_SESSION_MS = 60 * 1000;\n/** First retry delay after a failed connect; doubles up to {@link MAX_BACKOFF_MS}. */\nexport const INITIAL_BACKOFF_MS = 5 * 1000;\n/** Retry delay ceiling. */\nexport const MAX_BACKOFF_MS = 15 * 60 * 1000;\n/** Pause between healthy sessions — a scheduled rollover, not a failure. */\nexport const RECONNECT_DELAY_MS = 1000;\n/** After this many consecutive failed connects, log a diagnostic hint. */\nexport const SHORT_SESSION_HINT_THRESHOLD = 3;\n\n/** Outcome of the between-sessions REST token check. */\nexport type GatewayTokenCheck = 'valid' | 'invalid' | 'unreachable';\n\nexport interface GatewayLoopDeps {\n /**\n * Start one Gateway session (`DiscordAdapter.startGatewayListener`). The\n * session's lifetime promise is delivered through `options.waitUntil`; the\n * returned `Response` only reports whether the session could start.\n */\n startSession(\n options: { waitUntil: (promise: Promise<unknown>) => void },\n durationMs: number,\n signal: AbortSignal,\n ): Promise<Response>;\n /**\n * REST check of the bot token (`GET /applications/@me`), consulted after a\n * failed connect. `'invalid'` (401) parks the loop; `'unreachable'` keeps\n * backing off — a Discord outage must not be mistaken for a revoked token.\n */\n checkToken(): Promise<GatewayTokenCheck>;\n log(level: 'info' | 'warn' | 'error', message: string): void;\n /** Session duration override (tests). Defaults to {@link GATEWAY_SESSION_MS}. */\n sessionMs?: number;\n}\n\n/**\n * Run Gateway sessions back-to-back until `signal` aborts or the bot token is\n * confirmed revoked. Resolves when the loop stops; never rejects.\n */\nexport async function runGatewayLoop(deps: GatewayLoopDeps, signal: AbortSignal): Promise<void> {\n const sessionMs = deps.sessionMs ?? GATEWAY_SESSION_MS;\n let backoffMs = INITIAL_BACKOFF_MS;\n let consecutiveFailures = 0;\n\n while (!signal.aborted) {\n const startedAt = Date.now();\n try {\n let settle!: () => void;\n let fail!: (err: unknown) => void;\n const done = new Promise<void>((resolve, reject) => {\n settle = resolve;\n fail = reject;\n });\n let sessionDelivered = false;\n const response = await deps.startSession(\n {\n waitUntil: promise => {\n sessionDelivered = true;\n void promise.then(\n () => settle(),\n err => fail(err),\n );\n },\n },\n sessionMs,\n signal,\n );\n // The adapter reports pre-session failures (chat not initialized) as a\n // non-2xx Response without ever invoking waitUntil — awaiting `done`\n // there would hang forever.\n if (!response.ok || !sessionDelivered) {\n throw new Error(`Gateway session refused (status ${response.status})`);\n }\n await done;\n } catch (err) {\n deps.log('error', `Gateway session error: ${err instanceof Error ? err.message : String(err)}`);\n }\n if (signal.aborted) return;\n\n const elapsedMs = Date.now() - startedAt;\n if (elapsedMs >= HEALTHY_SESSION_MS) {\n backoffMs = INITIAL_BACKOFF_MS;\n consecutiveFailures = 0;\n deps.log('info', 'Gateway session ended; reconnecting');\n await sleep(RECONNECT_DELAY_MS, signal);\n continue;\n }\n\n // Short session: the adapter swallows login errors, so treat this as a\n // failed connect regardless of how \"clean\" the end looked.\n consecutiveFailures += 1;\n let check: GatewayTokenCheck;\n try {\n check = await deps.checkToken();\n } catch {\n check = 'unreachable';\n }\n if (signal.aborted) return;\n if (check === 'invalid') {\n deps.log(\n 'error',\n 'Discord rejected the bot token (401); Gateway reconnection is parked until configure() supplies new credentials. ' +\n 'Reset tokens must be rotated in the Discord Developer Portal and updated wherever this provider gets its credentials.',\n );\n return;\n }\n if (consecutiveFailures === SHORT_SESSION_HINT_THRESHOLD) {\n deps.log(\n 'error',\n `Gateway sessions keep ending within ${HEALTHY_SESSION_MS / 1000}s of starting. The token checks out over REST, ` +\n `so the usual cause is a rejected IDENTIFY: enable the \"Message Content Intent\" (a privileged intent) for this ` +\n `app in the Discord Developer Portal, or set gateway: false for an interactions-only deployment.`,\n );\n }\n deps.log(\n 'warn',\n `Gateway session ended after ${Math.round(elapsedMs / 1000)}s; retrying in ${Math.round(backoffMs / 1000)}s`,\n );\n await sleep(backoffMs, signal);\n backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);\n }\n}\n\n/** Abortable sleep: resolves early (never rejects) when the signal fires. */\nfunction sleep(ms: number, signal: AbortSignal): Promise<void> {\n if (signal.aborted) return Promise.resolve();\n return new Promise(resolve => {\n const onAbort = () => {\n clearTimeout(timer);\n resolve();\n };\n const timer = setTimeout(() => {\n signal.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal.addEventListener('abort', onAbort, { once: true });\n });\n}\n","import { createCipheriv, createDecipheriv, hkdfSync } from 'node:crypto';\n\n/**\n * Opt-in AES-256-GCM encryption for the app's bot token at rest, with\n * HKDF-SHA256 key derivation (mirrors `@mastra/telegram` / `@mastra/slack`'s\n * `crypto.ts`). Each value gets a fresh random 16-byte salt + 12-byte IV; the\n * salt travels in the ciphertext, so the same passphrase never derives the same\n * key twice. The algorithm prefix lets plaintext and encrypted values coexist\n * during migration, so {@link decrypt} can no-op on plaintext.\n *\n * Format: `aes-256-gcm-hkdf:base64(salt):base64(iv):base64(authTag):base64(ciphertext)`\n */\nconst ALGO_PREFIX = 'aes-256-gcm-hkdf';\nconst HKDF_INFO = 'mastra-discord-encryption';\n\nfunction deriveKey(passphrase: string, salt: Buffer): Buffer {\n return Buffer.from(hkdfSync('sha256', passphrase, salt, HKDF_INFO, 32));\n}\n\n/** Whether a stored value was produced by {@link encrypt}. */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(`${ALGO_PREFIX}:`);\n}\n\n/** Encrypt a UTF-8 string with a per-value random salt + IV. */\nexport function encrypt(plaintext: string, passphrase: string): string {\n const salt = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(16)));\n const iv = Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(12)));\n const cipher = createCipheriv('aes-256-gcm', deriveKey(passphrase, salt), iv);\n const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n return `${ALGO_PREFIX}:${salt.toString('base64')}:${iv.toString('base64')}:${tag.toString('base64')}:${enc.toString('base64')}`;\n}\n\n/** Decrypt a value from {@link encrypt}. Plaintext (unprefixed) is returned unchanged. */\nexport function decrypt(value: string, passphrase: string): string {\n if (!isEncrypted(value)) return value;\n const [, saltB64, ivB64, tagB64, ctB64] = value.split(':');\n if (!saltB64 || !ivB64 || !tagB64 || ctB64 === undefined) {\n throw new Error('Invalid ciphertext payload');\n }\n try {\n // Setup is inside the guard: base64 decoding never throws, it just yields\n // the wrong number of bytes, and setAuthTag rejects a wrong-length tag\n // outright (\"Invalid authentication tag length\"). A corrupt salt or iv still\n // surfaces at final() instead, but both paths belong to the same failure.\n const decipher = createDecipheriv(\n 'aes-256-gcm',\n deriveKey(passphrase, Buffer.from(saltB64, 'base64')),\n Buffer.from(ivB64, 'base64'),\n );\n decipher.setAuthTag(Buffer.from(tagB64, 'base64'));\n return Buffer.concat([decipher.update(Buffer.from(ctB64, 'base64')), decipher.final()]).toString('utf8');\n } catch (cause) {\n // GCM authentication failed. Node's own messages (\"Unsupported state or\n // unable to authenticate data\", \"Invalid initialization vector\") give an\n // operator nothing to act on, and this runs on the credential-load path —\n // name both causes. A wrong key is the common one, but a corrupt stored\n // value fails identically, and an operator who assumes the key would rotate\n // a perfectly good one and lose the rest of the secrets.\n throw new Error(\n 'Failed to decrypt a stored Discord secret — either the configured encryption key does not match the one it was encrypted with, or the stored value is corrupt. Check `encryptionKey` on DiscordProvider or MASTRA_ENCRYPTION_KEY.',\n { cause },\n );\n }\n}\n","import type { ChannelInstallationInfo } from '@mastra/core/channels';\nimport type { ChannelInstallation, ChannelsStorage } from '@mastra/core/storage';\nimport type { DiscordAppConfig, DiscordCommand, DiscordInstallation } from './types';\nimport { decrypt, encrypt, isEncrypted } from './crypto';\n\n/** Platform identifier used for every stored record, config, and route. */\nexport const PLATFORM = 'discord';\n\n/** Per-agent fields serialized into a {@link ChannelInstallation.data} blob (no secrets). */\ninterface DiscordInstallationData {\n guildIds?: string[];\n displayName?: string;\n commands?: DiscordCommand[];\n commandVersions?: Record<string, string>;\n /** Pending-invite reconcile baseline — see {@link DiscordInstallation.guildSnapshot}. */\n guildSnapshot?: string[];\n /** ISO timestamp of the invite flow that owns the baseline — see {@link DiscordInstallation.snapshotAt}. */\n snapshotAt?: string;\n /** Application the baseline was taken against — see {@link DiscordInstallation.snapshotApplicationId}. */\n snapshotApplicationId?: string;\n /** Explicit invite target — see {@link DiscordInstallation.targetGuildId}. */\n targetGuildId?: string;\n}\n\n/** App-config fields serialized into the platform `ChannelConfig.data` blob. */\ninterface DiscordAppConfigData {\n /** Bot token — the only secret; AES-256-GCM encrypted when an `encryptionKey` is set. */\n botToken?: string;\n /** Ed25519 public key (public — stored plaintext). */\n publicKey?: string;\n /** Application (client) id (public — stored plaintext). */\n applicationId?: string;\n}\n\n/**\n * Two-tier persistence for Discord, layered over the platform-agnostic\n * `ChannelsStorage` (the same store `@mastra/slack` / `@mastra/telegram` use).\n *\n * - **App tier** (`saveConfig`/`getConfig`) — the one application's\n * `botToken` + `publicKey` + `applicationId`, stored **once** (one app, many\n * guilds). `botToken` is AES-256-GCM encrypted at rest when an `encryptionKey`\n * is supplied.\n * - **Install tier** (`getInstallationByAgent`/`…ByWebhookId`/`saveInstallation`)\n * — one row per agent, keyed by agent, tracking the `guildIds` the bot is live\n * in. Secrets are **not** duplicated per row.\n */\nexport class DiscordInstallStore {\n constructor(\n private readonly storage: ChannelsStorage,\n private readonly encryptionKey?: string,\n ) {}\n\n // --- App tier (stored once) -----------------------------------------------\n\n /** The stored app config, if any (bot token decrypted). */\n async getAppConfig(): Promise<DiscordAppConfig | null> {\n const config = await this.storage.getConfig(PLATFORM);\n if (!config) return null;\n const data = (config.data ?? {}) as DiscordAppConfigData;\n if (!data.botToken || !data.publicKey || !data.applicationId) return null;\n return {\n botToken: this.#dec(data.botToken)!,\n publicKey: data.publicKey,\n applicationId: data.applicationId,\n };\n }\n\n /** Persist the app config once (bot token encrypted at rest when keyed). */\n async saveAppConfig(app: DiscordAppConfig): Promise<void> {\n const data: DiscordAppConfigData = {\n botToken: this.#enc(app.botToken),\n publicKey: app.publicKey,\n applicationId: app.applicationId,\n };\n await this.storage.saveConfig({ platform: PLATFORM, data: data as Record<string, unknown>, updatedAt: new Date() });\n }\n\n /** Remove the stored app config. */\n async deleteAppConfig(): Promise<void> {\n await this.storage.deleteConfig(PLATFORM);\n }\n\n // --- Install tier (one row per agent) -------------------------------------\n\n /** The installation for an agent, if any. */\n async getByAgent(agentId: string): Promise<DiscordInstallation | null> {\n const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);\n return record ? this.#fromRecord(record) : null;\n }\n\n /** Look up an installation by the routing id in its interactions-route path. */\n async getByWebhookId(webhookId: string): Promise<DiscordInstallation | null> {\n const record = await this.storage.getInstallationByWebhookId(webhookId);\n return record && record.platform === PLATFORM ? this.#fromRecord(record) : null;\n }\n\n /**\n * Find the installation that owns a guild (tenancy lookup by `guildId`). Used\n * when the routing key is a guild rather than a webhook id.\n *\n * `guildIds` is per-agent and nothing enforces exclusivity, so two agents can\n * legitimately be installed into the same guild. Returning the first match\n * would make routing depend on storage row order — the same interaction could\n * reach a different agent on the next lookup. Instead the **oldest** install\n * wins (ties broken by id), which is stable across restarts and storage\n * backends, and the ambiguity is logged once so an operator can see it.\n *\n * `ChannelsStorage` has no query-by-data-field, so this necessarily lists the\n * platform's installations; it is not on the interactions hot path, which\n * routes by `webhookId`.\n */\n async getByGuildId(guildId: string): Promise<DiscordInstallation | null> {\n const records = await this.storage.listInstallations(PLATFORM);\n const matches = records.filter(r => ((r.data as DiscordInstallationData)?.guildIds ?? []).includes(guildId));\n if (matches.length === 0) return null;\n if (matches.length > 1) {\n console.warn(\n `[Discord] Guild \"${guildId}\" is claimed by ${matches.length} installations (${matches\n .map(r => r.agentId)\n .join(', ')}). Routing to the oldest; disconnect the others to remove the ambiguity.`,\n );\n }\n const winner = matches.reduce((oldest, r) => {\n const a = r.createdAt?.getTime() ?? 0;\n const b = oldest.createdAt?.getTime() ?? 0;\n if (a !== b) return a < b ? r : oldest;\n return r.id < oldest.id ? r : oldest;\n });\n return this.#fromRecord(winner);\n }\n\n /** Insert or replace an installation. */\n async save(installation: DiscordInstallation): Promise<void> {\n await this.storage.saveInstallation(this.#toRecord(installation));\n }\n\n /** All Discord installations (active and pending). */\n async list(): Promise<DiscordInstallation[]> {\n const records = await this.storage.listInstallations(PLATFORM);\n return records.map(r => this.#fromRecord(r));\n }\n\n /** Remove an agent's installation, if present. */\n async deleteByAgent(agentId: string): Promise<void> {\n const record = await this.storage.getInstallationByAgent(PLATFORM, agentId);\n if (record) await this.storage.deleteInstallation(record.id);\n }\n\n #enc(value: string | undefined): string | undefined {\n return value && this.encryptionKey ? encrypt(value, this.encryptionKey) : value;\n }\n\n #dec(value: string | undefined): string | undefined {\n if (!value) return value;\n if (!this.encryptionKey) {\n // A deployment that stored an encrypted token and later lost its key would\n // otherwise hand the ciphertext to Discord as a bot token — surfacing as\n // opaque 401s instead of the configuration error it actually is.\n if (isEncrypted(value)) {\n throw new Error(\n 'The stored Discord bot token is encrypted at rest, but no encryption key is configured. Set `encryptionKey` on DiscordProvider or MASTRA_ENCRYPTION_KEY.',\n );\n }\n return value;\n }\n return decrypt(value, this.encryptionKey);\n }\n\n #toRecord(install: DiscordInstallation): ChannelInstallation {\n const data: DiscordInstallationData = {\n guildIds: install.guildIds,\n displayName: install.displayName,\n commands: install.commands,\n commandVersions: install.commandVersions,\n guildSnapshot: install.guildSnapshot,\n snapshotAt: install.snapshotAt?.toISOString(),\n snapshotApplicationId: install.snapshotApplicationId,\n targetGuildId: install.targetGuildId,\n };\n return {\n id: install.id,\n platform: PLATFORM,\n agentId: install.agentId,\n status: install.status,\n webhookId: install.webhookId,\n data: data as Record<string, unknown>,\n createdAt: install.installedAt,\n updatedAt: new Date(),\n };\n }\n\n #fromRecord(record: ChannelInstallation): DiscordInstallation {\n const data = (record.data ?? {}) as DiscordInstallationData;\n return {\n id: record.id,\n agentId: record.agentId,\n webhookId: record.webhookId ?? '',\n status: record.status === 'active' ? 'active' : 'pending',\n guildIds: data.guildIds ?? [],\n displayName: data.displayName,\n commands: data.commands,\n commandVersions: data.commandVersions,\n installedAt: record.createdAt,\n guildSnapshot: data.guildSnapshot,\n snapshotAt: data.snapshotAt ? new Date(data.snapshotAt) : undefined,\n snapshotApplicationId: data.snapshotApplicationId,\n targetGuildId: data.targetGuildId,\n };\n }\n}\n\n/** Project an installation to its public, secret-free info for the editor UI. */\nexport function toInstallationInfo(install: DiscordInstallation): ChannelInstallationInfo {\n return {\n id: install.id,\n platform: PLATFORM,\n agentId: install.agentId,\n status: install.status,\n displayName: install.displayName,\n installedAt: install.installedAt,\n };\n}\n","import type { DiscordInstallation } from './types';\n\n/**\n * How long a pending install's {@link DiscordInstallation.guildSnapshot} stays\n * eligible for reconciliation after the invite flow (re)started. Past this\n * window the baseline is stale evidence: the operator has plainly abandoned\n * the Connect flow, and a guild the bot joins days later (for any reason) must\n * not be attributed to it. An expired install still activates normally via its\n * first interaction.\n */\nexport const SNAPSHOT_TTL_MS = 30 * 60 * 1000;\n\n/** One activation the reconcile pass decided on (see {@link planReconcile}). */\nexport interface ReconcileActivation {\n installationId: string;\n agentId: string;\n webhookId: string;\n /** The single new, uncontested guild attributed to this install's invite. */\n guildId: string;\n}\n\n/**\n * Whether an install's invite flow is still *live*: started against the\n * current application and within {@link SNAPSHOT_TTL_MS}. Deliberately\n * independent of whether the baseline fetch succeeded — a live flow with a\n * missing baseline still poisons attribution (see {@link planReconcile}),\n * while rows from before these fields existed (no `snapshotAt`) are treated\n * as not live and fall back to first-interaction activation.\n */\nexport function isInviteFlowLive(\n installation: Pick<DiscordInstallation, 'status' | 'snapshotAt' | 'snapshotApplicationId'>,\n applicationId: string,\n now: Date,\n): boolean {\n return (\n installation.status === 'pending' &&\n installation.snapshotApplicationId === applicationId &&\n installation.snapshotAt != null &&\n now.getTime() - installation.snapshotAt.getTime() <= SNAPSHOT_TTL_MS\n );\n}\n\n/**\n * Whether a pending install can reconcile at all: a live invite flow (see\n * {@link isInviteFlowLive}) *with* a membership baseline to diff against.\n * Used as a cheap pre-check before the membership fetch.\n */\nexport function hasReconcilableSnapshot(installation: DiscordInstallation, applicationId: string, now: Date): boolean {\n return installation.guildSnapshot != null && isInviteFlowLive(installation, applicationId, now);\n}\n\n/**\n * Decide which pending installs the completed-invite reconcile pass may\n * activate, and on which guild. Pure claim analysis over plain data — all\n * store reads, membership fetches, and activations happen in the caller\n * ({@link DiscordProvider.reconcileInstallation}).\n *\n * Rules, in order:\n * - Only installs with a live invite flow ({@link isInviteFlowLive}) take part.\n * Expired/foreign-app baselines are ignored (first interaction remains their\n * activation path), as are legacy rows without snapshot metadata.\n * - A live flow whose baseline fetch failed at connect time poisons the whole\n * pass: any new guild might be ITS invite landing, so nothing is attributed.\n * - A guild any install already lists is owned and never claimed — it can\n * still look \"new\" relative to a stale baseline.\n * - An install with an explicit {@link DiscordInstallation.targetGuildId}\n * claims *only* that guild. The narrowing happens before claims are tallied,\n * so a targeted install never contests a guild it would refuse anyway — but\n * its target, if new, still blocks other installs from taking it.\n * - An install activates only on exactly one new guild, claimed by no other\n * live install. Zero or multiple candidates, or a contested guild, stay\n * pending and retry on the next pass.\n */\nexport function planReconcile(input: {\n installations: DiscordInstallation[];\n /** The bot's complete current guild membership. */\n currentGuildIds: string[];\n /** The application the membership was fetched for. */\n applicationId: string;\n now: Date;\n}): ReconcileActivation[] {\n const { installations, currentGuildIds, applicationId, now } = input;\n\n const live = installations.filter(i => isInviteFlowLive(i, applicationId, now));\n const claimants = live.filter(i => i.guildSnapshot != null);\n if (claimants.length === 0) return [];\n if (claimants.length !== live.length) return []; // live flow without a baseline — unattributable\n\n const owned = new Set(installations.flatMap(i => i.guildIds));\n\n const diffs = claimants.map(installation => {\n const snapshot = new Set(installation.guildSnapshot);\n let newGuilds = currentGuildIds.filter(id => !snapshot.has(id) && !owned.has(id));\n if (installation.targetGuildId != null) {\n newGuilds = newGuilds.includes(installation.targetGuildId) ? [installation.targetGuildId] : [];\n }\n return { installation, newGuilds };\n });\n\n const claims = new Map<string, number>();\n for (const { newGuilds } of diffs) {\n for (const id of newGuilds) claims.set(id, (claims.get(id) ?? 0) + 1);\n }\n\n const activations: ReconcileActivation[] = [];\n for (const { installation, newGuilds } of diffs) {\n const guildId = newGuilds.length === 1 ? newGuilds[0] : undefined;\n if (guildId == null || claims.get(guildId)! > 1) continue; // ambiguous — wait for an interaction\n activations.push({\n installationId: installation.id,\n agentId: installation.agentId,\n webhookId: installation.webhookId,\n guildId,\n });\n }\n return activations;\n}\n","import { AgentChannels, resolveWaitUntil } from '@mastra/core/channels';\nimport type {\n ChannelAdapterConfig,\n ChannelConnectResult,\n ChannelInstallationInfo,\n ChannelPlatformInfo,\n ChannelProvider,\n StreamingConfig,\n} from '@mastra/core/channels';\nimport type { Agent } from '@mastra/core/agent';\nimport type { Mastra } from '@mastra/core/mastra';\nimport type { ApiRoute, ApiRouteHandler } from '@mastra/core/server';\nimport { InMemoryChannelsStorage } from '@mastra/core/storage';\nimport type { ChannelsStorage } from '@mastra/core/storage';\nimport { createDiscordAdapter } from '@chat-adapter/discord';\nimport type { DiscordAdapter } from '@chat-adapter/discord';\nimport {\n buildInviteUrl,\n discordRequest,\n guildHealthCheck,\n listBotGuildIds,\n registerGlobalCommands,\n registerGuildCommands,\n validateApp,\n} from './discord-client';\nimport type { DiscordApplication } from './discord-client';\nimport { DEFAULT_COMMANDS, hashCommands, normalizeCommands } from './commands';\nimport { runGatewayLoop } from './gateway-loop';\nimport { DiscordInstallStore, PLATFORM, toInstallationInfo } from './install-store';\nimport { SNAPSHOT_TTL_MS, hasReconcilableSnapshot, planReconcile } from './reconcile';\nimport { DEFAULT_INVITE_PERMISSIONS, DEFAULT_INVITE_SCOPES, DISCORD_API_BASE_URL } from './types';\nimport type { DiscordAppConfig, DiscordConnectOptions, DiscordInstallation, DiscordProviderConfig } from './types';\n\n/**\n * Resolve the per-adapter config the provider applies to the Discord entry in\n * `AgentChannels.adapters`. Discord has native embeds + action-row buttons, so\n * `toolDisplay` defaults to `'cards'` (unlike Telegram's `'text'`); `streaming`\n * post-and-edits the interaction followup; `gateway` (default `true`) makes the\n * **provider** run the DM/mention Gateway reconnection loop. (The adapter entry\n * handed to `AgentChannels` always carries `gateway: false` — core's loop must\n * never start; see `#createAgentChannels`.)\n */\nexport function resolveDiscordAdapterConfig(\n config: Pick<DiscordProviderConfig, 'streaming' | 'typingStatus' | 'toolDisplay' | 'gateway'>,\n): {\n streaming: StreamingConfig;\n typingStatus: boolean;\n toolDisplay: ChannelAdapterConfig['toolDisplay'];\n gateway: boolean;\n} {\n return {\n streaming: config.streaming ?? true,\n typingStatus: config.typingStatus ?? true,\n toolDisplay: config.toolDisplay ?? 'cards',\n gateway: config.gateway ?? true,\n };\n}\n\n/**\n * Discord channel provider for Mastra — a {@link ChannelProvider} over\n * `@chat-adapter/discord`. The adapter handles the protocol (Ed25519 request\n * verification, PING/PONG, deferrals, embeds/buttons, the Gateway bridge,\n * post-and-edit streaming); this provider adds the lifecycle layer.\n *\n * **One app, many guilds.** Discord has no programmatic app creation, so a\n * single application's credentials (bot token / public key / application id) are\n * stored **once** and reused across every guild. \"Installing\" is a bot invite —\n * an OAuth2 authorize URL — not a token exchange, so `connect()` returns the\n * `oauth` variant. A guild is confirmed either eagerly (the bot is already a\n * member) or lazily off the first inbound interaction's authoritative `guild_id`\n * (see {@link activateGuild}).\n *\n * **Gateway is the provider's job.** The mounted interactions route only ever\n * receives HTTP Interactions (PING, slash commands, buttons). DMs / @mentions /\n * reactions arrive over the Gateway WebSocket, whose reconnection loop the\n * provider owns ({@link #startGatewayLoop} → `./gateway-loop`): one abortable\n * loop per installation, exponential backoff on failed connects, parked on a\n * revoked token. Core's `AgentChannels` loop is explicitly disabled\n * (`gateway: false` on the adapter entry) — it reconnects with no backoff and\n * cannot be stopped, which is how a failing IDENTIFY once tripped Discord's\n * 1000-connect abuse limit and got the bot token force-reset.\n *\n * Implemented (issues `mastra-discord-13x.2` + `.3`): the app-config +\n * guild-keyed install store, `connect()`/`disconnect()`, OAuth2 invite-URL\n * generation, the interactions route (raw-body delegation), and adapter/Gateway\n * wiring. Command registration lands in `mastra-discord-13x.4`.\n */\nexport class DiscordProvider implements ChannelProvider {\n readonly id = PLATFORM;\n\n #config: DiscordProviderConfig;\n #mastra?: Mastra;\n #store?: DiscordInstallStore;\n /** Live adapters, keyed by installation id (one per agent; all share the app's bot token). */\n #adapters = new Map<string, DiscordAdapter>();\n /**\n * Live Gateway reconnect loops, keyed by installation id. Aborting a\n * controller ends the loop and destroys its discord.js client. Exactly one\n * loop may exist per installation — {@link #startGatewayLoop} aborts any\n * predecessor, so credential rotations and adapter rebuilds can't leak loops\n * that keep IDENTIFYing with stale tokens (Discord force-resets the bot\n * token past ~1000 connects in a short window).\n */\n #gatewayLoops = new Map<string, AbortController>();\n /** Cached sync view of whether the app is configured (for {@link getInfo}). */\n #configured = false;\n #initPromise: Promise<void> | null = null;\n /**\n * Whether {@link #store} was built on the in-memory fallback rather than real\n * storage — i.e. it was resolved before a `Mastra` instance was available.\n */\n #storeIsFallback = false;\n /**\n * A fallback store dropped by {@link __attach}, held until the next store\n * access so its contents can be carried into the real storage. See\n * {@link #migrateFallback}.\n */\n #pendingMigration?: DiscordInstallStore;\n /**\n * The single in-flight store resolution. Concurrent callers share it rather\n * than each running {@link #resolveStore}, so the fallback migration can't be\n * raced: without this, a second caller reads the already-claimed\n * {@link #pendingMigration} as absent, skips the migration, and caches a store\n * over storage the first caller has not finished populating.\n */\n #storeResolution?: Promise<DiscordInstallStore | undefined>;\n /**\n * Bumped by every {@link __attach}. A resolution that started before an attach\n * was built against the previous storage target, so it must not cache its\n * result; it reports itself superseded and the caller resolves again.\n */\n #storeGeneration = 0;\n\n constructor(config: DiscordProviderConfig = {}) {\n this.#config = config;\n // The app is \"configured\" as soon as credentials are resolvable (config/env),\n // even before the first connect persists them. A bot token alone counts —\n // the rest is backfilled from `GET /applications/@me`.\n this.#configured = this.#hasSuppliedCredentials();\n }\n\n /**\n * Called by Mastra when this channel is registered.\n * @internal\n */\n __attach(mastra: Mastra): void {\n const isNewInstance = this.#mastra != null && this.#mastra !== mastra;\n // Any attach can change the storage target, so a resolution already in\n // flight is now stale — including the first attach, where the guard below\n // can't see it: #store is still undefined and #storeIsFallback still false\n // while that resolution runs, so it would sail past and pin the provider to\n // the in-memory fallback *after* real storage became reachable.\n this.#storeGeneration++;\n // Drop the cached store when it belongs to a superseded Mastra, and when a\n // public method (connect(), initialize(), …) ran before registration and\n // pinned the provider to the in-memory fallback — otherwise installs keep\n // going to memory and are lost on restart even though real storage is\n // configured. Whatever that fallback holds was written before real storage\n // was reachable, so hand it to the next store access to carry across rather\n // than discarding it: a pre-registration connect() would otherwise return an\n // installation that no longer exists by the time its route is hit.\n if (isNewInstance || this.#storeIsFallback) {\n if (this.#storeIsFallback) this.#pendingMigration = this.#store;\n this.#initPromise = null;\n this.#store = undefined;\n this.#storeIsFallback = false;\n }\n if (isNewInstance) {\n // Loops reference adapters and agents from the superseded instance.\n this.#stopAllGatewayLoops();\n this.#adapters.clear();\n // The store-derived half of #configured belongs to the old instance; only\n // credentials supplied via config/env survive a re-attach. Without this,\n // getInfo() can report a stale isConfigured before initialize() re-runs.\n this.#configured = this.#hasSuppliedCredentials();\n }\n this.#mastra = mastra;\n }\n\n /**\n * The interactions route: a single POST endpoint keyed by an opaque\n * `webhookId`. The handler passes the **RAW** request bytes straight to\n * `adapter.handleWebhook` — Ed25519 verification + PING/PONG live in the\n * adapter, and any middleware that parsed/re-serialized the body would break\n * every signature. `requiresAuth: false` (Discord authenticates via Ed25519,\n * not a bearer token). Auto-initializes on first hit (mirrors `@mastra/slack`).\n */\n getRoutes(): ApiRoute[] {\n const self = this;\n const withInit = (handler: ApiRouteHandler) => {\n return async ({ mastra }: { mastra: Mastra }): Promise<ApiRouteHandler> => {\n self.#mastra = mastra;\n await self.#autoInitialize();\n return handler.bind(self);\n };\n };\n return [\n {\n path: `/${PLATFORM}/events/:webhookId`,\n method: 'POST',\n requiresAuth: false,\n createHandler: withInit(this.#handleWebhook),\n },\n ];\n }\n\n /** Discovery metadata for the editor UI. */\n getInfo(): ChannelPlatformInfo {\n return {\n id: this.id,\n name: 'Discord',\n isConfigured: this.#configured,\n connectOptionsSchema: {\n type: 'object',\n properties: {\n guildId: {\n type: 'string',\n description:\n 'Guild (server) to install into. If the bot is already a member, connect binds immediately; otherwise you receive an invite URL.',\n },\n name: {\n type: 'string',\n description: \"Display name for the installation (defaults to the application's name).\",\n },\n commands: {\n type: 'array',\n items: { type: 'string' },\n description: 'Slash command names to register for this agent.',\n },\n },\n },\n };\n }\n\n /**\n * Restore state from storage: mark the provider configured when an app config\n * or any active installation exists, and rebuild an adapter + `AgentChannels`\n * per active install so its Gateway loop (core-owned) starts. Idempotent.\n */\n async initialize(): Promise<void> {\n if (this.#initPromise) return this.#initPromise;\n this.#initPromise = this.#doInitialize();\n try {\n await this.#initPromise;\n } catch (err) {\n this.#initPromise = null;\n throw err;\n }\n }\n\n async #doInitialize(): Promise<void> {\n const store = await this.#getStore();\n const active = (await store.list()).filter(i => i.status === 'active');\n const hasApp = (await store.getAppConfig()) != null || this.#hasSuppliedCredentials();\n this.#configured = hasApp || active.length > 0;\n for (const installation of active) {\n try {\n await this.#activateInstallation(installation);\n } catch (err) {\n console.error(`[Discord] Failed to restore installation \"${installation.id}\":`, err);\n }\n }\n }\n\n /**\n * Provide or clear the app credentials at runtime. An object merges/overrides\n * `botToken` / `publicKey` / `applicationId` (persisted on the next `connect`);\n * `null` clears the stored app config. Supplying a **different bot token**\n * replaces the app config instead of merging: the token identifies the\n * Discord application, so any `publicKey`/`applicationId` from the previous\n * application must not survive the switch.\n */\n async configure(credentials: Partial<DiscordAppConfig> | null): Promise<void> {\n if (credentials === null) {\n const store = await this.#getStore();\n await store.deleteAppConfig();\n this.#config = { ...this.#config, app: undefined };\n this.#configured = false;\n // Live adapters still hold the app credentials that were just revoked,\n // and running Gateway loops would keep IDENTIFYing with them.\n this.#stopAllGatewayLoops();\n this.#adapters.clear();\n this.#initPromise = null;\n return;\n }\n const previous = this.#config.app;\n this.#config = { ...this.#config, app: { ...previous, ...credentials } };\n this.#configured = this.#hasSuppliedCredentials() || this.#configured;\n\n // An adapter captures botToken / publicKey / applicationId at construction,\n // so cached ones keep verifying Ed25519 against the *old* public key and\n // authenticating with the *old* bot token. Rebuild them when any changes.\n const changed = (['botToken', 'publicKey', 'applicationId'] as const).some(\n k => credentials[k] !== undefined && credentials[k] !== previous?.[k],\n );\n if (!changed) return;\n\n const store = await this.#getStore();\n const stored = await store.getAppConfig();\n\n // A different bot token means a *different Discord application*. Merging\n // would carry the previous application's publicKey/applicationId into the\n // new config, and its Ed25519 key would keep verifying inbound webhooks —\n // letting the old application's owner forge interactions against the new\n // one. Replace: keep only what this call supplied and drop the stored\n // config so #resolveAppConfig re-derives identity from the new token.\n const tokenSupersedes = (base: Partial<DiscordAppConfig> | null | undefined) =>\n credentials.botToken !== undefined && base?.botToken !== undefined && base.botToken !== credentials.botToken;\n if (tokenSupersedes(previous) || tokenSupersedes(stored)) {\n this.#config = { ...this.#config, app: { ...credentials } };\n await store.deleteAppConfig();\n } else if (stored) {\n // Persist the rotation. Every credential consumer — #resolveAppConfig,\n // #activateInstallation, #handleWebhook — reads the *stored* app config,\n // so updating only #config.app would leave adapters rebuilding from the\n // old credentials and Ed25519 still verifying against the superseded key.\n const merged = { ...stored, ...credentials };\n if (merged.botToken && merged.publicKey && merged.applicationId) {\n await store.saveAppConfig(merged);\n }\n }\n\n const wasInitialized = this.#initPromise !== null;\n // Stop loops BEFORE re-initializing: they are bound to adapters that hold\n // the superseded credentials, and a leaked loop would keep IDENTIFYing\n // with the old token until Discord's connect tripwire resets it.\n this.#stopAllGatewayLoops();\n this.#adapters.clear();\n this.#initPromise = null;\n if (wasInitialized) await this.initialize();\n }\n\n /**\n * Connect an agent to Discord.\n *\n * - Ensures the app config exists (stored, or supplied via provider config /\n * `DISCORD_*` env and persisted once, after validation). Throws if none —\n * Discord apps are created in the Developer Portal, not programmatically.\n * - Validates the bot token via `GET /applications/@me`.\n * - If `options.guildId` is given **and the bot is already in that guild**,\n * binds immediately (`{ type: 'immediate' }`).\n * - Otherwise persists a pending install and returns the OAuth2 bot-invite URL\n * (`{ type: 'oauth', authorizationUrl }`); the guild activates lazily on the\n * first interaction.\n */\n async connect(agentId: string, options: DiscordConnectOptions = {}): Promise<ChannelConnectResult> {\n const store = await this.#getStore();\n const existing = await store.getByAgent(agentId);\n if (existing?.status === 'active') {\n throw new Error(\n `Agent \"${agentId}\" is already connected to Discord. Disconnect first to reconnect. ` +\n 'To add another server, invite the bot with the existing URL — new guilds activate on first use.',\n );\n }\n\n const { app, persisted, identity: resolvedIdentity } = await this.#resolveAppConfig(store);\n // Validate the token before persisting supplied credentials — an invalid\n // token must leave no app config and no install behind. Skip the second\n // call when #resolveAppConfig already validated it while backfilling.\n const identity = resolvedIdentity ?? (await validateApp(app.botToken, this.#apiBaseUrl()));\n if (!persisted) await store.saveAppConfig(app);\n\n const installationId = existing?.id ?? globalThis.crypto.randomUUID();\n const webhookId = existing?.webhookId ?? globalThis.crypto.randomUUID();\n const displayName = options.name ?? identity.name;\n const installedAt = existing?.installedAt ?? new Date();\n const commands = normalizeCommands(options.commands ?? this.#config.commands ?? DEFAULT_COMMANDS);\n\n // Eager bind: the bot is already a member of the target guild. The save\n // runs inside the per-webhook chain with a fresh re-read — between the\n // read at the top of connect() and this point, a first interaction or a\n // reconcile can have activated the pending row, and an unserialized save\n // would clobber its confirmed guildIds.\n if (options.guildId && (await guildHealthCheck(app.botToken, options.guildId, this.#apiBaseUrl()))) {\n const guildId = options.guildId;\n return await this.#enqueueWebhookTask(webhookId, async () => {\n const fresh = existing ? await store.getByWebhookId(webhookId) : null;\n if (fresh?.status === 'active') {\n // Activated while we validated — the agent is connected; nothing to redo.\n return { type: 'immediate' as const, installationId: fresh.id };\n }\n const installation: DiscordInstallation = {\n id: installationId,\n agentId,\n webhookId,\n status: 'active',\n guildIds: [guildId],\n displayName,\n commands: commands.length ? commands : undefined,\n commandVersions: fresh?.commandVersions ?? existing?.commandVersions,\n installedAt,\n };\n await store.save(installation);\n await this.#registerCommands(app, installation, guildId);\n await this.#activateInstallation(installation);\n this.#configured = true;\n await this.#config.onInstall?.(installation);\n return { type: 'immediate' as const, installationId };\n });\n }\n\n // Invite flow: persist pending, hand back the OAuth2 bot-invite URL.\n // Snapshot the bot's current guild membership first so the install can be\n // reconciled once the operator finishes the invite in another tab (see\n // reconcileInstallation) — without it, activation waits for the first\n // inbound interaction. Best-effort: a failed listing (or a bot in more\n // guilds than we page) just falls back to lazy activation. The save runs\n // inside the per-webhook chain with a fresh re-read so a racing\n // first-interaction/reconcile activation can't be flipped back to pending.\n const outcome = await this.#enqueueWebhookTask(\n webhookId,\n async (): Promise<ChannelConnectResult | DiscordInstallation> => {\n const fresh = existing ? await store.getByWebhookId(webhookId) : null;\n if (fresh?.status === 'active') {\n // The previous invite completed while this connect was validating.\n return { type: 'immediate', installationId: fresh.id };\n }\n const base = fresh ?? existing;\n\n // A re-connect while pending keeps the ORIGINAL baseline — the\n // operator may have already completed the previous invite (the bot\n // joined between the two connect calls), and a fresh snapshot would\n // include that guild and hide it from the reconcile diff forever —\n // but only while it's trustworthy: same application, within the\n // claim window. An expired or foreign-app baseline is replaced.\n const now = new Date();\n const reusable =\n base?.guildSnapshot != null &&\n base.snapshotApplicationId === app.applicationId &&\n base.snapshotAt != null &&\n now.getTime() - base.snapshotAt.getTime() <= SNAPSHOT_TTL_MS;\n let guildSnapshot: string[] | undefined = reusable ? base!.guildSnapshot : undefined;\n if (!guildSnapshot) {\n try {\n guildSnapshot = (await listBotGuildIds(app.botToken, this.#apiBaseUrl())) ?? undefined;\n } catch (err) {\n console.warn('[Discord] Could not snapshot guild membership for pending install:', err);\n }\n }\n const pending: DiscordInstallation = {\n id: installationId,\n agentId,\n webhookId,\n status: 'pending',\n guildIds: base?.guildIds ?? [],\n displayName,\n commands: commands.length ? commands : undefined,\n commandVersions: base?.commandVersions,\n installedAt,\n guildSnapshot,\n // Stamp the flow even when the snapshot fetch failed: a live flow\n // without a baseline must poison reconcile attribution (any new\n // guild might be THIS invite landing) until the window expires.\n snapshotAt: now,\n snapshotApplicationId: app.applicationId,\n // Remember an explicit target so reconciliation never auto-activates\n // this install on some other guild the bot happens to join. A\n // re-connect without a target keeps the original intent.\n targetGuildId: options.guildId ?? base?.targetGuildId,\n };\n await store.save(pending);\n return pending;\n },\n );\n if ('type' in outcome) return outcome; // activated concurrently\n const pending = outcome;\n // Global commands don't need a guild — register once now (best-effort).\n // Guild commands wait for the first-seen guild (lazy activation).\n await this.#registerCommands(app, pending);\n this.#configured = true;\n const authorizationUrl = buildInviteUrl({\n applicationId: app.applicationId,\n permissions: options.permissions ?? this.#config.permissions ?? DEFAULT_INVITE_PERMISSIONS,\n scopes: DEFAULT_INVITE_SCOPES,\n // Preselect the requested guild so the authorized one matches the install.\n ...(options.guildId !== undefined ? { guildId: options.guildId } : {}),\n });\n return { type: 'oauth', authorizationUrl, installationId };\n }\n\n /**\n * Confirm a guild off an inbound interaction's authoritative `guild_id` and\n * mark the installation active (lazy activation). Called by the interactions\n * route on first use. Returns the updated installation, or `null` if the\n * `webhookId` is unknown.\n */\n async activateGuild(webhookId: string, guildId: string): Promise<DiscordInstallation | null> {\n return this.#enqueueWebhookTask(webhookId, () => this.#activateGuildNow(webhookId, guildId));\n }\n\n #activationChains = new Map<string, Promise<unknown>>();\n\n /**\n * Serialize every mutation of one webhook's installation row — activations,\n * pending-row saves, runtime bring-up, disconnects — so each task's read\n * happens *after* the previous task's save. Without the chain, two\n * concurrent first-interactions can lose a guildId (both read the same row,\n * second save overwrites the first) or fire onInstall twice; a connect's\n * pending save can clobber a row a racing interaction just activated; and a\n * disconnect can land mid-activation and be resurrected by its post-HTTP\n * persistence. The chain entry is dropped once the last queued task settles\n * so keys don't accumulate forever.\n */\n async #enqueueWebhookTask<T>(webhookId: string, task: () => Promise<T>): Promise<T> {\n const prev = this.#activationChains.get(webhookId) ?? Promise.resolve();\n const next = prev.catch(() => {}).then(task);\n this.#activationChains.set(webhookId, next);\n try {\n return await next;\n } finally {\n // Only clear our own entry — a newer task may have replaced it.\n if (this.#activationChains.get(webhookId) === next) this.#activationChains.delete(webhookId);\n }\n }\n\n async #activateGuildNow(webhookId: string, guildId: string): Promise<DiscordInstallation | null> {\n const store = await this.#getStore();\n const installation = await store.getByWebhookId(webhookId);\n if (!installation) return null;\n\n const wasActive = installation.status === 'active';\n const known = installation.guildIds.includes(guildId);\n if (wasActive && known) return installation; // nothing to persist\n\n if (!known) installation.guildIds.push(guildId);\n installation.status = 'active';\n delete installation.guildSnapshot; // the reconcile baseline is spent once a guild is confirmed\n delete installation.snapshotAt;\n delete installation.snapshotApplicationId;\n delete installation.targetGuildId;\n await store.save(installation);\n // Register this newly-seen guild's commands (best-effort, hash-skipped).\n const app = await store.getAppConfig();\n if (app) await this.#registerCommands(app, installation, guildId);\n this.#configured = true;\n if (!wasActive) await this.#config.onInstall?.(installation);\n return installation;\n }\n\n /**\n * Disconnect an agent from Discord: abort its Gateway loop, remove its\n * installation row, and drop the adapter entry.\n */\n async disconnect(agentId: string): Promise<void> {\n const store = await this.#getStore();\n const existing = await store.getByAgent(agentId);\n if (!existing) {\n throw new Error(`No Discord installation found for agent \"${agentId}\"`);\n }\n // Serialize the delete behind any in-flight activation for this webhook.\n // An activation's post-HTTP persistence (the command-version save in\n // #registerCommands) would otherwise land AFTER the delete and resurrect\n // the disconnected row.\n await this.#enqueueWebhookTask(existing.webhookId, async () => {\n this.#stopGatewayLoop(existing.id);\n this.#adapters.delete(existing.id);\n await store.deleteByAgent(agentId);\n });\n this.#configured = (await store.getAppConfig()) != null || (await store.list()).some(i => i.status === 'active');\n }\n\n /**\n * List installations (public info only — no secrets). A pure read: pending\n * installs are confirmed through the write-authorized\n * {@link reconcileInstallation} path, never as a listing side effect.\n */\n async listInstallations(): Promise<ChannelInstallationInfo[]> {\n const store = await this.#getStore();\n const installations = await store.list();\n return installations.map(toInstallationInfo);\n }\n\n /**\n * Activate an agent's pending install if its invite has since been completed.\n *\n * The invite URL carries no `redirect_uri` (the app would have to allowlist\n * every operator origin), so nothing calls back when the operator authorizes\n * the bot — and a pending install has no adapter, so no interaction or\n * gateway event can reach it either. Instead, each pending install carries a\n * {@link DiscordInstallation.guildSnapshot} taken when the invite was issued;\n * this diffs it against the bot's current membership and activates the\n * install when exactly one new, uncontested guild has appeared (the full\n * claim rules live in {@link planReconcile}). The claim analysis spans every\n * install — other pending invites contest the diff — but only **this**\n * agent's install can activate: the caller holds write access for this agent\n * alone. Anything ambiguous stays pending and retries on the next call, with\n * the first interaction as the authoritative fallback. Best-effort: failures\n * log and resolve rather than throw.\n *\n * Called by the server's reconcile route (write-authorized); Studio invokes\n * it on window focus while a connect is in flight. Returns the (possibly\n * just-activated) installation's public info, or `null` when the agent has\n * no installation.\n *\n * Limitation: the bot-invite URL is per-app, not per-install, so when the\n * operator invites the bot to a new server *for an already-active install*\n * (as `connect()`'s already-connected error suggests) while an invite for\n * another agent is outstanding, nothing distinguishes the two intents — the\n * new guild can be attributed to the pending install. Reconciliation is only\n * reliable while a single invite flow is in progress.\n */\n async reconcileInstallation(agentId: string): Promise<ChannelInstallationInfo | null> {\n const store = await this.#getStore();\n const existing = await store.getByAgent(agentId);\n if (!existing) return null;\n if (existing.status === 'pending') {\n // Single-flight per agent: concurrent reconciles (e.g. rapid focus\n // events) share one pass instead of racing duplicate membership fetches.\n const inflight = this.#reconcileInflight.get(agentId);\n if (inflight) {\n await inflight;\n } else {\n const run = this.#reconcilePendingInstallation(store, agentId).finally(() => {\n this.#reconcileInflight.delete(agentId);\n });\n this.#reconcileInflight.set(agentId, run);\n await run;\n }\n }\n const fresh = await store.getByAgent(agentId);\n return fresh ? toInstallationInfo(fresh) : null;\n }\n\n #reconcileInflight = new Map<string, Promise<void>>();\n\n async #reconcilePendingInstallation(store: DiscordInstallStore, agentId: string): Promise<void> {\n try {\n const app = await store.getAppConfig();\n if (!app) return;\n // Cheap pre-check: skip the membership fetch when this agent's install\n // can't reconcile anyway (no baseline, expired window, foreign app).\n const target = await store.getByAgent(agentId);\n if (!target || target.status !== 'pending') return;\n if (!hasReconcilableSnapshot(target, app.applicationId, new Date())) return;\n\n const current = await listBotGuildIds(app.botToken, this.#apiBaseUrl());\n if (!current) return; // membership unknown (too many guilds to page) — stay pending\n\n // Read the claimant inventory AFTER the slow membership fetch. A\n // connect() landing during the fetch must be counted — computed against\n // a pre-fetch list, its invite's guild would look uncontested and could\n // be attributed to this older pending install.\n const all = await store.list();\n const activation = planReconcile({\n installations: all,\n currentGuildIds: current,\n applicationId: app.applicationId,\n now: new Date(),\n }).find(a => a.agentId === agentId);\n if (!activation) return;\n\n // Activate the row AND bring the runtime up inside one chain step, so a\n // racing first interaction (whose webhook handler also builds the\n // adapter) serializes behind the whole bring-up instead of just the row\n // update — two unserialized bring-ups would start two gateway loops.\n await this.#enqueueWebhookTask(activation.webhookId, async () => {\n const activated = await this.#activateGuildNow(activation.webhookId, activation.guildId);\n if (!activated || this.#adapters.has(activated.id)) return; // runtime already up\n try {\n await this.#activateInstallation(activated);\n } catch (err) {\n // The guild IS confirmed (row saved active) but the runtime didn't\n // come up. Drop the half-built adapter so the first webhook hit —\n // or a provider restart — rebuilds it cleanly.\n this.#adapters.delete(activated.id);\n throw err;\n }\n });\n } catch (err) {\n console.warn(`[Discord] Pending-install reconcile for agent \"${agentId}\" failed (will retry):`, err);\n }\n }\n\n /** The full installation for an agent (no secrets live on it), or `null`. */\n async getInstallation(agentId: string): Promise<DiscordInstallation | null> {\n const store = await this.#getStore();\n return (await store.getByAgent(agentId)) ?? null;\n }\n\n /** Whether the Discord app is configured (credentials resolvable). */\n isConfigured(): boolean {\n return this.#configured;\n }\n\n /** The live `DiscordAdapter` for an installation id, if one is active. */\n getAdapter(installationId: string): DiscordAdapter | undefined {\n return this.#adapters.get(installationId);\n }\n\n // ===========================================================================\n // Webhook handling\n // ===========================================================================\n\n async #handleWebhook(c: {\n req: { param: (k: string) => string | undefined; header: (k: string) => string | undefined; raw: Request };\n json: (body: unknown, status?: number) => Response;\n }): Promise<Response> {\n const webhookId = c.req.param('webhookId');\n if (!webhookId) return c.json({ error: 'Missing webhookId' }, 400);\n\n const store = await this.#getStore();\n const installation = await store.getByWebhookId(webhookId);\n if (!installation) return c.json({ error: 'Unknown webhook' }, 404);\n\n const app = await store.getAppConfig();\n if (!app) return c.json({ error: 'Discord app is not configured' }, 503);\n\n const adapter = this.#getOrCreateAdapter(installation, app);\n const waitUntil = this.#config.waitUntil ?? resolveWaitUntil(c as never);\n\n // Clone the raw bytes FIRST so the adapter still sees an untouched request\n // (Ed25519 footgun). We only schedule guild activation off this clone\n // *after* the adapter has verified the signature — a 2xx response is the\n // adapter's proof of a valid Ed25519 signature; 401/4xx means forged and we\n // must not mutate storage, fire `onInstall`, or hit Discord's API.\n const activationBody = c.req.raw.clone();\n const scheduleActivation = (response: Response): Response => {\n if (response.ok) {\n const activation = this.#activateFromInteraction(webhookId, activationBody);\n if (waitUntil) waitUntil(activation);\n else void activation.catch(() => {});\n }\n return response;\n };\n\n const agent = this.#resolveAgent(installation.agentId);\n if (!agent || !this.#mastra) {\n // No agent wired (endpoint-verification PING, or the agent was removed).\n // Delegate straight to the adapter so Ed25519 + PING/PONG still work.\n try {\n return scheduleActivation(await adapter.handleWebhook(c.req.raw, waitUntil ? { waitUntil } : undefined));\n } catch (err) {\n console.error('[Discord] adapter.handleWebhook error:', err);\n return c.json({ error: 'Internal error' }, 500);\n }\n }\n\n let channels = agent.getChannels();\n if (!channels || channels.adapters[PLATFORM] !== adapter) {\n channels = this.#createAgentChannels(agent, adapter);\n await channels.initialize(this.#mastra);\n // The adapter was rebuilt (first hit after boot or a credential change),\n // so any running loop is bound to the stale adapter — replace it.\n this.#startGatewayLoop(installation, adapter, app);\n }\n\n try {\n return scheduleActivation(\n await channels.handleWebhookEvent(PLATFORM, c.req.raw, waitUntil ? { waitUntil } : undefined),\n );\n } catch (err) {\n console.error('[Discord] Error delegating to AgentChannels:', err);\n return c.json({ error: 'Internal error' }, 500);\n }\n }\n\n /** Extract the guild from an interaction body (a clone) and activate it. Best-effort. */\n async #activateFromInteraction(webhookId: string, request: Request): Promise<void> {\n try {\n const body = (await request.json()) as { guild_id?: string };\n // PING (type 1) and DMs have no guild_id — nothing to activate.\n if (body?.guild_id) await this.activateGuild(webhookId, body.guild_id);\n } catch {\n // Unreadable/non-JSON body, or no guild — skip.\n }\n }\n\n // ===========================================================================\n // Internals\n // ===========================================================================\n\n #apiBaseUrl(): string {\n return this.#config.apiBaseUrl ?? DISCORD_API_BASE_URL;\n }\n\n /**\n * Register an installation's slash commands (best-effort — a failure logs and\n * doesn't block connect). Skips the `PUT` when the command hash is unchanged\n * for the scope key, so re-activation of a known guild costs no API call\n * (200 creates/day/guild). `guildId` is required for guild scope; global scope\n * ignores it and registers once under the `'global'` key.\n */\n async #registerCommands(app: DiscordAppConfig, installation: DiscordInstallation, guildId?: string): Promise<void> {\n const commands = installation.commands ?? [];\n if (!commands.length) return;\n const scope = this.#config.commandScope ?? 'guild';\n const key = scope === 'global' ? 'global' : guildId;\n if (!key) return; // guild scope with no guild yet — defer to lazy activation\n\n const hash = hashCommands(commands);\n const versions = installation.commandVersions ?? {};\n if (versions[key] === hash) return; // unchanged — skip (rate-limit aware)\n\n try {\n if (scope === 'global') {\n await registerGlobalCommands(app.botToken, app.applicationId, commands, this.#apiBaseUrl());\n } else {\n await registerGuildCommands(app.botToken, app.applicationId, guildId!, commands, this.#apiBaseUrl());\n }\n installation.commandVersions = { ...versions, [key]: hash };\n const store = await this.#getStore();\n // The PUT above is a long await — the installation may have been\n // disconnected meanwhile. Persist the version onto the LIVE row only;\n // saving the closed-over object would resurrect a deleted installation.\n const fresh = await store.getByWebhookId(installation.webhookId);\n if (!fresh) return;\n fresh.commandVersions = { ...fresh.commandVersions, [key]: hash };\n await store.save(fresh);\n } catch (err) {\n console.warn(`[Discord] command registration failed (${scope}${guildId ? `, guild ${guildId}` : ''}):`, err);\n }\n }\n\n /** Build (once) the adapter for an installation from the shared app config. */\n #getOrCreateAdapter(installation: DiscordInstallation, app: DiscordAppConfig): DiscordAdapter {\n const existing = this.#adapters.get(installation.id);\n if (existing) return existing;\n const adapter = createDiscordAdapter({\n botToken: app.botToken,\n publicKey: app.publicKey,\n applicationId: app.applicationId,\n apiUrl: this.#apiBaseUrl(),\n userName: installation.displayName,\n ...(this.#config.mentionRoleIds !== undefined ? { mentionRoleIds: this.#config.mentionRoleIds } : {}),\n ...(this.#config.interactionFlags !== undefined ? { interactionFlags: this.#config.interactionFlags } : {}),\n ...(this.#config.logger !== undefined ? { logger: this.#config.logger } : {}),\n });\n this.#adapters.set(installation.id, adapter);\n return adapter;\n }\n\n /** Rebuild the adapter and inject AgentChannels for an active installation. */\n async #activateInstallation(installation: DiscordInstallation): Promise<void> {\n const store = await this.#getStore();\n const app = await store.getAppConfig();\n if (!app) return;\n const agent = this.#resolveAgent(installation.agentId);\n const adapter = this.#getOrCreateAdapter(installation, app);\n if (agent && this.#mastra) {\n const channels = this.#createAgentChannels(agent, adapter);\n await channels.initialize(this.#mastra);\n this.#startGatewayLoop(installation, adapter, app);\n }\n }\n\n /**\n * Start (or restart) the Gateway reconnect loop for an installation. Any\n * previous loop for the same installation is aborted first, so adapter\n * rebuilds and credential rotations replace the loop instead of stacking\n * another one. No-op when the provider is configured `gateway: false`.\n *\n * The loop itself lives in {@link runGatewayLoop}; see that module for why\n * the provider owns this instead of core's `AgentChannels` loop.\n */\n #startGatewayLoop(installation: DiscordInstallation, adapter: DiscordAdapter, app: DiscordAppConfig): void {\n if (!resolveDiscordAdapterConfig(this.#config).gateway) return;\n this.#stopGatewayLoop(installation.id);\n const controller = new AbortController();\n this.#gatewayLoops.set(installation.id, controller);\n const apiBaseUrl = this.#apiBaseUrl();\n void runGatewayLoop(\n {\n startSession: (options, durationMs, signal) => adapter.startGatewayListener(options, durationMs, signal),\n checkToken: async () => {\n try {\n const response = await discordRequest(app.botToken, 'GET', '/applications/@me', apiBaseUrl);\n return response.status === 401 ? 'invalid' : response.ok ? 'valid' : 'unreachable';\n } catch {\n return 'unreachable';\n }\n },\n log: (level, message) => {\n const line = `[Discord] [gateway:${installation.agentId}] ${message}`;\n if (level === 'error') console.error(line);\n else if (level === 'warn') console.warn(line);\n else console.info(line);\n },\n },\n controller.signal,\n ).finally(() => {\n // A parked loop (revoked token) removes itself; a replaced loop's entry\n // already points at its successor's controller and must stay.\n if (this.#gatewayLoops.get(installation.id) === controller) {\n this.#gatewayLoops.delete(installation.id);\n }\n });\n }\n\n /** Abort an installation's Gateway loop (ends the session, destroys the client). */\n #stopGatewayLoop(installationId: string): void {\n const controller = this.#gatewayLoops.get(installationId);\n if (!controller) return;\n this.#gatewayLoops.delete(installationId);\n controller.abort();\n }\n\n /** Abort every Gateway loop — credential revocation or Mastra re-attach. */\n #stopAllGatewayLoops(): void {\n for (const installationId of [...this.#gatewayLoops.keys()]) {\n this.#stopGatewayLoop(installationId);\n }\n }\n\n /**\n * Create AgentChannels for an agent with the Discord adapter, preserving any\n * adapters/config the agent author already configured (mirrors `@mastra/slack`\n * / `@mastra/telegram`). The entry always carries `gateway: false`: core's\n * built-in gateway loop reconnects with no backoff and no abort handle, so\n * the provider owns the Gateway lifecycle instead ({@link #startGatewayLoop},\n * gated on the provider-level `gateway` config).\n */\n #createAgentChannels(agent: Agent, adapter: DiscordAdapter): AgentChannels {\n const existing = agent.getChannels();\n const existingConfig = existing?.channelConfig;\n const cfg = this.#config;\n const entry = {\n adapter,\n ...resolveDiscordAdapterConfig(cfg),\n // Never let core's AgentChannels start its own loop — see docstring.\n gateway: false,\n ...(cfg.cors !== undefined ? { cors: cfg.cors } : {}),\n ...(cfg.formatError !== undefined ? { formatError: cfg.formatError } : {}),\n } as ChannelAdapterConfig;\n const channels = new AgentChannels({\n ...existingConfig,\n adapters: { ...existingConfig?.adapters, [PLATFORM]: entry },\n userName: agent.name,\n handlers: cfg.handlers ?? existingConfig?.handlers,\n inlineMedia: cfg.inlineMedia ?? existingConfig?.inlineMedia,\n inlineLinks: cfg.inlineLinks ?? existingConfig?.inlineLinks,\n state: cfg.state ?? existingConfig?.state,\n threadContext: cfg.threadContext ?? existingConfig?.threadContext,\n chatOptions: cfg.chatOptions ?? existingConfig?.chatOptions,\n tools: cfg.tools ?? existingConfig?.tools,\n resolveResourceId: cfg.resolveResourceId ?? existingConfig?.resolveResourceId,\n waitUntil: cfg.waitUntil ?? existingConfig?.waitUntil,\n resolveWaitUntil: cfg.resolveWaitUntil ?? existingConfig?.resolveWaitUntil,\n });\n agent.setChannels(channels);\n return channels;\n }\n\n async #autoInitialize(): Promise<void> {\n if (!this.#mastra) return;\n await this.initialize();\n }\n\n #resolveAgent(agentId: string): Agent | undefined {\n try {\n return this.#mastra?.getAgentById(agentId) as Agent | undefined;\n } catch {\n return undefined;\n }\n }\n\n /**\n * App credentials from provider config or `DISCORD_*` env.\n *\n * A config-supplied bot token defines the application identity, so\n * `publicKey` / `applicationId` never fall back to the environment in that\n * case — stale `DISCORD_PUBLIC_KEY` / `DISCORD_APPLICATION_ID` from a\n * *different* application would otherwise complete the config and keep that\n * application's Ed25519 key trusted for webhook verification (bypassing the\n * `GET /applications/@me` backfill). Env fallback for those fields applies\n * only when the bot token itself comes from the environment.\n */\n #suppliedPartialAppConfig(): Partial<DiscordAppConfig> {\n const a = this.#config.app ?? {};\n if (a.botToken != null) {\n return { botToken: a.botToken, publicKey: a.publicKey, applicationId: a.applicationId };\n }\n return {\n botToken: process.env.DISCORD_BOT_TOKEN,\n publicKey: a.publicKey ?? process.env.DISCORD_PUBLIC_KEY,\n applicationId: a.applicationId ?? process.env.DISCORD_APPLICATION_ID,\n };\n }\n\n /** App credentials from provider config or `DISCORD_*` env, or `null` if incomplete. */\n #suppliedAppConfig(): DiscordAppConfig | null {\n const { botToken, publicKey, applicationId } = this.#suppliedPartialAppConfig();\n if (botToken && publicKey && applicationId) return { botToken, publicKey, applicationId };\n return null;\n }\n\n /**\n * Whether credentials are resolvable without storage. A bot token alone is\n * enough: `applicationId` and `publicKey` are backfilled from\n * `GET /applications/@me` by {@link #resolveAppConfig}.\n */\n #hasSuppliedCredentials(): boolean {\n return this.#suppliedPartialAppConfig().botToken != null;\n }\n\n /**\n * Resolve the app config: prefer the stored one, else the supplied credentials\n * (config / `DISCORD_*` env). A supplied `botToken` without `publicKey` /\n * `applicationId` is completed from `GET /applications/@me` — the application\n * object carries both the id and the Ed25519 `verify_key`. `persisted` says\n * whether it's already in storage, so the caller can persist supplied\n * credentials **once**, after validation. `identity` is set when the token was\n * already validated against `/applications/@me` here, so callers can skip a\n * second validation call. Throws if no credentials are available.\n */\n async #resolveAppConfig(\n store: DiscordInstallStore,\n ): Promise<{ app: DiscordAppConfig; persisted: boolean; identity?: DiscordApplication }> {\n const stored = await store.getAppConfig();\n if (stored) return { app: stored, persisted: true };\n const supplied = this.#suppliedAppConfig();\n if (supplied) return { app: supplied, persisted: false };\n const partial = this.#suppliedPartialAppConfig();\n if (partial.botToken) {\n const identity = await validateApp(partial.botToken, this.#apiBaseUrl());\n const publicKey = partial.publicKey ?? identity.verify_key;\n if (!publicKey) {\n throw new Error('Discord /applications/@me returned no verify_key; provide publicKey explicitly.');\n }\n return {\n app: { botToken: partial.botToken, publicKey, applicationId: partial.applicationId ?? identity.id },\n persisted: false,\n identity,\n };\n }\n throw new Error(\n 'Discord app is not configured. Provide at least a botToken via the provider `app` option, ' +\n 'the DISCORD_BOT_TOKEN env var, or configure() — publicKey and applicationId are resolved from Discord ' +\n 'automatically when omitted. Discord applications are created in the Developer Portal, not programmatically.',\n );\n }\n\n /**\n * The single entry point to the store. Every caller either gets the cached\n * store or joins the one in-flight resolution — nobody resolves in parallel,\n * so a caller that arrives mid-migration waits for it to finish instead of\n * building a second store over storage that isn't populated yet.\n */\n async #getStore(): Promise<DiscordInstallStore> {\n // Loop rather than await once: an `__attach` landing mid-resolution\n // supersedes it, and we then resolve again against the new storage target.\n for (;;) {\n if (this.#store) return this.#store;\n // Created synchronously, so two callers can't both start a resolution.\n const inFlight = (this.#storeResolution ??= this.#resolveStore(this.#storeGeneration));\n let resolved: DiscordInstallStore | undefined;\n try {\n resolved = await inFlight;\n } finally {\n // Only retract our own attempt: a superseding attempt may have replaced\n // it already, and clearing that would strand a later caller's promise.\n if (this.#storeResolution === inFlight) this.#storeResolution = undefined;\n }\n if (resolved) return resolved;\n }\n }\n\n /**\n * One resolution attempt, run under {@link #getStore}'s in-flight guard — so\n * the {@link #pendingMigration} claim below cannot be double-taken, and the\n * migration completes before any caller sees the store.\n *\n * Returns `undefined` when `__attach` superseded this attempt while it ran:\n * the storage target changed underneath it, so caching would pin the provider\n * to a store built for the previous one.\n */\n async #resolveStore(generation: number): Promise<DiscordInstallStore | undefined> {\n const encryptionKey = this.#config.encryptionKey ?? process.env.MASTRA_ENCRYPTION_KEY;\n const { storage, isFallback } = await this.#resolveStorage();\n // Superseded before anything was claimed — nothing to hand back.\n if (generation !== this.#storeGeneration) return undefined;\n const pending = this.#pendingMigration;\n this.#pendingMigration = undefined;\n // Storage is still unavailable, so a new fallback would be an empty one —\n // keep the store we already have instead of dropping its contents.\n if (pending && isFallback) {\n this.#storeIsFallback = true;\n this.#store = pending;\n return pending;\n }\n const store = new DiscordInstallStore(storage, encryptionKey);\n if (pending) await this.#migrateFallback(pending, store);\n if (generation !== this.#storeGeneration) {\n // Superseded while migrating. Re-queue the fallback so the next attempt\n // carries it into the new target too — #migrateFallback keeps whatever is\n // already persisted, so replaying it is harmless.\n if (pending && !this.#pendingMigration) this.#pendingMigration = pending;\n return undefined;\n }\n this.#storeIsFallback = isFallback;\n this.#store = store;\n return store;\n }\n\n /**\n * Copy what a pre-registration store wrote into the real storage that has\n * since become available. Anything already persisted wins — the real store is\n * the durable record, and a stale in-memory row must not overwrite it. Both\n * stores share an encryption key, so this round-trips through plaintext.\n *\n * Best-effort: a failure here must not take down `__attach`'s caller or the\n * request that triggered the resolution. The provider still works against the\n * real storage; only the pre-registration writes are missing, which is the\n * behaviour before this migration existed.\n */\n async #migrateFallback(from: DiscordInstallStore, to: DiscordInstallStore): Promise<void> {\n try {\n const app = await from.getAppConfig();\n if (app && !(await to.getAppConfig())) await to.saveAppConfig(app);\n for (const install of await from.list()) {\n if (await to.getByAgent(install.agentId)) continue;\n await to.save(install);\n }\n } catch (error) {\n console.warn(\n '[Discord] Failed to carry pre-registration installations into Mastra storage; ' +\n 'connect() again to re-create them.',\n error,\n );\n }\n }\n\n /**\n * Resolve the backing storage, reporting whether it is the in-memory fallback\n * so {@link __attach} can re-resolve a store that was built before Mastra was\n * available, carrying its contents across.\n */\n async #resolveStorage(): Promise<{ storage: ChannelsStorage; isFallback: boolean }> {\n if (this.#config.storage) return { storage: this.#config.storage, isFallback: false };\n const mastraStore = this.#mastra?.getStorage();\n if (mastraStore) {\n try {\n await mastraStore.init();\n const channels = await mastraStore.getStore('channels');\n if (channels) return { storage: channels, isFallback: false };\n } catch {\n // Fall through to the in-memory store below.\n }\n }\n // No persistent storage available — fall back to in-memory. Installs won't\n // survive a restart; pass `storage` or configure Mastra storage in prod.\n return { storage: new InMemoryChannelsStorage(), isFallback: true };\n }\n}\n"],"mappings":"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"}