From 8c3aec65af5b6a32051139729cfe453a32ae92be Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 12:05:38 -0800 Subject: [PATCH 01/14] Update adapter configs --- packages/adapter-discord/src/index.ts | 85 ++++++------- packages/adapter-discord/src/types.ts | 19 +-- packages/adapter-gchat/src/index.ts | 127 +++++++------------- packages/adapter-github/src/index.ts | 157 +++++++++++-------------- packages/adapter-github/src/types.ts | 23 +++- packages/adapter-linear/src/index.ts | 119 ++++++------------- packages/adapter-linear/src/types.ts | 33 ++++-- packages/adapter-slack/src/index.ts | 90 +++++++------- packages/adapter-teams/src/index.ts | 90 +++++++------- packages/adapter-telegram/src/index.ts | 78 +++++------- packages/adapter-telegram/src/types.ts | 14 ++- 11 files changed, 368 insertions(+), 467 deletions(-) diff --git a/packages/adapter-discord/src/index.ts b/packages/adapter-discord/src/index.ts index 6a9cd579..6f1f865a 100644 --- a/packages/adapter-discord/src/index.ts +++ b/packages/adapter-discord/src/index.ts @@ -99,15 +99,40 @@ export class DiscordAdapter implements Adapter { >(); private static readonly THREAD_PARENT_CACHE_TTL = 5 * 60 * 1000; - constructor( - config: DiscordAdapterConfig & { logger: Logger; userName?: string } - ) { - this.botToken = config.botToken; - this.publicKey = config.publicKey.trim().toLowerCase(); - this.applicationId = config.applicationId; - this.mentionRoleIds = config.mentionRoleIds ?? []; - this.botUserId = config.applicationId; // Discord app ID is the bot's user ID - this.logger = config.logger; + constructor(config: DiscordAdapterConfig = {}) { + const botToken = config.botToken ?? process.env.DISCORD_BOT_TOKEN; + if (!botToken) { + throw new ValidationError( + "discord", + "botToken is required. Set DISCORD_BOT_TOKEN or provide it in config." + ); + } + const publicKey = config.publicKey ?? process.env.DISCORD_PUBLIC_KEY; + if (!publicKey) { + throw new ValidationError( + "discord", + "publicKey is required. Set DISCORD_PUBLIC_KEY or provide it in config." + ); + } + const applicationId = + config.applicationId ?? process.env.DISCORD_APPLICATION_ID; + if (!applicationId) { + throw new ValidationError( + "discord", + "applicationId is required. Set DISCORD_APPLICATION_ID or provide it in config." + ); + } + + this.botToken = botToken; + this.publicKey = publicKey.trim().toLowerCase(); + this.applicationId = applicationId; + this.mentionRoleIds = + config.mentionRoleIds ?? + (process.env.DISCORD_MENTION_ROLE_IDS + ? process.env.DISCORD_MENTION_ROLE_IDS.split(",").map((id) => id.trim()) + : []); + this.botUserId = applicationId; // Discord app ID is the bot's user ID + this.logger = config.logger ?? new ConsoleLogger("info").child("discord"); this.userName = config.userName ?? "bot"; // Validate public key format @@ -2437,47 +2462,9 @@ export class DiscordAdapter implements Adapter { * Create a Discord adapter instance. */ export function createDiscordAdapter( - config?: Partial + config?: DiscordAdapterConfig ): DiscordAdapter { - const botToken = config?.botToken ?? process.env.DISCORD_BOT_TOKEN; - if (!botToken) { - throw new ValidationError( - "discord", - "botToken is required. Set DISCORD_BOT_TOKEN or provide it in config." - ); - } - const publicKey = config?.publicKey ?? process.env.DISCORD_PUBLIC_KEY; - if (!publicKey) { - throw new ValidationError( - "discord", - "publicKey is required. Set DISCORD_PUBLIC_KEY or provide it in config." - ); - } - const applicationId = - config?.applicationId ?? process.env.DISCORD_APPLICATION_ID; - if (!applicationId) { - throw new ValidationError( - "discord", - "applicationId is required. Set DISCORD_APPLICATION_ID or provide it in config." - ); - } - const mentionRoleIds = - config?.mentionRoleIds ?? - (process.env.DISCORD_MENTION_ROLE_IDS - ? process.env.DISCORD_MENTION_ROLE_IDS.split(",").map((id) => id.trim()) - : undefined); - const resolved: DiscordAdapterConfig & { - logger: Logger; - userName?: string; - } = { - botToken, - publicKey, - applicationId, - mentionRoleIds, - logger: config?.logger ?? new ConsoleLogger("info").child("discord"), - userName: config?.userName, - }; - return new DiscordAdapter(resolved); + return new DiscordAdapter(config ?? {}); } // Re-export card converter for advanced use diff --git a/packages/adapter-discord/src/types.ts b/packages/adapter-discord/src/types.ts index d752bc66..caaf66c2 100644 --- a/packages/adapter-discord/src/types.ts +++ b/packages/adapter-discord/src/types.ts @@ -2,6 +2,7 @@ * Discord adapter types. */ +import type { Logger } from "chat"; import type { APIEmbed, APIMessage, @@ -14,14 +15,18 @@ import type { * Discord adapter configuration. */ export interface DiscordAdapterConfig { - /** Discord application ID */ - applicationId: string; - /** Discord bot token */ - botToken: string; - /** Role IDs that should trigger mention handlers (in addition to direct user mentions) */ + /** Discord application ID. Defaults to DISCORD_APPLICATION_ID env var. */ + applicationId?: string; + /** Discord bot token. Defaults to DISCORD_BOT_TOKEN env var. */ + botToken?: string; + /** Logger instance for error reporting. Defaults to ConsoleLogger. */ + logger?: Logger; + /** Role IDs that should trigger mention handlers (in addition to direct user mentions). Defaults to DISCORD_MENTION_ROLE_IDS env var (comma-separated). */ mentionRoleIds?: string[]; - /** Discord application public key for webhook signature verification */ - publicKey: string; + /** Discord application public key for webhook signature verification. Defaults to DISCORD_PUBLIC_KEY env var. */ + publicKey?: string; + /** Override bot username (optional) */ + userName?: string; } /** diff --git a/packages/adapter-gchat/src/index.ts b/packages/adapter-gchat/src/index.ts index 5c1eb95a..3788a7df 100644 --- a/packages/adapter-gchat/src/index.ts +++ b/packages/adapter-gchat/src/index.ts @@ -80,14 +80,16 @@ export interface GoogleChatAdapterBaseConfig { * User email to impersonate for Workspace Events API calls. * Required when using domain-wide delegation. * This user must have access to the Chat spaces you want to subscribe to. + * Defaults to GOOGLE_CHAT_IMPERSONATE_USER env var. */ impersonateUser?: string; - /** Logger instance for error reporting */ - logger: Logger; + /** Logger instance for error reporting. Defaults to ConsoleLogger. */ + logger?: Logger; /** * Pub/Sub topic for receiving all messages via Workspace Events. * When set, the adapter will automatically create subscriptions when added to a space. * Format: "projects/my-project/topics/my-topic" + * Defaults to GOOGLE_CHAT_PUBSUB_TOPIC env var. */ pubsubTopic?: string; /** Override bot username (optional) */ @@ -98,7 +100,7 @@ export interface GoogleChatAdapterBaseConfig { export interface GoogleChatAdapterServiceAccountConfig extends GoogleChatAdapterBaseConfig { auth?: never; - /** Service account credentials JSON */ + /** Service account credentials JSON. Defaults to GOOGLE_CHAT_CREDENTIALS env var (JSON). */ credentials: ServiceAccountCredentials; useApplicationDefaultCredentials?: never; } @@ -115,6 +117,7 @@ export interface GoogleChatAdapterADCConfig * - Workload Identity Federation (external_account JSON) * - GCE/Cloud Run/Cloud Functions default service account * - gcloud auth application-default login (local development) + * Defaults to GOOGLE_CHAT_USE_ADC env var. */ useApplicationDefaultCredentials: true; } @@ -128,10 +131,19 @@ export interface GoogleChatAdapterCustomAuthConfig useApplicationDefaultCredentials?: never; } +/** Config with no auth fields - will auto-detect from env vars */ +export interface GoogleChatAdapterAutoConfig + extends GoogleChatAdapterBaseConfig { + auth?: never; + credentials?: never; + useApplicationDefaultCredentials?: never; +} + export type GoogleChatAdapterConfig = | GoogleChatAdapterServiceAccountConfig | GoogleChatAdapterADCConfig - | GoogleChatAdapterCustomAuthConfig; + | GoogleChatAdapterCustomAuthConfig + | GoogleChatAdapterAutoConfig; // Re-export GoogleChatThreadId from thread-utils export type { GoogleChatThreadId } from "./thread-utils"; @@ -267,13 +279,17 @@ export class GoogleChatAdapter implements Adapter { /** User info cache for display name lookups - initialized later in initialize() */ private userInfoCache: UserInfoCache; - constructor(config: GoogleChatAdapterConfig) { - this.logger = config.logger; + constructor( + config: GoogleChatAdapterConfig = {} as GoogleChatAdapterAutoConfig + ) { + this.logger = config.logger ?? new ConsoleLogger("info").child("gchat"); this.userName = config.userName || "bot"; // Initialize with null state - will be updated in initialize() this.userInfoCache = new UserInfoCache(null, this.logger); - this.pubsubTopic = config.pubsubTopic; - this.impersonateUser = config.impersonateUser; + this.pubsubTopic = + config.pubsubTopic ?? process.env.GOOGLE_CHAT_PUBSUB_TOPIC; + this.impersonateUser = + config.impersonateUser ?? process.env.GOOGLE_CHAT_IMPERSONATE_USER; this.endpointUrl = config.endpointUrl; let authClient: Parameters[0]["auth"]; @@ -310,10 +326,25 @@ export class GoogleChatAdapter implements Adapter { // Custom auth client provided directly (e.g., Vercel OIDC) this.customAuth = config.auth; authClient = config.auth; + } else if (process.env.GOOGLE_CHAT_CREDENTIALS) { + // Auto-detect from env vars: service account credentials + const credentialsJson = JSON.parse( + process.env.GOOGLE_CHAT_CREDENTIALS + ) as ServiceAccountCredentials; + this.credentials = credentialsJson; + authClient = new auth.JWT({ + email: credentialsJson.client_email, + key: credentialsJson.private_key, + scopes, + }); + } else if (process.env.GOOGLE_CHAT_USE_ADC === "true") { + // Auto-detect from env vars: ADC + this.useADC = true; + authClient = new auth.GoogleAuth({ scopes }); } else { throw new ValidationError( "gchat", - "GoogleChatAdapter requires one of: credentials, useApplicationDefaultCredentials, or auth" + "Authentication is required. Set GOOGLE_CHAT_CREDENTIALS or GOOGLE_CHAT_USE_ADC=true, or provide credentials/auth in config." ); } @@ -2488,80 +2519,10 @@ export class GoogleChatAdapter implements Adapter { } } -export function createGoogleChatAdapter(config?: { - auth?: Parameters[0]["auth"]; - credentials?: ServiceAccountCredentials; - endpointUrl?: string; - impersonateUser?: string; - logger?: Logger; - pubsubTopic?: string; - useApplicationDefaultCredentials?: boolean; - userName?: string; -}): GoogleChatAdapter { - const logger = config?.logger ?? new ConsoleLogger("info").child("gchat"); - - // Auto-detect auth mode. Only fall back to env vars for auth fields when - // the caller hasn't provided ANY auth field, so we don't mix auth modes. - const hasAuthConfig = !!( - config?.auth || - config?.credentials || - config?.useApplicationDefaultCredentials - ); - - if (config?.auth) { - return new GoogleChatAdapter({ - auth: config.auth, - endpointUrl: config.endpointUrl, - impersonateUser: - config.impersonateUser ?? process.env.GOOGLE_CHAT_IMPERSONATE_USER, - logger, - pubsubTopic: config.pubsubTopic ?? process.env.GOOGLE_CHAT_PUBSUB_TOPIC, - userName: config.userName, - }); - } - - // Service account credentials from config or env - let credentialsJson = config?.credentials; - if ( - !(credentialsJson || hasAuthConfig) && - process.env.GOOGLE_CHAT_CREDENTIALS - ) { - credentialsJson = JSON.parse( - process.env.GOOGLE_CHAT_CREDENTIALS - ) as ServiceAccountCredentials; - } - if (credentialsJson) { - return new GoogleChatAdapter({ - credentials: credentialsJson, - endpointUrl: config?.endpointUrl, - impersonateUser: - config?.impersonateUser ?? process.env.GOOGLE_CHAT_IMPERSONATE_USER, - logger, - pubsubTopic: config?.pubsubTopic ?? process.env.GOOGLE_CHAT_PUBSUB_TOPIC, - userName: config?.userName, - }); - } - - // Application Default Credentials - if ( - config?.useApplicationDefaultCredentials || - (!hasAuthConfig && process.env.GOOGLE_CHAT_USE_ADC === "true") - ) { - return new GoogleChatAdapter({ - useApplicationDefaultCredentials: true, - endpointUrl: config?.endpointUrl, - impersonateUser: - config?.impersonateUser ?? process.env.GOOGLE_CHAT_IMPERSONATE_USER, - logger, - pubsubTopic: config?.pubsubTopic ?? process.env.GOOGLE_CHAT_PUBSUB_TOPIC, - userName: config?.userName, - }); - } - - throw new ValidationError( - "gchat", - "Authentication is required. Set GOOGLE_CHAT_CREDENTIALS or GOOGLE_CHAT_USE_ADC=true, or provide credentials/auth in config." - ); +export function createGoogleChatAdapter( + config?: GoogleChatAdapterConfig +): GoogleChatAdapter { + return new GoogleChatAdapter(config); } // Re-export card converter for advanced use diff --git a/packages/adapter-github/src/index.ts b/packages/adapter-github/src/index.ts index fa857ac1..f8f0b24c 100644 --- a/packages/adapter-github/src/index.ts +++ b/packages/adapter-github/src/index.ts @@ -23,6 +23,7 @@ import { ConsoleLogger, convertEmojiPlaceholders, Message } from "chat"; import { cardToGitHubMarkdown } from "./cards"; import { GitHubFormatConverter } from "./markdown"; import type { + GitHubAdapterAutoConfig, GitHubAdapterConfig, GitHubIssueComment, GitHubRawMessage, @@ -125,17 +126,39 @@ export class GitHubAdapter return this.appCredentials !== null && this.octokit === null; } - constructor(config: GitHubAdapterConfig) { - this.webhookSecret = config.webhookSecret; - this.logger = config.logger; - this.userName = config.userName; + constructor(config: GitHubAdapterConfig = {} as GitHubAdapterAutoConfig) { + const webhookSecret = + config.webhookSecret ?? process.env.GITHUB_WEBHOOK_SECRET; + if (!webhookSecret) { + throw new ValidationError( + "github", + "webhookSecret is required. Set GITHUB_WEBHOOK_SECRET or provide it in config." + ); + } + this.webhookSecret = webhookSecret; + this.logger = config.logger ?? new ConsoleLogger("info").child("github"); + this.userName = + config.userName ?? process.env.GITHUB_BOT_USERNAME ?? "github-bot"; this._botUserId = config.botUserId ?? null; - // Create Octokit instance based on auth method + // Create Octokit instance based on auth method. + // Only fall back to env vars when NO auth field was explicitly provided, + // so we don't mix auth modes. + const hasExplicitAuth = !!( + ("token" in config && config.token) || + ("appId" in config && config.appId) || + ("privateKey" in config && config.privateKey) + ); + if ("token" in config && config.token) { // PAT mode - single Octokit instance this.octokit = new Octokit({ auth: config.token }); - } else if ("appId" in config && config.appId) { + } else if ( + "appId" in config && + config.appId && + "privateKey" in config && + config.privateKey + ) { if ("installationId" in config && config.installationId) { // Single-tenant app mode - fixed installation this.octokit = new Octokit({ @@ -156,10 +179,42 @@ export class GitHubAdapter "GitHub adapter initialized in multi-tenant mode (installation ID will be extracted from webhooks)" ); } - } else { - throw new Error( - "GitHubAdapter requires either token or appId/privateKey" + } else if (hasExplicitAuth) { + // Some auth fields were provided but not enough to configure + throw new ValidationError( + "github", + "Authentication is required. Set GITHUB_TOKEN or GITHUB_APP_ID/GITHUB_PRIVATE_KEY, or provide token/appId+privateKey in config." ); + } else { + // Auto-detect from env vars + const token = process.env.GITHUB_TOKEN; + if (token) { + this.octokit = new Octokit({ auth: token }); + } else { + const appId = process.env.GITHUB_APP_ID; + const privateKey = process.env.GITHUB_PRIVATE_KEY; + if (appId && privateKey) { + const installationIdRaw = process.env.GITHUB_INSTALLATION_ID + ? Number.parseInt(process.env.GITHUB_INSTALLATION_ID, 10) + : undefined; + if (installationIdRaw) { + this.octokit = new Octokit({ + authStrategy: createAppAuth, + auth: { appId, privateKey, installationId: installationIdRaw }, + }); + } else { + this.appCredentials = { appId, privateKey }; + this.logger.info( + "GitHub adapter initialized in multi-tenant mode (installation ID will be extracted from webhooks)" + ); + } + } else { + throw new ValidationError( + "github", + "Authentication is required. Set GITHUB_TOKEN or GITHUB_APP_ID/GITHUB_PRIVATE_KEY, or provide token/appId+privateKey in config." + ); + } + } } } @@ -1225,84 +1280,8 @@ export class GitHubAdapter * }); * ``` */ -export function createGitHubAdapter(config?: { - appId?: string; - botUserId?: number; - installationId?: number; - logger?: Logger; - privateKey?: string; - token?: string; - userName?: string; - webhookSecret?: string; -}): GitHubAdapter { - const logger = config?.logger ?? new ConsoleLogger("info").child("github"); - const webhookSecret = - config?.webhookSecret ?? process.env.GITHUB_WEBHOOK_SECRET; - if (!webhookSecret) { - throw new ValidationError( - "github", - "webhookSecret is required. Set GITHUB_WEBHOOK_SECRET or provide it in config." - ); - } - const userName = - config?.userName ?? process.env.GITHUB_BOT_USERNAME ?? "github-bot"; - - // Auto-detect auth mode. Only fall back to env vars for auth fields when - // the caller hasn't provided ANY auth field, so we don't mix auth modes. - const hasAuthConfig = !!( - config?.token || - config?.appId || - config?.privateKey - ); - - const token = - config?.token ?? (hasAuthConfig ? undefined : process.env.GITHUB_TOKEN); - if (token) { - return new GitHubAdapter({ - token, - webhookSecret, - userName, - botUserId: config?.botUserId, - logger, - }); - } - - const appId = - config?.appId ?? (hasAuthConfig ? undefined : process.env.GITHUB_APP_ID); - const privateKey = - config?.privateKey ?? - (hasAuthConfig ? undefined : process.env.GITHUB_PRIVATE_KEY); - if (appId && privateKey) { - const installationIdRaw = - config?.installationId ?? - (process.env.GITHUB_INSTALLATION_ID - ? Number.parseInt(process.env.GITHUB_INSTALLATION_ID, 10) - : undefined); - if (installationIdRaw) { - // Single-tenant app mode - return new GitHubAdapter({ - appId, - privateKey, - installationId: installationIdRaw, - webhookSecret, - userName, - botUserId: config?.botUserId, - logger, - }); - } - // Multi-tenant app mode - return new GitHubAdapter({ - appId, - privateKey, - webhookSecret, - userName, - botUserId: config?.botUserId, - logger, - }); - } - - throw new ValidationError( - "github", - "Authentication is required. Set GITHUB_TOKEN or GITHUB_APP_ID/GITHUB_PRIVATE_KEY, or provide token/appId+privateKey in config." - ); +export function createGitHubAdapter( + config?: GitHubAdapterConfig +): GitHubAdapter { + return new GitHubAdapter(config); } diff --git a/packages/adapter-github/src/types.ts b/packages/adapter-github/src/types.ts index d9c7a738..5c90716f 100644 --- a/packages/adapter-github/src/types.ts +++ b/packages/adapter-github/src/types.ts @@ -17,18 +17,20 @@ interface GitHubAdapterBaseConfig { * Used for self-message detection. If not provided, will be fetched on first API call. */ botUserId?: number; - /** Logger instance for error reporting */ - logger: Logger; + /** Logger instance for error reporting. Defaults to ConsoleLogger. */ + logger?: Logger; /** * Bot username (e.g., "my-bot" or "my-bot[bot]" for GitHub Apps). * Used for @-mention detection. + * Defaults to GITHUB_BOT_USERNAME env var or "github-bot". */ - userName: string; + userName?: string; /** * Webhook secret for HMAC-SHA256 verification. * Set this in your GitHub webhook settings. + * Defaults to GITHUB_WEBHOOK_SECRET env var. */ - webhookSecret: string; + webhookSecret?: string; } /** @@ -73,13 +75,24 @@ export interface GitHubAdapterMultiTenantAppConfig token?: never; } +/** + * Configuration with no auth fields - will auto-detect from env vars. + */ +export interface GitHubAdapterAutoConfig extends GitHubAdapterBaseConfig { + appId?: never; + installationId?: never; + privateKey?: never; + token?: never; +} + /** * GitHub adapter configuration - PAT, single-tenant App, or multi-tenant App. */ export type GitHubAdapterConfig = | GitHubAdapterPATConfig | GitHubAdapterAppConfig - | GitHubAdapterMultiTenantAppConfig; + | GitHubAdapterMultiTenantAppConfig + | GitHubAdapterAutoConfig; // ============================================================================= // Thread ID diff --git a/packages/adapter-linear/src/index.ts b/packages/adapter-linear/src/index.ts index 18657be3..98ac9490 100644 --- a/packages/adapter-linear/src/index.ts +++ b/packages/adapter-linear/src/index.ts @@ -21,6 +21,7 @@ import { cardToLinearMarkdown } from "./cards"; import { LinearFormatConverter } from "./markdown"; import type { CommentWebhookPayload, + LinearAdapterAutoConfig, LinearAdapterConfig, LinearCommentData, LinearRawMessage, @@ -111,10 +112,19 @@ export class LinearAdapter return this._botUserId ?? undefined; } - constructor(config: LinearAdapterConfig) { - this.webhookSecret = config.webhookSecret; - this.logger = config.logger; - this.userName = config.userName; + constructor(config: LinearAdapterConfig = {} as LinearAdapterAutoConfig) { + const webhookSecret = + config.webhookSecret ?? process.env.LINEAR_WEBHOOK_SECRET; + if (!webhookSecret) { + throw new ValidationError( + "linear", + "webhookSecret is required. Set LINEAR_WEBHOOK_SECRET or provide it in config." + ); + } + this.webhookSecret = webhookSecret; + this.logger = config.logger ?? new ConsoleLogger("info").child("linear"); + this.userName = + config.userName ?? process.env.LINEAR_BOT_USERNAME ?? "linear-bot"; // Create LinearClient based on auth method // @see https://linear.app/developers/sdk @@ -131,9 +141,27 @@ export class LinearAdapter clientSecret: config.clientSecret, }; } else { - throw new Error( - "LinearAdapter requires either apiKey, accessToken, or clientId/clientSecret" - ); + // Auto-detect from env vars + const apiKey = process.env.LINEAR_API_KEY; + if (apiKey) { + this.linearClient = new LinearClient({ apiKey }); + } else { + const accessToken = process.env.LINEAR_ACCESS_TOKEN; + if (accessToken) { + this.linearClient = new LinearClient({ accessToken }); + } else { + const clientId = process.env.LINEAR_CLIENT_ID; + const clientSecret = process.env.LINEAR_CLIENT_SECRET; + if (clientId && clientSecret) { + this.clientCredentials = { clientId, clientSecret }; + } else { + throw new ValidationError( + "linear", + "Authentication is required. Set LINEAR_API_KEY, LINEAR_ACCESS_TOKEN, or LINEAR_CLIENT_ID/LINEAR_CLIENT_SECRET, or provide auth in config." + ); + } + } + } } } @@ -902,77 +930,8 @@ export class LinearAdapter * }); * ``` */ -export function createLinearAdapter(config?: { - accessToken?: string; - apiKey?: string; - clientId?: string; - clientSecret?: string; - logger?: Logger; - userName?: string; - webhookSecret?: string; -}): LinearAdapter { - const logger = config?.logger ?? new ConsoleLogger("info").child("linear"); - const webhookSecret = - config?.webhookSecret ?? process.env.LINEAR_WEBHOOK_SECRET; - if (!webhookSecret) { - throw new ValidationError( - "linear", - "webhookSecret is required. Set LINEAR_WEBHOOK_SECRET or provide it in config." - ); - } - const userName = - config?.userName ?? process.env.LINEAR_BOT_USERNAME ?? "linear-bot"; - - // Auto-detect auth mode. Only fall back to env vars for auth fields when - // the caller hasn't provided ANY auth field, so we don't mix auth modes. - const hasAuthConfig = !!( - config?.apiKey || - config?.accessToken || - config?.clientId || - config?.clientSecret - ); - - const apiKey = - config?.apiKey ?? (hasAuthConfig ? undefined : process.env.LINEAR_API_KEY); - if (apiKey) { - return new LinearAdapter({ - apiKey, - webhookSecret, - userName, - logger, - }); - } - - const accessToken = - config?.accessToken ?? - (hasAuthConfig ? undefined : process.env.LINEAR_ACCESS_TOKEN); - if (accessToken) { - return new LinearAdapter({ - accessToken, - webhookSecret, - userName, - logger, - }); - } - - const clientId = - config?.clientId ?? - (hasAuthConfig ? undefined : process.env.LINEAR_CLIENT_ID); - const clientSecret = - config?.clientSecret ?? - (hasAuthConfig ? undefined : process.env.LINEAR_CLIENT_SECRET); - if (clientId && clientSecret) { - return new LinearAdapter({ - clientId, - clientSecret, - webhookSecret, - userName, - logger, - }); - } - - throw new ValidationError( - "linear", - "Authentication is required. Set LINEAR_API_KEY, LINEAR_ACCESS_TOKEN, or LINEAR_CLIENT_ID/LINEAR_CLIENT_SECRET, or provide auth in config." - ); +export function createLinearAdapter( + config?: LinearAdapterConfig +): LinearAdapter { + return new LinearAdapter(config); } diff --git a/packages/adapter-linear/src/types.ts b/packages/adapter-linear/src/types.ts index 45b60fbb..af13f39a 100644 --- a/packages/adapter-linear/src/types.ts +++ b/packages/adapter-linear/src/types.ts @@ -15,19 +15,21 @@ import type { Logger } from "chat"; * Base configuration options shared by all auth methods. */ interface LinearAdapterBaseConfig { - /** Logger instance for error reporting */ - logger: Logger; + /** Logger instance for error reporting. Defaults to ConsoleLogger. */ + logger?: Logger; /** * Bot display name used for @-mention detection. * For API key auth, this is typically the user's display name. * For OAuth app auth with actor=app, this is the app name. + * Defaults to LINEAR_BOT_USERNAME env var or "linear-bot". */ - userName: string; + userName?: string; /** * Webhook signing secret for HMAC-SHA256 verification. * Found on the webhook detail page in Linear settings. + * Defaults to LINEAR_WEBHOOK_SECRET env var. */ - webhookSecret: string; + webhookSecret?: string; } /** @@ -38,8 +40,10 @@ interface LinearAdapterBaseConfig { */ export interface LinearAdapterAPIKeyConfig extends LinearAdapterBaseConfig { accessToken?: never; - /** Personal API key from Linear Settings > Security & Access */ + /** Personal API key from Linear Settings > Security & Access. Defaults to LINEAR_API_KEY env var. */ apiKey: string; + clientId?: never; + clientSecret?: never; } /** @@ -49,7 +53,7 @@ export interface LinearAdapterAPIKeyConfig extends LinearAdapterBaseConfig { * @see https://linear.app/developers/oauth-2-0-authentication */ export interface LinearAdapterOAuthConfig extends LinearAdapterBaseConfig { - /** OAuth access token obtained through the OAuth flow */ + /** OAuth access token obtained through the OAuth flow. Defaults to LINEAR_ACCESS_TOKEN env var. */ accessToken: string; apiKey?: never; clientId?: never; @@ -68,19 +72,30 @@ export interface LinearAdapterOAuthConfig extends LinearAdapterBaseConfig { export interface LinearAdapterAppConfig extends LinearAdapterBaseConfig { accessToken?: never; apiKey?: never; - /** OAuth application client ID */ + /** OAuth application client ID. Defaults to LINEAR_CLIENT_ID env var. */ clientId: string; - /** OAuth application client secret */ + /** OAuth application client secret. Defaults to LINEAR_CLIENT_SECRET env var. */ clientSecret: string; } +/** + * Configuration with no auth fields - will auto-detect from env vars. + */ +export interface LinearAdapterAutoConfig extends LinearAdapterBaseConfig { + accessToken?: never; + apiKey?: never; + clientId?: never; + clientSecret?: never; +} + /** * Linear adapter configuration - API Key, OAuth token, or OAuth App (client credentials). */ export type LinearAdapterConfig = | LinearAdapterAPIKeyConfig | LinearAdapterOAuthConfig - | LinearAdapterAppConfig; + | LinearAdapterAppConfig + | LinearAdapterAutoConfig; // ============================================================================= // Thread ID diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index a2786f59..d93f894c 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -81,10 +81,10 @@ export interface SlackAdapterConfig { * Defaults to `slack:installation`. The full key will be `{prefix}:{teamId}`. */ installationKeyPrefix?: string; - /** Logger instance for error reporting */ - logger: Logger; - /** Signing secret for webhook verification */ - signingSecret: string; + /** Logger instance for error reporting. Defaults to ConsoleLogger. */ + logger?: Logger; + /** Signing secret for webhook verification. Defaults to SLACK_SIGNING_SECRET env var. */ + signingSecret?: string; /** Override bot username (optional) */ userName?: string; } @@ -333,21 +333,48 @@ export class SlackAdapter implements Adapter { return this._botUserId || undefined; } - constructor(config: SlackAdapterConfig) { - this.client = new WebClient(config.botToken); - this.signingSecret = config.signingSecret; - this.defaultBotToken = config.botToken; - this.logger = config.logger; + constructor(config: SlackAdapterConfig = {}) { + const signingSecret = + config.signingSecret ?? process.env.SLACK_SIGNING_SECRET; + if (!signingSecret) { + throw new ValidationError( + "slack", + "signingSecret is required. Set SLACK_SIGNING_SECRET or provide it in config." + ); + } + + // Auth fields (botToken, clientId, clientSecret) are modal: botToken's + // presence selects single-workspace mode, its absence selects multi-workspace + // (per-team token lookup via installations). Only fall back to env vars + // in zero-config mode (no config fields provided at all). + const zeroConfig = !( + config.botToken || + config.clientId || + config.clientSecret + ); + + const botToken = + config.botToken ?? (zeroConfig ? process.env.SLACK_BOT_TOKEN : undefined); + + this.client = new WebClient(botToken); + this.signingSecret = signingSecret; + this.defaultBotToken = botToken; + this.logger = config.logger ?? new ConsoleLogger("info").child("slack"); this.userName = config.userName || "bot"; this._botUserId = config.botUserId || null; - this.clientId = config.clientId; - this.clientSecret = config.clientSecret; + this.clientId = + config.clientId ?? (zeroConfig ? process.env.SLACK_CLIENT_ID : undefined); + this.clientSecret = + config.clientSecret ?? + (zeroConfig ? process.env.SLACK_CLIENT_SECRET : undefined); this.installationKeyPrefix = config.installationKeyPrefix ?? "slack:installation"; - if (config.encryptionKey) { - this.encryptionKey = decodeKey(config.encryptionKey); + const encryptionKey = + config.encryptionKey ?? process.env.SLACK_ENCRYPTION_KEY; + if (encryptionKey) { + this.encryptionKey = decodeKey(encryptionKey); } } @@ -3098,41 +3125,8 @@ export class SlackAdapter implements Adapter { } } -export function createSlackAdapter( - config?: Partial -): SlackAdapter { - const signingSecret = - config?.signingSecret ?? process.env.SLACK_SIGNING_SECRET; - if (!signingSecret) { - throw new ValidationError( - "slack", - "signingSecret is required. Set SLACK_SIGNING_SECRET or provide it in config." - ); - } - // Auth fields (botToken, clientId, clientSecret) are modal: botToken's - // presence selects single-workspace mode, its absence selects multi-workspace - // (per-team token lookup via installations). Only fall back to env vars - // in zero-config mode (no config provided at all). - const zeroConfig = !config; - - const resolved: SlackAdapterConfig = { - signingSecret, - botToken: - config?.botToken ?? - (zeroConfig ? process.env.SLACK_BOT_TOKEN : undefined), - clientId: - config?.clientId ?? - (zeroConfig ? process.env.SLACK_CLIENT_ID : undefined), - clientSecret: - config?.clientSecret ?? - (zeroConfig ? process.env.SLACK_CLIENT_SECRET : undefined), - encryptionKey: config?.encryptionKey ?? process.env.SLACK_ENCRYPTION_KEY, - installationKeyPrefix: config?.installationKeyPrefix, - logger: config?.logger ?? new ConsoleLogger("info").child("slack"), - userName: config?.userName, - botUserId: config?.botUserId, - }; - return new SlackAdapter(resolved); +export function createSlackAdapter(config?: SlackAdapterConfig): SlackAdapter { + return new SlackAdapter(config ?? {}); } // Re-export card converter for advanced use diff --git a/packages/adapter-teams/src/index.ts b/packages/adapter-teams/src/index.ts index ceb9fab9..26419140 100644 --- a/packages/adapter-teams/src/index.ts +++ b/packages/adapter-teams/src/index.ts @@ -100,16 +100,16 @@ interface GraphChatMessage { } export interface TeamsAdapterConfig { - /** Microsoft App ID */ - appId: string; - /** Microsoft App Password */ - appPassword: string; - /** Microsoft App Tenant ID */ + /** Microsoft App ID. Defaults to TEAMS_APP_ID env var. */ + appId?: string; + /** Microsoft App Password. Defaults to TEAMS_APP_PASSWORD env var. */ + appPassword?: string; + /** Microsoft App Tenant ID. Defaults to TEAMS_APP_TENANT_ID env var. */ appTenantId?: string; /** Microsoft App Type */ appType?: "MultiTenant" | "SingleTenant"; - /** Logger instance for error reporting */ - logger: Logger; + /** Logger instance for error reporting. Defaults to ConsoleLogger. */ + logger?: Logger; /** Override bot username (optional) */ userName?: string; } @@ -138,14 +138,38 @@ export class TeamsAdapter implements Adapter { private chat: ChatInstance | null = null; private readonly logger: Logger; private readonly formatConverter = new TeamsFormatConverter(); - private readonly config: TeamsAdapterConfig; + private readonly config: Required< + Pick + > & + TeamsAdapterConfig; + + constructor(config: TeamsAdapterConfig = {}) { + const appId = config.appId ?? process.env.TEAMS_APP_ID; + if (!appId) { + throw new ValidationError( + "teams", + "appId is required. Set TEAMS_APP_ID or provide it in config." + ); + } + const appPassword = config.appPassword ?? process.env.TEAMS_APP_PASSWORD; + if (!appPassword) { + throw new ValidationError( + "teams", + "appPassword is required. Set TEAMS_APP_PASSWORD or provide it in config." + ); + } + const appTenantId = config.appTenantId ?? process.env.TEAMS_APP_TENANT_ID; - constructor(config: TeamsAdapterConfig) { - this.config = config; - this.logger = config.logger; + this.config = { + ...config, + appId, + appPassword, + appTenantId, + }; + this.logger = config.logger ?? new ConsoleLogger("info").child("teams"); this.userName = config.userName || "bot"; - if (config.appType === "SingleTenant" && !config.appTenantId) { + if (config.appType === "SingleTenant" && !appTenantId) { throw new ValidationError( "teams", "appTenantId is required for SingleTenant app type" @@ -154,21 +178,21 @@ export class TeamsAdapter implements Adapter { // Pass empty config object, credentials go via factory const auth = new ConfigurationBotFrameworkAuthentication({ - MicrosoftAppId: config.appId, - MicrosoftAppPassword: config.appPassword, + MicrosoftAppId: appId, + MicrosoftAppPassword: appPassword, MicrosoftAppType: config.appType || "MultiTenant", MicrosoftAppTenantId: - config.appType === "SingleTenant" ? config.appTenantId : undefined, + config.appType === "SingleTenant" ? appTenantId : undefined, }); this.botAdapter = new ServerlessCloudAdapter(auth); // Initialize Microsoft Graph client for message history (requires tenant ID) - if (config.appTenantId) { + if (appTenantId) { const credential = new ClientSecretCredential( - config.appTenantId, - config.appId, - config.appPassword + appTenantId, + appId, + appPassword ); const authProvider = new TokenCredentialAuthenticationProvider( @@ -2368,32 +2392,8 @@ export class TeamsAdapter implements Adapter { } } -export function createTeamsAdapter( - config?: Partial -): TeamsAdapter { - const appId = config?.appId ?? process.env.TEAMS_APP_ID; - if (!appId) { - throw new ValidationError( - "teams", - "appId is required. Set TEAMS_APP_ID or provide it in config." - ); - } - const appPassword = config?.appPassword ?? process.env.TEAMS_APP_PASSWORD; - if (!appPassword) { - throw new ValidationError( - "teams", - "appPassword is required. Set TEAMS_APP_PASSWORD or provide it in config." - ); - } - const resolved: TeamsAdapterConfig = { - appId, - appPassword, - appTenantId: config?.appTenantId ?? process.env.TEAMS_APP_TENANT_ID, - appType: config?.appType, - logger: config?.logger ?? new ConsoleLogger("info").child("teams"), - userName: config?.userName, - }; - return new TeamsAdapter(resolved); +export function createTeamsAdapter(config?: TeamsAdapterConfig): TeamsAdapter { + return new TeamsAdapter(config ?? {}); } // Re-export card converter for advanced use diff --git a/packages/adapter-telegram/src/index.ts b/packages/adapter-telegram/src/index.ts index abf0ceca..a0098645 100644 --- a/packages/adapter-telegram/src/index.ts +++ b/packages/adapter-telegram/src/index.ts @@ -137,20 +137,36 @@ export class TelegramAdapter return this._runtimeMode; } - constructor( - config: TelegramAdapterConfig & { logger: Logger; userName?: string } - ) { - this.botToken = config.botToken; - this.apiBaseUrl = (config.apiBaseUrl ?? TELEGRAM_API_BASE).replace( - TRAILING_SLASHES_REGEX, - "" - ); - this.secretToken = config.secretToken; - this.logger = config.logger; - this._userName = this.normalizeUserName(config.userName ?? "bot"); - this.hasExplicitUserName = Boolean(config.userName); + constructor(config: TelegramAdapterConfig = {}) { + const botToken = config.botToken ?? process.env.TELEGRAM_BOT_TOKEN; + if (!botToken) { + throw new ValidationError( + "telegram", + "botToken is required. Set TELEGRAM_BOT_TOKEN or provide it in config." + ); + } + + this.botToken = botToken; + this.apiBaseUrl = ( + config.apiBaseUrl ?? + process.env.TELEGRAM_API_BASE_URL ?? + TELEGRAM_API_BASE + ).replace(TRAILING_SLASHES_REGEX, ""); + this.secretToken = + config.secretToken ?? process.env.TELEGRAM_WEBHOOK_SECRET_TOKEN; + this.logger = config.logger ?? new ConsoleLogger("info").child("telegram"); + const userName = config.userName ?? process.env.TELEGRAM_BOT_USERNAME; + this._userName = this.normalizeUserName(userName ?? "bot"); + this.hasExplicitUserName = Boolean(userName); this.mode = config.mode ?? "auto"; this.longPolling = config.longPolling; + + if (!["auto", "webhook", "polling"].includes(this.mode)) { + throw new ValidationError( + "telegram", + `Invalid mode: ${this.mode}. Expected "auto", "webhook", or "polling".` + ); + } } async initialize(chat: ChatInstance): Promise { @@ -1661,43 +1677,9 @@ export class TelegramAdapter } export function createTelegramAdapter( - config?: Partial< - TelegramAdapterConfig & { logger: Logger; userName?: string } - > + config?: TelegramAdapterConfig ): TelegramAdapter { - const botToken = config?.botToken ?? process.env.TELEGRAM_BOT_TOKEN; - if (!botToken) { - throw new ValidationError( - "telegram", - "botToken is required. Set TELEGRAM_BOT_TOKEN or provide it in config." - ); - } - - const apiBaseUrl = - config?.apiBaseUrl ?? - process.env.TELEGRAM_API_BASE_URL ?? - TELEGRAM_API_BASE; - const secretToken = - config?.secretToken ?? process.env.TELEGRAM_WEBHOOK_SECRET_TOKEN; - const userName = config?.userName ?? process.env.TELEGRAM_BOT_USERNAME; - const mode = config?.mode ?? "auto"; - - if (!["auto", "webhook", "polling"].includes(mode)) { - throw new ValidationError( - "telegram", - `Invalid mode: ${mode}. Expected "auto", "webhook", or "polling".` - ); - } - - return new TelegramAdapter({ - botToken, - apiBaseUrl, - mode, - longPolling: config?.longPolling, - secretToken, - logger: config?.logger ?? new ConsoleLogger("info").child("telegram"), - userName, - }); + return new TelegramAdapter(config ?? {}); } export { TelegramFormatConverter } from "./markdown"; diff --git a/packages/adapter-telegram/src/types.ts b/packages/adapter-telegram/src/types.ts index e59f623e..b313fa9e 100644 --- a/packages/adapter-telegram/src/types.ts +++ b/packages/adapter-telegram/src/types.ts @@ -2,14 +2,18 @@ * Telegram adapter types. */ +import type { Logger } from "chat"; + /** * Telegram adapter configuration. */ export interface TelegramAdapterConfig { - /** Optional custom API base URL (defaults to https://api.telegram.org). */ + /** Optional custom API base URL (defaults to https://api.telegram.org). Defaults to TELEGRAM_API_BASE_URL env var. */ apiBaseUrl?: string; - /** Telegram bot token from BotFather. */ - botToken: string; + /** Telegram bot token from BotFather. Defaults to TELEGRAM_BOT_TOKEN env var. */ + botToken?: string; + /** Logger instance for error reporting. Defaults to ConsoleLogger. */ + logger?: Logger; /** Optional long-polling configuration for getUpdates flow. */ longPolling?: TelegramLongPollingConfig; /** @@ -19,8 +23,10 @@ export interface TelegramAdapterConfig { * - polling: polling-only mode */ mode?: TelegramAdapterMode; - /** Optional webhook secret token checked against x-telegram-bot-api-secret-token. */ + /** Optional webhook secret token checked against x-telegram-bot-api-secret-token. Defaults to TELEGRAM_WEBHOOK_SECRET_TOKEN env var. */ secretToken?: string; + /** Override bot username (optional). Defaults to TELEGRAM_BOT_USERNAME env var. */ + userName?: string; } export type TelegramAdapterMode = "auto" | "webhook" | "polling"; From 8d2743a7eb3b05a17786e4f20392f561b2d62d8a Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 12:06:18 -0800 Subject: [PATCH 02/14] Update tests --- packages/adapter-github/src/index.test.ts | 2 +- packages/adapter-linear/src/index.test.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/adapter-github/src/index.test.ts b/packages/adapter-github/src/index.test.ts index f8442799..d80f0a07 100644 --- a/packages/adapter-github/src/index.test.ts +++ b/packages/adapter-github/src/index.test.ts @@ -229,7 +229,7 @@ describe("GitHubAdapter", () => { userName: "bot", logger: mockLogger, } as never) - ).toThrow("GitHubAdapter requires either token or appId/privateKey"); + ).toThrow("Authentication is required"); }); it("should set botUserId when provided in config", () => { diff --git a/packages/adapter-linear/src/index.test.ts b/packages/adapter-linear/src/index.test.ts index 25b2f0f2..48be8720 100644 --- a/packages/adapter-linear/src/index.test.ts +++ b/packages/adapter-linear/src/index.test.ts @@ -421,9 +421,7 @@ describe("constructor", () => { userName: "my-bot", logger: createMockLogger(), } as never) - ).toThrow( - "LinearAdapter requires either apiKey, accessToken, or clientId/clientSecret" - ); + ).toThrow("Authentication is required"); }); it("should have undefined botUserId before initialization", () => { From c0d3e06503308f6e7f517859efbde5dc37e551ff Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 12:09:48 -0800 Subject: [PATCH 03/14] Add new adapter config tests --- packages/adapter-discord/src/index.test.ts | 78 ++++++++++++- packages/adapter-gchat/src/index.test.ts | 66 ++++++++++- packages/adapter-slack/src/index.test.ts | 55 ++++++++- packages/adapter-teams/src/index.test.ts | 120 ++++++++++++-------- packages/adapter-telegram/src/index.test.ts | 64 +++++++++++ 5 files changed, 332 insertions(+), 51 deletions(-) diff --git a/packages/adapter-discord/src/index.test.ts b/packages/adapter-discord/src/index.test.ts index 38a20436..28231cd7 100644 --- a/packages/adapter-discord/src/index.test.ts +++ b/packages/adapter-discord/src/index.test.ts @@ -6,7 +6,7 @@ import { generateKeyPairSync, sign } from "node:crypto"; import { ValidationError } from "@chat-adapter/shared"; import type { ChatInstance, Logger } from "chat"; import { InteractionType } from "discord-api-types/v10"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createDiscordAdapter, DiscordAdapter } from "./index"; import { DiscordFormatConverter } from "./markdown"; @@ -107,6 +107,82 @@ describe("createDiscordAdapter", () => { }); }); +// ============================================================================ +// Constructor env var resolution +// ============================================================================ + +describe("constructor env var resolution", () => { + const savedEnv = { ...process.env }; + + beforeEach(() => { + for (const key of Object.keys(process.env)) { + if (key.startsWith("DISCORD_")) { + // biome-ignore lint/performance/noDelete: env var removal requires delete + delete process.env[key]; + } + } + }); + + afterEach(() => { + process.env = { ...savedEnv }; + }); + + it("should throw when botToken is missing and env var not set", () => { + expect(() => new DiscordAdapter({})).toThrow("botToken is required"); + }); + + it("should throw when publicKey is missing and env var not set", () => { + expect(() => new DiscordAdapter({ botToken: "test" })).toThrow( + "publicKey is required" + ); + }); + + it("should throw when applicationId is missing and env var not set", () => { + expect( + () => new DiscordAdapter({ botToken: "test", publicKey: testPublicKey }) + ).toThrow("applicationId is required"); + }); + + it("should resolve all fields from env vars", () => { + process.env.DISCORD_BOT_TOKEN = "env-token"; + process.env.DISCORD_PUBLIC_KEY = testPublicKey; + process.env.DISCORD_APPLICATION_ID = "env-app-id"; + const adapter = new DiscordAdapter(); + expect(adapter).toBeInstanceOf(DiscordAdapter); + expect(adapter.userName).toBe("bot"); + }); + + it("should resolve mentionRoleIds from DISCORD_MENTION_ROLE_IDS env var", () => { + process.env.DISCORD_BOT_TOKEN = "env-token"; + process.env.DISCORD_PUBLIC_KEY = testPublicKey; + process.env.DISCORD_APPLICATION_ID = "env-app-id"; + process.env.DISCORD_MENTION_ROLE_IDS = "role1, role2, role3"; + const adapter = new DiscordAdapter(); + expect(adapter).toBeInstanceOf(DiscordAdapter); + }); + + it("should default logger when not provided", () => { + process.env.DISCORD_BOT_TOKEN = "env-token"; + process.env.DISCORD_PUBLIC_KEY = testPublicKey; + process.env.DISCORD_APPLICATION_ID = "env-app-id"; + const adapter = new DiscordAdapter(); + expect(adapter).toBeInstanceOf(DiscordAdapter); + }); + + it("should prefer config values over env vars", () => { + process.env.DISCORD_BOT_TOKEN = "env-token"; + process.env.DISCORD_PUBLIC_KEY = testPublicKey; + process.env.DISCORD_APPLICATION_ID = "env-app-id"; + const adapter = new DiscordAdapter({ + botToken: "config-token", + publicKey: testPublicKey, + applicationId: "config-app-id", + userName: "mybot", + }); + expect(adapter.userName).toBe("mybot"); + }); +}); + // ============================================================================ // Thread ID Encoding/Decoding Tests // ============================================================================ diff --git a/packages/adapter-gchat/src/index.test.ts b/packages/adapter-gchat/src/index.test.ts index 564a4deb..272b5aaa 100644 --- a/packages/adapter-gchat/src/index.test.ts +++ b/packages/adapter-gchat/src/index.test.ts @@ -1,6 +1,6 @@ import { AdapterRateLimitError } from "@chat-adapter/shared"; import type { ChatInstance, Lock, Logger, StateAdapter } from "chat"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createGoogleChatAdapter, GoogleChatAdapter, @@ -278,6 +278,13 @@ describe("GoogleChatAdapter", () => { expect(adapter.name).toBe("gchat"); }); + it("should default logger when not provided", () => { + const adapter = new GoogleChatAdapter({ + useApplicationDefaultCredentials: true, + }); + expect(adapter).toBeInstanceOf(GoogleChatAdapter); + }); + it("should restore bot user ID from state on initialize", async () => { const adapter = createGoogleChatAdapter({ credentials: TEST_CREDENTIALS, @@ -309,6 +316,63 @@ describe("GoogleChatAdapter", () => { }); }); + describe("constructor env var resolution", () => { + const savedEnv = { ...process.env }; + + beforeEach(() => { + for (const key of Object.keys(process.env)) { + if (key.startsWith("GOOGLE_CHAT_")) { + // biome-ignore lint/performance/noDelete: env var removal requires delete + delete process.env[key]; + } + } + }); + + afterEach(() => { + process.env = { ...savedEnv }; + }); + + it("should throw when no auth is configured and no env vars set", () => { + expect(() => new GoogleChatAdapter()).toThrow( + "Authentication is required" + ); + }); + + it("should resolve credentials from GOOGLE_CHAT_CREDENTIALS env var", () => { + process.env.GOOGLE_CHAT_CREDENTIALS = JSON.stringify(TEST_CREDENTIALS); + const adapter = new GoogleChatAdapter(); + expect(adapter).toBeInstanceOf(GoogleChatAdapter); + }); + + it("should resolve ADC from GOOGLE_CHAT_USE_ADC env var", () => { + process.env.GOOGLE_CHAT_USE_ADC = "true"; + const adapter = new GoogleChatAdapter(); + expect(adapter).toBeInstanceOf(GoogleChatAdapter); + }); + + it("should resolve pubsubTopic from GOOGLE_CHAT_PUBSUB_TOPIC env var", () => { + process.env.GOOGLE_CHAT_CREDENTIALS = JSON.stringify(TEST_CREDENTIALS); + process.env.GOOGLE_CHAT_PUBSUB_TOPIC = "projects/test/topics/test"; + const adapter = new GoogleChatAdapter(); + expect(adapter).toBeInstanceOf(GoogleChatAdapter); + }); + + it("should resolve impersonateUser from GOOGLE_CHAT_IMPERSONATE_USER env var", () => { + process.env.GOOGLE_CHAT_CREDENTIALS = JSON.stringify(TEST_CREDENTIALS); + process.env.GOOGLE_CHAT_IMPERSONATE_USER = "user@example.com"; + const adapter = new GoogleChatAdapter(); + expect(adapter).toBeInstanceOf(GoogleChatAdapter); + }); + + it("should prefer config credentials over env vars", () => { + process.env.GOOGLE_CHAT_USE_ADC = "true"; + const adapter = new GoogleChatAdapter({ + credentials: TEST_CREDENTIALS, + }); + expect(adapter).toBeInstanceOf(GoogleChatAdapter); + }); + }); + describe("isDM", () => { it("should return true for DM thread IDs", () => { const adapter = createGoogleChatAdapter({ diff --git a/packages/adapter-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts index 005d516d..8b4903ed 100644 --- a/packages/adapter-slack/src/index.test.ts +++ b/packages/adapter-slack/src/index.test.ts @@ -5,7 +5,7 @@ import { createHmac, randomBytes } from "node:crypto"; import { ValidationError } from "@chat-adapter/shared"; import type { ChatInstance, Logger, StateAdapter } from "chat"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { SlackInstallation } from "./index"; import { createSlackAdapter, SlackAdapter } from "./index"; @@ -95,6 +95,59 @@ describe("createSlackAdapter", () => { }); }); +// ============================================================================ +// Constructor env var resolution +// ============================================================================ + +describe("constructor env var resolution", () => { + const savedEnv = { ...process.env }; + + beforeEach(() => { + for (const key of Object.keys(process.env)) { + if (key.startsWith("SLACK_")) { + // biome-ignore lint/performance/noDelete: env var removal requires delete + delete process.env[key]; + } + } + }); + + afterEach(() => { + process.env = { ...savedEnv }; + }); + + it("should throw when signingSecret is missing and env var not set", () => { + expect(() => new SlackAdapter({})).toThrow("signingSecret is required"); + }); + + it("should resolve signingSecret from SLACK_SIGNING_SECRET env var", () => { + process.env.SLACK_SIGNING_SECRET = "env-signing-secret"; + const adapter = new SlackAdapter(); + expect(adapter).toBeInstanceOf(SlackAdapter); + }); + + it("should resolve botToken from SLACK_BOT_TOKEN in zero-config mode", () => { + process.env.SLACK_SIGNING_SECRET = "env-signing-secret"; + process.env.SLACK_BOT_TOKEN = "xoxb-env-token"; + const adapter = new SlackAdapter(); + expect(adapter).toBeInstanceOf(SlackAdapter); + }); + + it("should default logger when not provided", () => { + process.env.SLACK_SIGNING_SECRET = "env-signing-secret"; + const adapter = new SlackAdapter(); + expect(adapter).toBeInstanceOf(SlackAdapter); + }); + + it("should prefer config values over env vars", () => { + process.env.SLACK_SIGNING_SECRET = "env-secret"; + const adapter = new SlackAdapter({ + signingSecret: "config-secret", + logger: mockLogger, + }); + expect(adapter).toBeInstanceOf(SlackAdapter); + }); +}); + // ============================================================================ // Thread ID Encoding/Decoding Tests // ============================================================================ diff --git a/packages/adapter-teams/src/index.test.ts b/packages/adapter-teams/src/index.test.ts index 2f326ebc..4c4bf392 100644 --- a/packages/adapter-teams/src/index.test.ts +++ b/packages/adapter-teams/src/index.test.ts @@ -10,7 +10,7 @@ import { } from "@chat-adapter/shared"; import type { Logger } from "chat"; import { NotImplementedError } from "chat"; -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createTeamsAdapter, TeamsAdapter } from "./index"; const TEAMS_PREFIX_PATTERN = /^teams:/; @@ -236,63 +236,87 @@ describe("TeamsAdapter", () => { }); // ========================================================================== - // createTeamsAdapter Factory Tests + // Constructor env var resolution // ========================================================================== - describe("createTeamsAdapter factory", () => { - it("should throw when appId is missing and env var not set", () => { - const origAppId = process.env.TEAMS_APP_ID; - const origAppPwd = process.env.TEAMS_APP_PASSWORD; - // biome-ignore lint/performance/noDelete: env var removal requires delete - delete process.env.TEAMS_APP_ID; - // biome-ignore lint/performance/noDelete: env var removal requires delete - delete process.env.TEAMS_APP_PASSWORD; - try { - expect(() => createTeamsAdapter({})).toThrow(ValidationError); - } finally { - if (origAppId !== undefined) { - process.env.TEAMS_APP_ID = origAppId; - } - if (origAppPwd !== undefined) { - process.env.TEAMS_APP_PASSWORD = origAppPwd; + describe("constructor env var resolution", () => { + const savedEnv = { ...process.env }; + + beforeEach(() => { + for (const key of Object.keys(process.env)) { + if (key.startsWith("TEAMS_")) { + // biome-ignore lint/performance/noDelete: env var removal requires delete + delete process.env[key]; } } }); + afterEach(() => { + process.env = { ...savedEnv }; + }); + + it("should throw when appId is missing and env var not set", () => { + expect(() => new TeamsAdapter({})).toThrow("appId is required"); + }); + it("should throw when appPassword is missing and env var not set", () => { - const origAppPwd = process.env.TEAMS_APP_PASSWORD; - // biome-ignore lint/performance/noDelete: env var removal requires delete - delete process.env.TEAMS_APP_PASSWORD; - try { - expect(() => - createTeamsAdapter({ appId: "test-id", logger: mockLogger }) - ).toThrow(ValidationError); - } finally { - if (origAppPwd !== undefined) { - process.env.TEAMS_APP_PASSWORD = origAppPwd; - } - } + expect(() => new TeamsAdapter({ appId: "test" })).toThrow( + "appPassword is required" + ); + }); + + it("should resolve appId from TEAMS_APP_ID env var", () => { + process.env.TEAMS_APP_ID = "env-app-id"; + process.env.TEAMS_APP_PASSWORD = "env-password"; + const adapter = new TeamsAdapter(); + expect(adapter).toBeInstanceOf(TeamsAdapter); + }); + + it("should resolve appPassword from TEAMS_APP_PASSWORD env var", () => { + process.env.TEAMS_APP_PASSWORD = "env-password"; + const adapter = new TeamsAdapter({ appId: "test" }); + expect(adapter).toBeInstanceOf(TeamsAdapter); }); - it("should pick up appTenantId from env", () => { - const origTenant = process.env.TEAMS_APP_TENANT_ID; + it("should resolve appTenantId from TEAMS_APP_TENANT_ID env var", () => { process.env.TEAMS_APP_TENANT_ID = "env-tenant"; - try { - // Should not throw - means it's initializing graph client with the tenant - const adapter = createTeamsAdapter({ - appId: "test", - appPassword: "test", - logger: mockLogger, - }); - expect(adapter).toBeInstanceOf(TeamsAdapter); - } finally { - if (origTenant !== undefined) { - process.env.TEAMS_APP_TENANT_ID = origTenant; - } else { - // biome-ignore lint/performance/noDelete: env var removal requires delete - delete process.env.TEAMS_APP_TENANT_ID; - } - } + const adapter = new TeamsAdapter({ + appId: "test", + appPassword: "test", + }); + expect(adapter).toBeInstanceOf(TeamsAdapter); + }); + + it("should default logger when not provided", () => { + process.env.TEAMS_APP_ID = "env-app-id"; + process.env.TEAMS_APP_PASSWORD = "env-password"; + const adapter = new TeamsAdapter(); + expect(adapter).toBeInstanceOf(TeamsAdapter); + }); + + it("should prefer config values over env vars", () => { + process.env.TEAMS_APP_ID = "env-app-id"; + const adapter = new TeamsAdapter({ + appId: "config-app-id", + appPassword: "test", + }); + expect(adapter).toBeInstanceOf(TeamsAdapter); + expect(adapter.name).toBe("teams"); + }); + }); + + // ========================================================================== + // createTeamsAdapter Factory Tests + // ========================================================================== + + describe("createTeamsAdapter factory", () => { + it("should delegate to constructor", () => { + const adapter = createTeamsAdapter({ + appId: "test", + appPassword: "test", + logger: mockLogger, + }); + expect(adapter).toBeInstanceOf(TeamsAdapter); }); }); diff --git a/packages/adapter-telegram/src/index.test.ts b/packages/adapter-telegram/src/index.test.ts index 90dce409..839835a9 100644 --- a/packages/adapter-telegram/src/index.test.ts +++ b/packages/adapter-telegram/src/index.test.ts @@ -165,6 +165,70 @@ describe("createTelegramAdapter", () => { }); }); +describe("constructor env var resolution", () => { + const savedEnv = { ...process.env }; + + beforeEach(() => { + for (const key of Object.keys(process.env)) { + if (key.startsWith("TELEGRAM_")) { + // biome-ignore lint/performance/noDelete: env var removal requires delete + delete process.env[key]; + } + } + }); + + afterEach(() => { + process.env = { ...savedEnv }; + }); + + it("should throw when botToken is missing and env var not set", () => { + expect(() => new TelegramAdapter({})).toThrow("botToken is required"); + }); + + it("should resolve botToken from TELEGRAM_BOT_TOKEN env var", () => { + process.env.TELEGRAM_BOT_TOKEN = "env-bot-token"; + const adapter = new TelegramAdapter(); + expect(adapter).toBeInstanceOf(TelegramAdapter); + }); + + it("should resolve secretToken from TELEGRAM_WEBHOOK_SECRET_TOKEN env var", () => { + process.env.TELEGRAM_BOT_TOKEN = "env-bot-token"; + process.env.TELEGRAM_WEBHOOK_SECRET_TOKEN = "env-secret"; + const adapter = new TelegramAdapter(); + expect(adapter).toBeInstanceOf(TelegramAdapter); + }); + + it("should resolve userName from TELEGRAM_BOT_USERNAME env var", () => { + process.env.TELEGRAM_BOT_TOKEN = "env-bot-token"; + process.env.TELEGRAM_BOT_USERNAME = "env_bot_name"; + const adapter = new TelegramAdapter(); + expect(adapter.userName).toBe("env_bot_name"); + }); + + it("should resolve apiBaseUrl from TELEGRAM_API_BASE_URL env var", () => { + process.env.TELEGRAM_BOT_TOKEN = "env-bot-token"; + process.env.TELEGRAM_API_BASE_URL = "https://custom-api.example.com"; + const adapter = new TelegramAdapter(); + expect(adapter).toBeInstanceOf(TelegramAdapter); + }); + + it("should default logger when not provided", () => { + process.env.TELEGRAM_BOT_TOKEN = "env-bot-token"; + const adapter = new TelegramAdapter(); + expect(adapter).toBeInstanceOf(TelegramAdapter); + }); + + it("should prefer config values over env vars", () => { + process.env.TELEGRAM_BOT_TOKEN = "env-token"; + process.env.TELEGRAM_BOT_USERNAME = "env-name"; + const adapter = new TelegramAdapter({ + botToken: "config-token", + userName: "config-name", + }); + expect(adapter.userName).toBe("config-name"); + }); +}); + describe("TelegramAdapter", () => { it("encodes and decodes thread IDs", () => { const adapter = createTelegramAdapter({ From 989195194cc0672b3377db089d042d6c57c9c785 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 12:13:01 -0800 Subject: [PATCH 04/14] Update index.mdx --- apps/docs/content/docs/adapters/index.mdx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/docs/content/docs/adapters/index.mdx b/apps/docs/content/docs/adapters/index.mdx index c2d91091..6c91527e 100644 --- a/apps/docs/content/docs/adapters/index.mdx +++ b/apps/docs/content/docs/adapters/index.mdx @@ -126,4 +126,14 @@ bot.onNewMention(async (thread) => { Each adapter auto-detects credentials from environment variables, so you only need to pass config when overriding defaults. +You can use either factory functions or constructors directly — both resolve from env vars: + +```typescript +// Factory function +const slack = createSlackAdapter(); + +// Constructor (equivalent) +const slack = new SlackAdapter(); +``` + Each adapter creates a webhook handler accessible via `bot.webhooks.slack`, `bot.webhooks.teams`, etc. From 3280b2151702c34395096f52bedab981118aad3d Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 13:15:04 -0800 Subject: [PATCH 05/14] Update index.test.ts --- packages/adapter-gchat/src/index.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/adapter-gchat/src/index.test.ts b/packages/adapter-gchat/src/index.test.ts index 272b5aaa..8ed8b409 100644 --- a/packages/adapter-gchat/src/index.test.ts +++ b/packages/adapter-gchat/src/index.test.ts @@ -245,6 +245,21 @@ describe("GoogleChatAdapter", () => { }); describe("constructor / initialization", () => { + const savedEnv = { ...process.env }; + + beforeEach(() => { + for (const key of Object.keys(process.env)) { + if (key.startsWith("GOOGLE_CHAT_")) { + // biome-ignore lint/performance/noDelete: env var removal requires delete + delete process.env[key]; + } + } + }); + + afterEach(() => { + process.env = { ...savedEnv }; + }); + it("should use provided userName", () => { const adapter = createGoogleChatAdapter({ credentials: TEST_CREDENTIALS, From 46270922e4496e4217aa7e542d3a3567be849796 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 13:15:55 -0800 Subject: [PATCH 06/14] Fix polynomial regex issue --- packages/adapter-telegram/src/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/adapter-telegram/src/index.ts b/packages/adapter-telegram/src/index.ts index a0098645..fe34c189 100644 --- a/packages/adapter-telegram/src/index.ts +++ b/packages/adapter-telegram/src/index.ts @@ -64,7 +64,11 @@ const TELEGRAM_CAPTION_LIMIT = 1024; const TELEGRAM_SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token"; const MESSAGE_ID_PATTERN = /^([^:]+):(\d+)$/; const TELEGRAM_MARKDOWN_PARSE_MODE = "Markdown"; -const TRAILING_SLASHES_REGEX = /\/+$/; +const trimTrailingSlashes = (url: string): string => { + let end = url.length; + while (end > 0 && url[end - 1] === "/") end--; + return url.slice(0, end); +}; const MESSAGE_SEQUENCE_PATTERN = /:(\d+)$/; const LEADING_AT_PATTERN = /^@+/; const EMOJI_PLACEHOLDER_PATTERN = /^\{\{emoji:([a-z0-9_]+)\}\}$/i; @@ -147,11 +151,11 @@ export class TelegramAdapter } this.botToken = botToken; - this.apiBaseUrl = ( + this.apiBaseUrl = trimTrailingSlashes( config.apiBaseUrl ?? - process.env.TELEGRAM_API_BASE_URL ?? - TELEGRAM_API_BASE - ).replace(TRAILING_SLASHES_REGEX, ""); + process.env.TELEGRAM_API_BASE_URL ?? + TELEGRAM_API_BASE + ); this.secretToken = config.secretToken ?? process.env.TELEGRAM_WEBHOOK_SECRET_TOKEN; this.logger = config.logger ?? new ConsoleLogger("info").child("telegram"); From f384e21ae9b688a184cfb6e8412b14785e2fcd5e Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 13:18:44 -0800 Subject: [PATCH 07/14] Update index.ts --- packages/adapter-telegram/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/adapter-telegram/src/index.ts b/packages/adapter-telegram/src/index.ts index fe34c189..a88c32cc 100644 --- a/packages/adapter-telegram/src/index.ts +++ b/packages/adapter-telegram/src/index.ts @@ -66,7 +66,9 @@ const MESSAGE_ID_PATTERN = /^([^:]+):(\d+)$/; const TELEGRAM_MARKDOWN_PARSE_MODE = "Markdown"; const trimTrailingSlashes = (url: string): string => { let end = url.length; - while (end > 0 && url[end - 1] === "/") end--; + while (end > 0 && url[end - 1] === "/") { + end--; + } return url.slice(0, end); }; const MESSAGE_SEQUENCE_PATTERN = /:(\d+)$/; From fc794a20d951934c2a833459d8e40ddd5cdb7073 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 13:21:29 -0800 Subject: [PATCH 08/14] Remove unused suppressions --- packages/adapter-discord/src/index.test.ts | 1 - packages/adapter-gchat/src/index.test.ts | 2 -- packages/adapter-slack/src/index.test.ts | 1 - packages/adapter-teams/src/index.test.ts | 1 - packages/adapter-telegram/src/index.test.ts | 1 - 5 files changed, 6 deletions(-) diff --git a/packages/adapter-discord/src/index.test.ts b/packages/adapter-discord/src/index.test.ts index 28231cd7..58f0869e 100644 --- a/packages/adapter-discord/src/index.test.ts +++ b/packages/adapter-discord/src/index.test.ts @@ -117,7 +117,6 @@ describe("constructor env var resolution", () => { beforeEach(() => { for (const key of Object.keys(process.env)) { if (key.startsWith("DISCORD_")) { - // biome-ignore lint/performance/noDelete: env var removal requires delete delete process.env[key]; } } diff --git a/packages/adapter-gchat/src/index.test.ts b/packages/adapter-gchat/src/index.test.ts index 8ed8b409..05a0da8c 100644 --- a/packages/adapter-gchat/src/index.test.ts +++ b/packages/adapter-gchat/src/index.test.ts @@ -250,7 +250,6 @@ describe("GoogleChatAdapter", () => { beforeEach(() => { for (const key of Object.keys(process.env)) { if (key.startsWith("GOOGLE_CHAT_")) { - // biome-ignore lint/performance/noDelete: env var removal requires delete delete process.env[key]; } } @@ -337,7 +336,6 @@ describe("GoogleChatAdapter", () => { beforeEach(() => { for (const key of Object.keys(process.env)) { if (key.startsWith("GOOGLE_CHAT_")) { - // biome-ignore lint/performance/noDelete: env var removal requires delete delete process.env[key]; } } diff --git a/packages/adapter-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts index 8b4903ed..6973fd5b 100644 --- a/packages/adapter-slack/src/index.test.ts +++ b/packages/adapter-slack/src/index.test.ts @@ -105,7 +105,6 @@ describe("constructor env var resolution", () => { beforeEach(() => { for (const key of Object.keys(process.env)) { if (key.startsWith("SLACK_")) { - // biome-ignore lint/performance/noDelete: env var removal requires delete delete process.env[key]; } } diff --git a/packages/adapter-teams/src/index.test.ts b/packages/adapter-teams/src/index.test.ts index 4c4bf392..1316ef77 100644 --- a/packages/adapter-teams/src/index.test.ts +++ b/packages/adapter-teams/src/index.test.ts @@ -245,7 +245,6 @@ describe("TeamsAdapter", () => { beforeEach(() => { for (const key of Object.keys(process.env)) { if (key.startsWith("TEAMS_")) { - // biome-ignore lint/performance/noDelete: env var removal requires delete delete process.env[key]; } } diff --git a/packages/adapter-telegram/src/index.test.ts b/packages/adapter-telegram/src/index.test.ts index 839835a9..20c2cc1d 100644 --- a/packages/adapter-telegram/src/index.test.ts +++ b/packages/adapter-telegram/src/index.test.ts @@ -171,7 +171,6 @@ describe("constructor env var resolution", () => { beforeEach(() => { for (const key of Object.keys(process.env)) { if (key.startsWith("TELEGRAM_")) { - // biome-ignore lint/performance/noDelete: env var removal requires delete delete process.env[key]; } } From cb371c75737e1f48e2a4ab72c4e2f3a077625800 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 13:27:40 -0800 Subject: [PATCH 09/14] Misc fixes --- packages/adapter-discord/src/index.ts | 6 +----- packages/adapter-slack/src/index.ts | 1 + 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/adapter-discord/src/index.ts b/packages/adapter-discord/src/index.ts index 6f1f865a..ddf9c7d6 100644 --- a/packages/adapter-discord/src/index.ts +++ b/packages/adapter-discord/src/index.ts @@ -146,11 +146,7 @@ export class DiscordAdapter implements Adapter { async initialize(chat: ChatInstance): Promise { this.chat = chat; - this.logger.info("Discord adapter initialized", { - applicationId: this.applicationId, - // Log full public key for debugging - it's public, not secret - publicKey: this.publicKey, - }); + this.logger.info("Discord adapter initialized"); } /** diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index d93f894c..2d253482 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -348,6 +348,7 @@ export class SlackAdapter implements Adapter { // (per-team token lookup via installations). Only fall back to env vars // in zero-config mode (no config fields provided at all). const zeroConfig = !( + config.signingSecret || config.botToken || config.clientId || config.clientSecret From e70678ff7e2aa9f83343f24a3b840ff316122a8b Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 13:34:01 -0800 Subject: [PATCH 10/14] Update index.test.ts --- packages/adapter-teams/src/index.test.ts | 27 ++++++++++++------------ 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/adapter-teams/src/index.test.ts b/packages/adapter-teams/src/index.test.ts index 1316ef77..885f56f3 100644 --- a/packages/adapter-teams/src/index.test.ts +++ b/packages/adapter-teams/src/index.test.ts @@ -82,6 +82,20 @@ describe("ESM compatibility", () => { }); describe("TeamsAdapter", () => { + const savedEnv = { ...process.env }; + + beforeEach(() => { + for (const key of Object.keys(process.env)) { + if (key.startsWith("TEAMS_")) { + delete process.env[key]; + } + } + }); + + afterEach(() => { + process.env = { ...savedEnv }; + }); + it("should export createTeamsAdapter function", () => { expect(typeof createTeamsAdapter).toBe("function"); }); @@ -240,19 +254,6 @@ describe("TeamsAdapter", () => { // ========================================================================== describe("constructor env var resolution", () => { - const savedEnv = { ...process.env }; - - beforeEach(() => { - for (const key of Object.keys(process.env)) { - if (key.startsWith("TEAMS_")) { - delete process.env[key]; - } - } - }); - - afterEach(() => { - process.env = { ...savedEnv }; - }); it("should throw when appId is missing and env var not set", () => { expect(() => new TeamsAdapter({})).toThrow("appId is required"); From 131cf5b8d8452bd101b83b12c90ad92c3c25069a Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 13:35:27 -0800 Subject: [PATCH 11/14] Update index.test.ts --- packages/adapter-teams/src/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/adapter-teams/src/index.test.ts b/packages/adapter-teams/src/index.test.ts index 885f56f3..dd2c4d3f 100644 --- a/packages/adapter-teams/src/index.test.ts +++ b/packages/adapter-teams/src/index.test.ts @@ -254,7 +254,6 @@ describe("TeamsAdapter", () => { // ========================================================================== describe("constructor env var resolution", () => { - it("should throw when appId is missing and env var not set", () => { expect(() => new TeamsAdapter({})).toThrow("appId is required"); }); From 3352d67d42b354a7f5f4dd7f9caff27a7fc747e4 Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Wed, 4 Mar 2026 13:40:16 -0800 Subject: [PATCH 12/14] Update index.ts --- packages/adapter-gchat/src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/adapter-gchat/src/index.ts b/packages/adapter-gchat/src/index.ts index 3788a7df..2369805f 100644 --- a/packages/adapter-gchat/src/index.ts +++ b/packages/adapter-gchat/src/index.ts @@ -415,7 +415,6 @@ export class GoogleChatAdapter implements Adapter { this.logger.info("onThreadSubscribe called", { threadId, hasPubsubTopic: !!this.pubsubTopic, - pubsubTopic: this.pubsubTopic, }); if (!this.pubsubTopic) { @@ -543,7 +542,6 @@ export class GoogleChatAdapter implements Adapter { this.logger.info("Creating Workspace Events subscription", { spaceName, - pubsubTopic, }); const result = await createSpaceSubscription( From 9107c015b6c3279e8e5bdbc6fbec3904709dee9b Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Thu, 5 Mar 2026 16:18:21 -0800 Subject: [PATCH 13/14] Update index.mdx --- apps/docs/content/docs/adapters/index.mdx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/docs/content/docs/adapters/index.mdx b/apps/docs/content/docs/adapters/index.mdx index 6c91527e..c2d91091 100644 --- a/apps/docs/content/docs/adapters/index.mdx +++ b/apps/docs/content/docs/adapters/index.mdx @@ -126,14 +126,4 @@ bot.onNewMention(async (thread) => { Each adapter auto-detects credentials from environment variables, so you only need to pass config when overriding defaults. -You can use either factory functions or constructors directly — both resolve from env vars: - -```typescript -// Factory function -const slack = createSlackAdapter(); - -// Constructor (equivalent) -const slack = new SlackAdapter(); -``` - Each adapter creates a webhook handler accessible via `bot.webhooks.slack`, `bot.webhooks.teams`, etc. From 579186b86212d20e5c8cef97195d9f4a3d66945d Mon Sep 17 00:00:00 2001 From: Hayden Bleasel Date: Thu, 5 Mar 2026 16:26:31 -0800 Subject: [PATCH 14/14] Test and lint fixes --- packages/adapter-teams/src/index.test.ts | 2 +- packages/adapter-teams/src/index.ts | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/adapter-teams/src/index.test.ts b/packages/adapter-teams/src/index.test.ts index 1dd1129e..006bd260 100644 --- a/packages/adapter-teams/src/index.test.ts +++ b/packages/adapter-teams/src/index.test.ts @@ -260,7 +260,7 @@ describe("TeamsAdapter", () => { it("should throw when appPassword is missing and env var not set", () => { expect(() => new TeamsAdapter({ appId: "test" })).toThrow( - "appPassword is required" + "One of appPassword, certificate, or federated must be provided" ); }); diff --git a/packages/adapter-teams/src/index.ts b/packages/adapter-teams/src/index.ts index 802221a4..9d7d7674 100644 --- a/packages/adapter-teams/src/index.ts +++ b/packages/adapter-teams/src/index.ts @@ -263,11 +263,9 @@ export class TeamsAdapter implements Adapter { } if (appTenantId) { - graphCredential = new ClientCertificateCredential( - appTenantId, - appId, - { certificate: certificatePrivateKey } - ); + graphCredential = new ClientCertificateCredential(appTenantId, appId, { + certificate: certificatePrivateKey, + }); } } else if (config.federated) { credentialsFactory = new FederatedServiceClientCredentialsFactory( @@ -291,9 +289,7 @@ export class TeamsAdapter implements Adapter { const auth = new ConfigurationBotFrameworkAuthentication( { ...botFrameworkConfig, - ...(appPassword - ? { MicrosoftAppPassword: appPassword } - : {}), + ...(appPassword ? { MicrosoftAppPassword: appPassword } : {}), }, credentialsFactory );