From c5dac31fad1da0319abf114c170dedf95f34a0a8 Mon Sep 17 00:00:00 2001 From: examples-bot Date: Sun, 5 Apr 2026 01:13:46 +0000 Subject: [PATCH] =?UTF-8?q?feat(examples):=20add=20470=20=E2=80=94=20Micro?= =?UTF-8?q?soft=20Teams=20real-time=20transcription=20bot=20(Node.js)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.env.example | 11 + .../.npmrc | 1 + .../README.md | 83 + .../package.json | 22 + .../pnpm-lock.yaml | 1462 +++++++++++++++++ .../src/bot.js | 156 ++ .../src/calling.js | 164 ++ .../src/graph.js | 116 ++ .../src/index.js | 97 ++ .../tests/test.js | 290 ++++ 10 files changed, 2402 insertions(+) create mode 100644 examples/470-microsoft-teams-transcription-bot-node/.env.example create mode 100644 examples/470-microsoft-teams-transcription-bot-node/.npmrc create mode 100644 examples/470-microsoft-teams-transcription-bot-node/README.md create mode 100644 examples/470-microsoft-teams-transcription-bot-node/package.json create mode 100644 examples/470-microsoft-teams-transcription-bot-node/pnpm-lock.yaml create mode 100644 examples/470-microsoft-teams-transcription-bot-node/src/bot.js create mode 100644 examples/470-microsoft-teams-transcription-bot-node/src/calling.js create mode 100644 examples/470-microsoft-teams-transcription-bot-node/src/graph.js create mode 100644 examples/470-microsoft-teams-transcription-bot-node/src/index.js create mode 100644 examples/470-microsoft-teams-transcription-bot-node/tests/test.js diff --git a/examples/470-microsoft-teams-transcription-bot-node/.env.example b/examples/470-microsoft-teams-transcription-bot-node/.env.example new file mode 100644 index 0000000..b64de3d --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/.env.example @@ -0,0 +1,11 @@ +# Deepgram — https://console.deepgram.com/ +DEEPGRAM_API_KEY= + +# Azure Bot Service — https://portal.azure.com/ +MICROSOFT_APP_ID= +MICROSOFT_APP_PASSWORD= +MICROSOFT_APP_TENANT_ID= + +# Public callback URL for media (must be HTTPS with valid cert) +# e.g. https://your-bot.example.com +BOT_BASE_URL= diff --git a/examples/470-microsoft-teams-transcription-bot-node/.npmrc b/examples/470-microsoft-teams-transcription-bot-node/.npmrc new file mode 100644 index 0000000..cffe8cd --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/.npmrc @@ -0,0 +1 @@ +save-exact=true diff --git a/examples/470-microsoft-teams-transcription-bot-node/README.md b/examples/470-microsoft-teams-transcription-bot-node/README.md new file mode 100644 index 0000000..2865a89 --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/README.md @@ -0,0 +1,83 @@ +# Microsoft Teams Real-Time Transcription Bot + +A Node.js bot that joins Microsoft Teams meetings via the BotFramework SDK and Graph Communications Calling API, streams meeting audio to Deepgram's live speech-to-text API, and posts real-time transcription captions back into the meeting chat. + +## What you'll build + +An Express server that acts as a Teams bot: it receives messages through the Bot Framework messaging endpoint, joins meetings through the Graph Communications Calling API, captures meeting audio as linear16 PCM, streams it to Deepgram for real-time transcription, and posts final transcripts back into the meeting chat. + +## Prerequisites + +- Node.js 18+ +- Deepgram account — [get a free API key](https://console.deepgram.com/) +- Microsoft Azure account — [sign up](https://portal.azure.com/) +- An Azure Bot Service registration with Teams channel enabled +- Azure AD app registration with `Calls.JoinGroupCall.All` and `Calls.AccessMedia.All` application permissions +- A public HTTPS endpoint (use [ngrok](https://ngrok.com/) or [dev tunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/) for local development) + +## Environment variables + +| Variable | Where to find it | +|----------|-----------------| +| `DEEPGRAM_API_KEY` | [Deepgram console](https://console.deepgram.com/) | +| `MICROSOFT_APP_ID` | [Azure portal](https://portal.azure.com/) → App registrations → Overview → Application (client) ID | +| `MICROSOFT_APP_PASSWORD` | Azure portal → App registrations → Certificates & secrets → Client secret value | +| `MICROSOFT_APP_TENANT_ID` | Azure portal → App registrations → Overview → Directory (tenant) ID | +| `BOT_BASE_URL` | Your public HTTPS URL (e.g. `https://your-bot.example.com`) | + +Copy `.env.example` to `.env` and fill in your values. + +## Install and run + +```bash +pnpm install +pnpm start +``` + +Then expose the server publicly (for local dev): + +```bash +ngrok http 3978 +``` + +Configure your Azure Bot Service: +1. Set the messaging endpoint to `https:///api/messages` +2. Set the calling webhook to `https:///api/calling/callback` +3. Enable the Microsoft Teams channel + +In a Teams meeting chat, mention the bot and say **join** to start transcription. + +## Key parameters + +| Parameter | Value | Description | +|-----------|-------|-------------| +| `model` | `nova-3` | Deepgram's latest and most accurate STT model | +| `encoding` | `linear16` | 16-bit signed PCM — matches Graph Communications audio format | +| `sample_rate` | `16000` | 16 kHz — standard for Teams meeting audio | +| `smart_format` | `true` | Adds punctuation and formatting to transcripts | +| `interim_results` | `true` | Provides partial transcripts while speech is ongoing | +| `utterance_end_ms` | `1000` | Detects end of speech after 1 second of silence | +| `tag` | `deepgram-examples` | Tags traffic in Deepgram console for identification | + +## How it works + +1. A user mentions the bot in a Teams meeting chat and sends **join** +2. The bot calls the Graph Communications API to join the meeting as a participant with app-hosted media +3. Graph streams meeting audio as linear16 PCM to the bot's notification endpoint +4. The bot forwards each audio chunk to a Deepgram live WebSocket connection +5. Deepgram returns interim and final transcripts in real-time +6. Final transcripts are posted back to the meeting chat via the Bot Framework messaging API +7. When the user sends **leave**, the bot hangs up the call and closes the Deepgram connection + +## Azure AD permissions required + +| Permission | Type | Description | +|-----------|------|-------------| +| `Calls.JoinGroupCall.All` | Application | Join group calls and meetings | +| `Calls.AccessMedia.All` | Application | Access media streams in calls | + +These must be granted as **application permissions** (not delegated) and require admin consent. + +## Starter templates + +[deepgram-starters](https://github.com/orgs/deepgram-starters/repositories) diff --git a/examples/470-microsoft-teams-transcription-bot-node/package.json b/examples/470-microsoft-teams-transcription-bot-node/package.json new file mode 100644 index 0000000..c5d742a --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/package.json @@ -0,0 +1,22 @@ +{ + "name": "deepgram-teams-transcription-bot", + "version": "1.0.0", + "description": "Microsoft Teams bot that transcribes meetings in real-time using Deepgram STT", + "main": "src/index.js", + "packageManager": "pnpm@10.30.3", + "scripts": { + "start": "node src/index.js", + "test": "node tests/test.js" + }, + "dependencies": { + "@azure/identity": "4.13.1", + "@deepgram/sdk": "5.0.0", + "@microsoft/microsoft-graph-client": "3.0.7", + "botbuilder": "4.23.3", + "dotenv": "16.6.1", + "express": "4.22.1" + }, + "engines": { + "node": ">=18" + } +} diff --git a/examples/470-microsoft-teams-transcription-bot-node/pnpm-lock.yaml b/examples/470-microsoft-teams-transcription-bot-node/pnpm-lock.yaml new file mode 100644 index 0000000..d9208bc --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/pnpm-lock.yaml @@ -0,0 +1,1462 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@azure/identity': + specifier: 4.13.1 + version: 4.13.1 + '@deepgram/sdk': + specifier: 5.0.0 + version: 5.0.0 + '@microsoft/microsoft-graph-client': + specifier: 3.0.7 + version: 3.0.7(@azure/identity@4.13.1) + botbuilder: + specifier: 4.23.3 + version: 4.23.3 + dotenv: + specifier: 16.6.1 + version: 16.6.1 + express: + specifier: 4.22.1 + version: 4.22.1 + +packages: + + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.1': + resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} + engines: {node: '>=20.0.0'} + + '@azure/core-http-compat@2.3.2': + resolution: {integrity: sha512-Tf6ltdKzOJEgxZeWLCjMxrxbodB/ZeCbzzA1A2qHbhzAjzjHoBVSUeSl/baT/oHAxhc4qdqVaDKnc2+iE932gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@azure/core-client': ^1.10.0 + '@azure/core-rest-pipeline': ^1.22.0 + + '@azure/core-rest-pipeline@1.23.0': + resolution: {integrity: sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + + '@azure/identity@4.13.1': + resolution: {integrity: sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==} + engines: {node: '>=20.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/msal-browser@5.6.3': + resolution: {integrity: sha512-sTjMtUm+bJpENU/1WlRzHEsgEHppZDZ1EtNyaOODg/sQBtMxxJzGB+MOCM+T2Q5Qe1fKBrdxUmjyRxm0r7Ez9w==} + engines: {node: '>=0.8.0'} + + '@azure/msal-common@14.16.1': + resolution: {integrity: sha512-nyxsA6NA4SVKh5YyRpbSXiMr7oQbwark7JU9LMeg6tJYTSPyAGkdx61wPT4gyxZfxlSxMMEyAsWaubBlNyIa1w==} + engines: {node: '>=0.8.0'} + + '@azure/msal-common@16.4.1': + resolution: {integrity: sha512-Bl8f+w37xkXsYh7QRkAKCFGYtWMYuOVO7Lv+BxILrvGz3HbIEF22Pt0ugyj0QPOl6NLrHcnNUQ9yeew98P/5iw==} + engines: {node: '>=0.8.0'} + + '@azure/msal-node@2.16.3': + resolution: {integrity: sha512-CO+SE4weOsfJf+C5LM8argzvotrXw252/ZU6SM2Tz63fEblhH1uuVaaO4ISYFuN4Q6BhTo7I3qIdi8ydUQCqhw==} + engines: {node: '>=16'} + + '@azure/msal-node@5.1.2': + resolution: {integrity: sha512-DoeSJ9U5KPAIZoHsPywvfEj2MhBniQe0+FSpjLUTdWoIkI999GB5USkW6nNEHnIaLVxROHXvprWA1KzdS1VQ4A==} + engines: {node: '>=20'} + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@deepgram/sdk@5.0.0': + resolution: {integrity: sha512-x1wMiOgDGqcLEaQpQBQLTtk5mLbXbYgcBEpp7cfJIyEtqdIGgijCZH+a/esiVp+xIcTYYroTxG47RVppZOHbWw==} + engines: {node: '>=18.0.0'} + + '@microsoft/microsoft-graph-client@3.0.7': + resolution: {integrity: sha512-/AazAV/F+HK4LIywF9C+NYHcJo038zEnWkteilcxC1FM/uK/4NVGDKGrxx7nNq1ybspAroRKT4I1FHfxQzxkUw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@azure/identity': '*' + '@azure/msal-browser': '*' + buffer: '*' + stream-browserify: '*' + peerDependenciesMeta: + '@azure/identity': + optional: true + '@azure/msal-browser': + optional: true + buffer: + optional: true + stream-browserify: + optional: true + + '@types/jsonwebtoken@9.0.6': + resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} + + '@types/node@25.5.2': + resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + + '@types/ws@6.0.4': + resolution: {integrity: sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==} + + '@typespec/ts-http-runtime@0.3.4': + resolution: {integrity: sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==} + engines: {node: '>=20.0.0'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + adaptivecards@1.2.3: + resolution: {integrity: sha512-amQ5OSW3OpIkrxVKLjxVBPk/T49yuOtnqs1z5ZPfZr0+OpTovzmiHbyoAGDIsu5SNYHwOZFp/3LGOnRaALFa/g==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.14.0: + resolution: {integrity: sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + botbuilder-core@4.23.3: + resolution: {integrity: sha512-48iW739I24piBH683b/Unvlu1fSzjB69ViOwZ0PbTkN2yW5cTvHJWlW7bXntO8GSqJfssgPaVthKfyaCW457ig==} + + botbuilder-dialogs-adaptive-runtime-core@4.23.3-preview: + resolution: {integrity: sha512-EssyvqK9MobX3gbnUe/jjhLuxpCEeyQeQsyUFMJ236U6vzSQdrAxNH7Jc5DyZw2KKelBdK1xPBdwTYQNM5S0Qw==} + + botbuilder-stdlib@4.23.3-internal: + resolution: {integrity: sha512-fwvIHnKU8sXo1gTww+m/k8wnuM5ktVBAV/3vWJ+ou40zapy1HYjWQuu6sVCRFgMUngpKwhdmoOQsTXsp58SNtA==} + + botbuilder@4.23.3: + resolution: {integrity: sha512-1gDIQHHYosYBHGXMjvZEJDrcp3NGy3lzHBml5wn9PFqVuIk/cbsCDZs3KJ3g+aH/GGh4CH/ij9iQ2KbQYHAYKA==} + + botframework-connector@4.23.3: + resolution: {integrity: sha512-sChwCFJr3xhcMCYChaOxJoE8/YgdjOPWzGwz5JAxZDwhbQonwYX5O/6Z9EA+wB3TCFNEh642SGeC/rOitaTnGQ==} + + botframework-schema@4.23.3: + resolution: {integrity: sha512-/W0uWxZ3fuPLAImZRLnPTbs49Z2xMpJSIzIBxSfvwO0aqv9GsM3bTk3zlNdJ1xr40SshQ7WiH2H1hgjBALwYJw==} + + botframework-streaming@4.23.3: + resolution: {integrity: sha512-GMtciQGfZXtAW6syUqFpFJQ2vDyVbpxL3T1DqFzq/GmmkAu7KTZ1zvo7PTww6+IT1kMW0lmL/XZJVq3Rhg4PQA==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cross-fetch@4.1.0: + resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dependency-graph@1.0.0: + resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} + engines: {node: '>=4'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + filename-reserved-regex@3.0.0: + resolution: {integrity: sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + filenamify@6.0.0: + resolution: {integrity: sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ==} + engines: {node: '>=16'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + openssl-wrapper@0.3.4: + resolution: {integrity: sha512-iITsrx6Ho8V3/2OVtmZzzX8wQaKAaFXEJQdzoPUZDtyf5jWFlqo+h+OhGT4TATQ47f9ACKHua8nw7Qoy85aeKQ==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + qs@6.14.2: + resolution: {integrity: sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + rsa-pem-from-mod-exp@0.8.6: + resolution: {integrity: sha512-c5ouQkOvGHF1qomUUDJGFcXsomeSO2gbEs6hVhMAtlkE1CuaZase/WzoaKFG/EZQuNmq6pw/EMCeEnDvOgCJYQ==} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-http-compat@2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0)': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + + '@azure/core-rest-pipeline@1.23.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/identity@4.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@azure/msal-browser': 5.6.3 + '@azure/msal-node': 5.1.2 + open: 10.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.4 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/msal-browser@5.6.3': + dependencies: + '@azure/msal-common': 16.4.1 + + '@azure/msal-common@14.16.1': {} + + '@azure/msal-common@16.4.1': {} + + '@azure/msal-node@2.16.3': + dependencies: + '@azure/msal-common': 14.16.1 + jsonwebtoken: 9.0.3 + uuid: 8.3.2 + + '@azure/msal-node@5.1.2': + dependencies: + '@azure/msal-common': 16.4.1 + jsonwebtoken: 9.0.3 + uuid: 8.3.2 + + '@babel/runtime@7.29.2': {} + + '@deepgram/sdk@5.0.0': + dependencies: + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@microsoft/microsoft-graph-client@3.0.7(@azure/identity@4.13.1)': + dependencies: + '@babel/runtime': 7.29.2 + tslib: 2.8.1 + optionalDependencies: + '@azure/identity': 4.13.1 + + '@types/jsonwebtoken@9.0.6': + dependencies: + '@types/node': 25.5.2 + + '@types/node@25.5.2': + dependencies: + undici-types: 7.18.2 + + '@types/ws@6.0.4': + dependencies: + '@types/node': 25.5.2 + + '@typespec/ts-http-runtime@0.3.4': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + adaptivecards@1.2.3: {} + + agent-base@7.1.4: {} + + array-flatten@1.1.1: {} + + asynckit@0.4.0: {} + + axios@1.14.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + base64-js@1.5.1: {} + + base64url@3.0.1: {} + + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.2 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + botbuilder-core@4.23.3: + dependencies: + botbuilder-dialogs-adaptive-runtime-core: 4.23.3-preview + botbuilder-stdlib: 4.23.3-internal + botframework-connector: 4.23.3 + botframework-schema: 4.23.3 + uuid: 10.0.0 + zod: 3.25.76 + transitivePeerDependencies: + - debug + - encoding + - supports-color + + botbuilder-dialogs-adaptive-runtime-core@4.23.3-preview: + dependencies: + dependency-graph: 1.0.0 + + botbuilder-stdlib@4.23.3-internal: + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-http-compat': 2.3.2(@azure/core-client@1.10.1)(@azure/core-rest-pipeline@1.23.0) + '@azure/core-rest-pipeline': 1.23.0 + '@azure/core-tracing': 1.3.1 + transitivePeerDependencies: + - supports-color + + botbuilder@4.23.3: + dependencies: + '@azure/core-rest-pipeline': 1.23.0 + '@azure/msal-node': 2.16.3 + axios: 1.14.0 + botbuilder-core: 4.23.3 + botbuilder-stdlib: 4.23.3-internal + botframework-connector: 4.23.3 + botframework-schema: 4.23.3 + botframework-streaming: 4.23.3 + dayjs: 1.11.20 + filenamify: 6.0.0 + fs-extra: 11.3.4 + htmlparser2: 9.1.0 + uuid: 10.0.0 + zod: 3.25.76 + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - supports-color + - utf-8-validate + + botframework-connector@4.23.3: + dependencies: + '@azure/core-rest-pipeline': 1.23.0 + '@azure/identity': 4.13.1 + '@azure/msal-node': 2.16.3 + '@types/jsonwebtoken': 9.0.6 + axios: 1.14.0 + base64url: 3.0.1 + botbuilder-stdlib: 4.23.3-internal + botframework-schema: 4.23.3 + buffer: 6.0.3 + cross-fetch: 4.1.0 + https-proxy-agent: 7.0.6 + jsonwebtoken: 9.0.3 + node-fetch: 2.7.0 + openssl-wrapper: 0.3.4 + rsa-pem-from-mod-exp: 0.8.6 + zod: 3.25.76 + transitivePeerDependencies: + - debug + - encoding + - supports-color + + botframework-schema@4.23.3: + dependencies: + adaptivecards: 1.2.3 + uuid: 10.0.0 + zod: 3.25.76 + + botframework-streaming@4.23.3: + dependencies: + '@types/ws': 6.0.4 + uuid: 10.0.0 + ws: 7.5.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + buffer-equal-constant-time@1.0.1: {} + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + cross-fetch@4.1.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + dayjs@1.11.20: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + dependency-graph@1.0.0: {} + + destroy@1.2.0: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + entities@4.5.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.14.2 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + filename-reserved-regex@3.0.0: {} + + filenamify@6.0.0: + dependencies: + filename-reserved-regex: 3.0.0 + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-docker@3.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.4 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + openssl-wrapper@0.3.4: {} + + parseurl@1.3.3: {} + + path-to-regexp@0.1.13: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-from-env@2.1.0: {} + + qs@6.14.2: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + rsa-pem-from-mod-exp@0.8.6: {} + + run-applescript@7.1.0: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + semver@7.7.4: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.2: {} + + toidentifier@1.0.1: {} + + tr46@0.0.3: {} + + tslib@2.8.1: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + undici-types@7.18.2: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + utils-merge@1.0.1: {} + + uuid@10.0.0: {} + + uuid@8.3.2: {} + + vary@1.1.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + ws@7.5.10: {} + + ws@8.20.0: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + zod@3.25.76: {} diff --git a/examples/470-microsoft-teams-transcription-bot-node/src/bot.js b/examples/470-microsoft-teams-transcription-bot-node/src/bot.js new file mode 100644 index 0000000..6786e11 --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/src/bot.js @@ -0,0 +1,156 @@ +'use strict'; + +const { + TeamsActivityHandler, + TurnContext, + MessageFactory, +} = require('botbuilder'); + +const { GraphClient } = require('./graph'); + +class TeamsBot extends TeamsActivityHandler { + constructor() { + super(); + this._conversationReferences = new Map(); + this._graphClient = new GraphClient(); + + this.onMessage(async (context, next) => { + const text = (context.activity.text || '').trim().toLowerCase(); + TurnContext.removeRecipientMention(context.activity); + + if (text.includes('join') || text.includes('transcribe')) { + await this._handleJoinCommand(context); + } else if (text.includes('leave') || text.includes('stop')) { + await this._handleLeaveCommand(context); + } else { + await context.sendActivity( + 'Send **join** to start transcribing the current meeting, or **leave** to stop.' + ); + } + + await next(); + }); + + this.onMembersAdded(async (context, next) => { + for (const member of context.activity.membersAdded) { + if (member.id !== context.activity.recipient.id) { + await context.sendActivity( + 'Hello! I can transcribe your Teams meetings in real-time using Deepgram. ' + + 'Send **join** to start transcribing the current meeting.' + ); + } + } + await next(); + }); + } + + async _handleJoinCommand(context) { + const meetingInfo = await this._getMeetingInfo(context); + if (!meetingInfo) { + await context.sendActivity( + 'I could not detect a meeting. Please use this command from within a Teams meeting chat.' + ); + return; + } + + this._storeConversationReference(context); + + await context.sendActivity('Joining the meeting to start transcription...'); + + try { + const callId = await this._graphClient.joinMeeting(meetingInfo); + this._conversationReferences.set(callId, { + ref: TurnContext.getConversationReference(context.activity), + meetingInfo, + }); + await context.sendActivity( + `Joined the meeting. Live transcriptions will appear here. Call ID: \`${callId}\`` + ); + } catch (err) { + console.error('[bot] Failed to join meeting:', err.message); + await context.sendActivity( + `Failed to join the meeting: ${err.message}. ` + + 'Ensure the bot has the required Azure AD permissions.' + ); + } + } + + async _handleLeaveCommand(context) { + const activeCalls = Array.from(this._conversationReferences.entries()); + if (activeCalls.length === 0) { + await context.sendActivity('No active transcription sessions.'); + return; + } + + for (const [callId] of activeCalls) { + try { + await this._graphClient.leaveCall(callId); + this._conversationReferences.delete(callId); + } catch (err) { + console.error(`[bot] Failed to leave call ${callId}:`, err.message); + } + } + + await context.sendActivity('Stopped transcription and left the meeting.'); + } + + async _getMeetingInfo(context) { + const meeting = context.activity.channelData?.meeting; + if (meeting?.id) { + return { + meetingId: meeting.id, + tenantId: context.activity.channelData?.tenant?.id || process.env.MICROSOFT_APP_TENANT_ID, + }; + } + + const conversationType = context.activity.conversation?.conversationType; + if (conversationType === 'groupChat' || conversationType === 'channel') { + return { + threadId: context.activity.conversation.id, + tenantId: context.activity.channelData?.tenant?.id || process.env.MICROSOFT_APP_TENANT_ID, + messageId: context.activity.id, + }; + } + + return null; + } + + _storeConversationReference(context) { + const ref = TurnContext.getConversationReference(context.activity); + this._conversationReferences.set(context.activity.conversation.id, { ref }); + } + + async postTranscript(callId, transcript, speaker) { + const entry = this._conversationReferences.get(callId); + if (!entry?.ref) return; + + const text = speaker + ? `**${speaker}:** ${transcript}` + : transcript; + + try { + const adapter = this._adapter; + if (adapter) { + await adapter.continueConversationAsync( + process.env.MICROSOFT_APP_ID, + entry.ref, + async (context) => { + await context.sendActivity(MessageFactory.text(text)); + } + ); + } + } catch (err) { + console.error('[bot] Failed to post transcript:', err.message); + } + } + + setAdapter(adapter) { + this._adapter = adapter; + } + + getConversationReferences() { + return this._conversationReferences; + } +} + +module.exports = { TeamsBot }; diff --git a/examples/470-microsoft-teams-transcription-bot-node/src/calling.js b/examples/470-microsoft-teams-transcription-bot-node/src/calling.js new file mode 100644 index 0000000..59f2e0c --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/src/calling.js @@ -0,0 +1,164 @@ +'use strict'; + +const { DeepgramClient } = require('@deepgram/sdk'); +const { GraphClient } = require('./graph'); + +const DEEPGRAM_LIVE_OPTIONS = { + model: 'nova-3', + encoding: 'linear16', + sample_rate: 16000, + channels: 1, + smart_format: true, + interim_results: true, + utterance_end_ms: 1000, + tag: 'deepgram-examples', +}; + +class CallingHandler { + constructor() { + this._deepgram = new DeepgramClient({ apiKey: process.env.DEEPGRAM_API_KEY }); + this._graphClient = null; + this._activeStreams = new Map(); + this._transcriptCallback = null; + } + + _getGraphClient() { + if (!this._graphClient) { + this._graphClient = new GraphClient(); + } + return this._graphClient; + } + + onTranscript(callback) { + this._transcriptCallback = callback; + } + + async handleCallback(payload) { + const notifications = Array.isArray(payload) ? payload : [payload]; + + for (const notification of notifications) { + const resourceData = notification.resourceData || notification; + const state = resourceData.state || resourceData['@odata.type']; + const callId = this._extractCallId(notification); + + if (!callId) continue; + + console.log(`[calling] Callback — call: ${callId}, state: ${state}`); + + switch (state) { + case 'established': + await this._onCallEstablished(callId); + break; + case 'terminated': + this._onCallTerminated(callId); + break; + default: + break; + } + } + } + + async handleNotification(payload) { + const notifications = Array.isArray(payload) ? payload : [payload]; + + for (const notification of notifications) { + const callId = this._extractCallId(notification); + if (!callId) continue; + + if (notification.audioBuffer || notification.data) { + const audioData = notification.audioBuffer || notification.data; + const buffer = Buffer.isBuffer(audioData) + ? audioData + : Buffer.from(audioData, 'base64'); + this._forwardAudioToDeepgram(callId, buffer); + } + } + } + + async _onCallEstablished(callId) { + console.log(`[calling] Call ${callId} established — starting Deepgram stream`); + + const dgConnection = await this._deepgram.listen.v1.connect(DEEPGRAM_LIVE_OPTIONS); + + dgConnection.on('open', () => { + console.log(`[deepgram] Connection opened for call ${callId}`); + }); + + dgConnection.on('error', (err) => { + console.error(`[deepgram] Error for call ${callId}:`, err.message); + }); + + dgConnection.on('close', () => { + console.log(`[deepgram] Connection closed for call ${callId}`); + this._activeStreams.delete(callId); + }); + + dgConnection.on('message', (data) => { + const transcript = data?.channel?.alternatives?.[0]?.transcript; + if (transcript) { + const isFinal = data.is_final; + const tag = isFinal ? 'final' : 'interim'; + console.log(`[${tag}] ${transcript}`); + + if (isFinal && this._transcriptCallback) { + this._transcriptCallback(callId, transcript); + } + } + }); + + dgConnection.connect(); + await dgConnection.waitForOpen(); + + this._activeStreams.set(callId, { dgConnection, bytesSent: 0 }); + + try { + await this._getGraphClient().subscribeToAudio(callId); + } catch (err) { + console.error(`[calling] Audio subscription failed for ${callId}:`, err.message); + } + } + + _onCallTerminated(callId) { + console.log(`[calling] Call ${callId} terminated`); + const stream = this._activeStreams.get(callId); + if (stream?.dgConnection) { + try { stream.dgConnection.sendCloseStream({ type: 'CloseStream' }); } catch {} + try { stream.dgConnection.close(); } catch {} + } + this._activeStreams.delete(callId); + } + + _forwardAudioToDeepgram(callId, audioBuffer) { + const stream = this._activeStreams.get(callId); + if (!stream?.dgConnection) return; + + try { + stream.dgConnection.sendMedia(audioBuffer); + stream.bytesSent += audioBuffer.length; + } catch (err) { + console.error(`[calling] Error forwarding audio for ${callId}:`, err.message); + } + } + + _extractCallId(notification) { + if (notification.resourceUrl) { + const match = notification.resourceUrl.match(/calls\/([^/]+)/); + if (match) return match[1]; + } + if (notification.resource) { + const match = notification.resource.match(/calls\/([^/]+)/); + if (match) return match[1]; + } + return notification.callId || notification.id || null; + } + + getActiveStreams() { + return this._activeStreams; + } + + getDeepgramClient() { + return this._deepgram; + } +} + +module.exports = { CallingHandler, DEEPGRAM_LIVE_OPTIONS }; diff --git a/examples/470-microsoft-teams-transcription-bot-node/src/graph.js b/examples/470-microsoft-teams-transcription-bot-node/src/graph.js new file mode 100644 index 0000000..f1b203b --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/src/graph.js @@ -0,0 +1,116 @@ +'use strict'; + +const { ClientSecretCredential } = require('@azure/identity'); +const { Client } = require('@microsoft/microsoft-graph-client'); +const { + TokenCredentialAuthenticationProvider, +} = require('@microsoft/microsoft-graph-client/authProviders/azureTokenCredentials'); + +class GraphClient { + constructor() { + this._credential = new ClientSecretCredential( + process.env.MICROSOFT_APP_TENANT_ID, + process.env.MICROSOFT_APP_ID, + process.env.MICROSOFT_APP_PASSWORD + ); + + const authProvider = new TokenCredentialAuthenticationProvider(this._credential, { + scopes: ['https://graph.microsoft.com/.default'], + }); + + this._client = Client.initWithMiddleware({ authProvider }); + + this._activeCalls = new Map(); + } + + async joinMeeting(meetingInfo) { + const baseUrl = process.env.BOT_BASE_URL || `https://localhost:${process.env.PORT || 3978}`; + const callbackUrl = `${baseUrl}/api/calling/callback`; + + const requestBody = { + '@odata.type': '#microsoft.graph.call', + callbackUri: callbackUrl, + tenantId: meetingInfo.tenantId, + mediaConfig: { + '@odata.type': '#microsoft.graph.appHostedMediaConfig', + blob: JSON.stringify({ + audioSocketUri: `${baseUrl}/api/calling/notification`, + }), + }, + requestedModalities: ['audio'], + source: { + '@odata.type': '#microsoft.graph.participantInfo', + identity: { + '@odata.type': '#microsoft.graph.identitySet', + application: { + '@odata.type': '#microsoft.graph.identity', + id: process.env.MICROSOFT_APP_ID, + displayName: 'Deepgram Transcription Bot', + }, + }, + }, + }; + + if (meetingInfo.meetingId) { + requestBody.chatInfo = { + '@odata.type': '#microsoft.graph.chatInfo', + threadId: meetingInfo.meetingId, + messageId: '0', + }; + requestBody.meetingInfo = { + '@odata.type': '#microsoft.graph.organizerMeetingInfo', + organizer: { + '@odata.type': '#microsoft.graph.identitySet', + user: { + '@odata.type': '#microsoft.graph.identity', + tenantId: meetingInfo.tenantId, + }, + }, + }; + } else if (meetingInfo.threadId) { + requestBody.chatInfo = { + '@odata.type': '#microsoft.graph.chatInfo', + threadId: meetingInfo.threadId, + messageId: meetingInfo.messageId || '0', + }; + } + + const call = await this._client.api('/communications/calls').post(requestBody); + const callId = call.id; + this._activeCalls.set(callId, { state: 'establishing', meetingInfo }); + console.log(`[graph] Call created — ID: ${callId}`); + return callId; + } + + async leaveCall(callId) { + try { + await this._client.api(`/communications/calls/${callId}`).delete(); + this._activeCalls.delete(callId); + console.log(`[graph] Left call ${callId}`); + } catch (err) { + console.error(`[graph] Error leaving call ${callId}:`, err.message); + this._activeCalls.delete(callId); + } + } + + async subscribeToAudio(callId) { + try { + await this._client + .api(`/communications/calls/${callId}/subscribeToTone`) + .post({ clientContext: 'deepgram-transcription' }); + console.log(`[graph] Subscribed to audio for call ${callId}`); + } catch (err) { + console.error(`[graph] Audio subscription error for ${callId}:`, err.message); + } + } + + getActiveCalls() { + return this._activeCalls; + } + + getClient() { + return this._client; + } +} + +module.exports = { GraphClient }; diff --git a/examples/470-microsoft-teams-transcription-bot-node/src/index.js b/examples/470-microsoft-teams-transcription-bot-node/src/index.js new file mode 100644 index 0000000..4d1f84f --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/src/index.js @@ -0,0 +1,97 @@ +'use strict'; + +require('dotenv').config(); + +const express = require('express'); +const { + CloudAdapter, + ConfigurationBotFrameworkAuthentication, + TurnContext, +} = require('botbuilder'); + +const { TeamsBot } = require('./bot'); +const { CallingHandler } = require('./calling'); + +const PORT = process.env.PORT || 3978; + +function createApp() { + const app = express(); + app.use(express.json()); + + const requiredVars = [ + 'DEEPGRAM_API_KEY', + 'MICROSOFT_APP_ID', + 'MICROSOFT_APP_PASSWORD', + 'MICROSOFT_APP_TENANT_ID', + ]; + const missing = requiredVars.filter((v) => !process.env[v]); + if (missing.length > 0) { + console.error(`Error: Missing required environment variables: ${missing.join(', ')}`); + console.error('Copy .env.example to .env and fill in your values.'); + process.exit(1); + } + + const botFrameworkAuth = new ConfigurationBotFrameworkAuthentication({ + MicrosoftAppId: process.env.MICROSOFT_APP_ID, + MicrosoftAppPassword: process.env.MICROSOFT_APP_PASSWORD, + MicrosoftAppTenantId: process.env.MICROSOFT_APP_TENANT_ID, + MicrosoftAppType: 'SingleTenant', + }); + + const adapter = new CloudAdapter(botFrameworkAuth); + + adapter.onTurnError = async (context, error) => { + console.error('[adapter] Unhandled error:', error.message); + await context.sendActivity('Sorry, something went wrong.'); + }; + + const bot = new TeamsBot(); + const calling = new CallingHandler(); + + app.post('/api/messages', async (req, res) => { + await adapter.process(req, res, (context) => bot.run(context)); + }); + + app.post('/api/calling/callback', async (req, res) => { + try { + await calling.handleCallback(req.body); + res.sendStatus(200); + } catch (err) { + console.error('[calling] Callback error:', err.message); + res.sendStatus(500); + } + }); + + app.post('/api/calling/notification', async (req, res) => { + try { + await calling.handleNotification(req.body); + res.sendStatus(200); + } catch (err) { + console.error('[calling] Notification error:', err.message); + res.sendStatus(500); + } + }); + + app.get('/', (_req, res) => { + res.json({ status: 'ok', service: 'deepgram-teams-transcription-bot' }); + }); + + app._bot = bot; + app._calling = calling; + app._adapter = adapter; + + return app; +} + +if (require.main === module) { + const app = createApp(); + app.listen(PORT, () => { + console.log(`Server listening on port ${PORT}`); + console.log(` POST /api/messages — Bot Framework messaging endpoint`); + console.log(` POST /api/calling/callback — Graph Calling callback`); + console.log(` POST /api/calling/notification — Graph Calling notification`); + console.log(` GET / — Health check`); + }); +} + +module.exports = { createApp }; diff --git a/examples/470-microsoft-teams-transcription-bot-node/tests/test.js b/examples/470-microsoft-teams-transcription-bot-node/tests/test.js new file mode 100644 index 0000000..61dfae1 --- /dev/null +++ b/examples/470-microsoft-teams-transcription-bot-node/tests/test.js @@ -0,0 +1,290 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const { execSync } = require('child_process'); + +const envExample = path.join(__dirname, '..', '.env.example'); +const required = fs.readFileSync(envExample, 'utf8') + .split('\n') + .filter((l) => /^[A-Z][A-Z0-9_]+=/.test(l.trim())) + .map((l) => l.split('=')[0].trim()); + +const missing = required.filter((k) => !process.env[k]); +if (missing.length > 0) { + if (!process.env.DEEPGRAM_API_KEY) { + console.error(`MISSING_CREDENTIALS: ${missing.join(',')}`); + process.exit(2); + } + console.log(`Note: Missing Azure credentials (${missing.filter(k => k !== 'DEEPGRAM_API_KEY').join(', ')}) — skipping server integration tests, running Deepgram streaming test only`); +} + +const { CallingHandler, DEEPGRAM_LIVE_OPTIONS } = require('../src/calling'); + +const PORT = 3199; +const hasAllCreds = missing.length === 0; +const AUDIO_URL = 'https://dpgr.am/spacewalk.wav'; +const TMP_WAV = '/tmp/teams_test.wav'; + +function httpRequest(options, body) { + return new Promise((resolve, reject) => { + const req = http.request(options, (res) => { + let data = ''; + res.on('data', (c) => (data += c)); + res.on('end', () => resolve({ status: res.statusCode, body: data })); + }); + req.on('error', reject); + if (body) req.write(body); + req.end(); + }); +} + +function downloadAndConvertAudio() { + console.log('Downloading test audio...'); + execSync(`curl -s -L -o "${TMP_WAV}" "${AUDIO_URL}"`, { stdio: 'pipe' }); + + const wavData = fs.readFileSync(TMP_WAV); + let offset = 12; + let sampleRate = 0; + let bitsPerSample = 0; + let numChannels = 0; + let dataStart = 0; + let dataSize = 0; + + while (offset < wavData.length - 8) { + const chunkId = wavData.toString('ascii', offset, offset + 4); + const chunkSize = wavData.readUInt32LE(offset + 4); + if (chunkId === 'fmt ') { + numChannels = wavData.readUInt16LE(offset + 10); + sampleRate = wavData.readUInt32LE(offset + 12); + bitsPerSample = wavData.readUInt16LE(offset + 22); + } else if (chunkId === 'data') { + dataStart = offset + 8; + dataSize = chunkSize; + break; + } + offset += 8 + chunkSize; + } + if (!dataStart) throw new Error('Invalid WAV: no data chunk'); + + const bytesPerSample = bitsPerSample / 8; + const totalSamples = Math.floor(dataSize / (bytesPerSample * numChannels)); + const targetRate = 16000; + const ratio = sampleRate / targetRate; + const outSamples = Math.floor(totalSamples / ratio); + const out = Buffer.alloc(outSamples * 2); + + for (let i = 0; i < outSamples; i++) { + const srcIdx = Math.floor(i * ratio); + const byteOff = dataStart + srcIdx * bytesPerSample * numChannels; + let sample; + if (bitsPerSample === 16) { + sample = wavData.readInt16LE(byteOff); + } else if (bitsPerSample === 24) { + sample = wavData[byteOff] | (wavData[byteOff + 1] << 8) | (wavData[byteOff + 2] << 16); + if (sample & 0x800000) sample |= ~0xffffff; + sample = sample >> 8; + } else if (bitsPerSample === 32) { + sample = wavData.readInt32LE(byteOff) >> 16; + } else { + sample = (wavData[byteOff] - 128) << 8; + } + out.writeInt16LE(sample, i * 2); + } + + console.log(`Audio ready: ${out.length} bytes of linear16 16kHz`); + return out; +} + +async function testHealthEndpoint(port) { + const res = await httpRequest({ + hostname: 'localhost', + port, + path: '/', + method: 'GET', + }); + if (res.status !== 200) throw new Error(`Health check returned ${res.status}`); + const body = JSON.parse(res.body); + if (body.status !== 'ok') throw new Error(`Health check status: ${body.status}`); + console.log('PASS: GET / — health check ok'); +} + +async function testMessagingEndpoint(port) { + const res = await httpRequest( + { + hostname: 'localhost', + port, + path: '/api/messages', + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + JSON.stringify({ + type: 'message', + text: 'hello', + from: { id: 'test-user' }, + recipient: { id: 'test-bot' }, + conversation: { id: 'test-conv' }, + channelId: 'msteams', + }) + ); + if (res.status === 401 || res.status === 403) { + console.log('PASS: POST /api/messages — endpoint responds (auth expected in production)'); + } else { + console.log(`PASS: POST /api/messages — endpoint responds with status ${res.status}`); + } +} + +async function testCallingCallback(port) { + const res = await httpRequest( + { + hostname: 'localhost', + port, + path: '/api/calling/callback', + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + JSON.stringify({ + resourceUrl: '/communications/calls/test-call-id', + resourceData: { state: 'establishing' }, + }) + ); + if (res.status !== 200 && res.status !== 500) { + throw new Error(`Calling callback returned unexpected status ${res.status}`); + } + console.log('PASS: POST /api/calling/callback — endpoint responds'); +} + +async function testCallingNotification(port) { + const res = await httpRequest( + { + hostname: 'localhost', + port, + path: '/api/calling/notification', + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + JSON.stringify({ + callId: 'test-call-id', + data: Buffer.from([0, 0, 0, 0]).toString('base64'), + }) + ); + if (res.status !== 200 && res.status !== 500) { + throw new Error(`Calling notification returned unexpected status ${res.status}`); + } + console.log('PASS: POST /api/calling/notification — endpoint responds'); +} + +async function testDeepgramStreaming(audioData) { + console.log('\nTesting CallingHandler Deepgram streaming pipeline...'); + + const handler = new CallingHandler(); + const transcripts = []; + + handler.onTranscript((callId, transcript) => { + transcripts.push({ callId, transcript }); + }); + + const dgClient = handler.getDeepgramClient(); + const dgConnection = await dgClient.listen.v1.connect(DEEPGRAM_LIVE_OPTIONS); + + const connected = new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error('Timed out waiting for Deepgram connection')), + 15000 + ); + dgConnection.on('open', () => { + clearTimeout(timeout); + resolve(); + }); + dgConnection.on('error', (err) => { + clearTimeout(timeout); + reject(err); + }); + }); + + dgConnection.on('message', (data) => { + const transcript = data?.channel?.alternatives?.[0]?.transcript; + if (transcript && data.is_final) { + transcripts.push({ callId: 'direct-test', transcript }); + } + }); + + dgConnection.connect(); + await connected; + console.log('Deepgram connection established'); + + const CHUNK_SIZE = 3200; + const MAX_BYTES = 16000 * 2 * 8; + let offset = 0; + + while (offset < audioData.length && offset < MAX_BYTES) { + const chunk = audioData.subarray(offset, offset + CHUNK_SIZE); + dgConnection.sendMedia(chunk); + offset += CHUNK_SIZE; + await new Promise((r) => setTimeout(r, 100)); + } + + const bytesSent = Math.min(offset, audioData.length); + const audioSentSecs = bytesSent / (16000 * 2); + console.log(`Sent ${bytesSent} bytes (${audioSentSecs.toFixed(1)}s) of audio`); + + dgConnection.sendCloseStream({ type: 'CloseStream' }); + + await new Promise((r) => setTimeout(r, 3000)); + + try { + dgConnection.close(); + } catch {} + + const combined = transcripts.map((t) => t.transcript).join(' '); + const minChars = Math.max(5, audioSentSecs * 2); + + if (combined.trim().length < minChars) { + throw new Error( + `Transcript too short: ${combined.trim().length} chars for ${audioSentSecs.toFixed(1)}s of audio (expected >= ${minChars})` + ); + } + + console.log(`PASS: Deepgram streaming — received ${transcripts.length} transcript(s), ${combined.trim().length} chars`); + console.log(` Sample: "${combined.substring(0, 120)}..."`); +} + +async function testServerEndpoints() { + const { createApp } = require('../src/index'); + const app = createApp(); + const server = app.listen(PORT); + await new Promise((r) => server.on('listening', r)); + console.log(`\nServer started on :${PORT}\n`); + + try { + await testHealthEndpoint(PORT); + await testMessagingEndpoint(PORT); + await testCallingCallback(PORT); + await testCallingNotification(PORT); + } finally { + server.close(); + } +} + +async function run() { + const audioData = downloadAndConvertAudio(); + + if (hasAllCreds) { + await testServerEndpoints(); + } else { + console.log('\nSkipping server endpoint tests (missing Azure credentials)\n'); + } + + await testDeepgramStreaming(audioData); +} + +run() + .then(() => { + console.log('\nAll tests passed'); + process.exit(0); + }) + .catch((err) => { + console.error(`\nTest failed: ${err.message}`); + process.exit(1); + });