@mastra/discord 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +77 -0
- package/LICENSE.md +32 -0
- package/README.md +175 -0
- package/dist/index.cjs +116012 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +554 -0
- package/dist/index.d.ts +554 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1110 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -50
- package/src/Discord.test.ts +0 -55
- package/src/assets/discord.png +0 -0
- package/src/index.ts +0 -201
- package/src/openapi-components.ts +0 -31769
- package/src/openapi-paths.ts +0 -15861
- package/src/openapi.ts +0 -23784
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["#dec","#enc","#fromRecord","#toRecord","#config","#configured","#suppliedAppConfig","#mastra","#storeGeneration","#storeIsFallback","#pendingMigration","#store","#initPromise","#adapters","#autoInitialize","#handleWebhook","#doInitialize","#getStore","#activateInstallation","#resolveAppConfig","#apiBaseUrl","#registerCommands","#activationChains","#activateGuildNow","#getOrCreateAdapter","#activateFromInteraction","#resolveAgent","#createAgentChannels","createDiscordAdapter","#storeResolution","#resolveStore","#resolveStorage","#migrateFallback"],"sources":["../src/types.ts","../src/discord-client.ts","../src/commands.ts","../src/crypto.ts","../src/install-store.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 * Whichever source resolves first 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","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\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/** 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, randomBytes } 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 = randomBytes(16);\n const iv = randomBytes(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}\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 };\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 };\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 { randomUUID } from 'node:crypto';\nimport { 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 registerGlobalCommands,\n registerGuildCommands,\n validateApp,\n} from './discord-client';\nimport { DEFAULT_COMMANDS, hashCommands, normalizeCommands } from './commands';\nimport { DiscordInstallStore, PLATFORM, toInstallationInfo } from './install-store';\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.\n this.#configured = this.#suppliedAppConfig() != null;\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.#suppliedAppConfig() != null;\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.#suppliedAppConfig() != null;\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.\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.#suppliedAppConfig() != null || 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 // Persist the rotation. Every credential consumer — #resolveAppConfig,\n // #activateInstallation, #handleWebhook — reads the *stored* app config, so\n // updating only #config.app would leave adapters rebuilding from the old\n // credentials and Ed25519 still verifying against the superseded key.\n const store = await this.#getStore();\n const stored = await store.getAppConfig();\n if (stored) {\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 } = 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.\n const identity = await validateApp(app.botToken, this.#apiBaseUrl());\n if (!persisted) await store.saveAppConfig(app);\n\n const installationId = existing?.id ?? randomUUID();\n const webhookId = existing?.webhookId ?? 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.\n if (options.guildId && (await guildHealthCheck(app.botToken, options.guildId, this.#apiBaseUrl()))) {\n const installation: DiscordInstallation = {\n id: installationId,\n agentId,\n webhookId,\n status: 'active',\n guildIds: [options.guildId],\n displayName,\n commands: commands.length ? commands : undefined,\n commandVersions: existing?.commandVersions,\n installedAt,\n };\n await store.save(installation);\n await this.#registerCommands(app, installation, options.guildId);\n await this.#activateInstallation(installation);\n this.#configured = true;\n await this.#config.onInstall?.(installation);\n return { type: 'immediate', installationId };\n }\n\n // Invite flow: persist pending, hand back the OAuth2 bot-invite URL.\n const pending: DiscordInstallation = {\n id: installationId,\n agentId,\n webhookId,\n status: 'pending',\n guildIds: existing?.guildIds ?? [],\n displayName,\n commands: commands.length ? commands : undefined,\n commandVersions: existing?.commandVersions,\n installedAt,\n };\n await store.save(pending);\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 // Serialize per-webhook activations so each read happens *after* the\n // previous save. Without this chain, two concurrent first-interactions can:\n // - lose a guildId (both reads see the same row, second save overwrites first)\n // - fire onInstall twice / send duplicate command-registration PUTs\n // The chain is dropped once the last queued activation settles so keys\n // don't accumulate forever.\n const prev = this.#activationChains.get(webhookId) ?? Promise.resolve();\n const next: Promise<DiscordInstallation | null> = prev\n .catch(() => {})\n .then(() => this.#activateGuildNow(webhookId, guildId));\n this.#activationChains.set(webhookId, next);\n try {\n return await next;\n } finally {\n // Only clear our own entry — a newer activation may have replaced it.\n if (this.#activationChains.get(webhookId) === next) this.#activationChains.delete(webhookId);\n }\n }\n\n #activationChains = new Map<string, Promise<unknown>>();\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 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 this.#adapters.delete(existing.id);\n await store.deleteByAgent(agentId);\n this.#configured = (await store.getAppConfig()) != null || (await store.list()).some(i => i.status === 'active');\n }\n\n /** List installations (public info only — no secrets). */\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 /** 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 await store.save(installation);\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 /** App credentials from provider config or `DISCORD_*` env, or `null` if incomplete. */\n #suppliedAppConfig(): DiscordAppConfig | null {\n const a = this.#config.app ?? {};\n const botToken = a.botToken ?? process.env.DISCORD_BOT_TOKEN;\n const publicKey = a.publicKey ?? process.env.DISCORD_PUBLIC_KEY;\n const applicationId = a.applicationId ?? process.env.DISCORD_APPLICATION_ID;\n if (botToken && publicKey && applicationId) return { botToken, publicKey, applicationId };\n return null;\n }\n\n /**\n * Resolve the app config: prefer the stored one, else the supplied credentials\n * (config / `DISCORD_*` env). `persisted` says whether it's already in storage,\n * so the caller can persist supplied credentials **once**, after validation.\n * Throws if neither is available.\n */\n async #resolveAppConfig(store: DiscordInstallStore): Promise<{ app: DiscordAppConfig; persisted: boolean }> {\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 throw new Error(\n 'Discord app is not configured. Provide botToken, publicKey, and applicationId via the provider `app` option, ' +\n 'the DISCORD_BOT_TOKEN / DISCORD_PUBLIC_KEY / DISCORD_APPLICATION_ID env vars, or configure() — ' +\n '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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"}
|
package/package.json
CHANGED
|
@@ -1,63 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mastra/discord",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Discord integration for Mastra agents — a ChannelProvider over @chat-adapter/discord with Ed25519 interaction verification, guild command registration, and streaming replies",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
5
7
|
"main": "dist/index.js",
|
|
6
|
-
"
|
|
7
|
-
"typings": "dist/index.d.ts",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
8
9
|
"files": [
|
|
9
10
|
"dist",
|
|
10
|
-
"
|
|
11
|
+
"CHANGELOG.md"
|
|
11
12
|
],
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"require": {
|
|
20
|
+
"types": "./dist/index.d.cts",
|
|
21
|
+
"default": "./dist/index.cjs"
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
24
|
},
|
|
17
|
-
"engines": {
|
|
18
|
-
"node": ">=20 <22"
|
|
19
|
-
},
|
|
20
|
-
"size-limit": [
|
|
21
|
-
{
|
|
22
|
-
"path": "dist/mylib.cjs.production.min.js",
|
|
23
|
-
"limit": "10 KB"
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"path": "dist/mylib.esm.js",
|
|
27
|
-
"limit": "10 KB"
|
|
28
|
-
}
|
|
29
|
-
],
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@jest/globals": "^29.7.0",
|
|
32
|
-
"@rollup/plugin-image": "^3.0.3",
|
|
33
|
-
"@size-limit/preset-small-lib": "^11.1.4",
|
|
34
|
-
"@tsconfig/recommended": "^1.0.7",
|
|
35
|
-
"@types/jest": "^29.5.12",
|
|
36
|
-
"@types/lodash": "^4.17.7",
|
|
37
|
-
"@types/node": "^22.1.0",
|
|
38
|
-
"dts-cli": "^2.0.5",
|
|
39
|
-
"husky": "^9.1.4",
|
|
40
|
-
"jest": "^29.7.0",
|
|
41
|
-
"size-limit": "^11.1.4",
|
|
42
|
-
"ts-jest": "^29.2.4",
|
|
43
|
-
"tslib": "^2.6.3",
|
|
44
|
-
"typescript": "^5.5.4"
|
|
45
|
-
},
|
|
46
|
-
"keywords": [],
|
|
47
|
-
"author": "",
|
|
48
|
-
"license": "ISC",
|
|
49
25
|
"dependencies": {
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
26
|
+
"@chat-adapter/discord": "4.40.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.20.1",
|
|
30
|
+
"tsdown": "0.22.9",
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
|
+
"undici": "^6.0.0",
|
|
33
|
+
"vitest": "^4.1.10",
|
|
34
|
+
"@mastra/core": "1.71.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@mastra/core": ">=1.22.0 <2.0.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"mastra",
|
|
44
|
+
"discord",
|
|
45
|
+
"chatbot",
|
|
46
|
+
"ai",
|
|
47
|
+
"agent"
|
|
48
|
+
],
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/mastra-ai/mastra.git",
|
|
52
|
+
"directory": "channels/discord"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://mastra.ai",
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/mastra-ai/mastra/issues"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=22.13.0"
|
|
53
60
|
},
|
|
54
61
|
"scripts": {
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"start": "dts watch",
|
|
61
|
-
"test": "jest"
|
|
62
|
+
"build": "tsdown && node scripts/write-cts-shim.mjs",
|
|
63
|
+
"dev": "tsdown --watch",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:watch": "vitest"
|
|
62
67
|
}
|
|
63
68
|
}
|
package/src/Discord.test.ts
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
describe,
|
|
3
|
-
it,
|
|
4
|
-
beforeAll,
|
|
5
|
-
afterAll, //expect
|
|
6
|
-
} from '@jest/globals';
|
|
7
|
-
import { Framework } from '@mastra/core';
|
|
8
|
-
|
|
9
|
-
import { DiscordIntegration } from '.';
|
|
10
|
-
|
|
11
|
-
// We need to OAuth from admin
|
|
12
|
-
|
|
13
|
-
const CLIENT_ID = process.env.CLIENT_ID!;
|
|
14
|
-
const CLIENT_SECRET = process.env.CLIENT_SECRET!;
|
|
15
|
-
|
|
16
|
-
const dbUri = process.env.DB_URL!;
|
|
17
|
-
const connectionId = process.env.CONNECTION_ID!;
|
|
18
|
-
|
|
19
|
-
const integrationName = 'DISCORD';
|
|
20
|
-
|
|
21
|
-
const integrationFramework = Framework.init({
|
|
22
|
-
name: 'TestFramework',
|
|
23
|
-
integrations: [
|
|
24
|
-
new DiscordIntegration({
|
|
25
|
-
config: {
|
|
26
|
-
CLIENT_ID,
|
|
27
|
-
CLIENT_SECRET,
|
|
28
|
-
undefined,
|
|
29
|
-
},
|
|
30
|
-
}),
|
|
31
|
-
],
|
|
32
|
-
systemApis: [],
|
|
33
|
-
systemEvents: {},
|
|
34
|
-
db: {
|
|
35
|
-
provider: 'postgres',
|
|
36
|
-
uri: dbUri,
|
|
37
|
-
},
|
|
38
|
-
systemHostURL: 'http://localhost:3000',
|
|
39
|
-
routeRegistrationPath: '/api/mastra',
|
|
40
|
-
blueprintDirPath: '',
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
//const integration = integrationFramework.getIntegration(integrationName) as DiscordIntegration
|
|
44
|
-
|
|
45
|
-
describe('discord', () => {
|
|
46
|
-
beforeAll(async () => {});
|
|
47
|
-
|
|
48
|
-
it('should 200 on some apis', async () => {
|
|
49
|
-
//const client = await integration.getApiClient({ connectionId });
|
|
50
|
-
//const response = await client['/2010-04-01/Accounts.json'].get();
|
|
51
|
-
//expect(response.status).toBe(200);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
afterAll(async () => {});
|
|
55
|
-
});
|
package/src/assets/discord.png
DELETED
|
Binary file
|
package/src/index.ts
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
import { Integration, OpenAPI, IntegrationCredentialType, IntegrationAuth } from '@mastra/core';
|
|
2
|
-
import { createClient, type OASClient, type NormalizeOAS } from 'fets';
|
|
3
|
-
|
|
4
|
-
// @ts-ignore
|
|
5
|
-
import DiscordLogo from './assets/discord.png';
|
|
6
|
-
import { openapi } from './openapi';
|
|
7
|
-
import { components } from './openapi-components';
|
|
8
|
-
import { paths } from './openapi-paths';
|
|
9
|
-
|
|
10
|
-
type DiscordConfig = {
|
|
11
|
-
CLIENT_ID: string;
|
|
12
|
-
CLIENT_SECRET: string;
|
|
13
|
-
|
|
14
|
-
[key: string]: any;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class DiscordIntegration extends Integration {
|
|
18
|
-
availableScopes = [
|
|
19
|
-
{
|
|
20
|
-
key: `activities.read`,
|
|
21
|
-
description: `allows your app to fetch data from a user's "Now Playing/Recently Played" list - requires Discord approval`,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
key: `activities.write`,
|
|
25
|
-
description: `allows your app to update a user's activity - requires Discord approval (NOT REQUIRED FOR GAMESDK ACTIVITY MANAGER)`,
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
key: `applications.builds.read`,
|
|
29
|
-
description: `allows your app to read build data for a user's applications`,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
key: `applications.builds.upload`,
|
|
33
|
-
description: `allows your app to upload/update builds for a user's applications - requires Discord approval`,
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
key: `applications.commands`,
|
|
37
|
-
description: `allows your app to use commands in a guild`,
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
key: `applications.commands.permissions.update`,
|
|
41
|
-
description: `allows your app to update permissions for its commands in a guild a user has permissions to`,
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
key: `applications.entitlements`,
|
|
45
|
-
description: `allows your app to read entitlements for a user's applications`,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
key: `applications.store.update`,
|
|
49
|
-
description: `allows your app to read and update store data (SKUs, store listings, achievements, etc.) for a user's applications`,
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
key: `bot`,
|
|
53
|
-
description: `for oauth2 bots, this puts the bot in the user's selected guild by default`,
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
key: `connections`,
|
|
57
|
-
description: `allows /users/@me/connections to return linked third-party accounts`,
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
key: `dm_channels.read`,
|
|
61
|
-
description: `allows your app to see information about the user's DMs and group DMs - requires Discord approval`,
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
key: `email`,
|
|
65
|
-
description: `enables /users/@me to return an email`,
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
key: `gdm.join`,
|
|
69
|
-
description: `allows your app to join users to a group dm`,
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
key: `guilds`,
|
|
73
|
-
description: `allows /users/@me/guilds to return basic information about all of a user's guilds`,
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
key: `guilds.join`,
|
|
77
|
-
description: `allows /guilds/{guild.id}/members/{user.id} to be used for joining users to a guild`,
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
key: `guilds.members.read`,
|
|
81
|
-
description: `allows /users/@me/guilds/{guild.id}/member to return a user's member information in a guild`,
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
key: `identify`,
|
|
85
|
-
description: `allows /users/@me without email`,
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
key: `messages.read`,
|
|
89
|
-
description: `for local rpc server api access, this allows you to read messages from all client channels (otherwise restricted to channels/guilds your app creates)`,
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
key: `relationships.read`,
|
|
93
|
-
description: `allows your app to know a user's friends and implicit relationships - requires Discord approval`,
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
key: `rpc`,
|
|
97
|
-
description: `for local rpc server access, this allows you to control a user's local Discord client - requires Discord approval`,
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
key: `rpc.activities.write`,
|
|
101
|
-
description: `for local rpc server access, this allows you to update a user's activity - requires Discord approval`,
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
key: `rpc.notifications.read`,
|
|
105
|
-
description: `for local rpc server access, this allows you to receive notifications pushed out to the user - requires Discord approval`,
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
key: `rpc.screenshare.read`,
|
|
109
|
-
description: `for local rpc server access, this allows you to read a user's screenshare status- requires Discord approval`,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
key: `rpc.screenshare.write`,
|
|
113
|
-
description: `for local rpc server access, this allows you to update a user's screenshare settings- requires Discord approval`,
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
key: `rpc.video.read`,
|
|
117
|
-
description: `for local rpc server access, this allows you to read a user's video status - requires Discord approval`,
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
key: `rpc.video.write`,
|
|
121
|
-
description: `for local rpc server access, this allows you to update a user's video settings - requires Discord approval`,
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
key: `rpc.voice.read`,
|
|
125
|
-
description: `for local rpc server access, this allows you to read a user's voice settings and listen for voice events - requires Discord approval`,
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
key: `rpc.voice.write`,
|
|
129
|
-
description: `for local rpc server access, this allows you to update a user's voice settings - requires Discord approval`,
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
key: `voice`,
|
|
133
|
-
description: `allows your app to connect to voice on user's behalf and see all the voice members - requires Discord approval`,
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
key: `webhook.incoming`,
|
|
137
|
-
description: `this generates a webhook that is returned in the oauth token response for authorization code grants`,
|
|
138
|
-
},
|
|
139
|
-
];
|
|
140
|
-
|
|
141
|
-
constructor({ config }: { config: DiscordConfig }) {
|
|
142
|
-
super({
|
|
143
|
-
...config,
|
|
144
|
-
authType: IntegrationCredentialType.OAUTH,
|
|
145
|
-
name: 'DISCORD',
|
|
146
|
-
logoUrl: DiscordLogo,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
getOpenApiSpec() {
|
|
151
|
-
return { paths, components } as unknown as OpenAPI;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
getApiClient = async ({ connectionId }: { connectionId: string }): Promise<OASClient<NormalizeOAS<openapi>>> => {
|
|
155
|
-
const connection = await this.dataLayer?.getConnection({ name: this.name, connectionId });
|
|
156
|
-
|
|
157
|
-
if (!connection) {
|
|
158
|
-
throw new Error(`Connection not found for connectionId: ${connectionId}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const authenticator = this.getAuthenticator();
|
|
162
|
-
const { accessToken } = await authenticator.getAuthToken({ k_id: connection.id });
|
|
163
|
-
|
|
164
|
-
const client = createClient<NormalizeOAS<openapi>>({
|
|
165
|
-
endpoint: `https://discord.com/api/v10`,
|
|
166
|
-
globalParams: {
|
|
167
|
-
headers: {
|
|
168
|
-
Authorization: `Bearer ${accessToken}`,
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
return client as any;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
registerEvents() {
|
|
177
|
-
this.events = {};
|
|
178
|
-
return this.events;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
getAuthenticator() {
|
|
182
|
-
return new IntegrationAuth({
|
|
183
|
-
dataAccess: this.dataLayer!,
|
|
184
|
-
// @ts-ignore
|
|
185
|
-
onConnectionCreated: () => {
|
|
186
|
-
// TODO
|
|
187
|
-
},
|
|
188
|
-
config: {
|
|
189
|
-
INTEGRATION_NAME: this.name,
|
|
190
|
-
AUTH_TYPE: this.config.authType,
|
|
191
|
-
CLIENT_ID: this.config.CLIENT_ID,
|
|
192
|
-
CLIENT_SECRET: this.config.CLIENT_SECRET,
|
|
193
|
-
REDIRECT_URI: this.config.REDIRECT_URI || this.corePresets.redirectURI,
|
|
194
|
-
SERVER: `https://discord.com/api/v10`,
|
|
195
|
-
AUTHORIZATION_ENDPOINT: `discord.com/oauth2/authorize`,
|
|
196
|
-
TOKEN_ENDPOINT: `https://discord.com/api/oauth2/token`,
|
|
197
|
-
SCOPES: this.config.SCOPES || [],
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
}
|