• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-js / 14082303413

26 Mar 2025 11:36AM UTC coverage: 84.47% (-0.05%) from 84.524%
14082303413

push

github

web-flow
Update elliptic, esbuild, and serialize-javascript (#4862)

8260 of 10940 branches covered (75.5%)

Branch coverage included in aggregate %.

20572 of 23193 relevant lines covered (88.7%)

4032.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.1
/libraries/botbuilder/src/botFrameworkAdapter.ts
1
/**
2
 * @module botbuilder
3
 */
4
/**
5
 * Copyright (c) Microsoft Corporation. All rights reserved.
6
 * Licensed under the MIT License.
7
 */
8

9
import * as z from 'zod';
1✔
10
import { BotFrameworkHttpAdapter } from './botFrameworkHttpAdapter';
11
import { ConnectorClientBuilder, Request, Response, ResponseT, WebRequest, WebResponse } from './interfaces';
1✔
12
import { INodeBufferT, INodeSocketT, LogicT } from './zod';
1✔
13
import { arch, release, type } from 'os';
1✔
14
import { delay, retry } from 'botbuilder-stdlib';
1✔
15
import { validateAndFixActivity } from './activityValidator';
1✔
16

17
import {
1✔
18
    Activity,
19
    ActivityEventNames,
20
    ActivityTypes,
21
    BotAdapter,
22
    BotCallbackHandlerKey,
23
    CallerIdConstants,
24
    ChannelAccount,
25
    Channels,
26
    ConversationParameters,
27
    ConversationReference,
28
    ConversationsResult,
29
    CoreAppCredentials,
30
    DeliveryModes,
31
    ExpectedReplies,
32
    ExtendedUserTokenProvider,
33
    INVOKE_RESPONSE_KEY,
34
    InvokeResponse,
35
    ResourceResponse,
36
    StatusCodes,
37
    TokenResponse,
38
    TurnContext,
39
    conversationParametersObject,
40
} from 'botbuilder-core';
41

42
import {
1✔
43
    AppCredentials,
44
    AuthenticationConfiguration,
45
    AuthenticationConstants,
46
    AuthenticationError,
47
    CertificateAppCredentials,
48
    ChannelValidation,
49
    Claim,
50
    ClaimsIdentity,
51
    ConnectorClient,
52
    ConnectorClientOptions,
53
    EmulatorApiClient,
54
    GovernmentChannelValidation,
55
    GovernmentConstants,
56
    JwtTokenValidation,
57
    MicrosoftAppCredentials,
58
    MicrosoftGovernmentAppCredentials,
59
    SignInUrlResponse,
60
    SimpleCredentialProvider,
61
    SkillValidation,
62
    TokenApiClient,
63
    TokenApiModels,
64
    TokenExchangeRequest,
65
    TokenStatus,
66
} from 'botframework-connector';
67

68
import {
1✔
69
    INodeBuffer,
70
    INodeSocket,
71
    IReceiveRequest,
72
    ISocket,
73
    IStreamingTransportServer,
74
    NamedPipeServer,
75
    NodeWebSocketFactory,
76
    NodeWebSocketFactoryBase,
77
    RequestHandler,
78
    StreamingResponse,
79
    WebSocketServer,
80
} from 'botframework-streaming';
81

82
import {
1✔
83
    defaultPipeName,
84
    GET,
85
    POST,
86
    MESSAGES_PATH,
87
    StreamingHttpClient,
88
    TokenResolver,
89
    VERSION_PATH,
90
} from './streaming';
91

92
/**
93
 * @deprecated Use `CloudAdapter` with `ConfigurationBotFrameworkAuthentication` instead to configure bot runtime.
94
 * Contains settings used to configure a [BotFrameworkAdapter](xref:botbuilder.BotFrameworkAdapter) instance.
95
 */
96
export interface BotFrameworkAdapterSettings {
97
    /**
98
     * The ID assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).
99
     */
100
    appId: string;
101

102
    /**
103
     * The password assigned to your bot in the [Bot Framework Portal](https://dev.botframework.com/).
104
     */
105
    appPassword: string;
106

107
    /**
108
     * Optional. The tenant to acquire the bot-to-channel token from.
109
     */
110
    channelAuthTenant?: string;
111

112
    /**
113
     * Optional. The OAuth API endpoint for your bot to use.
114
     */
115
    oAuthEndpoint?: string;
116

117
    /**
118
     * Optional. The OpenID Metadata endpoint for your bot to use.
119
     */
120
    openIdMetadata?: string;
121

122
    /**
123
     * Optional. The channel service option for this bot to validate connections from Azure or other channel locations.
124
     */
125
    channelService?: string;
126

127
    /**
128
     * Optional. Used to pass in a NodeWebSocketFactoryBase instance.
129
     */
130
    webSocketFactory?: NodeWebSocketFactoryBase;
131

132
    /**
133
     * Optional. Certificate thumbprint to authenticate the appId against AAD.
134
     */
135
    certificateThumbprint?: string;
136

137
    /**
138
     * Optional. Certificate key to authenticate the appId against AAD.
139
     */
140
    certificatePrivateKey?: string;
141

142
    /**
143
     * Optional. Used to require specific endorsements and verify claims. Recommended for Skills.
144
     */
145
    authConfig?: AuthenticationConfiguration;
146

147
    /**
148
     * Optional. Used when creating new ConnectorClients.
149
     */
150
    clientOptions?: ConnectorClientOptions;
151
}
152

153
// Retrieve additional information, i.e., host operating system, host OS release, architecture, Node.js version
154
const ARCHITECTURE = arch();
1✔
155
const TYPE = type();
1✔
156
const RELEASE = release();
1✔
157
const NODE_VERSION = process.version;
1✔
158

159
// eslint-disable-next-line @typescript-eslint/no-require-imports
160
const pjson: Record<'version', string> = require('../package.json');
1✔
161
export const USER_AGENT = `Microsoft-BotFramework/3.1 BotBuilder/${pjson.version} (Node.js,Version=${NODE_VERSION}; ${TYPE} ${RELEASE}; ${ARCHITECTURE})`;
1✔
162

163
const OAUTH_ENDPOINT = 'https://api.botframework.com';
1✔
164
const US_GOV_OAUTH_ENDPOINT = 'https://api.botframework.azure.us';
1✔
165

166
/**
167
 * A [BotAdapter](xref:botbuilder-core.BotAdapter) that can connect a bot to a service endpoint.
168
 * Implements [IUserTokenProvider](xref:botbuilder-core.IUserTokenProvider).
169
 *
170
 * @remarks
171
 * The bot adapter encapsulates authentication processes and sends activities to and receives
172
 * activities from the Bot Connector Service. When your bot receives an activity, the adapter
173
 * creates a turn context object, passes it to your bot application logic, and sends responses
174
 * back to the user's channel.
175
 *
176
 * The adapter processes and directs incoming activities in through the bot middleware pipeline to
177
 * your bot logic and then back out again. As each activity flows in and out of the bot, each
178
 * piece of middleware can inspect or act upon the activity, both before and after the bot logic runs.
179
 * Use the [use](xref:botbuilder-core.BotAdapter.use) method to add [Middleware](xref:botbuilder-core.Middleware)
180
 * objects to your adapter's middleware collection.
181
 *
182
 * For more information, see the articles on
183
 * [How bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and
184
 * [Middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware).
185
 *
186
 * For example:
187
 * ```JavaScript
188
 * const { BotFrameworkAdapter } = require('botbuilder');
189
 *
190
 * const adapter = new BotFrameworkAdapter({
191
 *     appId: process.env.MicrosoftAppId,
192
 *     appPassword: process.env.MicrosoftAppPassword
193
 * });
194
 *
195
 * adapter.onTurnError = async (context, error) => {
196
 *     // Catch-all logic for errors.
197
 * };
198
 * ```
199
 */
200

201
/**
202
 * @deprecated Use `CloudAdapter` instead.
203
 */
204
export class BotFrameworkAdapter
1✔
205
    extends BotAdapter
206
    implements BotFrameworkHttpAdapter, ConnectorClientBuilder, ExtendedUserTokenProvider, RequestHandler
207
{
208
    // These keys are public to permit access to the keys from the adapter when it's a being
209
    // from library that does not have access to static properties off of BotFrameworkAdapter.
210
    // E.g. botbuilder-dialogs
211
    readonly TokenApiClientCredentialsKey: symbol = Symbol('TokenApiClientCredentials');
215✔
212

213
    protected readonly credentials: AppCredentials;
214
    protected readonly credentialsProvider: SimpleCredentialProvider;
215
    protected readonly settings: BotFrameworkAdapterSettings;
216

217
    private isEmulatingOAuthCards: boolean;
218

219
    // Streaming-specific properties:
220
    private logic: (context: TurnContext) => Promise<void>;
221
    private streamingServer: IStreamingTransportServer;
222
    private webSocketFactory: NodeWebSocketFactoryBase;
223

224
    private authConfiguration: AuthenticationConfiguration;
225

226
    private namedPipeName?: string;
227

228
    /**
229
     * Creates a new instance of the [BotFrameworkAdapter](xref:botbuilder.BotFrameworkAdapter) class.
230
     *
231
     * @param settings Optional. The settings to use for this adapter instance.
232
     * @remarks
233
     * If the `settings` parameter does not include
234
     * [channelService](xref:botbuilder.BotFrameworkAdapterSettings.channelService) or
235
     * [openIdMetadata](xref:botbuilder.BotFrameworkAdapterSettings.openIdMetadata) values, the
236
     * constructor checks the process' environment variables for these values. These values may be
237
     * set when a bot is provisioned on Azure and if so are required for the bot to work properly
238
     * in the global cloud or in a national cloud.
239
     *
240
     * The [BotFrameworkAdapterSettings](xref:botbuilder.BotFrameworkAdapterSettings) class defines
241
     * the available adapter settings.
242
     */
243
    constructor(settings?: Partial<BotFrameworkAdapterSettings>) {
244
        super();
215✔
245
        this.settings = { appId: '', appPassword: '', ...settings };
215✔
246

247
        // If settings.certificateThumbprint & settings.certificatePrivateKey are provided,
248
        // use CertificateAppCredentials.
249
        if (this.settings.certificateThumbprint && this.settings.certificatePrivateKey) {
215✔
250
            this.credentials = new CertificateAppCredentials(
3✔
251
                this.settings.appId,
252
                settings.certificateThumbprint,
253
                settings.certificatePrivateKey,
254
                this.settings.channelAuthTenant,
255
            );
256
            this.credentialsProvider = new SimpleCredentialProvider(this.credentials.appId, '');
3✔
257
        } else {
258
            if (JwtTokenValidation.isGovernment(this.settings.channelService)) {
212✔
259
                this.credentials = new MicrosoftGovernmentAppCredentials(
3✔
260
                    this.settings.appId,
261
                    this.settings.appPassword || '',
6✔
262
                    this.settings.channelAuthTenant,
263
                );
264
            } else {
265
                this.credentials = new MicrosoftAppCredentials(
209✔
266
                    this.settings.appId,
267
                    this.settings.appPassword || '',
353✔
268
                    this.settings.channelAuthTenant,
269
                );
270
            }
271
            this.credentialsProvider = new SimpleCredentialProvider(
212✔
272
                this.credentials.appId,
273
                this.settings.appPassword || '',
359✔
274
            );
275
        }
276

277
        this.isEmulatingOAuthCards = false;
215✔
278

279
        // If no channelService or openIdMetadata values were passed in the settings, check the process' Environment Variables for values.
280
        // 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.
281
        this.settings.channelService =
215✔
282
            this.settings.channelService || process.env[AuthenticationConstants.ChannelService];
427✔
283
        this.settings.openIdMetadata =
215✔
284
            this.settings.openIdMetadata || process.env[AuthenticationConstants.BotOpenIdMetadataKey];
428✔
285

286
        this.authConfiguration = this.settings.authConfig || new AuthenticationConfiguration();
215✔
287

288
        if (this.settings.openIdMetadata) {
215✔
289
            ChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
3✔
290
            GovernmentChannelValidation.OpenIdMetadataEndpoint = this.settings.openIdMetadata;
3✔
291
        }
292

293
        // If a NodeWebSocketFactoryBase was passed in, set it on the BotFrameworkAdapter.
294
        if (this.settings.webSocketFactory) {
215✔
295
            this.webSocketFactory = this.settings.webSocketFactory;
1✔
296
        }
297

298
        // Relocate the tenantId field used by MS Teams to a new location (from channelData to conversation)
299
        // This will only occur on activities from teams that include tenant info in channelData but NOT in conversation,
300
        // thus should be future friendly.  However, once the the transition is complete. we can remove this.
301
        this.use(async (context, next): Promise<void> => {
215✔
302
            if (
48✔
303
                context.activity.channelId === 'msteams' &&
76✔
304
                context.activity &&
305
                context.activity.conversation &&
306
                !context.activity.conversation.tenantId &&
307
                context.activity.channelData &&
308
                context.activity.channelData.tenant
309
            ) {
310
                context.activity.conversation.tenantId = context.activity.channelData.tenant.id;
1✔
311
            }
312
            await next();
48✔
313
        });
314
    }
315

316
    /**
317
     * Used in streaming contexts to check if the streaming connection is still open for the bot to send activities.
318
     *
319
     * @returns True if the streaming connection is open, otherwise false.
320
     */
321
    get isStreamingConnectionOpen(): boolean {
322
        return this.streamingServer?.isConnected ?? false;
7✔
323
    }
324

325
    /**
326
     * Asynchronously resumes a conversation with a user, possibly after some time has gone by.
327
     *
328
     * @param reference A reference to the conversation to continue.
329
     * @param oAuthScope The intended recipient of any sent activities.
330
     * @param logic The asynchronous method to call after the adapter middleware runs.
331
     * @remarks
332
     * This is often referred to as a _proactive notification_, the bot can proactively
333
     * send a message to a conversation or user without waiting for an incoming message.
334
     * For example, a bot can use this method to send notifications or coupons to a user.
335
     *
336
     * To send a proactive message:
337
     * 1. Save a copy of a [ConversationReference](xref:botframework-schema.ConversationReference)
338
     *    from an incoming activity. For example, you can store the conversation reference in a database.
339
     * 1. Call this method to resume the conversation at a later time. Use the saved reference to access the conversation.
340
     * 1. On success, the adapter generates a [TurnContext](xref:botbuilder-core.TurnContext) object and calls the `logic` function handler.
341
     *    Use the `logic` function to send the proactive message.
342
     *
343
     * To copy the reference from any incoming activity in the conversation, use the
344
     * [TurnContext.getConversationReference](xref:botbuilder-core.TurnContext.getConversationReference) method.
345
     *
346
     * This method is similar to the [processActivity](xref:botbuilder.BotFrameworkAdapter.processActivity) method.
347
     * The adapter creates a [TurnContext](xref:botbuilder-core.TurnContext) and routes it through
348
     * its middleware before calling the `logic` handler. The created activity will have a
349
     * [type](xref:botframework-schema.Activity.type) of 'event' and a
350
     * [name](xref:botframework-schema.Activity.name) of 'continueConversation'.
351
     *
352
     * For example:
353
     * ```JavaScript
354
     * server.post('/api/notifyUser', async (req, res) => {
355
     *    // Lookup previously saved conversation reference.
356
     *    const reference = await findReference(req.body.refId);
357
     *
358
     *    // Proactively notify the user.
359
     *    if (reference) {
360
     *       await adapter.continueConversation(reference, async (context) => {
361
     *          await context.sendActivity(req.body.message);
362
     *       });
363
     *       res.send(200);
364
     *    } else {
365
     *       res.send(404);
366
     *    }
367
     * });
368
     * ```
369
     */
370
    async continueConversation(
371
        reference: Partial<ConversationReference>,
372
        logic: (context: TurnContext) => Promise<void>,
373
    ): Promise<void>;
374

375
    /**
376
     * Asynchronously resumes a conversation with a user, possibly after some time has gone by.
377
     *
378
     * @param reference [ConversationReference](xref:botframework-schema.ConversationReference) of the conversation to continue.
379
     * @param oAuthScope The intended recipient of any sent activities or the function to call to continue the conversation.
380
     * @param logic Optional. The asynchronous method to call after the adapter middleware runs.
381
     */
382
    async continueConversation(
383
        reference: Partial<ConversationReference>,
384
        oAuthScope: string,
385
        logic: (context: TurnContext) => Promise<void>,
386
    ): Promise<void>;
387

388
    /**
389
     * @internal
390
     */
391
    async continueConversation(
392
        reference: Partial<ConversationReference>,
393
        oAuthScopeOrlogic: string | ((context: TurnContext) => Promise<void>),
394
        maybeLogic?: (context: TurnContext) => Promise<void>,
395
    ): Promise<void> {
396
        let audience: string;
397
        if (LogicT.safeParse(oAuthScopeOrlogic).success) {
17✔
398
            // Because the OAuthScope parameter was not provided, get the correct value via the channelService.
399
            // In this scenario, the ConnectorClient for the continued conversation can only communicate with
400
            // official channels, not with other bots.
401
            audience = JwtTokenValidation.isGovernment(this.settings.channelService)
16✔
402
                ? GovernmentConstants.ToChannelFromBotOAuthScope
16✔
403
                : AuthenticationConstants.ToChannelFromBotOAuthScope;
404
        } else {
405
            audience = z.string().parse(oAuthScopeOrlogic);
1✔
406
        }
407
        const logicParse = LogicT.safeParse(oAuthScopeOrlogic);
17✔
408
        const logic = logicParse.success ? logicParse.data : LogicT.parse(maybeLogic);
17✔
409

410
        let credentials = this.credentials;
17✔
411

412
        // For authenticated flows (where the bot has an AppId), the ConversationReference's serviceUrl needs
413
        // to be trusted for the bot to acquire a token when sending activities to the conversation.
414
        // For anonymous flows, the serviceUrl should not be trusted.
415
        if (credentials.appId) {
17!
416
            // If the provided OAuthScope doesn't match the current one on the instance's credentials, create
417
            // a new AppCredentials with the correct OAuthScope.
418
            if (credentials.oAuthScope !== audience) {
×
419
                // The BotFrameworkAdapter JavaScript implementation supports one Bot per instance, so get
420
                // the botAppId from the credentials.
421
                credentials = await this.buildCredentials(credentials.appId, audience);
×
422
            }
423
        }
424

425
        const connectorClient = this.createConnectorClientInternal(reference.serviceUrl, credentials);
17✔
426

427
        const request = TurnContext.applyConversationReference(
17✔
428
            { type: ActivityTypes.Event, name: ActivityEventNames.ContinueConversation },
429
            reference,
430
            true,
431
        );
432

433
        const context = this.createContext(request);
17✔
434

435
        context.turnState.set(this.OAuthScopeKey, audience);
17✔
436
        context.turnState.set(this.ConnectorClientKey, connectorClient);
17✔
437

438
        await this.runMiddleware(context, logic);
17✔
439
    }
440

441
    /**
442
     * Asynchronously creates and starts a conversation with a user on a channel.
443
     *
444
     * @param {Partial<ConversationReference>} reference A reference for the conversation to create.
445
     * @param {(context: TurnContext) => Promise<void>} logic The asynchronous method to call after the adapter middleware runs.
446
     * @returns {Promise<void>} a promise representing the asynchronous operation
447
     * @summary
448
     * To use this method, you need both the bot's and the user's account information on a channel.
449
     * The Bot Connector service supports the creating of group conversations; however, this
450
     * method and most channels only support initiating a direct message (non-group) conversation.
451
     *
452
     * To create and start a new conversation:
453
     * 1. Get a copy of a [ConversationReference](xref:botframework-schema.ConversationReference) from an incoming activity.
454
     * 1. Set the [user](xref:botframework-schema.ConversationReference.user) property to the
455
     *    [ChannelAccount](xref:botframework-schema.ChannelAccount) value for the intended recipient.
456
     * 1. Call this method to request that the channel create a new conversation with the specified user.
457
     * 1. On success, the adapter generates a turn context and calls the `logic` function handler.
458
     *
459
     * To get the initial reference, use the
460
     * [TurnContext.getConversationReference](xref:botbuilder-core.TurnContext.getConversationReference)
461
     * method on any incoming activity in the conversation.
462
     *
463
     * If the channel establishes the conversation, the generated event activity's
464
     * [conversation](xref:botframework-schema.Activity.conversation) property will contain the
465
     * ID of the new conversation.
466
     *
467
     * This method is similar to the [processActivity](xref:botbuilder.BotFrameworkAdapter.processActivity) method.
468
     * The adapter creates a [TurnContext](xref:botbuilder-core.TurnContext) and routes it through
469
     * middleware before calling the `logic` handler. The created activity will have a
470
     * [type](xref:botframework-schema.Activity.type) of 'event' and a
471
     * [name](xref:botframework-schema.Activity.name) of 'createConversation'.
472
     *
473
     * For example:
474
     * ```JavaScript
475
     * // Get group members conversation reference
476
     * const reference = TurnContext.getConversationReference(context.activity);
477
     *
478
     * // ...
479
     * // Start a new conversation with the user
480
     * await adapter.createConversation(reference, async (ctx) => {
481
     *    await ctx.sendActivity(`Hi (in private)`);
482
     * });
483
     * ```
484
     */
485
    createConversation(
486
        reference: Partial<ConversationReference>,
487
        logic: (context: TurnContext) => Promise<void>,
488
    ): Promise<void>;
489

490
    /**
491
     * Asynchronously creates and starts a conversation with a user on a channel.
492
     *
493
     * @param {Partial<ConversationReference>} reference A reference for the conversation to create.
494
     * @param {Partial<ConversationParameters>} parameters Parameters used when creating the conversation
495
     * @param {(context: TurnContext) => Promise<void>} logic The asynchronous method to call after the adapter middleware runs.
496
     * @returns {Promise<void>} a promise representing the asynchronous operation
497
     */
498
    createConversation(
499
        reference: Partial<ConversationReference>,
500
        parameters: Partial<ConversationParameters>,
501
        logic: (context: TurnContext) => Promise<void>,
502
    ): Promise<void>;
503

504
    /**
505
     * @internal
506
     */
507
    async createConversation(
508
        reference: Partial<ConversationReference>,
509
        parametersOrLogic: Partial<ConversationParameters> | ((context: TurnContext) => Promise<void>),
510
        maybeLogic?: (context: TurnContext) => Promise<void>,
511
    ): Promise<void> {
512
        if (!reference.serviceUrl) {
5✔
513
            throw new Error('BotFrameworkAdapter.createConversation(): missing serviceUrl.');
1✔
514
        }
515
        const logicParse = LogicT.safeParse(parametersOrLogic);
4✔
516
        const parameterParse = conversationParametersObject.partial().safeParse(parametersOrLogic);
4✔
517

518
        const parameters = parameterParse.success ? parameterParse.data : {};
4✔
519

520
        const logic = logicParse.success ? logicParse.data : LogicT.parse(maybeLogic);
4!
521

522
        // Create conversation parameters, taking care to provide defaults that can be
523
        // overridden by passed in parameters
524
        const conversationParameters = Object.assign(
4✔
525
            {},
526
            {
527
                bot: reference.bot,
528
                members: [reference.user],
529
                isGroup: false,
530
                activity: null,
531
                channelData: null,
532
            },
533
            parameters,
534
        );
535

536
        const client = this.createConnectorClient(reference.serviceUrl);
4✔
537

538
        // Mix in the tenant ID if specified. This is required for MS Teams.
539
        if (reference.conversation && reference.conversation.tenantId) {
4✔
540
            // Putting tenantId in channelData is a temporary solution while we wait for the Teams API to be updated
541
            conversationParameters.channelData = { tenant: { id: reference.conversation.tenantId } };
1✔
542

543
            // Permanent solution is to put tenantId in parameters.tenantId
544
            conversationParameters.tenantId = reference.conversation.tenantId;
1✔
545
        }
546

547
        const response = await client.conversations.createConversation(conversationParameters);
4✔
548

549
        // Initialize request and copy over new conversation ID and updated serviceUrl.
550
        const request = TurnContext.applyConversationReference(
4✔
551
            { type: ActivityTypes.Event, name: ActivityEventNames.CreateConversation },
552
            reference,
553
            true,
554
        );
555

556
        request.conversation = {
4✔
557
            id: response.id,
558
            isGroup: conversationParameters.isGroup,
559
            conversationType: null,
560
            tenantId: reference.conversation.tenantId,
561
            name: null,
562
        };
563

564
        request.channelData = conversationParameters.channelData;
4✔
565

566
        if (response.serviceUrl) {
4✔
567
            request.serviceUrl = response.serviceUrl;
1✔
568
        }
569

570
        // Create context and run middleware
571
        const context = this.createContext(request);
4✔
572
        await this.runMiddleware(context, logic);
4✔
573
    }
574

575
    /**
576
     * Asynchronously deletes an existing activity.
577
     *
578
     * This interface supports the framework and is not intended to be called directly for your code.
579
     * Use [TurnContext.deleteActivity](xref:botbuilder-core.TurnContext.deleteActivity) to delete
580
     * an activity from your bot code.
581
     *
582
     * @param context The context object for the turn.
583
     * @param reference Conversation reference information for the activity to delete.
584
     * @remarks
585
     * Not all channels support this operation. For channels that don't, this call may throw an exception.
586
     */
587
    async deleteActivity(context: TurnContext, reference: Partial<ConversationReference>): Promise<void> {
588
        if (!reference.serviceUrl) {
4✔
589
            throw new Error('BotFrameworkAdapter.deleteActivity(): missing serviceUrl');
1✔
590
        }
591
        if (!reference.conversation || !reference.conversation.id) {
3✔
592
            throw new Error('BotFrameworkAdapter.deleteActivity(): missing conversation or conversation.id');
1✔
593
        }
594
        if (!reference.activityId) {
2✔
595
            throw new Error('BotFrameworkAdapter.deleteActivity(): missing activityId');
1✔
596
        }
597
        const client: ConnectorClient = this.getOrCreateConnectorClient(
1✔
598
            context,
599
            reference.serviceUrl,
600
            this.credentials,
601
        );
602
        await client.conversations.deleteActivity(reference.conversation.id, reference.activityId);
1✔
603
    }
604

605
    /**
606
     * Asynchronously removes a member from the current conversation.
607
     *
608
     * @param context The context object for the turn.
609
     * @param memberId The ID of the member to remove from the conversation.
610
     * @remarks
611
     * Remove a member's identity information from the conversation.
612
     *
613
     * Not all channels support this operation. For channels that don't, this call may throw an exception.
614
     */
615
    async deleteConversationMember(context: TurnContext, memberId: string): Promise<void> {
616
        if (!context.activity.serviceUrl) {
4✔
617
            throw new Error('BotFrameworkAdapter.deleteConversationMember(): missing serviceUrl');
1✔
618
        }
619
        if (!context.activity.conversation || !context.activity.conversation.id) {
3✔
620
            throw new Error('BotFrameworkAdapter.deleteConversationMember(): missing conversation or conversation.id');
2✔
621
        }
622
        const serviceUrl: string = context.activity.serviceUrl;
1✔
623
        const conversationId: string = context.activity.conversation.id;
1✔
624
        const client: ConnectorClient = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials);
1✔
625
        await client.conversations.deleteConversationMember(conversationId, memberId);
1✔
626
    }
627

628
    /**
629
     * Asynchronously lists the members of a given activity.
630
     *
631
     * @param context The context object for the turn.
632
     * @param activityId Optional. The ID of the activity to get the members of. If not specified, the current activity ID is used.
633
     * @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
634
     * the users involved in a given activity.
635
     * @remarks
636
     * Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
637
     * the users involved in a given activity.
638
     *
639
     * This is different from [getConversationMembers](xref:botbuilder.BotFrameworkAdapter.getConversationMembers)
640
     * in that it will return only those users directly involved in the activity, not all members of the conversation.
641
     */
642
    async getActivityMembers(context: TurnContext, activityId?: string): Promise<ChannelAccount[]> {
643
        if (!activityId) {
5✔
644
            activityId = context.activity.id;
4✔
645
        }
646
        if (!context.activity.serviceUrl) {
5✔
647
            throw new Error('BotFrameworkAdapter.getActivityMembers(): missing serviceUrl');
1✔
648
        }
649
        if (!context.activity.conversation || !context.activity.conversation.id) {
4✔
650
            throw new Error('BotFrameworkAdapter.getActivityMembers(): missing conversation or conversation.id');
2✔
651
        }
652
        if (!activityId) {
2✔
653
            throw new Error(
1✔
654
                'BotFrameworkAdapter.getActivityMembers(): missing both activityId and context.activity.id',
655
            );
656
        }
657
        const serviceUrl: string = context.activity.serviceUrl;
1✔
658
        const conversationId: string = context.activity.conversation.id;
1✔
659
        const client: ConnectorClient = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials);
1✔
660

661
        return await client.conversations.getActivityMembers(conversationId, activityId);
1✔
662
    }
663

664
    /**
665
     * Asynchronously lists the members of the current conversation.
666
     *
667
     * @param context The context object for the turn.
668
     * @returns An array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
669
     * all users currently involved in a conversation.
670
     * @remarks
671
     * Returns an array of [ChannelAccount](xref:botframework-schema.ChannelAccount) objects for
672
     * all users currently involved in a conversation.
673
     *
674
     * This is different from [getActivityMembers](xref:botbuilder.BotFrameworkAdapter.getActivityMembers)
675
     * in that it will return all members of the conversation, not just those directly involved in a specific activity.
676
     */
677
    async getConversationMembers(context: TurnContext): Promise<ChannelAccount[]> {
678
        if (!context.activity.serviceUrl) {
4✔
679
            throw new Error('BotFrameworkAdapter.getConversationMembers(): missing serviceUrl');
1✔
680
        }
681
        if (!context.activity.conversation || !context.activity.conversation.id) {
3✔
682
            throw new Error('BotFrameworkAdapter.getConversationMembers(): missing conversation or conversation.id');
2✔
683
        }
684
        const serviceUrl: string = context.activity.serviceUrl;
1✔
685
        const conversationId: string = context.activity.conversation.id;
1✔
686
        const client: ConnectorClient = this.getOrCreateConnectorClient(context, serviceUrl, this.credentials);
1✔
687

688
        return await client.conversations.getConversationMembers(conversationId);
1✔
689
    }
690

691
    /**
692
     * For the specified channel, asynchronously gets a page of the conversations in which this bot has participated.
693
     *
694
     * @param contextOrServiceUrl The URL of the channel server to query or a
695
     * [TurnContext](xref:botbuilder-core.TurnContext) object from a conversation on the channel.
696
     * @param continuationToken Optional. The continuation token from the previous page of results.
697
     * Omit this parameter or use `undefined` to retrieve the first page of results.
698
     * @returns A [ConversationsResult](xref:botframework-schema.ConversationsResult) object containing a page of results
699
     * and a continuation token.
700
     * @remarks
701
     * The the return value's [conversations](xref:botframework-schema.ConversationsResult.conversations) property contains a page of
702
     * [ConversationMembers](xref:botframework-schema.ConversationMembers) objects. Each object's
703
     * [id](xref:botframework-schema.ConversationMembers.id) is the ID of a conversation in which the bot has participated on this channel.
704
     * This method can be called from outside the context of a conversation, as only the bot's service URL and credentials are required.
705
     *
706
     * The channel batches results in pages. If the result's
707
     * [continuationToken](xref:botframework-schema.ConversationsResult.continuationToken) property is not empty, then
708
     * there are more pages to get. Use the returned token to get the next page of results.
709
     * If the `contextOrServiceUrl` parameter is a [TurnContext](xref:botbuilder-core.TurnContext), the URL of the channel server is
710
     * retrieved from
711
     * `contextOrServiceUrl`.[activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl).
712
     */
713
    async getConversations(
714
        contextOrServiceUrl: TurnContext | string,
715
        continuationToken?: string,
716
    ): Promise<ConversationsResult> {
717
        let client: ConnectorClient;
718
        if (typeof contextOrServiceUrl === 'object') {
4✔
719
            const context: TurnContext = contextOrServiceUrl as TurnContext;
2✔
720
            client = this.getOrCreateConnectorClient(context, context.activity.serviceUrl, this.credentials);
2✔
721
        } else {
722
            client = this.createConnectorClient(contextOrServiceUrl as string);
2✔
723
        }
724

725
        return await client.conversations.getConversations(
2✔
726
            continuationToken ? { continuationToken: continuationToken } : undefined,
2!
727
        );
728
    }
729

730
    /**
731
     * Asynchronously attempts to retrieve the token for a user that's in a login flow.
732
     *
733
     * @param context The context object for the turn.
734
     * @param connectionName The name of the auth connection to use.
735
     * @param magicCode Optional. The validation code the user entered.
736
     * @param oAuthAppCredentials AppCredentials for OAuth.
737
     * @returns A [TokenResponse](xref:botframework-schema.TokenResponse) object that contains the user token.
738
     */
739
    async getUserToken(context: TurnContext, connectionName: string, magicCode?: string): Promise<TokenResponse>;
740
    async getUserToken(
741
        context: TurnContext,
742
        connectionName: string,
743
        magicCode?: string,
744
        oAuthAppCredentials?: CoreAppCredentials,
745
    ): Promise<TokenResponse>;
746
    /**
747
     * Asynchronously attempts to retrieve the token for a user that's in a login flow.
748
     *
749
     * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
750
     * @param connectionName The name of the auth connection to use.
751
     * @param magicCode Optional. The validation code the user entered.
752
     * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
753
     * @returns A [TokenResponse](xref:botframework-schema.TokenResponse) object that contains the user token.
754
     */
755
    async getUserToken(
756
        context: TurnContext,
757
        connectionName: string,
758
        magicCode?: string,
759
        oAuthAppCredentials?: AppCredentials,
760
    ): Promise<TokenResponse> {
761
        if (!context.activity.from || !context.activity.from.id) {
4✔
762
            throw new Error('BotFrameworkAdapter.getUserToken(): missing from or from.id');
2✔
763
        }
764
        if (!connectionName) {
2✔
765
            throw new Error('getUserToken() requires a connectionName but none was provided.');
1✔
766
        }
767
        this.checkEmulatingOAuthCards(context);
1✔
768
        const userId: string = context.activity.from.id;
1✔
769
        const url: string = this.oauthApiUrl(context);
1✔
770
        const client: TokenApiClient = this.createTokenApiClient(url, oAuthAppCredentials);
1✔
771
        context.turnState.set(this.TokenApiClientCredentialsKey, client);
1✔
772

773
        const result: TokenApiModels.UserTokenGetTokenResponse = await client.userToken.getToken(
1✔
774
            userId,
775
            connectionName,
776
            { code: magicCode, channelId: context.activity.channelId },
777
        );
778
        if (!result || !result.token || result._response.status == 404) {
1!
779
            return undefined;
×
780
        } else {
781
            return result as TokenResponse;
1✔
782
        }
783
    }
784

785
    /**
786
     * Asynchronously signs out the user from the token server.
787
     *
788
     * @param context The context object for the turn.
789
     * @param connectionName The name of the auth connection to use.
790
     * @param userId The ID of user to sign out.
791
     * @param oAuthAppCredentials AppCredentials for OAuth.
792
     */
793
    async signOutUser(context: TurnContext, connectionName?: string, userId?: string): Promise<void>;
794
    async signOutUser(
795
        context: TurnContext,
796
        connectionName?: string,
797
        userId?: string,
798
        oAuthAppCredentials?: CoreAppCredentials,
799
    ): Promise<void>;
800
    /**
801
     * Asynchronously signs out the user from the token server.
802
     *
803
     * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
804
     * @param connectionName Optional. The name of the auth connection to use.
805
     * @param userId Optional. The ID of the user to sign out.
806
     * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
807
     */
808
    async signOutUser(
809
        context: TurnContext,
810
        connectionName?: string,
811
        userId?: string,
812
        oAuthAppCredentials?: AppCredentials,
813
    ): Promise<void> {
814
        if (!context.activity.from || !context.activity.from.id) {
3✔
815
            throw new Error('BotFrameworkAdapter.signOutUser(): missing from or from.id');
2✔
816
        }
817
        if (!userId) {
1✔
818
            userId = context.activity.from.id;
1✔
819
        }
820

821
        this.checkEmulatingOAuthCards(context);
1✔
822
        const url: string = this.oauthApiUrl(context);
1✔
823
        const client: TokenApiClient = this.createTokenApiClient(url, oAuthAppCredentials);
1✔
824
        context.turnState.set(this.TokenApiClientCredentialsKey, client);
1✔
825

826
        await client.userToken.signOut(userId, {
1✔
827
            connectionName: connectionName,
828
            channelId: context.activity.channelId,
829
        });
830
    }
831

832
    /**
833
     * Asynchronously gets a sign-in link from the token server that can be sent as part
834
     * of a [SigninCard](xref:botframework-schema.SigninCard).
835
     *
836
     * @param context The context object for the turn.
837
     * @param connectionName The name of the auth connection to use.
838
     * @param oAuthAppCredentials AppCredentials for OAuth.
839
     * @param userId The user id that will be associated with the token.
840
     * @param finalRedirect The final URL that the OAuth flow will redirect to.
841
     */
842
    async getSignInLink(
843
        context: TurnContext,
844
        connectionName: string,
845
        oAuthAppCredentials?: AppCredentials,
846
        userId?: string,
847
        finalRedirect?: string,
848
    ): Promise<string>;
849
    async getSignInLink(
850
        context: TurnContext,
851
        connectionName: string,
852
        oAuthAppCredentials?: CoreAppCredentials,
853
        userId?: string,
854
        finalRedirect?: string,
855
    ): Promise<string>;
856
    /**
857
     * Asynchronously gets a sign-in link from the token server that can be sent as part
858
     * of a [SigninCard](xref:botframework-schema.SigninCard).
859
     *
860
     * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
861
     * @param connectionName The name of the auth connection to use.
862
     * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
863
     * @param userId Optional. The user id that will be associated with the token.
864
     * @param finalRedirect Optional. The final URL that the OAuth flow will redirect to.
865
     * @returns The sign in link.
866
     */
867
    async getSignInLink(
868
        context: TurnContext,
869
        connectionName: string,
870
        oAuthAppCredentials?: AppCredentials,
871
        userId?: string,
872
        finalRedirect?: string,
873
    ): Promise<string> {
874
        if (userId && userId != context.activity.from.id) {
4✔
875
            throw new ReferenceError(
1✔
876
                "cannot retrieve OAuth signin link for a user that's different from the conversation",
877
            );
878
        }
879

880
        this.checkEmulatingOAuthCards(context);
3✔
881
        const conversation: Partial<ConversationReference> = TurnContext.getConversationReference(context.activity);
3✔
882
        const url: string = this.oauthApiUrl(context);
3✔
883
        const client: TokenApiClient = this.createTokenApiClient(url, oAuthAppCredentials);
3✔
884
        context.turnState.set(this.TokenApiClientCredentialsKey, client);
3✔
885
        const state: any = {
3✔
886
            ConnectionName: connectionName,
887
            Conversation: conversation,
888
            MsAppId: (client.credentials as AppCredentials).appId,
889
            RelatesTo: context.activity.relatesTo,
890
        };
891

892
        const finalState: string = Buffer.from(JSON.stringify(state)).toString('base64');
3✔
893
        return (
3✔
894
            await client.botSignIn.getSignInUrl(finalState, { channelId: context.activity.channelId, finalRedirect })
895
        )._response.bodyAsText;
896
    }
897

898
    /**
899
     * Asynchronously retrieves the token status for each configured connection for the given user.
900
     *
901
     * @param context The context object for the turn.
902
     * @param userId Optional. If present, the ID of the user to retrieve the token status for.
903
     *      Otherwise, the ID of the user who sent the current activity is used.
904
     * @param includeFilter Optional. A comma-separated list of connection's to include. If present,
905
     *      the `includeFilter` parameter limits the tokens this method returns.
906
     * @param oAuthAppCredentials AppCredentials for OAuth.
907
     * @returns The [TokenStatus](xref:botframework-connector.TokenStatus) objects retrieved.
908
     */
909
    async getTokenStatus(context: TurnContext, userId?: string, includeFilter?: string): Promise<TokenStatus[]>;
910
    async getTokenStatus(
911
        context: TurnContext,
912
        userId?: string,
913
        includeFilter?: string,
914
        oAuthAppCredentials?: CoreAppCredentials,
915
    ): Promise<TokenStatus[]>;
916
    /**
917
     * Asynchronously retrieves the token status for each configured connection for the given user.
918
     *
919
     * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
920
     * @param userId Optional. If present, the ID of the user to retrieve the token status for.
921
     * Otherwise, the ID of the user who sent the current activity is used.
922
     * @param includeFilter Optional. A comma-separated list of connection's to include. If present,
923
     * the `includeFilter` parameter limits the tokens this method returns.
924
     * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
925
     * @returns The [TokenStatus](xref:botframework-connector.TokenStatus) objects retrieved.
926
     */
927
    async getTokenStatus(
928
        context: TurnContext,
929
        userId?: string,
930
        includeFilter?: string,
931
        oAuthAppCredentials?: AppCredentials,
932
    ): Promise<TokenStatus[]> {
933
        if (!userId && (!context.activity.from || !context.activity.from.id)) {
4✔
934
            throw new Error('BotFrameworkAdapter.getTokenStatus(): missing from or from.id');
2✔
935
        }
936
        this.checkEmulatingOAuthCards(context);
2✔
937
        userId = userId || context.activity.from.id;
2!
938
        const url: string = this.oauthApiUrl(context);
2✔
939
        const client: TokenApiClient = this.createTokenApiClient(url, oAuthAppCredentials);
2✔
940
        context.turnState.set(this.TokenApiClientCredentialsKey, client);
2✔
941

942
        return (
2✔
943
            await client.userToken.getTokenStatus(userId, {
944
                channelId: context.activity.channelId,
945
                include: includeFilter,
946
            })
947
        )._response.parsedBody;
948
    }
949

950
    /**
951
     * Asynchronously signs out the user from the token server.
952
     *
953
     * @param context The context object for the turn.
954
     * @param connectionName The name of the auth connection to use.
955
     * @param resourceUrls The list of resource URLs to retrieve tokens for.
956
     * @param oAuthAppCredentials AppCredentials for OAuth.
957
     * @returns A map of the [TokenResponse](xref:botframework-schema.TokenResponse) objects by resource URL.
958
     */
959
    async getAadTokens(
960
        context: TurnContext,
961
        connectionName: string,
962
        resourceUrls: string[],
963
    ): Promise<{ [propertyName: string]: TokenResponse }>;
964
    async getAadTokens(
965
        context: TurnContext,
966
        connectionName: string,
967
        resourceUrls: string[],
968
        oAuthAppCredentials?: CoreAppCredentials,
969
    ): Promise<{ [propertyName: string]: TokenResponse }>;
970
    /**
971
     * Asynchronously signs out the user from the token server.
972
     *
973
     * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
974
     * @param connectionName The name of the auth connection to use.
975
     * @param resourceUrls The list of resource URLs to retrieve tokens for.
976
     * @param oAuthAppCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
977
     * @returns A map of the [TokenResponse](xref:botframework-schema.TokenResponse) objects by resource URL.
978
     */
979
    async getAadTokens(
980
        context: TurnContext,
981
        connectionName: string,
982
        resourceUrls: string[],
983
        oAuthAppCredentials?: AppCredentials,
984
    ): Promise<{ [propertyName: string]: TokenResponse }> {
985
        if (!context.activity.from || !context.activity.from.id) {
3✔
986
            throw new Error('BotFrameworkAdapter.getAadTokens(): missing from or from.id');
2✔
987
        }
988
        this.checkEmulatingOAuthCards(context);
1✔
989
        const userId: string = context.activity.from.id;
1✔
990
        const url: string = this.oauthApiUrl(context);
1✔
991
        const client: TokenApiClient = this.createTokenApiClient(url, oAuthAppCredentials);
1✔
992
        context.turnState.set(this.TokenApiClientCredentialsKey, client);
1✔
993

994
        return (
1✔
995
            await client.userToken.getAadTokens(
996
                userId,
997
                connectionName,
998
                { resourceUrls: resourceUrls },
999
                { channelId: context.activity.channelId },
1000
            )
1001
        )._response.parsedBody as { [propertyName: string]: TokenResponse };
1002
    }
1003

1004
    /**
1005
     * Asynchronously Get the raw signin resource to be sent to the user for signin.
1006
     *
1007
     * @param context The context object for the turn.
1008
     * @param connectionName The name of the auth connection to use.
1009
     * @param userId The user id that will be associated with the token.
1010
     * @param finalRedirect The final URL that the OAuth flow will redirect to.
1011
     * @param appCredentials Optional. The CoreAppCredentials for OAuth.
1012
     * @returns The [BotSignInGetSignInResourceResponse](xref:botframework-connector.BotSignInGetSignInResourceResponse) object.
1013
     */
1014
    async getSignInResource(
1015
        context: TurnContext,
1016
        connectionName: string,
1017
        userId?: string,
1018
        finalRedirect?: string,
1019
        appCredentials?: CoreAppCredentials,
1020
    ): Promise<SignInUrlResponse> {
1021
        if (!connectionName) {
5✔
1022
            throw new Error('getUserToken() requires a connectionName but none was provided.');
1✔
1023
        }
1024

1025
        if (!context.activity.from || !context.activity.from.id) {
4✔
1026
            throw new Error('BotFrameworkAdapter.getSignInResource(): missing from or from.id');
2✔
1027
        }
1028

1029
        // The provided userId doesn't match the from.id on the activity. (same for finalRedirect)
1030
        if (userId && context.activity.from.id !== userId) {
2✔
1031
            throw new Error(
1✔
1032
                'BotFrameworkAdapter.getSiginInResource(): cannot get signin resource for a user that is different from the conversation',
1033
            );
1034
        }
1035

1036
        const url: string = this.oauthApiUrl(context);
1✔
1037
        const credentials = appCredentials as AppCredentials;
1✔
1038
        const client: TokenApiClient = this.createTokenApiClient(url, credentials);
1✔
1039
        const conversation: Partial<ConversationReference> = TurnContext.getConversationReference(context.activity);
1✔
1040

1041
        const state: any = {
1✔
1042
            ConnectionName: connectionName,
1043
            Conversation: conversation,
1044
            relatesTo: context.activity.relatesTo,
1045
            MSAppId: (client.credentials as AppCredentials).appId,
1046
        };
1047
        const finalState: string = Buffer.from(JSON.stringify(state)).toString('base64');
1✔
1048
        const options: TokenApiModels.BotSignInGetSignInResourceOptionalParams = { finalRedirect: finalRedirect };
1✔
1049

1050
        return await client.botSignIn.getSignInResource(finalState, options);
1✔
1051
    }
1052

1053
    /**
1054
     * Asynchronously Performs a token exchange operation such as for single sign-on.
1055
     *
1056
     * @param context Context for the current turn of conversation with the user.
1057
     * @param connectionName Name of the auth connection to use.
1058
     * @param userId The user id that will be associated with the token.
1059
     * @param tokenExchangeRequest The exchange request details, either a token to exchange or a uri to exchange.
1060
     * @param appCredentials Optional. The CoreAppCredentials for OAuth.
1061
     */
1062
    async exchangeToken(
1063
        context: TurnContext,
1064
        connectionName: string,
1065
        userId: string,
1066
        tokenExchangeRequest: TokenExchangeRequest,
1067
        appCredentials?: CoreAppCredentials,
1068
    ): Promise<TokenResponse>;
1069
    /**
1070
     * Asynchronously Performs a token exchange operation such as for single sign-on.
1071
     *
1072
     * @param context The [TurnContext](xref:botbuilder-core.TurnContext) for the turn.
1073
     * @param connectionName Name of the auth connection to use.
1074
     * @param userId The user id that will be associated with the token.
1075
     * @param tokenExchangeRequest The [TokenExchangeRequest](xref:botbuilder-schema.TokenExchangeRequest), either a token to exchange or a uri to exchange.
1076
     * @param appCredentials Optional. [AppCredentials](xref:botframework-connector.AppCredentials) for OAuth.
1077
     * @returns A `Promise` representing the exchanged [TokenResponse](xref:botframework-schema.TokenResponse).
1078
     */
1079
    async exchangeToken(
1080
        context: TurnContext,
1081
        connectionName: string,
1082
        userId: string,
1083
        tokenExchangeRequest: TokenExchangeRequest,
1084
        appCredentials?: AppCredentials,
1085
    ): Promise<TokenResponse> {
1086
        if (!connectionName) {
4✔
1087
            throw new Error('exchangeToken() requires a connectionName but none was provided.');
1✔
1088
        }
1089

1090
        if (!userId) {
3✔
1091
            throw new Error('exchangeToken() requires an userId but none was provided.');
1✔
1092
        }
1093

1094
        if (tokenExchangeRequest && !tokenExchangeRequest.token && !tokenExchangeRequest.uri) {
2✔
1095
            throw new Error(
1✔
1096
                'BotFrameworkAdapter.exchangeToken(): Either a Token or Uri property is required on the TokenExchangeRequest',
1097
            );
1098
        }
1099

1100
        const url: string = this.oauthApiUrl(context);
1✔
1101
        const client: TokenApiClient = this.createTokenApiClient(url, appCredentials);
1✔
1102

1103
        return (
1✔
1104
            await client.userToken.exchangeAsync(
1105
                userId,
1106
                connectionName,
1107
                context.activity.channelId,
1108
                tokenExchangeRequest,
1109
            )
1110
        )._response.parsedBody as TokenResponse;
1111
    }
1112

1113
    /**
1114
     * Asynchronously sends an emulated OAuth card for a channel.
1115
     *
1116
     * This method supports the framework and is not intended to be called directly for your code.
1117
     *
1118
     * @param contextOrServiceUrl The URL of the emulator.
1119
     * @param emulate `true` to send an emulated OAuth card to the emulator; or `false` to not send the card.
1120
     * @remarks
1121
     * When testing a bot in the Bot Framework Emulator, this method can emulate the OAuth card interaction.
1122
     */
1123
    async emulateOAuthCards(contextOrServiceUrl: TurnContext | string, emulate: boolean): Promise<void> {
1124
        this.isEmulatingOAuthCards = emulate;
×
1125
        const url: string = this.oauthApiUrl(contextOrServiceUrl);
×
1126
        await EmulatorApiClient.emulateOAuthCards(this.credentials, url, emulate);
×
1127
    }
1128

1129
    /**
1130
     * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity.
1131
     *
1132
     * @param req An Express or Restify style request object.
1133
     * @param res An Express or Restify style response object.
1134
     * @param logic The function to call at the end of the middleware pipeline.
1135
     * @remarks
1136
     * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method:
1137
     *
1138
     * 1. Parses and authenticates an incoming request.
1139
     *    - The activity is read from the body of the incoming request. An error will be returned
1140
     *      if the activity can't be parsed.
1141
     *    - The identity of the sender is authenticated as either the Emulator or a valid Microsoft
1142
     *      server, using the bot's `appId` and `appPassword`. The request is rejected if the sender's
1143
     *      identity is not verified.
1144
     * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity.
1145
     *    - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable).
1146
     *    - When this method completes, the proxy is revoked.
1147
     * 1. Sends the turn context through the adapter's middleware pipeline.
1148
     * 1. Sends the turn context to the `logic` function.
1149
     *    - The bot may perform additional routing or processing at this time.
1150
     *      Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete.
1151
     *    - After the `logic` function completes, the promise chain set up by the middleware is resolved.
1152
     *
1153
     * > [!TIP]
1154
     * > If you see the error `TypeError: Cannot perform 'set' on a proxy that has been revoked`
1155
     * > in your bot's console output, the likely cause is that an async function was used
1156
     * > without using the `await` keyword. Make sure all async functions use await!
1157
     *
1158
     * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the
1159
     * `logic` function is not called; however, all middleware prior to this point still run to completion.
1160
     * For more information about the middleware pipeline, see the
1161
     * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and
1162
     * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles.
1163
     * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter.
1164
     *
1165
     * For example:
1166
     * ```JavaScript
1167
     * server.post('/api/messages', (req, res) => {
1168
     *    // Route received request to adapter for processing
1169
     *    adapter.processActivity(req, res, async (context) => {
1170
     *        // Process any messages received
1171
     *        if (context.activity.type === ActivityTypes.Message) {
1172
     *            await context.sendActivity(`Hello World`);
1173
     *        }
1174
     *    });
1175
     * });
1176
     * ```
1177
     */
1178
    async processActivity(
1179
        req: WebRequest,
1180
        res: WebResponse,
1181
        logic: (context: TurnContext) => Promise<any>,
1182
    ): Promise<void> {
1183
        let body: any;
1184
        let status: number;
1185
        let processError: Error;
1186
        try {
21✔
1187
            // Parse body of request
1188
            status = 400;
21✔
1189
            const request = await parseRequest(req);
21✔
1190

1191
            // Authenticate the incoming request
1192
            status = 401;
19✔
1193
            const authHeader: string = req.headers.authorization || req.headers.Authorization || '';
19✔
1194

1195
            const identity = await this.authenticateRequestInternal(request, authHeader);
19✔
1196

1197
            // Set the correct callerId value and discard values received over the wire
1198
            request.callerId = await this.generateCallerId(identity);
18✔
1199

1200
            // Process received activity
1201
            status = 500;
18✔
1202
            const context: TurnContext = this.createContext(request);
18✔
1203
            context.turnState.set(this.BotIdentityKey, identity);
18✔
1204
            const connectorClient = await this.createConnectorClientWithIdentity(request.serviceUrl, identity);
18✔
1205
            context.turnState.set(this.ConnectorClientKey, connectorClient);
18✔
1206

1207
            const oAuthScope: string = SkillValidation.isSkillClaim(identity.claims)
18✔
1208
                ? JwtTokenValidation.getAppIdFromClaims(identity.claims)
18✔
1209
                : this.credentials.oAuthScope;
1210
            context.turnState.set(this.OAuthScopeKey, oAuthScope);
18✔
1211

1212
            context.turnState.set(BotCallbackHandlerKey, logic);
18✔
1213
            await this.runMiddleware(context, logic);
18✔
1214

1215
            // NOTE: The factoring of the code differs here when compared to C# as processActivity() returns Promise<void>.
1216
            //       This is due to the fact that the response to the incoming activity is sent from inside this implementation.
1217
            //       In C#, ProcessActivityAsync() returns Task<InvokeResponse> and ASP.NET handles sending of the response.
1218
            if (request.deliveryMode === DeliveryModes.ExpectReplies) {
17✔
1219
                // Handle "expectReplies" scenarios where all the activities have been buffered and sent back at once
1220
                // in an invoke response.
1221
                let activities = context.bufferedReplyActivities;
4✔
1222

1223
                // If the channel is not the emulator, do not send trace activities.
1224
                // Fixes: https://github.com/microsoft/botbuilder-js/issues/2732
1225
                if (request.channelId !== Channels.Emulator) {
4✔
1226
                    activities = activities.filter((a) => a.type !== ActivityTypes.Trace);
5✔
1227
                }
1228

1229
                body = { activities } as ExpectedReplies;
4✔
1230
                status = StatusCodes.OK;
4✔
1231
            } else if (request.type === ActivityTypes.Invoke) {
13✔
1232
                // Retrieve a cached Invoke response to handle Invoke scenarios.
1233
                // These scenarios deviate from the request/request model as the Bot should return a specific body and status.
1234
                const invokeResponse: any = context.turnState.get(INVOKE_RESPONSE_KEY);
2✔
1235
                if (invokeResponse && invokeResponse.value) {
2✔
1236
                    const value: InvokeResponse = invokeResponse.value;
1✔
1237
                    status = value.status;
1✔
1238
                    body = value.body;
1✔
1239
                } else {
1240
                    status = StatusCodes.NOT_IMPLEMENTED;
1✔
1241
                }
1242
            } else {
1243
                status = StatusCodes.OK;
11✔
1244
            }
1245
        } catch (err) {
1246
            // Catch the error to try and throw the stacktrace out of processActivity()
1247
            processError = err;
4✔
1248
            body = err.toString();
4✔
1249
        }
1250

1251
        // Return status
1252
        res.status(status);
21✔
1253
        if (body) {
21✔
1254
            res.send(body);
9✔
1255
        }
1256
        res.end();
21✔
1257

1258
        // Check for an error
1259
        if (status >= 400) {
21✔
1260
            if (processError && (processError as Error).stack) {
5✔
1261
                throw new Error(`BotFrameworkAdapter.processActivity(): ${status} ERROR\n ${processError.stack}`);
4✔
1262
            } else {
1263
                throw new Error(`BotFrameworkAdapter.processActivity(): ${status} ERROR`);
1✔
1264
            }
1265
        }
1266
    }
1267

1268
    /**
1269
     * Asynchronously creates a turn context and runs the middleware pipeline for an incoming activity.
1270
     *
1271
     * Use [CloudAdapter.processActivityDirect] instead.
1272
     *
1273
     * @param activity The activity to process.
1274
     * @param logic The function to call at the end of the middleware pipeline.
1275
     * @remarks
1276
     * This is the main way a bot receives incoming messages and defines a turn in the conversation. This method:
1277
     *
1278
     * 1. Creates a [TurnContext](xref:botbuilder-core.TurnContext) object for the received activity.
1279
     *    - This object is wrapped with a [revocable proxy](https://www.ecma-international.org/ecma-262/6.0/#sec-proxy.revocable).
1280
     *    - When this method completes, the proxy is revoked.
1281
     * 1. Sends the turn context through the adapter's middleware pipeline.
1282
     * 1. Sends the turn context to the `logic` function.
1283
     *    - The bot may perform additional routing or processing at this time.
1284
     *      Returning a promise (or providing an `async` handler) will cause the adapter to wait for any asynchronous operations to complete.
1285
     *    - After the `logic` function completes, the promise chain set up by the middleware is resolved.
1286
     *
1287
     * Middleware can _short circuit_ a turn. When this happens, subsequent middleware and the
1288
     * `logic` function is not called; however, all middleware prior to this point still run to completion.
1289
     * For more information about the middleware pipeline, see the
1290
     * [how bots work](https://docs.microsoft.com/azure/bot-service/bot-builder-basics) and
1291
     * [middleware](https://docs.microsoft.com/azure/bot-service/bot-builder-concept-middleware) articles.
1292
     * Use the adapter's [use](xref:botbuilder-core.BotAdapter.use) method to add middleware to the adapter.
1293
     */
1294
    async processActivityDirect(activity: Activity, logic: (context: TurnContext) => Promise<any>): Promise<void> {
1295
        let processError: Error;
1296
        try {
6✔
1297
            // Process activity
1298
            const context: TurnContext = this.createContext(activity);
6✔
1299
            context.turnState.set(BotCallbackHandlerKey, logic);
6✔
1300
            await this.runMiddleware(context, logic);
6✔
1301
        } catch (err) {
1302
            // Catch the error to try and throw the stacktrace out of processActivity()
1303
            processError = err;
2✔
1304
        }
1305

1306
        if (processError) {
6✔
1307
            if (processError && (processError as Error).stack) {
2✔
1308
                throw new Error(`BotFrameworkAdapter.processActivityDirect(): ERROR\n ${processError.stack}`);
1✔
1309
            } else {
1310
                throw new Error('BotFrameworkAdapter.processActivityDirect(): ERROR');
1✔
1311
            }
1312
        }
1313
    }
1314

1315
    /**
1316
     * Asynchronously sends a set of outgoing activities to a channel server.
1317
     *
1318
     * This method supports the framework and is not intended to be called directly for your code.
1319
     * Use the turn context's [sendActivity](xref:botbuilder-core.TurnContext.sendActivity) or
1320
     * [sendActivities](xref:botbuilder-core.TurnContext.sendActivities) method from your bot code.
1321
     *
1322
     * @param context The context object for the turn.
1323
     * @param activities The activities to send.
1324
     * @returns An array of [ResourceResponse](xref:)
1325
     * @remarks
1326
     * The activities will be sent one after another in the order in which they're received. A
1327
     * response object will be returned for each sent activity. For `message` activities this will
1328
     * contain the ID of the delivered message.
1329
     */
1330
    async sendActivities(context: TurnContext, activities: Partial<Activity>[]): Promise<ResourceResponse[]> {
1331
        const responses: ResourceResponse[] = [];
17✔
1332
        for (let i = 0; i < activities.length; i++) {
17✔
1333
            const activity: Partial<Activity> = activities[i];
22✔
1334
            switch (activity.type) {
22✔
1335
                case 'delay':
22✔
1336
                    await delay(typeof activity.value === 'number' ? activity.value : 1000);
2✔
1337
                    responses.push({} as ResourceResponse);
2✔
1338
                    break;
2✔
1339
                case ActivityTypes.InvokeResponse:
1340
                    // Cache response to context object. This will be retrieved when turn completes.
1341
                    context.turnState.set(INVOKE_RESPONSE_KEY, activity);
4✔
1342
                    responses.push({} as ResourceResponse);
4✔
1343
                    break;
4✔
1344
                default: {
1345
                    if (!activity.serviceUrl) {
16✔
1346
                        throw new Error('BotFrameworkAdapter.sendActivity(): missing serviceUrl.');
1✔
1347
                    }
1348
                    if (!activity.conversation || !activity.conversation.id) {
15✔
1349
                        throw new Error('BotFrameworkAdapter.sendActivity(): missing conversation id.');
1✔
1350
                    }
1351
                    if (activity && BotFrameworkAdapter.isStreamingServiceUrl(activity.serviceUrl)) {
14✔
1352
                        if (!this.isStreamingConnectionOpen) {
1✔
1353
                            throw new Error(
1✔
1354
                                'BotFrameworkAdapter.sendActivities(): Unable to send activity as Streaming connection is closed.',
1355
                            );
1356
                        }
1357
                        TokenResolver.checkForOAuthCards(this, context, activity as Activity);
×
1358
                    }
1359
                    const client = this.getOrCreateConnectorClient(context, activity.serviceUrl, this.credentials);
13✔
1360
                    if (activity.type === ActivityTypes.Trace && activity.channelId !== Channels.Emulator) {
13!
1361
                        // Just eat activity
1362
                        responses.push({} as ResourceResponse);
×
1363
                    } else if (activity.replyToId) {
13✔
1364
                        responses.push(
12✔
1365
                            await client.conversations.replyToActivity(
1366
                                activity.conversation.id,
1367
                                activity.replyToId,
1368
                                activity,
1369
                            ),
1370
                        );
1371
                    } else {
1372
                        responses.push(
1✔
1373
                            await client.conversations.sendToConversation(activity.conversation.id, activity),
1374
                        );
1375
                    }
1376
                    break;
12✔
1377
                }
1378
            }
1379
        }
1380
        return responses;
13✔
1381
    }
1382

1383
    /**
1384
     * Asynchronously replaces a previous activity with an updated version.
1385
     *
1386
     * This interface supports the framework and is not intended to be called directly for your code.
1387
     * Use [TurnContext.updateActivity](xref:botbuilder-core.TurnContext.updateActivity) to update
1388
     * an activity from your bot code.
1389
     *
1390
     * @param context The context object for the turn.
1391
     * @param activity The updated version of the activity to replace.
1392
     * @returns A `Promise` representing the [ResourceResponse](xref:botframework-schema.ResourceResponse) for the operation.
1393
     * @remarks
1394
     * Not all channels support this operation. For channels that don't, this call may throw an exception.
1395
     */
1396
    async updateActivity(context: TurnContext, activity: Partial<Activity>): Promise<ResourceResponse | void> {
1397
        if (!activity.serviceUrl) {
4✔
1398
            throw new Error('BotFrameworkAdapter.updateActivity(): missing serviceUrl');
1✔
1399
        }
1400
        if (!activity.conversation || !activity.conversation.id) {
3✔
1401
            throw new Error('BotFrameworkAdapter.updateActivity(): missing conversation or conversation.id');
1✔
1402
        }
1403
        if (!activity.id) {
2✔
1404
            throw new Error('BotFrameworkAdapter.updateActivity(): missing activity.id');
1✔
1405
        }
1406

1407
        const client = this.getOrCreateConnectorClient(context, activity.serviceUrl, this.credentials);
1✔
1408
        return client.conversations.updateActivity(activity.conversation.id, activity.id, activity);
1✔
1409
    }
1410

1411
    /**
1412
     * Creates a connector client.
1413
     *
1414
     * @param serviceUrl The client's service URL.
1415
     * @returns The [ConnectorClient](xref:botbuilder-connector.ConnectorClient) instance.
1416
     * @remarks
1417
     * Override this in a derived class to create a mock connector client for unit testing.
1418
     */
1419
    createConnectorClient(serviceUrl: string): ConnectorClient {
1420
        return this.createConnectorClientInternal(serviceUrl, this.credentials);
43✔
1421
    }
1422

1423
    /**
1424
     * Create a ConnectorClient with a ClaimsIdentity.
1425
     *
1426
     * @remarks
1427
     * If the ClaimsIdentity contains the claims for a Skills request, create a ConnectorClient for use with Skills.
1428
     * Derives the correct audience from the ClaimsIdentity, or the instance's credentials property.
1429
     * @param serviceUrl The client's service URL.
1430
     * @param identity ClaimsIdentity
1431
     */
1432
    async createConnectorClientWithIdentity(serviceUrl: string, identity: ClaimsIdentity): Promise<ConnectorClient>;
1433
    /**
1434
     * Create a ConnectorClient with a ClaimsIdentity and an explicit audience.
1435
     *
1436
     * @remarks
1437
     * If the trimmed audience is not a non-zero length string, the audience will be derived from the ClaimsIdentity or
1438
     * the instance's credentials property.
1439
     * @param serviceUrl The client's service URL.
1440
     * @param identity ClaimsIdentity
1441
     * @param audience The recipient of the ConnectorClient's messages. Normally the Bot Framework Channel Service or the AppId of another bot.
1442
     */
1443
    async createConnectorClientWithIdentity(
1444
        serviceUrl: string,
1445
        identity: ClaimsIdentity,
1446
        audience: string,
1447
    ): Promise<ConnectorClient>;
1448
    /**
1449
     * Create a [ConnectorClient](xref:botbuilder-connector.ConnectorClient) with a [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity).
1450
     *
1451
     * @remarks
1452
     * 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.
1453
     * Derives the correct audience from the [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity), or the instance's credentials property.
1454
     * @param serviceUrl The client's service URL.
1455
     * @param identity [ClaimsIdentity](xref:botbuilder-connector.ClaimsIdentity).
1456
     * @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.
1457
     * @returns The client.
1458
     */
1459
    async createConnectorClientWithIdentity(
1460
        serviceUrl: string,
1461
        identity: ClaimsIdentity,
1462
        audience?: string,
1463
    ): Promise<ConnectorClient> {
1464
        if (!identity) {
11✔
1465
            throw new Error('BotFrameworkAdapter.createConnectorClientWithIdentity(): invalid identity parameter.');
1✔
1466
        }
1467

1468
        const botAppId =
1469
            identity.getClaimValue(AuthenticationConstants.AudienceClaim) ||
10✔
1470
            identity.getClaimValue(AuthenticationConstants.AppIdClaim);
1471
        // Check if the audience is a string and when trimmed doesn't have a length of 0.
1472
        const validAudience = typeof audience === 'string' && audience.trim().length > 0;
10✔
1473
        const oAuthScope = validAudience ? audience : await this.getOAuthScope(botAppId, identity.claims);
10✔
1474
        const credentials = await this.buildCredentials(botAppId, oAuthScope);
10✔
1475

1476
        const client: ConnectorClient = this.createConnectorClientInternal(serviceUrl, credentials);
10✔
1477
        return client;
10✔
1478
    }
1479

1480
    private createConnectorClientInternal(serviceUrl: string, credentials: AppCredentials): ConnectorClient {
1481
        const options: ConnectorClientOptions = { ...this.settings.clientOptions };
65✔
1482
        if (BotFrameworkAdapter.isStreamingServiceUrl(serviceUrl)) {
65✔
1483
            // Check if we have a streaming server. Otherwise, requesting a connector client
1484
            // for a non-existent streaming connection results in an error
1485
            if (!this.streamingServer) {
2!
1486
                throw new Error(
×
1487
                    `Cannot create streaming connector client for serviceUrl ${serviceUrl} without a streaming connection. Call 'useWebSocket' or 'useNamedPipe' to start a streaming connection.`,
1488
                );
1489
            }
1490

1491
            options.httpClient = new StreamingHttpClient(this.streamingServer);
2✔
1492
        }
1493

1494
        options.baseUri = serviceUrl;
65✔
1495
        const userAgent = typeof options.userAgent === 'function' ? options.userAgent(USER_AGENT) : options.userAgent;
65!
1496
        options.userAgent = `${USER_AGENT} ${userAgent ?? ''}`;
65✔
1497

1498
        options.requestPolicyFactories = [
65✔
1499
            {
1500
                create: (nextPolicy) => ({
13✔
1501
                    sendRequest: (httpRequest) => {
1502
                        if (!httpRequest.headers.contains('accept')) {
13✔
1503
                            httpRequest.headers.set('accept', '*/*');
13✔
1504
                        }
1505
                        return nextPolicy.sendRequest(httpRequest);
13✔
1506
                    },
1507
                }),
1508
            },
1509
        ];
1510

1511
        return new ConnectorClient(credentials, options);
65✔
1512
    }
1513

1514
    // Retrieves the ConnectorClient from the TurnContext or creates a new ConnectorClient with the provided serviceUrl and credentials.
1515
    private getOrCreateConnectorClient(
1516
        context: TurnContext,
1517
        serviceUrl: string,
1518
        credentials: AppCredentials,
1519
    ): ConnectorClient {
1520
        if (!context || !context.turnState) throw new Error('invalid context parameter');
5!
1521
        if (!serviceUrl) throw new Error('invalid serviceUrl');
5!
1522
        if (!credentials) throw new Error('invalid credentials');
5!
1523

1524
        let client: ConnectorClient = context.turnState.get(this.ConnectorClientKey);
5✔
1525
        // Inspect the retrieved client to confirm that the serviceUrl is correct, if it isn't, create a new one.
1526
        if (!client || client['baseUri'] !== serviceUrl) {
5✔
1527
            client = this.createConnectorClientInternal(serviceUrl, credentials);
1✔
1528
        }
1529

1530
        return client;
5✔
1531
    }
1532

1533
    /**
1534
     * Returns the correct [OAuthScope](xref:botframework-connector.AppCredentials.OAuthScope) for [AppCredentials](xref:botframework-connector.AppCredentials).
1535
     *
1536
     * @param botAppId The bot's AppId.
1537
     * @param claims The [Claim](xref:botbuilder-connector.Claim) list to check.
1538
     * @returns The current credentials' OAuthScope.
1539
     */
1540
    private async getOAuthScope(botAppId: string, claims: Claim[]): Promise<string> {
1541
        // If the Claims are for skills, we need to create an AppCredentials instance with
1542
        // the correct scope for communication between the caller and the skill.
1543
        if (botAppId && SkillValidation.isSkillClaim(claims)) {
15✔
1544
            return JwtTokenValidation.getAppIdFromClaims(claims);
2✔
1545
        }
1546

1547
        // Return the current credentials' OAuthScope.
1548
        return this.credentials.oAuthScope;
13✔
1549
    }
1550

1551
    /**
1552
     *
1553
     * @remarks
1554
     * When building credentials for bot-to-bot communication, oAuthScope must be passed in.
1555
     * @param appId The application id.
1556
     * @param oAuthScope The optional OAuth scope.
1557
     * @returns The app credentials to be used to acquire tokens.
1558
     */
1559
    protected async buildCredentials(appId: string, oAuthScope?: string): Promise<AppCredentials> {
1560
        // There is no cache for AppCredentials in JS as opposed to C#.
1561
        // Instead of retrieving an AppCredentials from the Adapter instance, generate a new one
1562
        const appPassword = await this.credentialsProvider.getAppPassword(appId);
9✔
1563

1564
        let credentials: AppCredentials;
1565
        if (this.settings.certificateThumbprint && this.settings.certificatePrivateKey) {
9✔
1566
            credentials = new CertificateAppCredentials(
1✔
1567
                appId,
1568
                this.settings.certificateThumbprint,
1569
                this.settings.certificatePrivateKey,
1570
                this.settings.channelAuthTenant,
1571
            );
1572
        } else {
1573
            if (JwtTokenValidation.isGovernment(this.settings.channelService)) {
8✔
1574
                credentials = new MicrosoftGovernmentAppCredentials(
1✔
1575
                    appId,
1576
                    appPassword,
1577
                    this.settings.channelAuthTenant,
1578
                    oAuthScope,
1579
                );
1580
            } else {
1581
                credentials = new MicrosoftAppCredentials(
7✔
1582
                    appId,
1583
                    appPassword,
1584
                    this.settings.channelAuthTenant,
1585
                    oAuthScope,
1586
                );
1587
            }
1588
        }
1589

1590
        return credentials;
9✔
1591
    }
1592

1593
    /**
1594
     * Creates an OAuth API client.
1595
     *
1596
     * @param serviceUrl The client's service URL.
1597
     * @param oAuthAppCredentials AppCredentials for OAuth.
1598
     * @remarks
1599
     * Override this in a derived class to create a mock OAuth API client for unit testing.
1600
     */
1601
    protected createTokenApiClient(serviceUrl: string, oAuthAppCredentials?: CoreAppCredentials): TokenApiClient;
1602
    /**
1603
     * Creates an OAuth API client.
1604
     *
1605
     * @param serviceUrl The client's service URL.
1606
     * @param oAuthAppCredentials Optional. The [AppCredentials](xref:botframework-connector.AppCredentials)for OAuth.
1607
     * @remarks
1608
     * Override this in a derived class to create a mock OAuth API client for unit testing.
1609
     * @returns The client.
1610
     */
1611
    protected createTokenApiClient(serviceUrl: string, oAuthAppCredentials?: AppCredentials): TokenApiClient {
1612
        const tokenApiClientCredentials = oAuthAppCredentials ? oAuthAppCredentials : this.credentials;
×
1613
        const client = new TokenApiClient(tokenApiClientCredentials, { baseUri: serviceUrl, userAgent: USER_AGENT });
×
1614

1615
        return client;
×
1616
    }
1617

1618
    /**
1619
     * Allows for the overriding of authentication in unit tests.
1620
     *
1621
     * @param request Received request.
1622
     * @param authHeader Received authentication header.
1623
     */
1624
    protected async authenticateRequest(request: Partial<Activity>, authHeader: string): Promise<void> {
1625
        const identity = await this.authenticateRequestInternal(request, authHeader);
2✔
1626
        if (!identity.isAuthenticated) {
2!
1627
            throw new Error('Unauthorized Access. Request is not authorized');
×
1628
        }
1629

1630
        // Set the correct callerId value and discard values received over the wire
1631
        request.callerId = await this.generateCallerId(identity);
2✔
1632
    }
1633

1634
    /**
1635
     * @ignore
1636
     * @private
1637
     * Returns the actual ClaimsIdentity from the JwtTokenValidation.authenticateRequest() call.
1638
     * @remarks
1639
     * This method is used instead of authenticateRequest() in processActivity() to obtain the ClaimsIdentity for caching in the TurnContext.turnState.
1640
     *
1641
     * @param request Received request.
1642
     * @param authHeader Received authentication header.
1643
     */
1644
    private authenticateRequestInternal(request: Partial<Activity>, authHeader: string): Promise<ClaimsIdentity> {
1645
        return JwtTokenValidation.authenticateRequest(
8✔
1646
            request,
1647
            authHeader,
1648
            this.credentialsProvider,
1649
            this.settings.channelService,
1650
            this.authConfiguration,
1651
        );
1652
    }
1653

1654
    /**
1655
     * Generates the CallerId property for the activity based on
1656
     * https://github.com/microsoft/botframework-obi/blob/main/protocols/botframework-activity/botframework-activity.md#appendix-v---caller-id-values.
1657
     *
1658
     * @param identity The inbound claims.
1659
     * @returns {Promise<string>} a promise representing the generated callerId.
1660
     */
1661
    private async generateCallerId(identity: ClaimsIdentity): Promise<string> {
1662
        if (!identity) {
20!
1663
            throw new TypeError('BotFrameworkAdapter.generateCallerId(): Missing identity parameter.');
×
1664
        }
1665

1666
        // Is the bot accepting all incoming messages?
1667
        const isAuthDisabled = await this.credentialsProvider.isAuthenticationDisabled();
20✔
1668
        if (isAuthDisabled) {
20✔
1669
            // Return undefined so that the callerId is cleared.
1670
            return;
16✔
1671
        }
1672

1673
        // Is the activity from another bot?
1674
        if (SkillValidation.isSkillClaim(identity.claims)) {
4✔
1675
            const callerId = JwtTokenValidation.getAppIdFromClaims(identity.claims);
1✔
1676
            return `${CallerIdConstants.BotToBotPrefix}${callerId}`;
1✔
1677
        }
1678

1679
        // Is the activity from Public Azure?
1680
        if (!this.settings.channelService || this.settings.channelService.length === 0) {
3✔
1681
            return CallerIdConstants.PublicAzureChannel;
2✔
1682
        }
1683

1684
        // Is the activity from Azure Gov?
1685
        if (JwtTokenValidation.isGovernment(this.settings.channelService)) {
1✔
1686
            return CallerIdConstants.USGovChannel;
1✔
1687
        }
1688

1689
        // Return undefined so that the callerId is cleared.
1690
    }
1691

1692
    /**
1693
     * Gets the OAuth API endpoint.
1694
     *
1695
     * @param contextOrServiceUrl The URL of the channel server to query or
1696
     * a [TurnContext](xref:botbuilder-core.TurnContext). For a turn context, the context's
1697
     * [activity](xref:botbuilder-core.TurnContext.activity).[serviceUrl](xref:botframework-schema.Activity.serviceUrl)
1698
     * is used for the URL.
1699
     * @returns The endpoint used for the API requests.
1700
     * @remarks
1701
     * Override this in a derived class to create a mock OAuth API endpoint for unit testing.
1702
     */
1703
    protected oauthApiUrl(contextOrServiceUrl: TurnContext | string): string {
1704
        return this.isEmulatingOAuthCards
11✔
1705
            ? typeof contextOrServiceUrl === 'object'
11!
1706
                ? contextOrServiceUrl.activity.serviceUrl
×
1707
                : contextOrServiceUrl
1708
            : this.settings.oAuthEndpoint
1709
              ? this.settings.oAuthEndpoint
11✔
1710
              : JwtTokenValidation.isGovernment(this.settings.channelService)
1711
                ? US_GOV_OAUTH_ENDPOINT
10!
1712
                : OAUTH_ENDPOINT;
1713
    }
1714

1715
    /**
1716
     * Checks the environment and can set a flag to emulate OAuth cards.
1717
     *
1718
     * @param context The context object for the turn.
1719
     * @remarks
1720
     * Override this in a derived class to control how OAuth cards are emulated for unit testing.
1721
     */
1722
    protected checkEmulatingOAuthCards(context: TurnContext): void {
1723
        if (!this.isEmulatingOAuthCards && context.activity.channelId === 'emulator' && !this.credentials.appId) {
8!
1724
            this.isEmulatingOAuthCards = true;
×
1725
        }
1726
    }
1727

1728
    /**
1729
     * Creates a turn context.
1730
     *
1731
     * @param request An incoming request body.
1732
     * @returns A new [TurnContext](xref:botbuilder-core.TurnContext) instance.
1733
     * @remarks
1734
     * Override this in a derived class to modify how the adapter creates a turn context.
1735
     */
1736
    protected createContext(request: Partial<Activity>): TurnContext {
1737
        return new TurnContext(this, request);
43✔
1738
    }
1739

1740
    /**
1741
     * Checks the validity of the request and attempts to map it the correct virtual endpoint,
1742
     * then generates and returns a response if appropriate.
1743
     *
1744
     * @param request A ReceiveRequest from the connected channel.
1745
     * @returns A response created by the BotAdapter to be sent to the client that originated the request.
1746
     */
1747
    async processRequest(request: IReceiveRequest): Promise<StreamingResponse> {
1748
        const response = new StreamingResponse();
13✔
1749

1750
        if (!request) {
13!
1751
            response.statusCode = StatusCodes.BAD_REQUEST;
×
1752
            response.setBody('No request provided.');
×
1753
            return response;
×
1754
        }
1755

1756
        if (!request.verb || !request.path) {
13✔
1757
            response.statusCode = StatusCodes.BAD_REQUEST;
2✔
1758
            response.setBody(`Request missing verb and/or path. Verb: ${request.verb}. Path: ${request.path}`);
2✔
1759
            return response;
2✔
1760
        }
1761

1762
        if (request.verb.toLocaleUpperCase() !== POST && request.verb.toLocaleUpperCase() !== GET) {
11✔
1763
            response.statusCode = StatusCodes.METHOD_NOT_ALLOWED;
1✔
1764
            response.setBody(`Invalid verb received. Only GET and POST are accepted. Verb: ${request.verb}`);
1✔
1765
        }
1766

1767
        if (request.path.toLocaleLowerCase() === VERSION_PATH) {
11✔
1768
            return await this.handleVersionRequest(request, response);
4✔
1769
        }
1770

1771
        // Convert the StreamingRequest into an activity the Adapter can understand.
1772
        let body: Activity;
1773
        try {
7✔
1774
            body = await this.readRequestBodyAsString(request);
7✔
1775
        } catch (error) {
1776
            response.statusCode = StatusCodes.BAD_REQUEST;
1✔
1777
            response.setBody(`Request body missing or malformed: ${error}`);
1✔
1778
            return response;
1✔
1779
        }
1780

1781
        if (request.path.toLocaleLowerCase() !== MESSAGES_PATH) {
6✔
1782
            response.statusCode = StatusCodes.NOT_FOUND;
1✔
1783
            response.setBody(`Path ${request.path.toLocaleLowerCase()} not not found. Expected ${MESSAGES_PATH}}.`);
1✔
1784
            return response;
1✔
1785
        }
1786

1787
        if (request.verb.toLocaleUpperCase() !== POST) {
5!
1788
            response.statusCode = StatusCodes.METHOD_NOT_ALLOWED;
×
1789
            response.setBody(
×
1790
                `Invalid verb received for ${request.verb.toLocaleLowerCase()}. Only GET and POST are accepted. Verb: ${
1791
                    request.verb
1792
                }`,
1793
            );
1794

1795
            return response;
×
1796
        }
1797

1798
        try {
5✔
1799
            const context = new TurnContext(this, body);
5✔
1800
            await this.runMiddleware(context, this.logic);
5✔
1801

1802
            if (body.type === ActivityTypes.Invoke) {
3✔
1803
                const invokeResponse: any = context.turnState.get(INVOKE_RESPONSE_KEY);
2✔
1804
                if (invokeResponse && invokeResponse.value) {
2!
1805
                    const value: InvokeResponse = invokeResponse.value;
2✔
1806
                    response.statusCode = value.status;
2✔
1807
                    if (value.body) {
2!
1808
                        response.setBody(value.body);
×
1809
                    }
1810
                } else {
1811
                    response.statusCode = StatusCodes.NOT_IMPLEMENTED;
×
1812
                }
1813
            } else if (body.deliveryMode === DeliveryModes.ExpectReplies) {
1!
1814
                const replies: ExpectedReplies = { activities: context.bufferedReplyActivities as Activity[] };
×
1815
                response.setBody(replies);
×
1816
                response.statusCode = StatusCodes.OK;
×
1817
            } else {
1818
                response.statusCode = StatusCodes.OK;
1✔
1819
            }
1820
        } catch (error) {
1821
            response.statusCode = StatusCodes.INTERNAL_SERVER_ERROR;
2✔
1822
            response.setBody(error);
2✔
1823
            return response;
2✔
1824
        }
1825

1826
        return response;
3✔
1827
    }
1828

1829
    /**
1830
     * Process a web request by applying a logic function.
1831
     *
1832
     * @param req An incoming HTTP [Request](xref:botbuilder.Request)
1833
     * @param res The corresponding HTTP [Response](xref:botbuilder.Response)
1834
     * @param logic The logic function to apply
1835
     * @returns a promise representing the asynchronous operation.
1836
     */
1837
    async process(req: Request, res: Response, logic: (context: TurnContext) => Promise<void>): Promise<void>;
1838

1839
    /**
1840
     * Handle a web socket connection by applying a logic function to
1841
     * each streaming request.
1842
     *
1843
     * @param req An incoming HTTP [Request](xref:botbuilder.Request)
1844
     * @param socket The corresponding [INodeSocket](xref:botframework-streaming.INodeSocket)
1845
     * @param head The corresponding [INodeBuffer](xref:botframework-streaming.INodeBuffer)
1846
     * @param logic The logic function to apply
1847
     * @returns a promise representing the asynchronous operation.
1848
     */
1849
    async process(
1850
        req: Request,
1851
        socket: INodeSocket,
1852
        head: INodeBuffer,
1853
        logic: (context: TurnContext) => Promise<void>,
1854
    ): Promise<void>;
1855

1856
    /**
1857
     * @internal
1858
     */
1859
    async process(
1860
        req: Request,
1861
        resOrSocket: Response | INodeSocket,
1862
        logicOrHead: ((context: TurnContext) => Promise<void>) | INodeBuffer,
1863
        maybeLogic?: (context: TurnContext) => Promise<void>,
1864
    ): Promise<void> {
1865
        if (maybeLogic) {
×
1866
            return this.useWebSocket(
×
1867
                req,
1868
                INodeSocketT.parse(resOrSocket),
1869
                INodeBufferT.parse(logicOrHead),
1870
                LogicT.parse(maybeLogic),
1871
            );
1872
        } else {
1873
            return this.processActivity(req, ResponseT.parse(resOrSocket), LogicT.parse(logicOrHead));
×
1874
        }
1875
    }
1876

1877
    /**
1878
     * Connects the handler to a Named Pipe server and begins listening for incoming requests.
1879
     *
1880
     * @param logic The logic that will handle incoming requests.
1881
     * @param pipeName The name of the named pipe to use when creating the server.
1882
     * @param retryCount Number of times to attempt to bind incoming and outgoing pipe
1883
     * @param onListen Optional callback that fires once when server is listening on both incoming and outgoing pipe
1884
     */
1885
    async useNamedPipe(
1886
        logic: (context: TurnContext) => Promise<any>,
1887
        pipeName = defaultPipeName,
×
1888
        retryCount = 7,
5✔
1889
        onListen?: () => void,
1890
    ): Promise<void> {
1891
        if (!logic) {
5!
1892
            throw new Error('Bot logic needs to be provided to `useNamedPipe`');
×
1893
        }
1894

1895
        if (this.isStreamingConnectionOpen) {
5✔
1896
            if (this.namedPipeName === pipeName) {
2✔
1897
                // Idempotent operation
1898
                return;
1✔
1899
            } else {
1900
                // BotFrameworkAdapters are scoped to one streaming connection.
1901
                // Switching streams is an advanced scenario, one not innately supported by the SDK.
1902
                // Each BotFrameworkAdapter instance is scoped to a stream, so switching streams
1903
                // results in dropped conversations that the bot cannot reconnect to.
1904
                throw new Error(
1✔
1905
                    'This BotFrameworkAdapter instance is already connected to a different stream. Use a new instance to connect to the provided pipeName.',
1906
                );
1907
            }
1908
        }
1909

1910
        this.logic = logic;
3✔
1911

1912
        await retry(() => this.startNamedPipeServer(pipeName, onListen), retryCount);
3✔
1913
    }
1914

1915
    /**
1916
     * Process the initial request to establish a long lived connection via a streaming server.
1917
     *
1918
     * @param req The connection request.
1919
     * @param socket The raw socket connection between the bot (server) and channel/caller (client).
1920
     * @param head The first packet of the upgraded stream.
1921
     * @param logic The logic that handles incoming streaming requests for the lifetime of the WebSocket connection.
1922
     */
1923
    async useWebSocket(
1924
        req: WebRequest,
1925
        socket: INodeSocket,
1926
        head: INodeBuffer,
1927
        logic: (context: TurnContext) => Promise<any>,
1928
    ): Promise<void> {
1929
        // Use the provided NodeWebSocketFactoryBase on BotFrameworkAdapter construction,
1930
        // otherwise create a new NodeWebSocketFactory.
1931
        const webSocketFactory = this.webSocketFactory || new NodeWebSocketFactory();
7✔
1932

1933
        if (!logic) {
7✔
1934
            throw new Error('Streaming logic needs to be provided to `useWebSocket`');
1✔
1935
        }
1936

1937
        this.logic = logic;
6✔
1938

1939
        try {
6✔
1940
            await this.authenticateConnection(req, this.settings.channelService);
6✔
1941
        } catch (err) {
1942
            abortWebSocketUpgrade(socket, err);
2✔
1943
            throw err;
2✔
1944
        }
1945

1946
        const nodeWebSocket = await webSocketFactory.createWebSocket(req, socket, head);
4✔
1947

1948
        await this.startWebSocket(nodeWebSocket);
4✔
1949
    }
1950

1951
    private async startNamedPipeServer(pipeName: string, onListen?: () => void): Promise<void> {
1952
        this.namedPipeName = pipeName;
1✔
1953
        this.streamingServer = new NamedPipeServer(pipeName, this);
1✔
1954

1955
        try {
1✔
1956
            await this.streamingServer.start(onListen);
1✔
1957
        } finally {
1958
            this.namedPipeName = undefined;
×
1959
        }
1960
    }
1961

1962
    private async authenticateConnection(req: WebRequest, channelService?: string): Promise<void> {
1963
        if (!this.credentials.appId) {
6✔
1964
            // auth is disabled
1965
            return;
4✔
1966
        }
1967

1968
        const authHeader: string = req.headers.authorization || req.headers.Authorization || '';
2✔
1969
        const channelIdHeader: string = req.headers.channelid || req.headers.ChannelId || req.headers.ChannelID || '';
2✔
1970
        // Validate the received Upgrade request from the channel.
1971
        const claims = await JwtTokenValidation.validateAuthHeader(
2✔
1972
            authHeader,
1973
            this.credentialsProvider,
1974
            channelService,
1975
            channelIdHeader,
1976
        );
1977

1978
        if (!claims.isAuthenticated) {
×
1979
            throw new AuthenticationError('Unauthorized Access. Request is not authorized', StatusCodes.UNAUTHORIZED);
×
1980
        }
1981
    }
1982

1983
    /**
1984
     * Connects the handler to a WebSocket server and begins listening for incoming requests.
1985
     *
1986
     * @param socket The socket to use when creating the server.
1987
     */
1988
    private async startWebSocket(socket: ISocket): Promise<void> {
1989
        this.streamingServer = new WebSocketServer(socket, this);
4✔
1990
        await this.streamingServer.start();
4✔
1991
    }
1992

1993
    private async readRequestBodyAsString(request: IReceiveRequest): Promise<Activity> {
1994
        const [activityStream, ...attachmentStreams] = request.streams;
7✔
1995

1996
        const activity = await activityStream.readAsJson<Activity>();
6✔
1997

1998
        activity.attachments = await Promise.all(
6✔
1999
            attachmentStreams.map(async (attachmentStream) => {
×
2000
                const contentType = attachmentStream.contentType;
×
2001

2002
                const content =
2003
                    contentType === 'application/json'
×
2004
                        ? await attachmentStream.readAsJson()
×
2005
                        : await attachmentStream.readAsString();
2006

2007
                return { contentType, content };
×
2008
            }),
2009
        );
2010

2011
        return activity;
6✔
2012
    }
2013

2014
    private async handleVersionRequest(
2015
        request: IReceiveRequest,
2016
        response: StreamingResponse,
2017
    ): Promise<StreamingResponse> {
2018
        if (request.verb.toLocaleUpperCase() === GET) {
4✔
2019
            response.statusCode = StatusCodes.OK;
3✔
2020

2021
            if (!this.credentials.appId) {
3✔
2022
                response.setBody({ UserAgent: USER_AGENT });
3✔
2023
                return response;
3✔
2024
            }
2025

2026
            let token = '';
×
2027
            try {
×
2028
                token = await this.credentials.getToken();
×
2029
            } catch (err) {
2030
                /**
2031
                 * In reality a missing BotToken will cause the channel to close the connection,
2032
                 * but we still send the response and allow the channel to make that decision
2033
                 * instead of proactively disconnecting. This allows the channel to know why
2034
                 * the connection has been closed and make the choice not to make endless reconnection
2035
                 * attempts that will end up right back here.
2036
                 */
2037
                console.error(err.message);
×
2038
            }
2039
            response.setBody({ UserAgent: USER_AGENT, BotToken: token });
×
2040
        } else {
2041
            response.statusCode = StatusCodes.METHOD_NOT_ALLOWED;
1✔
2042
            response.setBody(
1✔
2043
                `Invalid verb received for path: ${request.path}. Only GET is accepted. Verb: ${request.verb}`,
2044
            );
2045
        }
2046

2047
        return response;
1✔
2048
    }
2049

2050
    /**
2051
     * Determine if the serviceUrl was sent via an Http/Https connection or Streaming
2052
     * This can be determined by looking at the ServiceUrl property:
2053
     * (1) All channels that send messages via http/https are not streaming
2054
     * (2) Channels that send messages via streaming have a ServiceUrl that does not begin with http/https.
2055
     *
2056
     * @param serviceUrl the serviceUrl provided in the resquest.
2057
     * @returns True if the serviceUrl is a streaming url, otherwise false.
2058
     */
2059
    private static isStreamingServiceUrl(serviceUrl: string): boolean {
2060
        return serviceUrl && !serviceUrl.toLowerCase().startsWith('http');
83✔
2061
    }
2062
}
2063

2064
/**
2065
 * Handles incoming webhooks from the botframework
2066
 * @private
2067
 * @param req incoming web request
2068
 */
2069
function parseRequest(req: WebRequest): Promise<Activity> {
2070
    return new Promise((resolve: any, reject: any): void => {
21✔
2071
        if (req.body) {
21✔
2072
            try {
4✔
2073
                const activity = validateAndFixActivity(req.body);
4✔
2074
                resolve(activity);
4✔
2075
            } catch (err) {
2076
                reject(err);
×
2077
            }
2078
        } else {
2079
            let requestData = '';
17✔
2080
            req.on('data', (chunk: string): void => {
17✔
2081
                requestData += chunk;
17✔
2082
            });
2083
            req.on('end', (): void => {
17✔
2084
                try {
17✔
2085
                    req.body = JSON.parse(requestData);
17✔
2086
                    const activity = validateAndFixActivity(req.body);
17✔
2087
                    resolve(activity);
15✔
2088
                } catch (err) {
2089
                    reject(err);
2✔
2090
                }
2091
            });
2092
        }
2093
    });
2094
}
2095

2096
/**
2097
 * Creates an error message with status code to write to socket, then closes the connection.
2098
 *
2099
 * @param socket The raw socket connection between the bot (server) and channel/caller (client).
2100
 * @param err The error. If the error includes a status code, it will be included in the message written to the socket.
2101
 */
2102
function abortWebSocketUpgrade(socket: INodeSocket, err: any): void {
2103
    if (socket.writable) {
2✔
2104
        const connectionHeader = "Connection: 'close'\r\n";
2✔
2105

2106
        let message = '';
2✔
2107
        if (AuthenticationError.isStatusCodeError(err)) {
2!
2108
            message = `HTTP/1.1 ${err.statusCode} ${StatusCodes[err.statusCode]}\r\n${
2✔
2109
                err.message
2110
            }\r\n${connectionHeader}\r\n`;
2111
        } else {
2112
            message = AuthenticationError.determineStatusCodeAndBuildMessage(err);
×
2113
        }
2114

2115
        socket.write(message);
2✔
2116
    }
2117
    socket.destroy();
2✔
2118
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc