From 428a1cdfc78c9581abeeb6d80579fbd8eb14021e Mon Sep 17 00:00:00 2001 From: Jhonatan Sandoval Velasco <122501764+JhontSouth@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:56:36 -0500 Subject: [PATCH 1/4] update d3-format and use it with tsup (#4842) --- .gitignore | 3 +++ libraries/adaptive-expressions/package.json | 10 ++++++---- .../src/builtinFunctions/formatNumber.ts | 2 +- .../src/builtinFunctions/string.ts | 2 +- package.json | 6 ++++-- yarn.lock | 8 ++++---- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 693133ba2a..1dc6b5c9c3 100644 --- a/.gitignore +++ b/.gitignore @@ -316,3 +316,6 @@ coverage # typescript assets *.tsbuildinfo *.js.map + +# tsup vendors folders +libraries/**/vendors diff --git a/libraries/adaptive-expressions/package.json b/libraries/adaptive-expressions/package.json index c709d5d30b..a04a096a20 100644 --- a/libraries/adaptive-expressions/package.json +++ b/libraries/adaptive-expressions/package.json @@ -36,7 +36,7 @@ "big-integer": "^1.6.52", "@types/xmldom": "^0.1.34", "btoa-lite": "^1.0.0", - "d3-format": "^2.0.0", + "d3-format": "^3.1.0", "dayjs": "^1.11.13", "jspath": "^0.4.0", "lodash": "^4.17.21", @@ -58,18 +58,20 @@ "build:browser": "npm-run-all build:browser:clean build:browser:run", "build:browser:clean": "rimraf --glob lib/browser.*", "build:browser:run": "tsup --config ../../tsup/browser.config.ts", - "clean": "rimraf lib tsconfig.tsbuildinfo", - "depcheck": "depcheck --config ../../.depcheckrc --ignores sinon,@types/xmldom", + "clean": "rimraf lib vendors tsconfig.tsbuildinfo", + "depcheck": "depcheck --config ../../.depcheckrc --ignores @types/xmldom,d3-format,sinon", "build-docs": "typedoc --theme markdown --entryPoint adaptive-expressions --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out ..\\..\\doc\\adaptive-expressions .\\lib\\index.d.ts --hideGenerator --name \"Bot Builder SDK - Expression\" --readme none", "test": "yarn build && mocha tests --timeout 60000", "test:compat": "api-extractor run --verbose", "lint": "eslint .", + "prebuild": "tsup ./node_modules/d3-format/src/*.js --format cjs --out-dir vendors/d3-format --clean --sourcemap", "antlr-build-expression": "antlr4ts src/parser/ExpressionAntlrLexer.g4 -o src/parser/generated && antlr4ts src/parser/ExpressionAntlrParser.g4 -visitor -o src/parser/generated", "antlr-build-commonregex": "antlr4ts src/CommonRegex.g4 -o src/generated -visitor" }, "files": [ "lib", "src", - "types" + "types", + "vendors" ] } diff --git a/libraries/adaptive-expressions/src/builtinFunctions/formatNumber.ts b/libraries/adaptive-expressions/src/builtinFunctions/formatNumber.ts index e07fe83f0d..179ca2839f 100644 --- a/libraries/adaptive-expressions/src/builtinFunctions/formatNumber.ts +++ b/libraries/adaptive-expressions/src/builtinFunctions/formatNumber.ts @@ -6,7 +6,7 @@ * Licensed under the MIT License. */ -import { formatLocale as d3formatLocale, format as d3format } from 'd3-format'; +import { formatLocale as d3formatLocale, format as d3format } from '../../vendors/d3-format'; import { Expression } from '../expression'; import { EvaluateExpressionDelegate, ExpressionEvaluator } from '../expressionEvaluator'; import { ExpressionType } from '../expressionType'; diff --git a/libraries/adaptive-expressions/src/builtinFunctions/string.ts b/libraries/adaptive-expressions/src/builtinFunctions/string.ts index bc3ddc6757..7d8641b32e 100644 --- a/libraries/adaptive-expressions/src/builtinFunctions/string.ts +++ b/libraries/adaptive-expressions/src/builtinFunctions/string.ts @@ -6,7 +6,7 @@ * Licensed under the MIT License. */ -import { formatLocale as d3formatLocale, format as d3format } from 'd3-format'; +import { formatLocale as d3formatLocale, format as d3format } from '../../vendors/d3-format'; import { EvaluateExpressionDelegate, ExpressionEvaluator, ValueWithError } from '../expressionEvaluator'; import { ExpressionType } from '../expressionType'; import { FunctionUtils } from '../functionUtils'; diff --git a/package.json b/package.json index 990f7822bf..50db8395d9 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ ], "nohoist": [ "**/@types/selenium-webdriver", - "botbuilder/filenamify" + "botbuilder/filenamify", + "adaptive-expressions/d3-format" ], "nohoistComments": { "**/@types/selenium-webdriver": "This package is excluded from the root @types folder as it requires ES2015+, whereas some BotBuilder libraries support ES5+.", - "botbuilder/filenamify": "This package is excluded because it's compiled as CJS by tsup as it's ESM-only." + "botbuilder/filenamify": "This package is excluded because it's compiled as CJS by tsup as it's ESM-only.", + "adaptive-expressions/d3-format": "This package is excluded because it's compiled as CJS by tsup as it's ESM-only." } }, "scripts": { diff --git a/yarn.lock b/yarn.lock index a85d6d6480..f893dc5a8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6920,10 +6920,10 @@ csv@^6.2.2: csv-stringify "^6.5.0" stream-transform "^3.3.2" -d3-format@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" - integrity sha512-Ab3S6XuE/Q+flY96HXT0jOXcM4EAClYFnRGY5zsjRGNy6qCYrQsMffs7cV5Q9xejb35zxW5hf/guKw34kvIKsA== +d3-format@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" + integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== dashdash@^1.12.0: version "1.14.1" From adca2e026bdaa3b2deb719309504e1ac7224530f Mon Sep 17 00:00:00 2001 From: Cecilia Avila <44245136+ceciliaavila@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:39:46 -0300 Subject: [PATCH 2/4] Run the coveralls step only for windows (#4843) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 94a21d62fc..bde786251e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -50,7 +50,7 @@ jobs: run: yarn test:github - name: coveralls - if: matrix.node-version == '22.x' + if: matrix.node-version == '22.x' && matrix.os == 'windows' uses: coverallsapp/github-action@v1.1.2 with: github-token: ${{ secrets.GITHUB_TOKEN }} From cd05a193db5489d3dcb1d68a4c77dd09ae64e6aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:17:11 -0600 Subject: [PATCH 3/4] chore(deps): bump nanoid from 3.3.6 to 3.3.8 (#4812) Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.6 to 3.3.8. - [Release notes](https://github.com/ai/nanoid/releases) - [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md) - [Commits](https://github.com/ai/nanoid/compare/3.3.6...3.3.8) --- updated-dependencies: - dependency-name: nanoid dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: CeciliaAvila --- yarn.lock | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index f893dc5a8b..59d35d3abe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11656,15 +11656,10 @@ nan@^2.17.0, nan@^2.18.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - -nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +nanoid@^3.3.6, nanoid@^3.3.7: + version "3.3.8" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" + integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== napi-build-utils@^1.0.1: version "1.0.2" From 31c72e9c43152e77b6c6645f26d6f0b4d9d82af0 Mon Sep 17 00:00:00 2001 From: Benjamin Tsai <52517294+bentsai10@users.noreply.github.com> Date: Tue, 14 Jan 2025 07:01:29 -0800 Subject: [PATCH 4/4] feat: Support Sso for SharePoint bot ACEs (#4806) * initial commit * add export for middleware * update api file * update api file again * update core api * update botbuilder api * address comments * Fix lint issues * Update botbuilder.api.md --------- Co-authored-by: bentsai Co-authored-by: CeciliaAvila --- .../etc/botbuilder-core.api.md | 3 + .../botbuilder-core/src/signInConstants.ts | 1 + libraries/botbuilder/etc/botbuilder.api.md | 7 + libraries/botbuilder/src/index.ts | 1 + .../sharepoint/sharePointActivityHandler.ts | 14 +- .../sharePointSSOTokenExchangeMiddleware.ts | 192 ++++++++++++++++++ .../etc/botframework-schema.api.md | 4 + libraries/botframework-schema/src/index.ts | 1 + .../src/sharepoint/quickViewResponse.ts | 8 + 9 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 libraries/botbuilder/src/sharepoint/sharePointSSOTokenExchangeMiddleware.ts diff --git a/libraries/botbuilder-core/etc/botbuilder-core.api.md b/libraries/botbuilder-core/etc/botbuilder-core.api.md index 425beadb03..cde4b222af 100644 --- a/libraries/botbuilder-core/etc/botbuilder-core.api.md +++ b/libraries/botbuilder-core/etc/botbuilder-core.api.md @@ -570,6 +570,9 @@ export enum Severity { Warning = 2 } +// @public (undocumented) +export const sharePointTokenExchange = "cardExtension/token"; + // @public export class ShowTypingMiddleware implements Middleware { constructor(delay?: number, period?: number); diff --git a/libraries/botbuilder-core/src/signInConstants.ts b/libraries/botbuilder-core/src/signInConstants.ts index 52bdade84e..a90df0571b 100644 --- a/libraries/botbuilder-core/src/signInConstants.ts +++ b/libraries/botbuilder-core/src/signInConstants.ts @@ -9,3 +9,4 @@ export const verifyStateOperationName = 'signin/verifyState'; export const tokenExchangeOperationName = 'signin/tokenExchange'; export const tokenResponseEventName = 'tokens/response'; +export const sharePointTokenExchange = 'cardExtension/token'; diff --git a/libraries/botbuilder/etc/botbuilder.api.md b/libraries/botbuilder/etc/botbuilder.api.md index b2c5138a13..9896b7fc8b 100644 --- a/libraries/botbuilder/etc/botbuilder.api.md +++ b/libraries/botbuilder/etc/botbuilder.api.md @@ -352,6 +352,13 @@ export class SharePointActivityHandler extends ActivityHandler { protected onSharePointTaskGetQuickViewAsync(_context: TurnContext, _aceRequest: AceRequest): Promise; protected onSharePointTaskHandleActionAsync(_context: TurnContext, _aceRequest: AceRequest): Promise; protected onSharePointTaskSetPropertyPaneConfigurationAsync(_context: TurnContext, _aceRequest: AceRequest): Promise; + protected onSignInInvoke(_context: TurnContext): Promise; +} + +// @public +export class SharePointSSOTokenExchangeMiddleware implements Middleware { + constructor(storage: Storage_2, oAuthConnectionName: string); + onTurn(context: TurnContext, _next: () => Promise): Promise; } // @public @deprecated (undocumented) diff --git a/libraries/botbuilder/src/index.ts b/libraries/botbuilder/src/index.ts index 072b3edc80..c90620dc93 100644 --- a/libraries/botbuilder/src/index.ts +++ b/libraries/botbuilder/src/index.ts @@ -30,3 +30,4 @@ export { Request, Response, WebRequest, WebResponse } from './interfaces'; export { StatusCodeError } from './statusCodeError'; export { StreamingHttpClient, TokenResolver } from './streaming'; export { SharePointActivityHandler } from './sharepoint/sharePointActivityHandler'; +export { SharePointSSOTokenExchangeMiddleware } from './sharepoint/sharePointSSOTokenExchangeMiddleware'; diff --git a/libraries/botbuilder/src/sharepoint/sharePointActivityHandler.ts b/libraries/botbuilder/src/sharepoint/sharePointActivityHandler.ts index 821251e3f8..2647cc6aa8 100644 --- a/libraries/botbuilder/src/sharepoint/sharePointActivityHandler.ts +++ b/libraries/botbuilder/src/sharepoint/sharePointActivityHandler.ts @@ -66,6 +66,9 @@ export class SharePointActivityHandler extends ActivityHandler { return ActivityHandler.createInvokeResponse( await this.onSharePointTaskHandleActionAsync(context, context.activity.value as AceRequest), ); + case 'cardExtension/token': + await this.onSignInInvoke(context); + return ActivityHandler.createInvokeResponse(); default: return super.onInvokeActivity(context); } @@ -137,7 +140,7 @@ export class SharePointActivityHandler extends ActivityHandler { } /** - * Override this in a derived class to provide logic for setting configuration pane properties. + * Override this in a derived class to provide logic for handling ACE action. * * @param _context - A strongly-typed context object for this turn * @param _aceRequest - The Ace invoke request value payload @@ -149,4 +152,13 @@ export class SharePointActivityHandler extends ActivityHandler { ): Promise { throw new Error('NotImplemented'); } + + /** + * Override this method to support channel-specific behavior across multiple channels. + * + * @param _context - A strongly-typed context object for this turn + */ + protected async onSignInInvoke(_context: TurnContext): Promise { + throw new Error('NotImplemented'); + } } diff --git a/libraries/botbuilder/src/sharepoint/sharePointSSOTokenExchangeMiddleware.ts b/libraries/botbuilder/src/sharepoint/sharePointSSOTokenExchangeMiddleware.ts new file mode 100644 index 0000000000..01e32e647e --- /dev/null +++ b/libraries/botbuilder/src/sharepoint/sharePointSSOTokenExchangeMiddleware.ts @@ -0,0 +1,192 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as z from 'zod'; + +import { + ActivityTypes, + Channels, + ExtendedUserTokenProvider, + Middleware, + StatusCodes, + Storage, + StoreItem, + AceRequest, + TokenExchangeInvokeResponse, + TokenResponse, + TurnContext, + sharePointTokenExchange, + CloudAdapterBase, +} from 'botbuilder-core'; +import { UserTokenClient } from 'botframework-connector'; + +function getStorageKey(context: TurnContext): string { + const activity = context.activity; + + const channelId = activity.channelId; + if (!channelId) { + throw new Error('invalid activity. Missing channelId'); + } + + const conversationId = activity.conversation?.id; + if (!conversationId) { + throw new Error('invalid activity. Missing conversation.id'); + } + + const value = activity.value; + if (!value?.id) { + throw new Error('Invalid signin/tokenExchange. Missing activity.value.id.'); + } + + return `${channelId}/${conversationId}/${value.id}`; +} + +async function sendInvokeResponse(context: TurnContext, body: unknown = null, status = StatusCodes.OK): Promise { + await context.sendActivity({ + type: ActivityTypes.InvokeResponse, + value: { body, status }, + }); +} + +const ExchangeToken = z.custom>( + (val: any) => typeof val.exchangeToken === 'function', + { message: 'ExtendedUserTokenProvider' }, +); + +/** + * If the activity name is cardExtension/token, this middleware will attempt to + * exchange the token, and deduplicate the incoming call, ensuring only one + * exchange request is processed. + * + * If a user is signed into multiple devices, the Bot could receive a + * "cardExtension/token" from each device. Each token exchange request for a + * specific user login will have an identical activity.value.id. + * + * Only one of these token exchange requests should be processed by the bot. + * The others return [StatusCodes.PRECONDITION_FAILED](xref:botframework-schema:StatusCodes.PRECONDITION_FAILED). + * For a distributed bot in production, this requires distributed storage + * ensuring only one token exchange is processed. This middleware supports + * CosmosDb storage found in botbuilder-azure, or MemoryStorage for local development. + */ +export class SharePointSSOTokenExchangeMiddleware implements Middleware { + /** + * Initializes a new instance of the SharePointSSOTokenExchangeMiddleware class. + * + * @param storage The [Storage](xref:botbuilder-core.Storage) to use for deduplication + * @param oAuthConnectionName The connection name to use for the single sign on token exchange + */ + constructor( + private readonly storage: Storage, + private readonly oAuthConnectionName: string, + ) { + if (!storage) { + throw new TypeError('`storage` parameter is required'); + } + + if (!oAuthConnectionName) { + throw new TypeError('`oAuthConnectionName` parameter is required'); + } + } + + /** + * Called each time the bot receives a new request. + * + * @param context Context for current turn of conversation with the user. + * @param _next Function to call to continue execution to the next step in the middleware chain. + */ + async onTurn(context: TurnContext, _next: () => Promise): Promise { + if (context.activity.channelId === Channels.M365 && context.activity.name === sharePointTokenExchange) { + // If the TokenExchange is NOT successful, the response will have already been sent by exchangedToken + if (!(await this.exchangedToken(context))) { + return; + } + + // Only one token exchange should proceed from here. Deduplication is performed second because in the case + // of failure due to consent required, every caller needs to receive a response + if (!(await this.deduplicatedTokenExchangeId(context))) { + // If the token is not exchangeable, do not process this activity further. + return; + } + } + + return; + } + + private async deduplicatedTokenExchangeId(context: TurnContext): Promise { + // Create a StoreItem with Etag of the unique 'signin/tokenExchange' request + const storeItem: StoreItem = { + eTag: context.activity.value?.id, + }; + + try { + // Writing the IStoreItem with ETag of unique id will succeed only once + await this.storage.write({ + [getStorageKey(context)]: storeItem, + }); + } catch (err) { + const message = err.message?.toLowerCase(); + + // Do NOT proceed processing this message, some other thread or machine already has processed it. + // Send 200 invoke response. + if (message.includes('etag conflict') || message.includes('precondition is not met')) { + await sendInvokeResponse(context); + return false; + } + + throw err; + } + + return true; + } + + private async exchangedToken(context: TurnContext): Promise { + let tokenExchangeResponse: TokenResponse; + const aceRequest: AceRequest = context.activity.value; + + try { + const userTokenClient = context.turnState.get( + (context.adapter as CloudAdapterBase).UserTokenClientKey, + ); + const exchangeToken = ExchangeToken.safeParse(context.adapter); + + if (userTokenClient) { + tokenExchangeResponse = await userTokenClient.exchangeToken( + context.activity.from.id, + this.oAuthConnectionName, + context.activity.channelId, + { token: aceRequest.data as string }, + ); + } else if (exchangeToken.success) { + tokenExchangeResponse = await exchangeToken.data.exchangeToken( + context, + this.oAuthConnectionName, + context.activity.from.id, + { token: aceRequest.data as string }, + ); + } else { + new Error('Token Exchange is not supported by the current adapter.'); + } + } catch (_err) { + // Ignore Exceptions + // If token exchange failed for any reason, tokenExchangeResponse above stays null, + // and hence we send back a failure invoke response to the caller. + } + + if (!tokenExchangeResponse?.token) { + // The token could not be exchanged (which could be due to a consent requirement) + // Notify the sender that PreconditionFailed so they can respond accordingly. + + const invokeResponse: TokenExchangeInvokeResponse = { + id: 'FAKE ID', + connectionName: this.oAuthConnectionName, + failureDetail: 'The bot is unable to exchange token. Proceed with regular login.', + }; + + await sendInvokeResponse(context, invokeResponse, StatusCodes.PRECONDITION_FAILED); + + return false; + } + + return true; + } +} diff --git a/libraries/botframework-schema/etc/botframework-schema.api.md b/libraries/botframework-schema/etc/botframework-schema.api.md index efb0d7dca9..f4e015eee5 100644 --- a/libraries/botframework-schema/etc/botframework-schema.api.md +++ b/libraries/botframework-schema/etc/botframework-schema.api.md @@ -613,6 +613,8 @@ export enum Channels { // (undocumented) Line = "line", // (undocumented) + M365 = "m365extensions", + // (undocumented) Msteams = "msteams", // (undocumented) Omni = "omnichannel", @@ -2899,6 +2901,8 @@ export interface QuickViewResponse { data: QuickViewData; externalLink: ExternalLinkActionParameters; focusParameters: FocusParameters; + postSsoViewId?: string; + requiresSso?: boolean; template: AdaptiveCard; title: string; viewId: string; diff --git a/libraries/botframework-schema/src/index.ts b/libraries/botframework-schema/src/index.ts index e7b0486810..6f330f47db 100644 --- a/libraries/botframework-schema/src/index.ts +++ b/libraries/botframework-schema/src/index.ts @@ -2295,6 +2295,7 @@ export enum Channels { */ Kik = 'kik', Line = 'line', + M365 = 'm365extensions', Msteams = 'msteams', Omni = 'omnichannel', Outlook = 'outlook', diff --git a/libraries/botframework-schema/src/sharepoint/quickViewResponse.ts b/libraries/botframework-schema/src/sharepoint/quickViewResponse.ts index 933209604b..1cb2073d24 100644 --- a/libraries/botframework-schema/src/sharepoint/quickViewResponse.ts +++ b/libraries/botframework-schema/src/sharepoint/quickViewResponse.ts @@ -34,4 +34,12 @@ export interface QuickViewResponse { * An optional focus element to set focus when the view is rendered for accessibility purposes. */ focusParameters: FocusParameters; + /** + * Value indicating whether the client should trigger a single sign on flow + */ + requiresSso?: boolean; + /** + * Value representing the view id of the view to load once SSO is complete + */ + postSsoViewId?: string; }