"use strict"; /** * @module botbuilder */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BotFrameworkAdapter = exports.USER_AGENT = void 0; const z = __importStar(require("zod")); const interfaces_1 = require("./interfaces"); const core_http_1 = require("@azure/core-http"); const zod_1 = require("./zod"); const os_1 = require("os"); const botbuilder_stdlib_1 = require("botbuilder-stdlib"); const activityValidator_1 = require("./activityValidator"); const botbuilder_core_1 = require("botbuilder-core"); const botframework_connector_1 = require("botframework-connector"); const botframework_streaming_1 = require("botframework-streaming"); const streaming_1 = require("./streaming"); // Retrieve additional information, i.e., host operating system, host OS release, architecture, Node.js version const ARCHITECTURE = (0, os_1.arch)(); const TYPE = (0, os_1.type)(); const RELEASE = (0, os_1.release)(); const NODE_VERSION = process.version; // eslint-disable-next-line @typescript-eslint/no-require-imports const pjson = require('../package.json'); exports.USER_AGENT = `Microsoft-BotFramework/3.1 BotBuilder/${pjson.version} (Node.js,Version=${NODE_VERSION}; ${TYPE} ${RELEASE}; ${ARCHITECTURE})`; const OAUTH_ENDPOINT = 'https://api.botframework.com'; const US_GOV_OAUTH_ENDPOINT = 'https://api.botframework.azure.us'; /** * A [BotAdapter](xref:botbuilder-core.BotAdapter) that can connect a bot to a service endpoint. * Implements [IUserTokenProvider](xref:botbuilder-core.IUserTokenProvider). * * @remarks * The bot adapter encapsulates authentication processes and sends activities to and receives * activities from the Bot Connector Service. When your bot receives an activity, the adapter * creates a turn context object, passes it to your bot application logic, and sends responses * back to the user's channel. * * The adapter processes and directs incoming activities in through the bot middleware pipeline to * your bot logic and then back out again. As each activity flows in and out of the bot, each * piece of middleware can inspect or act upon the activity, both before and after the bot logic runs. * Use the [use](xref:botbuilder-core.BotAdapter.use) method to add [Middleware](xref:botbuilder-core.Middleware) * objects to your adapter's middleware collection. * * For more information, see the articles on * [How bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and * [Middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware). * * For example: * ```JavaScript * const { BotFrameworkAdapter } = require('botbuilder'); * * const adapter = new BotFrameworkAdapter({ * appId: process.env.MicrosoftAppId, * appPassword: process.env.MicrosoftAppPassword * }); * * adapter.onTurnError = async (context, error) => { * // Catch-all logic for errors. * }; * ``` */ /** * @deprecated Use `CloudAdapter` instead. */ class BotFrameworkAdapter extends botbuilder_core_1.BotAdapter { /** * Creates a new instance of the [BotFrameworkAdapter](xref:botbuilder.BotFrameworkAdapter) class. * * @param settings Optional. The settings to use for this adapter instance. * @remarks * If the `settings` parameter does not include * [channelService](xref:botbuilder.BotFrameworkAdapterSettings.channelService) or * [openIdMetadata](xref:botbuilder.BotFrameworkAdapterSettings.openIdMetadata) values, the * constructor checks the process' environment variables for these values. These values may be * set when a bot is provisioned on Azure and if so are required for the bot to work properly * in the global cloud or in a national cloud. * * The [BotFrameworkAdapterSettings](xref:botbuilder.BotFrameworkAdapterSettings) class defines * the available adapter settings. */ constructor(settings) { super(); // These keys are public to permit access to the keys from the adapter when it's a being // from library that does not have access to static properties off of BotFrameworkAdapter. // E.g. botbuilder-dialogs this.TokenApiClientCredentialsKey = Symbol('TokenApiClientCredentials'); this.settings = Object.assign({ appId: '', appPassword: '' }, settings); // If settings.certificateThumbprint & settings.certificatePrivateKey are provided, // use CertificateAppCredentials. if (this.settings.certificateThumbprint && this.settings.certificatePrivateKey) { this.credentials = new botframework_connector_1.CertificateAppCredentials(this.settings.appId, settings.certificateThumbprint, settings.certificatePrivateKey, this.settings.channelAuthTenant); this.credentialsProvider = new botframework_connector_1.SimpleCredentialProvider(this.credentials.appId, ''); } else { if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) { this.credentials = new botframework_connector_1.MicrosoftGovernmentAppCredentials(this.settings.appId, this.settings.appPassword || '', this.settings.channelAuthTenant); } else { this.credentials = new botframework_connector_1.MicrosoftAppCredentials(this.settings.appId, this.settings.appPassword || '', this.settings.channelAuthTenant); } this.credentialsProvider = new botframework_connector_1.SimpleCredentialProvider(this.credentials.appId, this.settings.appPassword || ''); } this.isEmulatingOAuthCards = false; // If no channelService or openIdMetadata values were passed in the settings, check the process' Environment Variables for values. // These values may be set when a bot is provisioned on Azure and if so are required for the bot to properly work in Public Azure or a National Cloud. this.settings.channelService = this.settings.channelService || process.env[botframework_connector_1.AuthenticationConstants.ChannelService]; this.settings.openIdMetadata = this.settings.openIdMetadata || process.env[botframework_connector_1.AuthenticationConstants.BotOpenIdMetadataKey]; this.authConfiguration = this.settings.authConfig || new botframework_connector_1.AuthenticationConfiguration(); if (this.settings.openIdMetadata) { botframework_connector_1.ChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata; botframework_connector_1.GovernmentChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata; } // If a NodeWebSocketFactoryBase was passed in, set it on the BotFrameworkAdapter. if (this.settings.webSocketFactory) { this.webSocketFactory = this.settings.webSocketFactory; } // Relocate the tenantId field used by MS Teams to a new location (from channelData to conversation) // This will only occur on activities from teams that include tenant info in channelData but NOT in conversation, // thus should be future friendly. However, once the the transition is complete. we can remove this. this.use((context, next) => __awaiter(this, void 0, void 0, function* () { if (context.activity.channelId === 'msteams' && context.activity && context.activity.conversation && !context.activity.conversation.tenantId && context.activity.channelData && context.activity.channelData.tenant) { context.activity.conversation.tenantId = context.activity.channelData.tenant.id; } yield next(); })); } /** * Used in streaming contexts to check if the streaming connection is still open for the bot to send activities. * * @returns True if the streaming connection is open, otherwise false. */ get isStreamingConnectionOpen() { var _a, _b; return (_b = (_a = this.streamingServer) === null || _a === void 0 ? void 0 : _a.isConnected) !== null && _b !== void 0 ? _b : false; } /** * @internal */ continueConversation(reference, oAuthScopeOrlogic, maybeLogic) { return __awaiter(this, void 0, void 0, function* () { let audience; if (zod_1.LogicT.safeParse(oAuthScopeOrlogic).success) { // Because the OAuthScope parameter was not provided, get the correct value via the channelService. // In this scenario, the ConnectorClient for the continued conversation can only communicate with // official channels, not with other bots. audience = botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService) ? botframework_connector_1.GovernmentConstants.ToChannelFromBotOAuthScope : botframework_connector_1.AuthenticationConstants.ToChannelFromBotOAuthScope; } else { audience = z.string().parse(oAuthScopeOrlogic); } const logicParse = zod_1.LogicT.safeParse(oAuthScopeOrlogic); const logic = logicParse.success ? logicParse.data : zod_1.LogicT.parse(maybeLogic); let credentials = this.credentials; // For authenticated flows (where the bot has an AppId), the ConversationReference's serviceUrl needs // to be trusted for the bot to acquire a token when sending activities to the conversation. // For anonymous flows, the serviceUrl should not be trusted. if (credentials.appId) { // If the provided OAuthScope doesn't match the current one on the instance's credentials, create // a new AppCredentials with the correct OAuthScope. if (credentials.oAuthScope !== audience) { // The BotFrameworkAdapter JavaScript implementation supports one Bot per instance, so get // the botAppId from the credentials. credentials = yield this.buildCredentials(credentials.appId, audience); } } const connectorClient = this.createConnectorClientInternal(reference.serviceUrl, credentials); const request = botbuilder_core_1.TurnContext.applyConversationReference({ type: botbuilder_core_1.ActivityTypes.Event, name: botbuilder_core_1.ActivityEventNames.ContinueConversation }, reference, true); const context = this.createContext(request); context.turnState.set(this.OAuthScopeKey, audience); context.turnState.set(this.ConnectorClientKey, connectorClient); yield this.runMiddleware(context, logic); }); } /** * @internal */ createConversation(reference, parametersOrLogic, maybeLogic) { return __awaiter(this, void 0, void 0, function* () { if (!reference.serviceUrl) { throw new Error('BotFrameworkAdapter.createConversation(): missing serviceUrl.'); } const logicParse = zod_1.LogicT.safeParse(parametersOrLogic); const parameterParse = botbuilder_core_1.conversationParametersObject.partial().safeParse(parametersOrLogic); const parameters = parameterParse.success ? parameterParse.data : {}; const logic = logicParse.success ? logicParse.data : zod_1.LogicT.parse(maybeLogic); // Create conversation parameters, taking care to provide defaults that can be // overridden by passed in parameters const conversationParameters = Object.assign({}, { bot: reference.bot, members: [reference.user], isGroup: false, activity: null, channelData: null, }, parameters); const client = this.createConnectorClient(reference.serviceUrl); // Mix in the tenant ID if specified. This is required for MS Teams. if (reference.conversation && reference.conversation.tenantId) { // Putting tenantId in channelData is a temporary solution while we wait for the Teams API to be updated conversationParameters.channelData = { tenant: { id: reference.conversation.tenantId } }; // Permanent solution is to put tenantId in parameters.tenantId conversationParameters.tenantId = reference.conversation.tenantId; } const response = yield client.conversations.createConversation(conversationParameters); // Initialize request and copy over new conversation ID and updated serviceUrl. const request = botbuilder_core_1.TurnContext.applyConversationReference({ type: botbuilder_core_1.ActivityTypes.Event, name: botbuilder_core_1.ActivityEventNames.CreateConversation }, reference, true); request.conversation = { id: response.id, isGroup: conversationParameters.isGroup, conversationType: null, tenantId: reference.conversation.tenantId, name: null, }; request.channelData = conversationParameters.channelData; if (response.serviceUrl) { request.serviceUrl = response.serviceUrl; } // Create context and run middleware const context = this.createContext(request); yield this.runMiddleware(context, logic); }); } /** * Asynchronously deletes an existing activity. * * This interface supports the framework and is not intended to be called directly for your code. * Use [TurnContext.deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) to delete * an activity from your bot code. * * @param context The context object for the turn. * @param reference Conversation reference information for the activity to delete. * @remarks * Not all channels support this operation. For channels that don't, this call may throw an exception. */ deleteActivity(context, reference) { return __awaiter(this, void 0, void 0, function* () { if (!reference.serviceUrl) { throw new Error('BotFrameworkAdapter.deleteActivity(): missing serviceUrl'); } if (!reference.conversation || !reference.conversation.id) { throw new Error('BotFrameworkAdapter.deleteActivity(): missing conversation or conversation.id'); } if (!reference.activityId) { throw new Error('BotFrameworkAdapter.deleteActivity(): missing activityId'); } const client = this.getOrCreateConnectorClient(context, reference.serviceUrl, this.credentials); yield client.conversations.deleteActivity(reference.conversation.id, reference.activityId); }); } /** * Asynchronously removes a member from the current conversation. * * @param context The context object for the turn. * @param memberId The ID of the member to remove from the conversation. * @remarks * Remove a member's identity information from the conversation. * * Not all channels support this operation. For channels that don't, this call may throw an exception. */ deleteConversationMember(context, memberId) { return __awaiter(this, void 0, void 0, function* () { if (!context.activity.serviceUrl) { throw new Error('BotFrameworkAdapter.deleteConversationMember(): missing serviceUrl'); } if (!context.activity.conversation || !context.activity.conversation.id) { throw new Error('BotFrameworkAdapter.deleteConversationMember(): missing conversation or conversation.id'); } const serviceUrl = context.activity.serviceUrl; const conversationId = context.activity.conversation.id; const client = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials); yield client.conversations.deleteConversationMember(conversationId, memberId); }); } /** * Asynchronously lists the members of a given activity. * * @param context The context object for the turn. * @param activityId Optional. The ID of the activity to get the members of. If not specified, the current activity ID is used. * @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for * the users involved in a given activity. * @remarks * Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for * the users involved in a given activity. * * This is different from [getConversationMembers](xref:botbuilder.BotFrameworkAdapter.getConversationMembers) * in that it will return only those users directly involved in the activity, not all members of the conversation. */ getActivityMembers(context, activityId) { return __awaiter(this, void 0, void 0, function* () { if (!activityId) { activityId = context.activity.id; } if (!context.activity.serviceUrl) { throw new Error('BotFrameworkAdapter.getActivityMembers(): missing serviceUrl'); } if (!context.activity.conversation || !context.activity.conversation.id) { throw new Error('BotFrameworkAdapter.getActivityMembers(): missing conversation or conversation.id'); } if (!activityId) { throw new Error('BotFrameworkAdapter.getActivityMembers(): missing both activityId and context.activity.id'); } const serviceUrl = context.activity.serviceUrl; const conversationId = context.activity.conversation.id; const client = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials); return yield client.conversations.getActivityMembers(conversationId, activityId); }); } /** * Asynchronously lists the members of the current conversation. * * @param context The context object for the turn. * @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for * all users currently involved in a conversation. * @remarks * Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for * all users currently involved in a conversation. * * This is different from [getActivityMembers](xref:botbuilder.BotFrameworkAdapter.getActivityMembers) * in that it will return all members of the conversation, not just those directly involved in a specific activity. */ getConversationMembers(context) { return __awaiter(this, void 0, void 0, function* () { if (!context.activity.serviceUrl) { throw new Error('BotFrameworkAdapter.getConversationMembers(): missing serviceUrl'); } if (!context.activity.conversation || !context.activity.conversation.id) { throw new Error('BotFrameworkAdapter.getConversationMembers(): missing conversation or conversation.id'); } const serviceUrl = context.activity.serviceUrl; const conversationId = context.activity.conversation.id; const client = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials); return yield client.conversations.getConversationMembers(conversationId); }); } /** * For the specified channel, asynchronously gets a page of the conversations in which this bot has participated. * * @param contextOrServiceUrl The URL of the channel server to query or a * [TurnContext](xref:botbuilder-core.TurnContext) object from a conversation on the channel. * @param continuationToken Optional. The continuation token from the previous page of results. * Omit this parameter or use `undefined` to retrieve the first page of results. * @returns A [ConversationsResult](xref:botframework-schema.ConversationsResult) object containing a page of results * and a continuation token. * @remarks * The the return value's [conversations](xref:botframework-schema.ConversationsResult.conversations) property contains a page of * [ConversationMembers](xref:botframework-schema.ConversationMembers) objects. Each object's * [id](xref:botframework-schema.ConversationMembers.id) is the ID of a conversation in which the bot has participated on this channel. * This method can be called from outside the context of a conversation, as only the bot's service URL and credentials are required. * * The channel batches results in pages. If the result's * [continuationToken](xref:botframework-schema.ConversationsResult.continuationToken) property is not empty, then * there are more pages to get. Use the returned token to get the next page of results. * If the `contextOrServiceUrl` parameter is a [TurnContext](xref:botbuilder-core.TurnContext), the URL of the channel server is * retrieved from * `contextOrServiceUrl`.[activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl). */ getConversations(contextOrServiceUrl, continuationToken) { return __awaiter(this, void 0, void 0, function* () { let client; if (typeof contextOrServiceUrl === 'object') { const context = contextOrServiceUrl; client = this.getOrCreateConnectorClient(context, context.activity.serviceUrl, this.credentials); } else { client = this.createConnectorClient(contextOrServiceUrl); } return yield client.conversations.getConversations(continuationToken ? { continuationToken: continuationToken } : undefined); }); } /** * Asynchronously attempts to retrieve the token for a user that's in a login flow. * * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn. * @param connectionName The name of the auth connection to use. * @param magicCode Optional. The validation code the user entered. * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth. * @returns A [TokenResponse](xref:botframework-schema.TokenResponse) object that contains the user token. */ getUserToken(context, connectionName, magicCode, oAuthAppCredentials) { return __awaiter(this, void 0, void 0, function* () { if (!context.activity.from || !context.activity.from.id) { throw new Error('BotFrameworkAdapter.getUserToken(): missing from or from.id'); } if (!connectionName) { throw new Error('getUserToken() requires a connectionName but none was provided.'); } this.checkEmulatingOAuthCards(context); const userId = context.activity.from.id; const url = this.oauthApiUrl(context); const client = this.createTokenApiClient(url, oAuthAppCredentials); context.turnState.set(this.TokenApiClientCredentialsKey, client); const result = yield client.userToken.getToken(userId, connectionName, { code: magicCode, channelId: context.activity.channelId }); if (!result || !result.token || result._response.status == 404) { return undefined; } else { return result; } }); } /** * Asynchronously signs out the user from the token server. * * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn. * @param connectionName Optional. The name of the auth connection to use. * @param userId Optional. The ID of the user to sign out. * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth. */ signOutUser(context, connectionName, userId, oAuthAppCredentials) { return __awaiter(this, void 0, void 0, function* () { if (!context.activity.from || !context.activity.from.id) { throw new Error('BotFrameworkAdapter.signOutUser(): missing from or from.id'); } if (!userId) { userId = context.activity.from.id; } this.checkEmulatingOAuthCards(context); const url = this.oauthApiUrl(context); const client = this.createTokenApiClient(url, oAuthAppCredentials); context.turnState.set(this.TokenApiClientCredentialsKey, client); yield client.userToken.signOut(userId, { connectionName: connectionName, channelId: context.activity.channelId, }); }); } /** * Asynchronously gets a sign-in link from the token server that can be sent as part * of a [SigninCard](xref:botframework-schema.SigninCard). * * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn. * @param connectionName The name of the auth connection to use. * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth. * @param userId Optional. The user id that will be associated with the token. * @param finalRedirect Optional. The final URL that the OAuth flow will redirect to. * @returns The sign in link. */ getSignInLink(context, connectionName, oAuthAppCredentials, userId, finalRedirect) { return __awaiter(this, void 0, void 0, function* () { if (userId && userId != context.activity.from.id) { throw new ReferenceError("cannot retrieve OAuth signin link for a user that's different from the conversation"); } this.checkEmulatingOAuthCards(context); const conversation = botbuilder_core_1.TurnContext.getConversationReference(context.activity); const url = this.oauthApiUrl(context); const client = this.createTokenApiClient(url, oAuthAppCredentials); context.turnState.set(this.TokenApiClientCredentialsKey, client); const state = { ConnectionName: connectionName, Conversation: conversation, MsAppId: client.credentials.appId, RelatesTo: context.activity.relatesTo, }; const finalState = Buffer.from(JSON.stringify(state)).toString('base64'); return (yield client.botSignIn.getSignInUrl(finalState, { channelId: context.activity.channelId, finalRedirect }))._response.bodyAsText; }); } /** * Asynchronously retrieves the token status for each configured connection for the given user. * * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn. * @param userId Optional. If present, the ID of the user to retrieve the token status for. * Otherwise, the ID of the user who sent the current activity is used. * @param includeFilter Optional. A comma-separated list of connection's to include. If present, * the `includeFilter` parameter limits the tokens this method returns. * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth. * @returns The [TokenStatus](xref:botframework-connector.TokenStatus) objects retrieved. */ getTokenStatus(context, userId, includeFilter, oAuthAppCredentials) { return __awaiter(this, void 0, void 0, function* () { if (!userId && (!context.activity.from || !context.activity.from.id)) { throw new Error('BotFrameworkAdapter.getTokenStatus(): missing from or from.id'); } this.checkEmulatingOAuthCards(context); userId = userId || context.activity.from.id; const url = this.oauthApiUrl(context); const client = this.createTokenApiClient(url, oAuthAppCredentials); context.turnState.set(this.TokenApiClientCredentialsKey, client); return (yield client.userToken.getTokenStatus(userId, { channelId: context.activity.channelId, include: includeFilter, }))._response.parsedBody; }); } /** * Asynchronously signs out the user from the token server. * * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn. * @param connectionName The name of the auth connection to use. * @param resourceUrls The list of resource URLs to retrieve tokens for. * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth. * @returns A map of the [TokenResponse](xref:botframework-schema.TokenResponse) objects by resource URL. */ getAadTokens(context, connectionName, resourceUrls, oAuthAppCredentials) { return __awaiter(this, void 0, void 0, function* () { if (!context.activity.from || !context.activity.from.id) { throw new Error('BotFrameworkAdapter.getAadTokens(): missing from or from.id'); } this.checkEmulatingOAuthCards(context); const userId = context.activity.from.id; const url = this.oauthApiUrl(context); const client = this.createTokenApiClient(url, oAuthAppCredentials); context.turnState.set(this.TokenApiClientCredentialsKey, client); return (yield client.userToken.getAadTokens(userId, connectionName, { resourceUrls: resourceUrls }, { channelId: context.activity.channelId }))._response.parsedBody; }); } /** * Asynchronously Get the raw signin resource to be sent to the user for signin. * * @param context The context object for the turn. * @param connectionName The name of the auth connection to use. * @param userId The user id that will be associated with the token. * @param finalRedirect The final URL that the OAuth flow will redirect to. * @param appCredentials Optional. The CoreAppCredentials for OAuth. * @returns The [BotSignInGetSignInResourceResponse](xref:botframework-connector.BotSignInGetSignInResourceResponse) object. */ getSignInResource(context, connectionName, userId, finalRedirect, appCredentials) { return __awaiter(this, void 0, void 0, function* () { if (!connectionName) { throw new Error('getUserToken() requires a connectionName but none was provided.'); } if (!context.activity.from || !context.activity.from.id) { throw new Error('BotFrameworkAdapter.getSignInResource(): missing from or from.id'); } // The provided userId doesn't match the from.id on the activity. (same for finalRedirect) if (userId && context.activity.from.id !== userId) { throw new Error('BotFrameworkAdapter.getSiginInResource(): cannot get signin resource for a user that is different from the conversation'); } const url = this.oauthApiUrl(context); const credentials = appCredentials; const client = this.createTokenApiClient(url, credentials); const conversation = botbuilder_core_1.TurnContext.getConversationReference(context.activity); const state = { ConnectionName: connectionName, Conversation: conversation, relatesTo: context.activity.relatesTo, MSAppId: client.credentials.appId, }; const finalState = Buffer.from(JSON.stringify(state)).toString('base64'); const options = { finalRedirect: finalRedirect }; return yield client.botSignIn.getSignInResource(finalState, options); }); } /** * Asynchronously Performs a token exchange operation such as for single sign-on. * * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn. * @param connectionName Name of the auth connection to use. * @param userId The user id that will be associated with the token. * @param tokenExchangeRequest The [TokenExchangeRequest](xref:botbuilder-schema.TokenExchangeRequest), either a token to exchange or a uri to exchange. * @param appCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth. * @returns A `Promise` representing the exchanged [TokenResponse](xref:botframework-schema.TokenResponse). */ exchangeToken(context, connectionName, userId, tokenExchangeRequest, appCredentials) { return __awaiter(this, void 0, void 0, function* () { if (!connectionName) { throw new Error('exchangeToken() requires a connectionName but none was provided.'); } if (!userId) { throw new Error('exchangeToken() requires an userId but none was provided.'); } if (tokenExchangeRequest && !tokenExchangeRequest.token && !tokenExchangeRequest.uri) { throw new Error('BotFrameworkAdapter.exchangeToken(): Either a Token or Uri property is required on the TokenExchangeRequest'); } const url = this.oauthApiUrl(context); const client = this.createTokenApiClient(url, appCredentials); return (yield client.userToken.exchangeAsync(userId, connectionName, context.activity.channelId, tokenExchangeRequest))._response.parsedBody; }); } /** * Asynchronously sends an emulated OAuth card for a channel. * * This method supports the framework and is not intended to be called directly for your code. * * @param contextOrServiceUrl The URL of the emulator. * @param emulate `true` to send an emulated OAuth card to the emulator; or `false` to not send the card. * @remarks * When testing a bot in the Bot Framework Emulator, this method can emulate the OAuth card interaction. */ emulateOAuthCards(contextOrServiceUrl, emulate) { return __awaiter(this, void 0, void 0, function* () { this.isEmulatingOAuthCards = emulate; const url = this.oauthApiUrl(contextOrServiceUrl); yield botframework_connector_1.EmulatorApiClient.emulateOAuthCards(this.credentials, url, emulate); }); } /** * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity. * * @param req An Express or Restify style request object. * @param res An Express or Restify style response object. * @param logic The function to call at the end of the middleware pipeline. * @remarks * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method: * * 1. Parses and authenticates an incoming request. * - The activity is read from the body of the incoming request. An error will be returned * if the activity can't be parsed. * - The identity of the sender is authenticated as either the Emulator or a valid Microsoft * server, using the bot's `appId` and `appPassword`. The request is rejected if the sender's * identity is not verified. * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity. * - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable). * - When this method completes, the proxy is revoked. * 1. Sends the turn context through the adapter's middleware pipeline. * 1. Sends the turn context to the `logic` function. * - The bot may perform additional routing or processing at this time. * Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete. * - After the `logic` function completes, the promise chain set up by the middleware is resolved. * * > [!TIP] * > If you see the error `TypeError: Cannot perform 'set' on a proxy that has been revoked` * > in your bot's console output, the likely cause is that an async function was used * > without using the `await` keyword. Make sure all async functions use await! * * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the * `logic` function is not called; however, all middleware prior to this point still run to completion. * For more information about the middleware pipeline, see the * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles. * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter. * * For example: * ```JavaScript * server.post('/api/messages', (req, res) => { * // Route received request to adapter for processing * adapter.processActivity(req, res, async (context) => { * // Process any messages received * if (context.activity.type === ActivityTypes.Message) { * await context.sendActivity(`Hello World`); * } * }); * }); * ``` */ processActivity(req, res, logic) { return __awaiter(this, void 0, void 0, function* () { let body; let status; let processError; try { // Parse body of request status = 400; const request = yield parseRequest(req); // Authenticate the incoming request status = 401; const authHeader = req.headers.authorization || req.headers.Authorization || ''; const identity = yield this.authenticateRequestInternal(request, authHeader); // Set the correct callerId value and discard values received over the wire request.callerId = yield this.generateCallerId(identity); // Process received activity status = 500; const context = this.createContext(request); context.turnState.set(this.BotIdentityKey, identity); const connectorClient = yield this.createConnectorClientWithIdentity(request.serviceUrl, identity); context.turnState.set(this.ConnectorClientKey, connectorClient); const oAuthScope = botframework_connector_1.SkillValidation.isSkillClaim(identity.claims) ? botframework_connector_1.JwtTokenValidation.getAppIdFromClaims(identity.claims) : this.credentials.oAuthScope; context.turnState.set(this.OAuthScopeKey, oAuthScope); context.turnState.set(botbuilder_core_1.BotCallbackHandlerKey, logic); yield this.runMiddleware(context, logic); // NOTE: The factoring of the code differs here when compared to C# as processActivity() returns Promise. // This is due to the fact that the response to the incoming activity is sent from inside this implementation. // In C#, ProcessActivityAsync() returns Task and ASP.NET handles sending of the response. if (request.deliveryMode === botbuilder_core_1.DeliveryModes.ExpectReplies) { // Handle "expectReplies" scenarios where all the activities have been buffered and sent back at once // in an invoke response. let activities = context.bufferedReplyActivities; // If the channel is not the emulator, do not send trace activities. // Fixes: https://github.com/microsoft/botbuilder-js/issues/2732 if (request.channelId !== botbuilder_core_1.Channels.Emulator) { activities = activities.filter((a) => a.type !== botbuilder_core_1.ActivityTypes.Trace); } body = { activities }; status = botbuilder_core_1.StatusCodes.OK; } else if (request.type === botbuilder_core_1.ActivityTypes.Invoke) { // Retrieve a cached Invoke response to handle Invoke scenarios. // These scenarios deviate from the request/request model as the Bot should return a specific body and status. const invokeResponse = context.turnState.get(botbuilder_core_1.INVOKE_RESPONSE_KEY); if (invokeResponse && invokeResponse.value) { const value = invokeResponse.value; status = value.status; body = value.body; } else { status = botbuilder_core_1.StatusCodes.NOT_IMPLEMENTED; } } else { status = botbuilder_core_1.StatusCodes.OK; } } catch (err) { // Catch the error to try and throw the stacktrace out of processActivity() processError = err; body = err.toString(); } // Return status res.status(status); if (body) { res.send(body); } res.end(); // Check for an error if (status >= 400) { if (processError && processError.stack) { throw new Error(`BotFrameworkAdapter.processActivity(): ${status} ERROR\n ${processError.stack}`); } else { throw new Error(`BotFrameworkAdapter.processActivity(): ${status} ERROR`); } } }); } /** * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity. * * Use [CloudAdapter.processActivityDirect] instead. * * @param activity The activity to process. * @param logic The function to call at the end of the middleware pipeline. * @remarks * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method: * * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity. * - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable). * - When this method completes, the proxy is revoked. * 1. Sends the turn context through the adapter's middleware pipeline. * 1. Sends the turn context to the `logic` function. * - The bot may perform additional routing or processing at this time. * Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete. * - After the `logic` function completes, the promise chain set up by the middleware is resolved. * * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the * `logic` function is not called; however, all middleware prior to this point still run to completion. * For more information about the middleware pipeline, see the * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles. * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter. */ processActivityDirect(activity, logic) { return __awaiter(this, void 0, void 0, function* () { let processError; try { // Process activity const context = this.createContext(activity); context.turnState.set(botbuilder_core_1.BotCallbackHandlerKey, logic); yield this.runMiddleware(context, logic); } catch (err) { // Catch the error to try and throw the stacktrace out of processActivity() processError = err; } if (processError) { if (processError && processError.stack) { throw new Error(`BotFrameworkAdapter.processActivityDirect(): ERROR\n ${processError.stack}`); } else { throw new Error('BotFrameworkAdapter.processActivityDirect(): ERROR'); } } }); } /** * Asynchronously sends a set of outgoing activities to a channel server. * * This method supports the framework and is not intended to be called directly for your code. * Use the turn context's [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) or * [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) method from your bot code. * * @param context The context object for the turn. * @param activities The activities to send. * @returns An array of [ResourceResponse](xref:) * @remarks * The activities will be sent one after another in the order in which they're received. A * response object will be returned for each sent activity. For `message` activities this will * contain the ID of the delivered message. */ sendActivities(context, activities) { return __awaiter(this, void 0, void 0, function* () { const responses = []; for (let i = 0; i < activities.length; i++) { const activity = activities[i]; switch (activity.type) { case 'delay': yield (0, botbuilder_stdlib_1.delay)(typeof activity.value === 'number' ? activity.value : 1000); responses.push({}); break; case botbuilder_core_1.ActivityTypes.InvokeResponse: // Cache response to context object. This will be retrieved when turn completes. context.turnState.set(botbuilder_core_1.INVOKE_RESPONSE_KEY, activity); responses.push({}); break; default: { if (!activity.serviceUrl) { throw new Error('BotFrameworkAdapter.sendActivity(): missing serviceUrl.'); } if (!activity.conversation || !activity.conversation.id) { throw new Error('BotFrameworkAdapter.sendActivity(): missing conversation id.'); } if (activity && BotFrameworkAdapter.isStreamingServiceUrl(activity.serviceUrl)) { if (!this.isStreamingConnectionOpen) { throw new Error('BotFrameworkAdapter.sendActivities(): Unable to send activity as Streaming connection is closed.'); } streaming_1.TokenResolver.checkForOAuthCards(this, context, activity); } const client = this.getOrCreateConnectorClient(context, activity.serviceUrl, this.credentials); if (activity.type === botbuilder_core_1.ActivityTypes.Trace && activity.channelId !== botbuilder_core_1.Channels.Emulator) { // Just eat activity responses.push({}); } else if (activity.replyToId) { responses.push(yield client.conversations.replyToActivity(activity.conversation.id, activity.replyToId, activity)); } else { responses.push(yield client.conversations.sendToConversation(activity.conversation.id, activity)); } break; } } } return responses; }); } /** * Asynchronously replaces a previous activity with an updated version. * * This interface supports the framework and is not intended to be called directly for your code. * Use [TurnContext.updateActivity](xref:botbuilder-core.TurnContext.updateActivity) to update * an activity from your bot code. * * @param context The context object for the turn. * @param activity The updated version of the activity to replace. * @returns A `Promise` representing the [ResourceResponse](xref:botframework-schema.ResourceResponse) for the operation. * @remarks * Not all channels support this operation. For channels that don't, this call may throw an exception. */ updateActivity(context, activity) { return __awaiter(this, void 0, void 0, function* () { if (!activity.serviceUrl) { throw new Error('BotFrameworkAdapter.updateActivity(): missing serviceUrl'); } if (!activity.conversation || !activity.conversation.id) { throw new Error('BotFrameworkAdapter.updateActivity(): missing conversation or conversation.id'); } if (!activity.id) { throw new Error('BotFrameworkAdapter.updateActivity(): missing activity.id'); } const client = this.getOrCreateConnectorClient(context, activity.serviceUrl, this.credentials); return client.conversations.updateActivity(activity.conversation.id, activity.id, activity); }); } /** * Creates a connector client. * * @param serviceUrl The client's service URL. * @returns The [ConnectorClient](xref:botbuilder-connector.ConnectorClient) instance. * @remarks * Override this in a derived class to create a mock connector client for unit testing. */ createConnectorClient(serviceUrl) { return this.createConnectorClientInternal(serviceUrl, this.credentials); } /** * Create a [ConnectorClient](xref:botbuilder-connector.ConnectorClient) with a [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity). * * @remarks * If the [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity) contains the claims for a Skills request, create a [ConnectorClient](xref:botbuilder-connector.ConnectorClient) for use with Skills. * Derives the correct audience from the [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity), or the instance's credentials property. * @param serviceUrl The client's service URL. * @param identity [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity). * @param audience Optional. The recipient of the [ConnectorClient](xref:botbuilder-connector.ConnectorClient)'s messages. Normally the Bot Framework Channel Service or the AppId of another bot. * @returns The client. */ createConnectorClientWithIdentity(serviceUrl, identity, audience) { return __awaiter(this, void 0, void 0, function* () { if (!identity) { throw new Error('BotFrameworkAdapter.createConnectorClientWithIdentity(): invalid identity parameter.'); } const botAppId = identity.getClaimValue(botframework_connector_1.AuthenticationConstants.AudienceClaim) || identity.getClaimValue(botframework_connector_1.AuthenticationConstants.AppIdClaim); // Check if the audience is a string and when trimmed doesn't have a length of 0. const validAudience = typeof audience === 'string' && audience.trim().length > 0; const oAuthScope = validAudience ? audience : yield this.getOAuthScope(botAppId, identity.claims); const credentials = yield this.buildCredentials(botAppId, oAuthScope); const client = this.createConnectorClientInternal(serviceUrl, credentials); return client; }); } createConnectorClientInternal(serviceUrl, credentials) { if (BotFrameworkAdapter.isStreamingServiceUrl(serviceUrl)) { // Check if we have a streaming server. Otherwise, requesting a connector client // for a non-existent streaming connection results in an error if (!this.streamingServer) { throw new Error(`Cannot create streaming connector client for serviceUrl ${serviceUrl} without a streaming connection. Call 'useWebSocket' or 'useNamedPipe' to start a streaming connection.`); } const clientOptions = this.getClientOptions(serviceUrl, new streaming_1.StreamingHttpClient(this.streamingServer)); return new botframework_connector_1.ConnectorClient(credentials, clientOptions); } const clientOptions = this.getClientOptions(serviceUrl); return new botframework_connector_1.ConnectorClient(credentials, clientOptions); } getClientOptions(serviceUrl, httpClient) { var _a; const _b = (_a = this.settings.clientOptions) !== null && _a !== void 0 ? _a : {}, { requestPolicyFactories } = _b, clientOptions = __rest(_b, ["requestPolicyFactories"]); const options = Object.assign({}, { baseUri: serviceUrl }, clientOptions); if (httpClient) { options.httpClient = httpClient; } const userAgent = typeof options.userAgent === 'function' ? options.userAgent(exports.USER_AGENT) : options.userAgent; const setUserAgent = (0, core_http_1.userAgentPolicy)({ value: `${exports.USER_AGENT}${userAgent !== null && userAgent !== void 0 ? userAgent : ''}`, }); const acceptHeader = { create: (nextPolicy) => ({ sendRequest: (httpRequest) => { if (!httpRequest.headers.contains('accept')) { httpRequest.headers.set('accept', '*/*'); } return nextPolicy.sendRequest(httpRequest); }, }), }; // Resolve any user request policy factories, then include our user agent via a factory policy options.requestPolicyFactories = (defaultRequestPolicyFactories) => { let defaultFactories = []; if (requestPolicyFactories) { if (typeof requestPolicyFactories === 'function') { const newDefaultFactories = requestPolicyFactories(defaultRequestPolicyFactories); if (newDefaultFactories) { defaultFactories = newDefaultFactories; } } else if (requestPolicyFactories) { defaultFactories = [...requestPolicyFactories]; } // If the user has supplied custom factories, allow them to optionally set user agent // before we do. defaultFactories = [...defaultFactories, acceptHeader, setUserAgent]; } else { // In the case that there are no user supplied factories, inject our user agent as // the first policy to ensure none of the default policies override it. defaultFactories = [acceptHeader, setUserAgent, ...defaultRequestPolicyFactories]; } return defaultFactories; }; return options; } // Retrieves the ConnectorClient from the TurnContext or creates a new ConnectorClient with the provided serviceUrl and credentials. getOrCreateConnectorClient(context, serviceUrl, credentials) { if (!context || !context.turnState) throw new Error('invalid context parameter'); if (!serviceUrl) throw new Error('invalid serviceUrl'); if (!credentials) throw new Error('invalid credentials'); let client = context.turnState.get(this.ConnectorClientKey); // Inspect the retrieved client to confirm that the serviceUrl is correct, if it isn't, create a new one. if (!client || client['baseUri'] !== serviceUrl) { client = this.createConnectorClientInternal(serviceUrl, credentials); } return client; } /** * Returns the correct [OAuthScope](xref:botframework-connector.AppCredentials.OAuthScope) for [AppCredentials](xref:botframework-connector.AppCredentials). * * @param botAppId The bot's AppId. * @param claims The [Claim](xref:botbuilder-connector.Claim) list to check. * @returns The current credentials' OAuthScope. */ getOAuthScope(botAppId, claims) { return __awaiter(this, void 0, void 0, function* () { // If the Claims are for skills, we need to create an AppCredentials instance with // the correct scope for communication between the caller and the skill. if (botAppId && botframework_connector_1.SkillValidation.isSkillClaim(claims)) { return botframework_connector_1.JwtTokenValidation.getAppIdFromClaims(claims); } // Return the current credentials' OAuthScope. return this.credentials.oAuthScope; }); } /** * * @remarks * When building credentials for bot-to-bot communication, oAuthScope must be passed in. * @param appId The application id. * @param oAuthScope The optional OAuth scope. * @returns The app credentials to be used to acquire tokens. */ buildCredentials(appId, oAuthScope) { return __awaiter(this, void 0, void 0, function* () { // There is no cache for AppCredentials in JS as opposed to C#. // Instead of retrieving an AppCredentials from the Adapter instance, generate a new one const appPassword = yield this.credentialsProvider.getAppPassword(appId); let credentials; if (this.settings.certificateThumbprint && this.settings.certificatePrivateKey) { credentials = new botframework_connector_1.CertificateAppCredentials(appId, this.settings.certificateThumbprint, this.settings.certificatePrivateKey, this.settings.channelAuthTenant); } else { if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) { credentials = new botframework_connector_1.MicrosoftGovernmentAppCredentials(appId, appPassword, this.settings.channelAuthTenant, oAuthScope); } else { credentials = new botframework_connector_1.MicrosoftAppCredentials(appId, appPassword, this.settings.channelAuthTenant, oAuthScope); } } return credentials; }); } /** * Creates an OAuth API client. * * @param serviceUrl The client's service URL. * @param oAuthAppCredentials Optional. The [AppCredentials](xref:botframework-connector.AppCredentials)for OAuth. * @remarks * Override this in a derived class to create a mock OAuth API client for unit testing. * @returns The client. */ createTokenApiClient(serviceUrl, oAuthAppCredentials) { const tokenApiClientCredentials = oAuthAppCredentials ? oAuthAppCredentials : this.credentials; const client = new botframework_connector_1.TokenApiClient(tokenApiClientCredentials, { baseUri: serviceUrl, userAgent: exports.USER_AGENT }); return client; } /** * Allows for the overriding of authentication in unit tests. * * @param request Received request. * @param authHeader Received authentication header. */ authenticateRequest(request, authHeader) { return __awaiter(this, void 0, void 0, function* () { const identity = yield this.authenticateRequestInternal(request, authHeader); if (!identity.isAuthenticated) { throw new Error('Unauthorized Access. Request is not authorized'); } // Set the correct callerId value and discard values received over the wire request.callerId = yield this.generateCallerId(identity); }); } /** * @ignore * @private * Returns the actual ClaimsIdentity from the JwtTokenValidation.authenticateRequest() call. * @remarks * This method is used instead of authenticateRequest() in processActivity() to obtain the ClaimsIdentity for caching in the TurnContext.turnState. * * @param request Received request. * @param authHeader Received authentication header. */ authenticateRequestInternal(request, authHeader) { return botframework_connector_1.JwtTokenValidation.authenticateRequest(request, authHeader, this.credentialsProvider, this.settings.channelService, this.authConfiguration); } /** * Generates the CallerId property for the activity based on * https://github.com/microsoft/botframework-obi/blob/main/protocols/botframework-activity/botframework-activity.md#appendix-v---caller-id-values. * * @param identity The inbound claims. * @returns {Promise} a promise representing the generated callerId. */ generateCallerId(identity) { return __awaiter(this, void 0, void 0, function* () { if (!identity) { throw new TypeError('BotFrameworkAdapter.generateCallerId(): Missing identity parameter.'); } // Is the bot accepting all incoming messages? const isAuthDisabled = yield this.credentialsProvider.isAuthenticationDisabled(); if (isAuthDisabled) { // Return undefined so that the callerId is cleared. return; } // Is the activity from another bot? if (botframework_connector_1.SkillValidation.isSkillClaim(identity.claims)) { const callerId = botframework_connector_1.JwtTokenValidation.getAppIdFromClaims(identity.claims); return `${botbuilder_core_1.CallerIdConstants.BotToBotPrefix}${callerId}`; } // Is the activity from Public Azure? if (!this.settings.channelService || this.settings.channelService.length === 0) { return botbuilder_core_1.CallerIdConstants.PublicAzureChannel; } // Is the activity from Azure Gov? if (botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService)) { return botbuilder_core_1.CallerIdConstants.USGovChannel; } // Return undefined so that the callerId is cleared. }); } /** * Gets the OAuth API endpoint. * * @param contextOrServiceUrl The URL of the channel server to query or * a [TurnContext](xref:botbuilder-core.TurnContext). For a turn context, the context's * [activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl) * is used for the URL. * @returns The endpoint used for the API requests. * @remarks * Override this in a derived class to create a mock OAuth API endpoint for unit testing. */ oauthApiUrl(contextOrServiceUrl) { return this.isEmulatingOAuthCards ? typeof contextOrServiceUrl === 'object' ? contextOrServiceUrl.activity.serviceUrl : contextOrServiceUrl : this.settings.oAuthEndpoint ? this.settings.oAuthEndpoint : botframework_connector_1.JwtTokenValidation.isGovernment(this.settings.channelService) ? US_GOV_OAUTH_ENDPOINT : OAUTH_ENDPOINT; } /** * Checks the environment and can set a flag to emulate OAuth cards. * * @param context The context object for the turn. * @remarks * Override this in a derived class to control how OAuth cards are emulated for unit testing. */ checkEmulatingOAuthCards(context) { if (!this.isEmulatingOAuthCards && context.activity.channelId === 'emulator' && !this.credentials.appId) { this.isEmulatingOAuthCards = true; } } /** * Creates a turn context. * * @param request An incoming request body. * @returns A new [TurnContext](xref:botbuilder-core.TurnContext) instance. * @remarks * Override this in a derived class to modify how the adapter creates a turn context. */ createContext(request) { return new botbuilder_core_1.TurnContext(this, request); } /** * Checks the validity of the request and attempts to map it the correct virtual endpoint, * then generates and returns a response if appropriate. * * @param request A ReceiveRequest from the connected channel. * @returns A response created by the BotAdapter to be sent to the client that originated the request. */ processRequest(request) { return __awaiter(this, void 0, void 0, function* () { const response = new botframework_streaming_1.StreamingResponse(); if (!request) { response.statusCode = botbuilder_core_1.StatusCodes.BAD_REQUEST; response.setBody('No request provided.'); return response; } if (!request.verb || !request.path) { response.statusCode = botbuilder_core_1.StatusCodes.BAD_REQUEST; response.setBody(`Request missing verb and/or path. Verb: ${request.verb}. Path: ${request.path}`); return response; } if (request.verb.toLocaleUpperCase() !== streaming_1.POST && request.verb.toLocaleUpperCase() !== streaming_1.GET) { response.statusCode = botbuilder_core_1.StatusCodes.METHOD_NOT_ALLOWED; response.setBody(`Invalid verb received. Only GET and POST are accepted. Verb: ${request.verb}`); } if (request.path.toLocaleLowerCase() === streaming_1.VERSION_PATH) { return yield this.handleVersionRequest(request, response); } // Convert the StreamingRequest into an activity the Adapter can understand. let body; try { body = yield this.readRequestBodyAsString(request); } catch (error) { response.statusCode = botbuilder_core_1.StatusCodes.BAD_REQUEST; response.setBody(`Request body missing or malformed: ${error}`); return response; } if (request.path.toLocaleLowerCase() !== streaming_1.MESSAGES_PATH) { response.statusCode = botbuilder_core_1.StatusCodes.NOT_FOUND; response.setBody(`Path ${request.path.toLocaleLowerCase()} not not found. Expected ${streaming_1.MESSAGES_PATH}}.`); return response; } if (request.verb.toLocaleUpperCase() !== streaming_1.POST) { response.statusCode = botbuilder_core_1.StatusCodes.METHOD_NOT_ALLOWED; response.setBody(`Invalid verb received for ${request.verb.toLocaleLowerCase()}. Only GET and POST are accepted. Verb: ${request.verb}`); return response; } try { const context = new botbuilder_core_1.TurnContext(this, body); yield this.runMiddleware(context, this.logic); if (body.type === botbuilder_core_1.ActivityTypes.Invoke) { const invokeResponse = context.turnState.get(botbuilder_core_1.INVOKE_RESPONSE_KEY); if (invokeResponse && invokeResponse.value) { const value = invokeResponse.value; response.statusCode = value.status; if (value.body) { response.setBody(value.body); } } else { response.statusCode = botbuilder_core_1.StatusCodes.NOT_IMPLEMENTED; } } else if (body.deliveryMode === botbuilder_core_1.DeliveryModes.ExpectReplies) { const replies = { activities: context.bufferedReplyActivities }; response.setBody(replies); response.statusCode = botbuilder_core_1.StatusCodes.OK; } else { response.statusCode = botbuilder_core_1.StatusCodes.OK; } } catch (error) { response.statusCode = botbuilder_core_1.StatusCodes.INTERNAL_SERVER_ERROR; response.setBody(error); return response; } return response; }); } /** * @internal */ process(req, resOrSocket, logicOrHead, maybeLogic) { return __awaiter(this, void 0, void 0, function* () { if (maybeLogic) { return this.useWebSocket(req, zod_1.INodeSocketT.parse(resOrSocket), zod_1.INodeBufferT.parse(logicOrHead), zod_1.LogicT.parse(maybeLogic)); } else { return this.processActivity(req, interfaces_1.ResponseT.parse(resOrSocket), zod_1.LogicT.parse(logicOrHead)); } }); } /** * Connects the handler to a Named Pipe server and begins listening for incoming requests. * * @param logic The logic that will handle incoming requests. * @param pipeName The name of the named pipe to use when creating the server. * @param retryCount Number of times to attempt to bind incoming and outgoing pipe * @param onListen Optional callback that fires once when server is listening on both incoming and outgoing pipe */ useNamedPipe(logic, pipeName = streaming_1.defaultPipeName, retryCount = 7, onListen) { return __awaiter(this, void 0, void 0, function* () { if (!logic) { throw new Error('Bot logic needs to be provided to `useNamedPipe`'); } if (this.isStreamingConnectionOpen) { if (this.namedPipeName === pipeName) { // Idempotent operation return; } else { // BotFrameworkAdapters are scoped to one streaming connection. // Switching streams is an advanced scenario, one not innately supported by the SDK. // Each BotFrameworkAdapter instance is scoped to a stream, so switching streams // results in dropped conversations that the bot cannot reconnect to. throw new Error('This BotFrameworkAdapter instance is already connected to a different stream. Use a new instance to connect to the provided pipeName.'); } } this.logic = logic; yield (0, botbuilder_stdlib_1.retry)(() => this.startNamedPipeServer(pipeName, onListen), retryCount); }); } /** * Process the initial request to establish a long lived connection via a streaming server. * * @param req The connection request. * @param socket The raw socket connection between the bot (server) and channel/caller (client). * @param head The first packet of the upgraded stream. * @param logic The logic that handles incoming streaming requests for the lifetime of the WebSocket connection. */ useWebSocket(req, socket, head, logic) { return __awaiter(this, void 0, void 0, function* () { // Use the provided NodeWebSocketFactoryBase on BotFrameworkAdapter construction, // otherwise create a new NodeWebSocketFactory. const webSocketFactory = this.webSocketFactory || new botframework_streaming_1.NodeWebSocketFactory(); if (!logic) { throw new Error('Streaming logic needs to be provided to `useWebSocket`'); } this.logic = logic; try { yield this.authenticateConnection(req, this.settings.channelService); } catch (err) { abortWebSocketUpgrade(socket, err); throw err; } const nodeWebSocket = yield webSocketFactory.createWebSocket(req, socket, head); yield this.startWebSocket(nodeWebSocket); }); } startNamedPipeServer(pipeName, onListen) { return __awaiter(this, void 0, void 0, function* () { this.namedPipeName = pipeName; this.streamingServer = new botframework_streaming_1.NamedPipeServer(pipeName, this); try { yield this.streamingServer.start(onListen); } finally { this.namedPipeName = undefined; } }); } authenticateConnection(req, channelService) { return __awaiter(this, void 0, void 0, function* () { if (!this.credentials.appId) { // auth is disabled return; } const authHeader = req.headers.authorization || req.headers.Authorization || ''; const channelIdHeader = req.headers.channelid || req.headers.ChannelId || req.headers.ChannelID || ''; // Validate the received Upgrade request from the channel. const claims = yield botframework_connector_1.JwtTokenValidation.validateAuthHeader(authHeader, this.credentialsProvider, channelService, channelIdHeader); if (!claims.isAuthenticated) { throw new botframework_connector_1.AuthenticationError('Unauthorized Access. Request is not authorized', botbuilder_core_1.StatusCodes.UNAUTHORIZED); } }); } /** * Connects the handler to a WebSocket server and begins listening for incoming requests. * * @param socket The socket to use when creating the server. */ startWebSocket(socket) { return __awaiter(this, void 0, void 0, function* () { this.streamingServer = new botframework_streaming_1.WebSocketServer(socket, this); yield this.streamingServer.start(); }); } readRequestBodyAsString(request) { return __awaiter(this, void 0, void 0, function* () { const [activityStream, ...attachmentStreams] = request.streams; const activity = yield activityStream.readAsJson(); activity.attachments = yield Promise.all(attachmentStreams.map((attachmentStream) => __awaiter(this, void 0, void 0, function* () { const contentType = attachmentStream.contentType; const content = contentType === 'application/json' ? yield attachmentStream.readAsJson() : yield attachmentStream.readAsString(); return { contentType, content }; }))); return activity; }); } handleVersionRequest(request, response) { return __awaiter(this, void 0, void 0, function* () { if (request.verb.toLocaleUpperCase() === streaming_1.GET) { response.statusCode = botbuilder_core_1.StatusCodes.OK; if (!this.credentials.appId) { response.setBody({ UserAgent: exports.USER_AGENT }); return response; } let token = ''; try { token = yield this.credentials.getToken(); } catch (err) { /** * In reality a missing BotToken will cause the channel to close the connection, * but we still send the response and allow the channel to make that decision * instead of proactively disconnecting. This allows the channel to know why * the connection has been closed and make the choice not to make endless reconnection * attempts that will end up right back here. */ console.error(err.message); } response.setBody({ UserAgent: exports.USER_AGENT, BotToken: token }); } else { response.statusCode = botbuilder_core_1.StatusCodes.METHOD_NOT_ALLOWED; response.setBody(`Invalid verb received for path: ${request.path}. Only GET is accepted. Verb: ${request.verb}`); } return response; }); } /** * Determine if the serviceUrl was sent via an Http/Https connection or Streaming * This can be determined by looking at the ServiceUrl property: * (1) All channels that send messages via http/https are not streaming * (2) Channels that send messages via streaming have a ServiceUrl that does not begin with http/https. * * @param serviceUrl the serviceUrl provided in the resquest. * @returns True if the serviceUrl is a streaming url, otherwise false. */ static isStreamingServiceUrl(serviceUrl) { return serviceUrl && !serviceUrl.toLowerCase().startsWith('http'); } } exports.BotFrameworkAdapter = BotFrameworkAdapter; /** * Handles incoming webhooks from the botframework * @private * @param req incoming web request */ function parseRequest(req) { return new Promise((resolve, reject) => { if (req.body) { try { const activity = (0, activityValidator_1.validateAndFixActivity)(req.body); resolve(activity); } catch (err) { reject(err); } } else { let requestData = ''; req.on('data', (chunk) => { requestData += chunk; }); req.on('end', () => { try { req.body = JSON.parse(requestData); const activity = (0, activityValidator_1.validateAndFixActivity)(req.body); resolve(activity); } catch (err) { reject(err); } }); } }); } /** * Creates an error message with status code to write to socket, then closes the connection. * * @param socket The raw socket connection between the bot (server) and channel/caller (client). * @param err The error. If the error includes a status code, it will be included in the message written to the socket. */ function abortWebSocketUpgrade(socket, err) { if (socket.writable) { const connectionHeader = "Connection: 'close'\r\n"; let message = ''; if (botframework_connector_1.AuthenticationError.isStatusCodeError(err)) { message = `HTTP/1.1 ${err.statusCode} ${botbuilder_core_1.StatusCodes[err.statusCode]}\r\n${err.message}\r\n${connectionHeader}\r\n`; } else { message = botframework_connector_1.AuthenticationError.determineStatusCodeAndBuildMessage(err); } socket.write(message); } socket.destroy(); } //# sourceMappingURL=botFrameworkAdapter.js.map