├── start ├── client │ ├── .env.sample │ ├── src │ │ ├── favicon.ico │ │ ├── index.js │ │ └── style.css │ ├── package.json │ ├── webpack.config.js │ └── index.html └── server │ ├── .funcignore │ ├── tsconfig.json │ ├── host.json │ ├── sample.local.settings.json │ ├── src │ ├── functions │ │ ├── getStocks.ts │ │ └── setPrice.ts │ ├── data.json │ └── update.ts │ ├── package.json │ └── .gitignore ├── solution ├── client │ ├── .env.sample │ ├── src │ │ ├── favicon.ico │ │ ├── env.js │ │ ├── style.css │ │ └── index.js │ ├── package.json │ ├── webpack.config.js │ └── index.html └── server │ ├── .funcignore │ ├── src │ ├── functions │ │ ├── status.ts │ │ ├── signalr-open-connection.ts │ │ ├── getStocks.ts │ │ ├── setPrice.ts │ │ └── signalr-send-message.ts │ ├── data.json │ └── update.ts │ ├── tsconfig.json │ ├── host.json │ ├── sample.local.settings.json │ ├── package.json │ └── .gitignore ├── .devcontainer ├── post-create-command.sh └── devcontainer.json ├── setup-resources ├── tsconfig.json ├── codespace-url.sh ├── env.sh ├── src │ ├── db.ts │ └── upload.ts ├── data.json ├── package.json ├── create-signalr-resources.sh ├── configure-static-web-app.sh ├── create-start-resources.sh └── package-lock.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── troubleshooting.md ├── LICENSE.txt ├── example-client-workflow.yml ├── SECURITY.md ├── example-server-workflow.yml ├── .gitignore └── README.md /start/client/.env.sample: -------------------------------------------------------------------------------- 1 | BACKEND_URL=http://localhost:7071 -------------------------------------------------------------------------------- /solution/client/.env.sample: -------------------------------------------------------------------------------- 1 | BACKEND_URL=http://localhost:7071 -------------------------------------------------------------------------------- /.devcontainer/post-create-command.sh: -------------------------------------------------------------------------------- 1 | npm install -g azure-functions-core-tools@4.0.5611 --unsafe-perm true 2 | -------------------------------------------------------------------------------- /start/client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftDocs/mslearn-advocates.azure-functions-and-signalr/HEAD/start/client/src/favicon.ico -------------------------------------------------------------------------------- /solution/client/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MicrosoftDocs/mslearn-advocates.azure-functions-and-signalr/HEAD/solution/client/src/favicon.ico -------------------------------------------------------------------------------- /solution/client/src/env.js: -------------------------------------------------------------------------------- 1 | const BACKEND_URL = process.env.BACKEND_URL; 2 | 3 | console.log(`CLIENT ENV BACKEND_URL: ${BACKEND_URL}`); 4 | 5 | export { 6 | BACKEND_URL 7 | }; -------------------------------------------------------------------------------- /solution/server/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | __azurite_db*__.json 6 | __blobstorage__ 7 | __queuestorage__ 8 | local.settings.json 9 | test 10 | tsconfig.json -------------------------------------------------------------------------------- /start/server/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | __azurite_db*__.json 6 | __blobstorage__ 7 | __queuestorage__ 8 | local.settings.json 9 | test 10 | tsconfig.json -------------------------------------------------------------------------------- /setup-resources/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "sourceMap": true, 8 | "strict": false, 9 | 10 | } 11 | } -------------------------------------------------------------------------------- /setup-resources/codespace-url.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SERVER_URL="https://${CODESPACE_NAME}-7071.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" 4 | echo "Server URL: $SERVER_URL" 5 | 6 | CLIENT_URL="https://${CODESPACE_NAME}-3000.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" 7 | echo "Client URL: $CLIENT_URL" 8 | 9 | -------------------------------------------------------------------------------- /solution/server/src/functions/status.ts: -------------------------------------------------------------------------------- 1 | import { app, input } from "@azure/functions"; 2 | 3 | app.http('status', { 4 | methods: ['GET'], 5 | authLevel: 'anonymous', 6 | handler: (request, context) => { 7 | 8 | return { 9 | jsonBody: process.env, 10 | }; 11 | }, 12 | }); -------------------------------------------------------------------------------- /solution/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "rootDir": ".", 7 | "sourceMap": true, 8 | "strict": false, 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | }, 12 | "include": ["src/**/*.ts", "src/**/*.json"], 13 | } -------------------------------------------------------------------------------- /start/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "rootDir": ".", 7 | "sourceMap": true, 8 | "strict": false, 9 | "allowJs": true, 10 | "resolveJsonModule": true, 11 | }, 12 | "include": ["src/**/*.ts", "src/**/*.json"], 13 | } -------------------------------------------------------------------------------- /start/server/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /solution/server/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "logging": { 4 | "applicationInsights": { 5 | "samplingSettings": { 6 | "isEnabled": true, 7 | "excludedTypes": "Request" 8 | } 9 | } 10 | }, 11 | "extensionBundle": { 12 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 13 | "version": "[4.*, 5.0.0)" 14 | } 15 | } -------------------------------------------------------------------------------- /setup-resources/env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run the command and save the output 4 | output=$(azd env get) 5 | 6 | # Parse the output and write to .env file 7 | echo "$output" | while IFS= read -r line 8 | do 9 | # Split the line into name and value 10 | name=$(echo $line | cut -d' ' -f1) 11 | value=$(echo $line | cut -d' ' -f2-) 12 | 13 | # Write to .env file 14 | echo "$name=$value" >> .env 15 | done -------------------------------------------------------------------------------- /start/server/sample.local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "node", 6 | "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", 7 | "COSMOSDB_CONNECTION_STRING": "" 8 | }, 9 | "Host" : { 10 | "LocalHttpPort": 7071, 11 | "CORS": "http://localhost:3000", 12 | "CORSCredentials": true 13 | } 14 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns -------------------------------------------------------------------------------- /setup-resources/src/db.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | 3 | import { 4 | CosmosClient, 5 | } from "@azure/cosmos"; 6 | 7 | let client = null; 8 | 9 | const connString = process.env.COSMOSDB_CONNECTION_STRING; 10 | 11 | console.log('Connection string: ' + process.env.COSMOSDB_CONNECTION_STRING); 12 | 13 | if(connString) { 14 | client = new CosmosClient(connString); 15 | } else { 16 | console.log('Cannot locate Cosmos DB endpoint from connection string.'); 17 | } 18 | 19 | export default client; -------------------------------------------------------------------------------- /solution/server/sample.local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "node", 6 | "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", 7 | "COSMOSDB_CONNECTION_STRING": "", 8 | "SIGNALR_CONNECTION_STRING": "" 9 | }, 10 | "Host" : { 11 | "LocalHttpPort": 7071, 12 | "CORS": "http://localhost:3000", 13 | "CORSCredentials": true 14 | } 15 | } -------------------------------------------------------------------------------- /solution/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server-end", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "tsc", 7 | "watch": "tsc -w", 8 | "clean": "rimraf dist", 9 | "prestart": "npm run clean && npm run build", 10 | "start": "func start" 11 | }, 12 | "dependencies": { 13 | "@azure/functions": "^4.0.0" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^18.19.29", 17 | "azure-functions-core-tools": "^4.0.5611", 18 | "rimraf": "^5.0.0", 19 | "typescript": "^4.0.0" 20 | }, 21 | "main": "dist/src/functions/*.js" 22 | } 23 | -------------------------------------------------------------------------------- /solution/server/src/functions/signalr-open-connection.ts: -------------------------------------------------------------------------------- 1 | import { app, input } from '@azure/functions'; 2 | 3 | const inputSignalR = input.generic({ 4 | type: 'signalRConnectionInfo', 5 | name: 'connectionInfo', 6 | hubName: 'default', 7 | connectionStringSetting: 'SIGNALR_CONNECTION_STRING', 8 | }); 9 | 10 | app.http('open-signalr-connection', { 11 | authLevel: 'anonymous', 12 | handler: (request, context) => { 13 | return { body: JSON.stringify(context.extraInputs.get(inputSignalR)) } 14 | }, 15 | route: 'negotiate', 16 | extraInputs: [inputSignalR] 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /start/server/src/functions/getStocks.ts: -------------------------------------------------------------------------------- 1 | import { app, input } from "@azure/functions"; 2 | 3 | const cosmosInput = input.cosmosDB({ 4 | databaseName: 'stocksdb', 5 | containerName: 'stocks', 6 | connection: 'COSMOSDB_CONNECTION_STRING', 7 | sqlQuery: 'SELECT * from c', 8 | }); 9 | 10 | app.http('getStocks', { 11 | methods: ['GET'], 12 | authLevel: 'anonymous', 13 | extraInputs: [cosmosInput], 14 | handler: (request, context) => { 15 | const stocks = context.extraInputs.get(cosmosInput); 16 | 17 | return { 18 | jsonBody: stocks, 19 | }; 20 | }, 21 | }); -------------------------------------------------------------------------------- /setup-resources/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "e0eb6e85-176d-4ce6-89ae-1f699aaa0bab", 4 | "symbol": "ABC", 5 | "price": "100.00", 6 | "change": "1.00", 7 | "changeDirection": "+" 8 | }, 9 | { 10 | "id": "ebe2e863-bf84-439a-89f8-39975e7d6766", 11 | "symbol": "DEF", 12 | "price": "45.89", 13 | "change": "1.25", 14 | "changeDirection": "-" 15 | }, 16 | { 17 | "id": "80bc1751-3831-4749-99ea-5c6a63105ae7", 18 | "symbol": "GHI", 19 | "price": "156.21", 20 | "change": "6.81", 21 | "changeDirection": "+" 22 | } 23 | ] -------------------------------------------------------------------------------- /start/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "signalr-server-start", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "tsc", 7 | "watch": "tsc -w", 8 | "clean": "rimraf dist", 9 | "prestart": "npm run clean && npm run build", 10 | "start": "func start" 11 | }, 12 | "dependencies": { 13 | "@azure/cosmos": "^4.0.0", 14 | "@azure/functions": "^4.0.0" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^18.x", 18 | "azure-functions-core-tools": "^4.0.5611", 19 | "rimraf": "^5.0.0", 20 | "typescript": "^4.0.0" 21 | }, 22 | "main": "dist/src/functions/*.js" 23 | } 24 | -------------------------------------------------------------------------------- /start/server/src/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "e0eb6e85-176d-4ce6-89ae-1f699aaa0bab", 4 | "symbol": "ABC", 5 | "price": "100.00", 6 | "change": "1.00", 7 | "changeDirection": "+" 8 | }, 9 | { 10 | "id": "ebe2e863-bf84-439a-89f8-39975e7d6766", 11 | "symbol": "DEF", 12 | "price": "45.89", 13 | "change": "1.25", 14 | "changeDirection": "-" 15 | }, 16 | { 17 | "id": "80bc1751-3831-4749-99ea-5c6a63105ae7", 18 | "symbol": "GHI", 19 | "price": "156.21", 20 | "change": "6.81", 21 | "changeDirection": "+" 22 | } 23 | ] -------------------------------------------------------------------------------- /solution/server/src/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "e0eb6e85-176d-4ce6-89ae-1f699aaa0bab", 4 | "symbol": "ABC", 5 | "price": "100.00", 6 | "change": "1.00", 7 | "changeDirection": "+" 8 | }, 9 | { 10 | "id": "ebe2e863-bf84-439a-89f8-39975e7d6766", 11 | "symbol": "DEF", 12 | "price": "45.89", 13 | "change": "1.25", 14 | "changeDirection": "-" 15 | }, 16 | { 17 | "id": "80bc1751-3831-4749-99ea-5c6a63105ae7", 18 | "symbol": "GHI", 19 | "price": "156.21", 20 | "change": "6.81", 21 | "changeDirection": "+" 22 | } 23 | ] -------------------------------------------------------------------------------- /solution/server/src/functions/getStocks.ts: -------------------------------------------------------------------------------- 1 | import { app, input } from "@azure/functions"; 2 | 3 | const cosmosInput = input.cosmosDB({ 4 | databaseName: 'stocksdb', 5 | containerName: 'stocks', 6 | connection: 'COSMOSDB_CONNECTION_STRING', 7 | sqlQuery: 'SELECT * from c', 8 | }); 9 | 10 | app.http('getStocks', { 11 | methods: ['GET'], 12 | authLevel: 'anonymous', 13 | extraInputs: [cosmosInput], 14 | handler: (request, context) => { 15 | try{ 16 | const stocks = context.extraInputs.get(cosmosInput); 17 | return { 18 | jsonBody: stocks, 19 | }; 20 | } catch (error) { 21 | return { 22 | status: 500, 23 | body: error.message, 24 | }; 25 | } 26 | 27 | }, 28 | }); -------------------------------------------------------------------------------- /setup-resources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup", 3 | "version": "1.0.0", 4 | "main": "upload.js", 5 | "scripts": { 6 | "start": "npm run upload", 7 | "prestart": "npm run clean && npm run build", 8 | "clean": "rimraf dist", 9 | "build": "tsc", 10 | "setup:start": "bash create-start-resources.sh", 11 | "setup:signalr": "bash create-signalr-resources.sh", 12 | "upload": "node dist/upload.js", 13 | "move:env": "cp .env ../server-start/.env && cp .env ../client-start/.env" 14 | }, 15 | "license": "ISC", 16 | "dependencies": { 17 | "@azure/cosmos": "^4.0.0", 18 | "dotenv": "^16.4.5" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20.11.30", 22 | "rimraf": "^5.0.5", 23 | "typescript": "^5.4.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /start/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-start", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "start": "serve dist", 8 | "start:dev": "webpack serve --mode development", 9 | "prestart": "npm run clean && npm run build", 10 | "build": "webpack", 11 | "clean": "rimraf dist" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/core": "^7.24.3", 17 | "@babel/preset-env": "^7.24.3", 18 | "babel-loader": "^9.1.3", 19 | "copy-webpack-plugin": "^12.0.2", 20 | "css-loader": "^6.10.0", 21 | "dotenv": "^16.4.5", 22 | "dotenv-webpack": "^8.1.0", 23 | "rimraf": "^5.0.5", 24 | "serve": "^14.2.1", 25 | "style-loader": "^3.3.4", 26 | "webpack": "^5.91.0", 27 | "webpack-cli": "^5.1.4", 28 | "webpack-dev-server": "^5.0.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /start/server/src/functions/setPrice.ts: -------------------------------------------------------------------------------- 1 | import { app, InvocationContext, CosmosDBOutputOptions, Timer, output } from "@azure/functions"; 2 | import { updateData } from "../update"; 3 | 4 | const cosmosOutputConfig: CosmosDBOutputOptions = { 5 | databaseName: 'stocksdb', 6 | containerName: 'stocks', 7 | connection: 'COSMOSDB_CONNECTION_STRING', 8 | partitionKey: 'symbol' 9 | }; 10 | 11 | const cosmosOutput = output.cosmosDB(cosmosOutputConfig); 12 | 13 | export async function setPrice(myTimer: Timer, context: InvocationContext): Promise { 14 | context.log('Timer function processed request.'); 15 | 16 | const newStock = await updateData(); 17 | console.log(JSON.stringify(newStock)); 18 | context.extraOutputs.set(cosmosOutput, newStock); 19 | } 20 | 21 | app.timer('setPrice', { 22 | schedule: '0 * * * * *', 23 | extraOutputs: [cosmosOutput], 24 | handler: setPrice 25 | }); -------------------------------------------------------------------------------- /solution/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-end", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "start": "serve dist", 8 | "start:dev": "webpack serve --mode development", 9 | "prestart": "npm run clean && npm run build", 10 | "build": "webpack --mode development", 11 | "clean": "rimraf dist" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@babel/core": "^7.24.3", 17 | "@babel/preset-env": "^7.24.3", 18 | "babel-loader": "^9.1.3", 19 | "copy-webpack-plugin": "^12.0.2", 20 | "cross-env": "^7.0.3", 21 | "css-loader": "^6.10.0", 22 | "dotenv": "^16.4.5", 23 | "dotenv-webpack": "^8.1.0", 24 | "rimraf": "^5.0.5", 25 | "serve": "^14.2.1", 26 | "style-loader": "^3.3.4", 27 | "webpack": "^5.91.0", 28 | "webpack-cli": "^5.1.4", 29 | "webpack-dev-server": "^5.0.4" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to 4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 5 | and actually do, grant us the rights to use your contribution. For details, visit 6 | https://cla.microsoft.com. 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need 9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 10 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 11 | 12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 14 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -------------------------------------------------------------------------------- /setup-resources/src/upload.ts: -------------------------------------------------------------------------------- 1 | import client from './db.js'; 2 | import {promises as fs} from 'fs'; 3 | 4 | 5 | const databaseDefinition = { id: "stocksdb" }; 6 | const collectionDefinition = { id: "stocks", 7 | partitionKey: { 8 | paths: ["/symbol"] 9 | } }; 10 | 11 | const setupAndSeedDatabase = async () => { 12 | 13 | const data = await fs.readFile('data.json', 'utf8'); 14 | const items = JSON.parse(data); 15 | 16 | const { database: db } = await client.databases.createIfNotExists(databaseDefinition); 17 | console.log('Database created.'); 18 | 19 | const { container } = await db.containers.createIfNotExists(collectionDefinition); 20 | console.log('Collection created.'); 21 | 22 | for await (const item of items) { 23 | await container.items.create(item); 24 | console.log(`Seed data added. Symbol ${item.symbol}`); 25 | } 26 | }; 27 | 28 | setupAndSeedDatabase().catch(err => { 29 | console.error('Error setting up database:', err); 30 | }); 31 | -------------------------------------------------------------------------------- /solution/server/src/functions/setPrice.ts: -------------------------------------------------------------------------------- 1 | import { app, InvocationContext, Timer, output } from "@azure/functions"; 2 | import { updateData } from "../update"; 3 | 4 | const cosmosOutput = output.cosmosDB({ 5 | databaseName: 'stocksdb', 6 | containerName: 'stocks', 7 | connection: 'COSMOSDB_CONNECTION_STRING', 8 | partitionKey: 'symbol' 9 | }); 10 | 11 | export async function setPrice(myTimer: Timer, context: InvocationContext): Promise { 12 | 13 | try { 14 | context.log(`Timer trigger started at ${new Date().toISOString()}`); 15 | 16 | const newStock = await updateData(); 17 | 18 | context.log(`Set Price ${newStock.symbol} ${newStock.price}`); 19 | context.extraOutputs.set(cosmosOutput, newStock); 20 | } catch (error) { 21 | context.log(`Error: ${error}`); 22 | } 23 | } 24 | 25 | app.timer('setPrice', { 26 | schedule: '0 * * * * *', 27 | extraOutputs: [cosmosOutput], 28 | handler: setPrice 29 | }); -------------------------------------------------------------------------------- /troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | Log an issue and @mention the [`javascript-docs`](https://github.com/orgs/MicrosoftDocs/teams/javascript-docs) team. 4 | 5 | ### GitHub Actions workflows troubleshooting 6 | 7 | * If the server doesn't build correctly, add debugging to make sure the package path is correct. 8 | 9 | ```yml 10 | - name: Echo package path 11 | run: echo ${{ env.PACKAGE_PATH }} 12 | ``` 13 | 14 | The value should be: `start/server`. You can also use `solution/server`. 15 | 16 | * If the client doesn't deploy correctly, add debugging to make sure it is build with the **BACKEND_URL**. 17 | 18 | ```yml 19 | - name: Echo BACKEND_URL variable 20 | run: echo ${{ variables.BACKEND_URL }} 21 | ``` 22 | 23 | ### Codespaces troubleshooting 24 | 25 | * If the request from the client is refused by the serverless app, and you have CORS correctly configured in `local.settings.json`, change the visibility of the 7071 port from `private` to `public` for testing purposes only. This should allow the client to find the server in Codspaces. 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Microsoft Corporation. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /start/client/src/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | 3 | function getApiUrl() { 4 | 5 | const backend = process.env.BACKEND_URL; 6 | 7 | const url = (backend) ? `${backend}` : ``; 8 | return url; 9 | } 10 | 11 | const app = new Vue({ 12 | el: '#app', 13 | interval: null, 14 | data() { 15 | return { 16 | stocks: [] 17 | } 18 | }, 19 | methods: { 20 | async update() { 21 | try { 22 | 23 | const url = `${getApiUrl()}/api/getStocks`; 24 | console.log('Fetching stocks from ', url); 25 | 26 | const response = await fetch(url); 27 | if (!response.ok) { 28 | throw new Error(`HTTP error! status: ${response.status}`); 29 | } 30 | 31 | app.stocks = await response.json(); 32 | } catch (ex) { 33 | console.error(ex); 34 | } 35 | }, 36 | startPoll() { 37 | this.interval = setInterval(this.update, 5000); 38 | } 39 | }, 40 | created() { 41 | this.update(); 42 | this.startPoll(); 43 | } 44 | }); -------------------------------------------------------------------------------- /solution/server/src/functions/signalr-send-message.ts: -------------------------------------------------------------------------------- 1 | import { app, output, CosmosDBv4FunctionOptions, InvocationContext } from "@azure/functions"; 2 | 3 | const goingOutToSignalR = output.generic({ 4 | type: 'signalR', 5 | name: 'signalR', 6 | hubName: 'default', 7 | connectionStringSetting: 'SIGNALR_CONNECTION_STRING', 8 | }); 9 | 10 | export async function dataToMessage(documents: unknown[], context: InvocationContext): Promise { 11 | 12 | try { 13 | 14 | context.log(`Documents: ${JSON.stringify(documents)}`); 15 | 16 | documents.map(stock => { 17 | // @ts-ignore 18 | context.log(`Get price ${stock.symbol} ${stock.price}`); 19 | context.extraOutputs.set(goingOutToSignalR, 20 | { 21 | 'target': 'updated', 22 | 'arguments': [stock] 23 | }); 24 | }); 25 | } catch (error) { 26 | context.log(`Error: ${error}`); 27 | } 28 | } 29 | 30 | const options: CosmosDBv4FunctionOptions = { 31 | connection: 'COSMOSDB_CONNECTION_STRING', 32 | databaseName: 'stocksdb', 33 | containerName: 'stocks', 34 | createLeaseContainerIfNotExists: true, 35 | feedPollDelay: 500, 36 | handler: dataToMessage, 37 | extraOutputs: [goingOutToSignalR], 38 | }; 39 | 40 | app.cosmosDB('send-signalr-messages', options); 41 | -------------------------------------------------------------------------------- /solution/server/src/update.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import * as path from 'path'; 3 | 4 | const MIN = 100; 5 | const MAX = 999; 6 | 7 | const getPriceChange = () => { 8 | const change = MIN + (Math.random() * (MAX - MIN)); 9 | const value = Math.round(change); 10 | return parseFloat((value / 100).toFixed(2)); 11 | } 12 | 13 | const getStockChangeValues = (existingStock) => { 14 | 15 | console.log(`existingStock: ${JSON.stringify(existingStock.symbol)}`); 16 | 17 | const isChangePositive = !(existingStock.changeDirection === '+'); 18 | const change = Number(getPriceChange()); 19 | let price = isChangePositive ? Number(existingStock.price) + change : Number(existingStock.price) - change; 20 | price = parseFloat(price.toFixed(2)); 21 | 22 | return { 23 | price, 24 | change, 25 | changeDirection: isChangePositive ? '+' : '-' 26 | }; 27 | }; 28 | 29 | export const updateData = async () => { 30 | 31 | const pathToData = path.join(__dirname, './data.json'); 32 | const data = await fs.readFile(pathToData, 'utf8'); 33 | const items = JSON.parse(data); 34 | 35 | const existingStock = items[Math.floor(Math.random() * items.length)]; 36 | 37 | const updates = getStockChangeValues(existingStock); 38 | const updatedStock = { ...existingStock, ...updates }; 39 | 40 | console.log(`updatedStock: ${JSON.stringify(updatedStock)}`); 41 | 42 | return updatedStock; 43 | 44 | }; -------------------------------------------------------------------------------- /start/server/src/update.ts: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import * as path from 'path'; 3 | 4 | const MIN = 100; 5 | const MAX = 999; 6 | 7 | const getPriceChange = () => { 8 | const change = MIN + (Math.random() * (MAX - MIN)); 9 | const value = Math.round(change); 10 | return parseFloat((value / 100).toFixed(2)); 11 | } 12 | 13 | const getStockChangeValues = (existingStock) => { 14 | 15 | console.log(`existingStock: ${JSON.stringify(existingStock.symbol)}`); 16 | 17 | const isChangePositive = !(existingStock.changeDirection === '+'); 18 | const change = Number(getPriceChange()); 19 | let price = isChangePositive ? Number(existingStock.price) + change : Number(existingStock.price) - change; 20 | price = parseFloat(price.toFixed(2)); 21 | 22 | return { 23 | price, 24 | change, 25 | changeDirection: isChangePositive ? '+' : '-' 26 | }; 27 | }; 28 | 29 | export const updateData = async () => { 30 | 31 | const pathToData = path.join(__dirname, './data.json'); 32 | const data = await fs.readFile(pathToData, 'utf8'); 33 | const items = JSON.parse(data); 34 | 35 | const existingStock = items[Math.floor(Math.random() * items.length)]; 36 | 37 | const updates = getStockChangeValues(existingStock); 38 | const updatedStock = { ...existingStock, ...updates }; 39 | 40 | console.log(`updatedStock: ${JSON.stringify(updatedStock)}`); 41 | 42 | return updatedStock; 43 | 44 | }; -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node 3 | { 4 | "name": "Repo owner - all projects", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/typescript-node:0-18", 7 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 8 | "forwardPorts": [ 9 | 3000, 10 | 7071 11 | ], 12 | // Use 'postCreateCommand' to run commands after the container is created. 13 | "postCreateCommand": "bash .devcontainer/post-create-command.sh", 14 | "features": { 15 | "ghcr.io/devcontainers/features/azure-cli:1": { 16 | "version": "latest", 17 | "installBicep": true 18 | }, 19 | "ghcr.io/azure/azure-dev/azd:latest": {}, 20 | "ghcr.io/jsburckhardt/devcontainer-features/gitleaks:1": {} 21 | }, 22 | // Configure tool-specific properties. 23 | "customizations": { 24 | "vscode": { 25 | "extensions": [ 26 | "ms-azuretools.vscode-cosmosdb", 27 | "ms-azuretools.vscode-azurefunctions", 28 | "ms-azuretools.vscode-azurestaticwebapps", 29 | "esbenp.prettier-vscode", 30 | "dbaeumer.vscode-eslint", 31 | "GitHub.copilot", 32 | "GitHub.copilot-chat", 33 | "ms-azuretools.vscode-azureresourcegroups" 34 | ] 35 | } 36 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 37 | // "remoteUser": "root" 38 | } 39 | } -------------------------------------------------------------------------------- /start/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const Dotenv = require('dotenv-webpack'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | 7 | entry: './src/index.js', 8 | output: { 9 | path: path.resolve(__dirname, 'dist'), 10 | filename: 'bundle.js' 11 | }, 12 | devServer: { 13 | static: { 14 | directory: path.join(__dirname, 'dist'), 15 | }, 16 | compress: true, 17 | port: 3000, 18 | allowedHosts: 'all', 19 | client: { 20 | logging: 'verbose', 21 | overlay: true, 22 | }, 23 | proxy: [ 24 | { 25 | context: ['/api'], 26 | target: 'http://localhost:7071', 27 | }, 28 | ], 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.css$/i, 34 | use: ['style-loader', 'css-loader'], 35 | }, 36 | { 37 | "test": /\.js$/, 38 | "exclude": /node_modules/, 39 | "use": { 40 | "loader": "babel-loader", 41 | "options": { 42 | "presets": [ 43 | "@babel/preset-env", 44 | ] 45 | } 46 | } 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | new Dotenv(), 52 | new CopyWebpackPlugin({ 53 | patterns: [ 54 | { from: './src/favicon.ico', to: './' }, 55 | { from: './index.html', to: './'} 56 | ], 57 | }), 58 | ] 59 | 60 | } -------------------------------------------------------------------------------- /solution/client/src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 40px; 3 | } 4 | 5 | #app { 6 | padding-top: 25px; 7 | } 8 | 9 | .stock { 10 | width: 200px; 11 | border-radius: 3px; 12 | padding: 10px; 13 | border: solid 1px #57c508; 14 | text-align: center; 15 | box-shadow: -1px 0px 2px 0px #eee; 16 | -webkit-box-shadow: -1px 0px 2px 0px #eee; 17 | -moz-box-shadow: -1px 0px 2px 0px #eee; 18 | margin: 20px 20px 20px 0; 19 | } 20 | 21 | .stock:hover { 22 | border: solid 1px rgb(212, 212, 212); 23 | } 24 | 25 | .lead { 26 | font-size: 1.5em; 27 | font-weight: 400; 28 | } 29 | 30 | .is-up, 31 | .is-down { 32 | font-weight: bold; 33 | } 34 | 35 | .is-up { 36 | color: green; 37 | } 38 | 39 | .is-down { 40 | color: maroon; 41 | } 42 | 43 | .change { 44 | text-transform: uppercase; 45 | color: #999; 46 | font-size: .8em; 47 | } 48 | 49 | .no-border, 50 | .no-border:active { 51 | border:0; 52 | } 53 | 54 | .bottom { 55 | vertical-align: bottom; 56 | } 57 | 58 | /* active means while it is happening */ 59 | .list-enter-active, 60 | .list-leave-active { 61 | transition: all 10s; 62 | background-color: lightgray; 63 | } 64 | 65 | /* i'm about to enter */ 66 | .list-enter, 67 | /* i already left */ 68 | .list-leave-to /* .list-leave-active below version 2.1.8 */ { 69 | opacity: 0; 70 | background-color: lightblue; 71 | } 72 | 73 | .list-item { 74 | transition: all 1s; 75 | } 76 | 77 | .fade-enter-active { 78 | transition: all 10s ease; 79 | background-color: lightgray; 80 | } 81 | .fade-enter, 82 | .fade-leave-to { 83 | opacity: 0; 84 | background-color: lightblue; 85 | } -------------------------------------------------------------------------------- /start/client/src/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 40px; 3 | } 4 | 5 | #app { 6 | padding-top: 25px; 7 | } 8 | 9 | .stock { 10 | width: 200px; 11 | border-radius: 3px; 12 | padding: 10px; 13 | border: solid 1px #eee; 14 | text-align: center; 15 | box-shadow: -1px 0px 2px 0px #eee; 16 | -webkit-box-shadow: -1px 0px 2px 0px #eee; 17 | -moz-box-shadow: -1px 0px 2px 0px #eee; 18 | margin: 20px 20px 20px 0; 19 | } 20 | 21 | .stock:hover { 22 | border: solid 1px rgb(212, 212, 212); 23 | } 24 | 25 | .lead { 26 | font-size: 1.5em; 27 | font-weight: 400; 28 | } 29 | 30 | .is-up, 31 | .is-down { 32 | font-weight: bold; 33 | } 34 | 35 | .is-up { 36 | color: green; 37 | } 38 | 39 | .is-down { 40 | color: maroon; 41 | } 42 | 43 | .change { 44 | text-transform: uppercase; 45 | color: #999; 46 | font-size: .8em; 47 | } 48 | 49 | .no-border, 50 | .no-border:active { 51 | border:0; 52 | } 53 | 54 | .bottom { 55 | vertical-align: bottom; 56 | } 57 | 58 | /* active means while it is happening */ 59 | .list-enter-active, 60 | .list-leave-active { 61 | transition: all 10s; 62 | background-color: lightgray; 63 | } 64 | 65 | /* i'm about to enter */ 66 | .list-enter, 67 | /* i already left */ 68 | .list-leave-to /* .list-leave-active below version 2.1.8 */ { 69 | opacity: 0; 70 | background-color: lightblue; 71 | } 72 | 73 | 74 | .list-item { 75 | transition: all 1s; 76 | } 77 | 78 | .fade-enter-active { 79 | transition: all 10s ease; 80 | background-color: lightgray; 81 | } 82 | .fade-enter, 83 | .fade-leave-to { 84 | opacity: 0; 85 | background-color: lightblue; 86 | } -------------------------------------------------------------------------------- /solution/client/webpack.config.js: -------------------------------------------------------------------------------- 1 | const Dotenv = require('dotenv-webpack'); 2 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 3 | const path = require('path'); 4 | 5 | module.exports = (env) => { 6 | 7 | console.log('env: ', env) 8 | console.log('process.env: ', process.env) 9 | 10 | return { 11 | entry: './src/index.js', 12 | output: { 13 | path: path.resolve(__dirname, 'dist'), 14 | filename: 'bundle.js' 15 | }, 16 | stats: 'verbose', 17 | devServer: { 18 | static: { 19 | directory: path.join(__dirname, 'dist'), 20 | }, 21 | compress: true, 22 | port: 3000, 23 | allowedHosts: 'all', 24 | client: { 25 | logging: 'verbose', 26 | overlay: true, 27 | }, 28 | proxy: [ 29 | { 30 | context: ['/api'], 31 | target: process.env.BACKEND_URL || 'http://localhost:7071', 32 | }, 33 | ], 34 | }, 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.css$/i, 39 | use: ['style-loader', 'css-loader'], 40 | }, 41 | { 42 | "test": /\.js$/, 43 | "exclude": /node_modules/, 44 | "use": { 45 | "loader": "babel-loader", 46 | "options": { 47 | "presets": [ 48 | "@babel/preset-env", 49 | ] 50 | } 51 | } 52 | } 53 | ] 54 | }, 55 | plugins: [ 56 | new Dotenv({ 57 | systemvars: true 58 | }), 59 | new CopyWebpackPlugin({ 60 | patterns: [ 61 | { from: './src/favicon.ico', to: './' }, 62 | { from: './index.html', to: './' } 63 | ], 64 | }) 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /setup-resources/create-signalr-resources.sh: -------------------------------------------------------------------------------- 1 | # To use the Microsoft Learn Sandbox in the training module 2 | # https://learn.microsoft.com/training/modules/automatic-update-of-a-webapp-using-azure-functions-and-signalr 3 | # To run: sign in to Azure CLI with `az login` 4 | set -e 5 | 6 | 7 | # Check if user is logged into Azure CLI 8 | if ! az account show &> /dev/null 9 | then 10 | prinft "You are not logged into Azure CLI. Please log in with 'az login' and try again.\n" 11 | exit 1 12 | fi 13 | printf "User logged in\n" 14 | 15 | NODE_ENV_FILE="./.env" 16 | 17 | # Get the default subscription 18 | SUBSCRIPTION_NAME=$(az account show --query 'name' -o tsv) 19 | 20 | 21 | # Set the SignalR service name 22 | SIGNALR_SERVICE_NAME="msl-sigr-signalr$(openssl rand -hex 5)" 23 | 24 | printf "Subscription Name: ""$SUBSCRIPTION_NAME"" \n" 25 | printf "SignalR Service Name: $SIGNALR_SERVICE_NAME\n" 26 | 27 | printf "Creating SignalR Account\n" 28 | az signalr create \ 29 | --name "$SIGNALR_SERVICE_NAME" \ 30 | --sku Free_DS2 31 | 32 | printf "Configure SignalR for serverless mode\n" 33 | 34 | az resource update \ 35 | --resource-type Microsoft.SignalRService/SignalR \ 36 | --name "$SIGNALR_SERVICE_NAME" \ 37 | --set properties.features[flag=ServiceMode].value=Serverless 38 | 39 | printf "Get SignalR connection string\n" 40 | 41 | SIGNALR_CONNECTION_STRING=$(az signalr key list \ 42 | --name $(az signalr list \ 43 | --query [0].name -o tsv) \ 44 | --query primaryConnectionString -o tsv) 45 | 46 | # Add the SignalR connection string to the .env file 47 | echo -e "\nSIGNALR_CONNECTION_STRING=$SIGNALR_CONNECTION_STRING" >> $NODE_ENV_FILE 48 | 49 | printf "\n\nReplace with:\n$SIGNALR_CONNECTION_STRING" 50 | 51 | -------------------------------------------------------------------------------- /start/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Stocks | Enable automatic updates in a web application using Azure Functions and SignalR 11 | 12 | 13 | 14 | 15 |
16 |

Stocks

17 |
18 |
19 |
{{ stock.symbol }}: ${{ stock.price }}
20 |
Change: 21 | 22 | {{ stock.changeDirection }}{{ stock.change }} 23 |
24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /solution/server/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json 95 | 96 | # Azurite artifacts 97 | __blobstorage__ 98 | __queuestorage__ 99 | __azurite_db*__.json -------------------------------------------------------------------------------- /start/server/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json 95 | 96 | # Azurite artifacts 97 | __blobstorage__ 98 | __queuestorage__ 99 | __azurite_db*__.json -------------------------------------------------------------------------------- /solution/client/src/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | import { BACKEND_URL } from './env'; 3 | 4 | const app = new Vue({ 5 | el: '#app', 6 | data() { 7 | return { 8 | stocks: [] 9 | } 10 | }, 11 | methods: { 12 | async getStocks() { 13 | try { 14 | 15 | const url = `${BACKEND_URL}/api/getStocks`; 16 | console.log('Fetching stocks from ', url); 17 | 18 | const response = await fetch(url); 19 | if (!response.ok) { 20 | throw new Error(`HTTP error! status: ${response.status} - ${response.statusText}`); 21 | } 22 | app.stocks = await response.json(); 23 | } catch (ex) { 24 | console.error(ex); 25 | } 26 | } 27 | }, 28 | created() { 29 | this.getStocks(); 30 | } 31 | }); 32 | 33 | const connect = () => { 34 | 35 | const signalR_URL = `${BACKEND_URL}/api`; 36 | console.log(`Connecting to SignalR...${signalR_URL}`) 37 | 38 | const connection = new signalR.HubConnectionBuilder() 39 | .withUrl(signalR_URL) 40 | .configureLogging(signalR.LogLevel.Information) 41 | .build(); 42 | 43 | connection.onclose(() => { 44 | console.log('SignalR connection disconnected'); 45 | setTimeout(() => connect(), 2000); 46 | }); 47 | 48 | connection.on('updated', updatedStock => { 49 | console.log('Stock updated message received', updatedStock); 50 | const index = app.stocks.findIndex(s => s.id === updatedStock.id); 51 | console.log(`${updatedStock.symbol} Old price: ${app.stocks[index].price}, New price: ${updatedStock.price}`); 52 | app.stocks.splice(index, 1, updatedStock); 53 | }); 54 | 55 | connection.start().then(() => { 56 | console.log("SignalR connection established"); 57 | }); 58 | }; 59 | 60 | connect(); -------------------------------------------------------------------------------- /solution/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 12 | Stock Notifications | Enable automatic updates in a web application using Azure Functions and SignalR 13 | 14 | 15 | 16 |
17 |

Stocks

18 |
19 |
20 | 21 |
22 |
{{ stock.symbol }}: ${{ stock.price }}
23 |
Change: 24 | 26 | {{ stock.changeDirection }}{{ stock.change }} 27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /setup-resources/configure-static-web-app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get the default subscription 4 | SUBSCRIPTION_NAME=$(az account show --query 'name' -o tsv) 5 | echo "Using default subscription: $SUBSCRIPTION_NAME" 6 | 7 | # Set the resource group name 8 | RESOURCE_GROUP_NAME="stock-prototype" 9 | 10 | # Get the first Cosmos DB account in the resource group 11 | echo "Getting the first Cosmos DB account in the resource group..." 12 | cosmosDbAccount=$(az cosmosdb list --resource-group $resourceGroup --query '[0].name' -o tsv) 13 | echo "Cosmos DB Account: $cosmosDbAccount" 14 | 15 | # Get the connection string for the Cosmos DB account 16 | echo "Getting the connection string for the Cosmos DB account..." 17 | cosmosDbConnectionString=$(az cosmosdb keys list --name $cosmosDbAccount --resource-group $resourceGroup --type connection-strings --query 'connectionStrings[0].connectionString' -o tsv) 18 | echo "Cosmos DB Connection String: $cosmosDbConnectionString" 19 | 20 | # Get the first SignalR service in the resource group 21 | echo "Getting the first SignalR service in the resource group..." 22 | signalrName=$(az signalr list --resource-group $resourceGroup --query '[0].name' -o tsv) 23 | echo "SignalR Service Name: $signalrName" 24 | 25 | # Get the connection string for the SignalR service 26 | echo "Getting the connection string for the SignalR service..." 27 | signalrConnectionString=$(az signalr key list --name $signalrName --resource-group $resourceGroup --query 'primaryConnectionString' -o tsv) 28 | echo "SignalR Connection String: $signalrConnectionString" 29 | 30 | # Get the first Static Web App in the resource group 31 | echo "Getting the first Static Web App in the resource group..." 32 | staticWebAppName=$(az staticwebapp list --resource-group $resourceGroup --query '[0].name' -o tsv) 33 | echo "Static Web App Name: $staticWebAppName" 34 | 35 | # Add the environment variable to the Static Web App 36 | echo "Adding the environment variables to the Static Web App..." 37 | az staticwebapp appsettings set --name $staticWebAppName --resource-group $resourceGroup --setting-names SIGNALR_CONNECTION_STRING=$signalrConnectionString COSMOSDB_CONNECTION_STRING=$cosmosDbConnectionString 38 | echo "Set environment variables." -------------------------------------------------------------------------------- /example-client-workflow.yml: -------------------------------------------------------------------------------- 1 | ```yaml 2 | name: Client - Azure Static Web Apps CI/CD 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | types: [opened, synchronize, reopened, closed] 10 | branches: 11 | - main 12 | 13 | jobs: 14 | build_and_deploy_job: 15 | if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') 16 | runs-on: ubuntu-latest 17 | name: Build and Deploy Job 18 | steps: 19 | 20 | - uses: actions/checkout@v3 21 | with: 22 | submodules: true 23 | lfs: false 24 | 25 | #Debug only - Did GitHub action variable get into workflow correctly? 26 | - name: Echo 27 | run: | 28 | echo "BACKEND_URL: ${{ vars.BACKEND_URL }}" 29 | 30 | - name: Build And Deploy 31 | id: builddeploy 32 | uses: Azure/static-web-apps-deploy@v1 33 | with: 34 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_123 }} 35 | repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) 36 | action: "upload" 37 | ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### 38 | # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig 39 | app_location: "/solution/client" # App source code path 40 | api_location: "" # Api source code path - optional 41 | output_location: "dist" # Built app content directory - optional 42 | ###### End of Repository/Build Configurations ###### 43 | env: 44 | BACKEND_URL: ${{ vars.BACKEND_URL }} 45 | 46 | close_pull_request_job: 47 | if: github.event_name == 'pull_request' && github.event.action == 'closed' 48 | runs-on: ubuntu-latest 49 | name: Close Pull Request Job 50 | steps: 51 | - name: Close Pull Request 52 | id: closepullrequest 53 | uses: Azure/static-web-apps-deploy@v1 54 | with: 55 | azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_123 }} 56 | action: "close" 57 | ``` -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | -------------------------------------------------------------------------------- /example-server-workflow.yml: -------------------------------------------------------------------------------- 1 | # Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action 2 | # More GitHub Actions for Azure: https://github.com/Azure/actions 3 | 4 | name: Server 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | env: 13 | AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root 14 | NODE_VERSION: '20.x' # set this to the node version to use (supports 8.x, 10.x, 12.x) 15 | PACKAGE_PATH: 'start/server' 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: 'Checkout GitHub Action' 22 | uses: actions/checkout@v4 23 | 24 | - name: Setup Node ${{ env.NODE_VERSION }} Environment 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ env.NODE_VERSION }} 28 | 29 | - name: 'Resolve Project Dependencies Using Npm' 30 | shell: bash 31 | run: | 32 | pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/${{ env.PACKAGE_PATH }}' 33 | npm install 34 | npm run build --if-present 35 | npm run test --if-present 36 | popd 37 | 38 | - name: Zip artifact for deployment 39 | run: | 40 | pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/${{ env.PACKAGE_PATH }}' 41 | zip -r release.zip . 42 | popd 43 | cp ./${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/${{ env.PACKAGE_PATH }}/release.zip ./release.zip 44 | 45 | - name: Upload artifact for deployment job 46 | uses: actions/upload-artifact@v3 47 | with: 48 | name: node-app 49 | path: release.zip 50 | 51 | 52 | deploy: 53 | runs-on: ubuntu-latest 54 | needs: build 55 | environment: 56 | name: 'Production' 57 | url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} 58 | permissions: 59 | id-token: write #This is required for requesting the JWT 60 | 61 | steps: 62 | - name: Download artifact from build job 63 | uses: actions/download-artifact@v3 64 | with: 65 | name: node-app 66 | 67 | - name: Unzip artifact for deployment 68 | run: unzip release.zip 69 | 70 | - name: Login to Azure 71 | uses: azure/login@v1 72 | with: 73 | client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_D810AAE223B944DAB5BE10C3C7BD2517 }} 74 | tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_B50AC9A10C2043CF8659AB1430C99691 }} 75 | subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_A1EE22F1D9DD426783AF476510834AC2 }} 76 | 77 | - name: 'Run Azure Functions Action' 78 | uses: Azure/functions-action@v1 79 | id: fa 80 | with: 81 | app-name: 'api-jamie' 82 | slot-name: 'Production' 83 | package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} 84 | 85 | -------------------------------------------------------------------------------- /setup-resources/create-start-resources.sh: -------------------------------------------------------------------------------- 1 | # To use the Microsoft Learn Sandbox in the training module 2 | # https://learn.microsoft.com/training/modules/automatic-update-of-a-webapp-using-azure-functions-and-signalr 3 | # To run: sign in to Azure CLI with `az login` 4 | # 5 | # bash create-start-resources.sh "SUBSCRIPTION_NAME_OR_ID" 6 | # 7 | 8 | 9 | set -e 10 | 11 | printf "Param 1: $1\n" 12 | 13 | LOCATION="eastus2" 14 | printf "Location: $LOCATION\n" 15 | 16 | # Check if user is logged into Azure CLI 17 | if ! az account show &> /dev/null 18 | then 19 | printf "You are not logged into Azure CLI. Please log in with 'az login' and try again." 20 | exit 1 21 | fi 22 | printf "User logged in\n" 23 | 24 | NODE_ENV_FILE="./.env" 25 | 26 | # Get user name 27 | USER_NAME=$(az account show --query 'user.name' -o tsv) 28 | # Capture name before `@` in user.name 29 | USER_NAME=${USER_NAME%%@*} 30 | printf "User name: $USER_NAME\n" 31 | 32 | # Get the default subscription if not provided as a parameter 33 | SUBSCRIPTION_NAME=$1 34 | # Set default subscription 35 | az configure --defaults subscription="$SUBSCRIPTION_NAME" 36 | printf "Using subscription: ""$SUBSCRIPTION_NAME""\n" 37 | 38 | # Set the resource group name if not provided as a parameter 39 | RANDOM_STRING=$(openssl rand -hex 5) 40 | #printf "Using random string: $RANDOM_STRING\n" 41 | RESOURCE_GROUP_NAME="$USER_NAME-signalr-$RANDOM_STRING" 42 | 43 | # Create a resource group 44 | az group create \ 45 | --subscription "$SUBSCRIPTION_NAME" \ 46 | --name "$RESOURCE_GROUP_NAME" \ 47 | --location $LOCATION 48 | 49 | # Set default resource group 50 | az configure --defaults group="$RESOURCE_GROUP_NAME" 51 | 52 | printf "Using resource group $RESOURCE_GROUP_NAME\n" 53 | 54 | export STORAGE_ACCOUNT_NAME=signalr$(openssl rand -hex 5) 55 | export COMSOSDB_NAME=signalr-cosmos-$(openssl rand -hex 5) 56 | 57 | printf "Subscription Name: ""$SUBSCRIPTION_NAME"" \n" 58 | printf "Resource Group Name: $RESOURCE_GROUP_NAME\n" 59 | printf "Storage Account Name: $STORAGE_ACCOUNT_NAME\n" 60 | printf "CosmosDB Name: $COMSOSDB_NAME\n" 61 | 62 | printf "Creating Storage Account\n" 63 | 64 | az storage account create \ 65 | --subscription "$SUBSCRIPTION_NAME" \ 66 | --name $STORAGE_ACCOUNT_NAME \ 67 | --resource-group $RESOURCE_GROUP_NAME \ 68 | --kind StorageV2 \ 69 | --sku Standard_LRS 70 | 71 | printf "Creating CosmosDB Account\n" 72 | 73 | az cosmosdb create \ 74 | --subscription "$SUBSCRIPTION_NAME" \ 75 | --name $COMSOSDB_NAME \ 76 | --resource-group $RESOURCE_GROUP_NAME 77 | 78 | printf "Creating CosmosDB db\n" 79 | # Create stocksdb database 80 | az cosmosdb sql database create \ 81 | --account-name $COMSOSDB_NAME \ 82 | --name stocksdb 83 | 84 | printf "Creating CosmosDB container\n" 85 | # Create stocks container 86 | az cosmosdb sql container create \ 87 | --account-name $COMSOSDB_NAME \ 88 | --database-name stocksdb \ 89 | --name stocks \ 90 | --partition-key-path /symbol 91 | 92 | printf "Get storage connection string\n" 93 | 94 | STORAGE_CONNECTION_STRING=$(az storage account show-connection-string \ 95 | --name $(az storage account list \ 96 | --resource-group $RESOURCE_GROUP_NAME \ 97 | --query [0].name -o tsv) \ 98 | --resource-group $RESOURCE_GROUP_NAME \ 99 | --query "connectionString" -o tsv) 100 | 101 | printf "Get account name \n" 102 | 103 | COSMOSDB_ACCOUNT_NAME=$(az cosmosdb list \ 104 | --subscription "$SUBSCRIPTION_NAME" \ 105 | --resource-group $RESOURCE_GROUP_NAME \ 106 | --query [0].name -o tsv) 107 | 108 | printf "Get CosmosDB connection string \n" 109 | 110 | COSMOSDB_CONNECTION_STRING=$(az cosmosdb keys list --type connection-strings \ 111 | --name $COSMOSDB_ACCOUNT_NAME \ 112 | --resource-group $RESOURCE_GROUP_NAME \ 113 | --subscription "$SUBSCRIPTION_NAME" \ 114 | --query "connectionStrings[?description=='Primary SQL Connection String'].connectionString" -o tsv) 115 | 116 | printf "\n\nReplace with:\n$STORAGE_CONNECTION_STRING\n\nReplace with:\n$COSMOSDB_CONNECTION_STRING" 117 | 118 | # create a .env file with the connection strings and keys 119 | cat >> $NODE_ENV_FILE <> $NODE_ENV_FILE 126 | printf "\n\nRESOURCE_GROUP_NAME=$RESOURCE_GROUP_NAME" 127 | 128 | 129 | # Validate the .env file 130 | if [ -f "$NODE_ENV_FILE" ]; then 131 | printf "\n\nThe .env file was created successfully." 132 | else 133 | printf "\n\nThe .env file was not created." 134 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | 332 | **/temp.json 333 | **/dist 334 | **/.env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - javascript 5 | - typescript 6 | - nodejs 7 | products: 8 | - azure 9 | - azure-cosmos-db 10 | - azure-functions 11 | - azure-signalr 12 | - azure-storage 13 | - vs-code 14 | urlFragment: azure-functions-and-signalr-javascript 15 | name: "Enable real-time updates in a web application using Azure Functions and SignalR Service" 16 | description: Change a JavaScript web app update mechanism from polling to real-time push-based architecture with SignalR Service, Azure Cosmos DB and Azure Functions. Use Vue.js and JavaScript to use SignalR using Visual Studio Code. 17 | --- 18 | 19 | # SignalR with Azure Functions triggers and bindings 20 | 21 | This repository is the companion to the following training module: 22 | 23 | * [Enable automatic updates in a web application using Azure Functions and SignalR Service](https://learn.microsoft.com/training/modules/automatic-update-of-a-webapp-using-azure-functions-and-signalr/) 24 | 25 | This solution displays fake stock prices as they update: 26 | 27 | * [Start](./start): uses polling every minute 28 | * [Solution](./solution): uses database change notifications and SignalR 29 | 30 | ## Set up resources 31 | 32 | The Azure resources are created from bash scripts in the `setup-resources` folder. This includes creating: 33 | 34 | * Azure Cosmos DB 35 | * Azure SignalR 36 | * Azure Storage (for Azure Function triggers) 37 | 38 | ## Ports 39 | 40 | * Client: 3000: built with webpack 41 | * Server: 7071: build with Azure Functions core tools 42 | 43 | ## Starting project without SignalR 44 | 45 | The starting project updates stock prices in a Cosmos DB database every minute with an Azure Function app and a timer trigger. The client polls for all the stock prices. 46 | 47 | ## Ending project 48 | 49 | The starting project updates stock prices in a Cosmos DB database every minute with an Azure Function app and a timer trigger. The client uses SignalR to recieve on the Cosmos DB items with change notifications through an Azure Functions app. 50 | 51 | ## Deploy to Azure Static Web Apps and Azure Functions App 52 | 53 | 1. Deploy the backend to Azure Functions App. 54 | 55 | GitHub Action workflow file should look like: 56 | 57 | ```yaml 58 | # Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action 59 | # More GitHub Actions for Azure: https://github.com/Azure/actions 60 | 61 | name: Server - Build and deploy Node.js project to Azure Function App 62 | 63 | on: 64 | push: 65 | branches: 66 | - main 67 | workflow_dispatch: 68 | 69 | env: 70 | # solution/server is the finished path 71 | # start/server is the original path you work on 72 | PACKAGE_PATH: 'solution/server' # set this to the path to your web app project, defaults to the repository root 73 | AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' 74 | NODE_VERSION: '20.x' # set this to the node version to use (supports 8.x, 10.x, 12.x) 75 | 76 | jobs: 77 | build: 78 | runs-on: ubuntu-latest 79 | steps: 80 | - name: 'Checkout GitHub Action' 81 | uses: actions/checkout@v4 82 | 83 | - name: Setup Node ${{ env.NODE_VERSION }} Environment 84 | uses: actions/setup-node@v3 85 | with: 86 | node-version: ${{ env.NODE_VERSION }} 87 | 88 | - name: 'Resolve Project Dependencies Using Npm' 89 | shell: bash 90 | run: | 91 | pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/${{ env.PACKAGE_PATH}}' 92 | npm install 93 | npm run build --if-present 94 | npm run test --if-present 95 | popd 96 | 97 | - name: Zip artifact for deployment 98 | run: | 99 | pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/${{ env.PACKAGE_PATH}}' 100 | zip -r release.zip . 101 | popd 102 | cp ./${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/${{ env.PACKAGE_PATH }}/release.zip ./release.zip 103 | 104 | - name: Upload artifact for deployment job 105 | uses: actions/upload-artifact@v3 106 | with: 107 | name: node-app 108 | path: release.zip 109 | 110 | deploy: 111 | runs-on: ubuntu-latest 112 | needs: build 113 | environment: 114 | name: 'Production' 115 | url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} 116 | permissions: 117 | id-token: write #This is required for requesting the JWT 118 | 119 | steps: 120 | - name: Download artifact from build job 121 | uses: actions/download-artifact@v3 122 | with: 123 | name: node-app 124 | 125 | - name: Unzip artifact for deployment 126 | run: unzip release.zip 127 | 128 | - name: Login to Azure 129 | uses: azure/login@v1 130 | with: 131 | client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_123 }} 132 | tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_123 }} 133 | subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_123 }} 134 | 135 | - name: 'Run Azure Functions Action' 136 | uses: Azure/functions-action@v1 137 | id: fa 138 | with: 139 | app-name: 'signalr-3' 140 | slot-name: 'Production' 141 | package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} 142 | ``` 143 | 144 | 145 | 146 | 1. Deploy the frontend to Azure Static Web Apps in Standard plan type (not free) in order to use [bring your own backend](https://learn.microsoft.com/azure/static-web-apps/functions-bring-your-own) (byob). 147 | 148 | * Azure Cloud: Choose the Vue.js prebuild template then edit for this example, using the [example client workflow](example-client-workflow.yml). The build relies on the injection of the repo variable to find the BACKEND_URL value during SWA build and deploy. The webpack Dotenv param for `systemvars: true` brings in the value from the GitHub step to the webpack build. 149 | 150 | * Local: The client build on the local machine depends on a `.env` file at the root of the client application. 151 | 152 | 1. In Azure Functions, enable CORS for the client URL and check `Enable Access-Control-Allow-Credentials`. 153 | 154 | ## Resources 155 | 156 | * [Azure SignalR service documentation](https://learn.microsoft.com/azure/azure-signalr/) 157 | * [Azure SignalR service samples](https://github.com/aspnet/AzureSignalR-samples) 158 | * [Azure Functions triggers and bindings for SignalR](https://learn.microsoft.com/azure/azure-functions/functions-bindings-signalr-service) 159 | 160 | ## Trademark Notice 161 | 162 | Trademarks This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft’s Trademark & Brand Guidelines](https://www.microsoft.com/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party’s policies. 163 | 164 | ## Contributing 165 | 166 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 167 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 168 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 169 | 170 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 171 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 172 | provided by the bot. You will only need to do this once across all repos using our CLA. 173 | 174 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 175 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 176 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 177 | 178 | ## Legal Notices 179 | 180 | Microsoft and any contributors grant you a license to the Microsoft documentation and other content 181 | in this repository under the [Creative Commons Attribution 4.0 International Public License](https://creativecommons.org/licenses/by/4.0/legalcode), 182 | see the [LICENSE](LICENSE) file, and grant you a license to any code in the repository under the [MIT License](https://opensource.org/licenses/MIT), see the 183 | [LICENSE-CODE](LICENSE-CODE) file. 184 | 185 | Microsoft, Windows, Microsoft Azure and/or other Microsoft products and services referenced in the documentation 186 | may be either trademarks or registered trademarks of Microsoft in the United States and/or other countries. 187 | The licenses for this project do not grant you rights to use any Microsoft names, logos, or trademarks. 188 | Microsoft's general trademark guidelines can be found at http://go.microsoft.com/fwlink/?LinkID=254653. 189 | 190 | Privacy information can be found at https://privacy.microsoft.com/en-us/ 191 | 192 | Microsoft and any contributors reserve all other rights, whether under their respective copyrights, patents, 193 | or trademarks, whether by implication, estoppel or otherwise. 194 | -------------------------------------------------------------------------------- /setup-resources/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "setup", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@azure/cosmos": "^4.0.0", 13 | "dotenv": "^16.4.5" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^20.11.30", 17 | "rimraf": "^5.0.5", 18 | "typescript": "^5.4.3" 19 | } 20 | }, 21 | "node_modules/@azure/abort-controller": { 22 | "version": "1.1.0", 23 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", 24 | "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", 25 | "dependencies": { 26 | "tslib": "^2.2.0" 27 | }, 28 | "engines": { 29 | "node": ">=12.0.0" 30 | } 31 | }, 32 | "node_modules/@azure/core-auth": { 33 | "version": "1.7.1", 34 | "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.1.tgz", 35 | "integrity": "sha512-dyeQwvgthqs/SlPVQbZQetpslXceHd4i5a7M/7z/lGEAVwnSluabnQOjF2/dk/hhWgMISusv1Ytp4mQ8JNy62A==", 36 | "dependencies": { 37 | "@azure/abort-controller": "^2.0.0", 38 | "@azure/core-util": "^1.1.0", 39 | "tslib": "^2.6.2" 40 | }, 41 | "engines": { 42 | "node": ">=18.0.0" 43 | } 44 | }, 45 | "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { 46 | "version": "2.1.1", 47 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.1.tgz", 48 | "integrity": "sha512-NhzeNm5zu2fPlwGXPUjzsRCRuPx5demaZyNcyNYJDqpa/Sbxzvo/RYt9IwUaAOnDW5+r7J9UOE6f22TQnb9nhQ==", 49 | "dependencies": { 50 | "tslib": "^2.6.2" 51 | }, 52 | "engines": { 53 | "node": ">=18.0.0" 54 | } 55 | }, 56 | "node_modules/@azure/core-rest-pipeline": { 57 | "version": "1.15.1", 58 | "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.15.1.tgz", 59 | "integrity": "sha512-ZxS6i3eHxh86u+1eWZJiYywoN2vxvsSoAUx60Mny8cZ4nTwvt7UzVVBJO+m2PW2KIJfNiXMt59xBa59htOWL4g==", 60 | "dependencies": { 61 | "@azure/abort-controller": "^2.0.0", 62 | "@azure/core-auth": "^1.4.0", 63 | "@azure/core-tracing": "^1.0.1", 64 | "@azure/core-util": "^1.3.0", 65 | "@azure/logger": "^1.0.0", 66 | "http-proxy-agent": "^7.0.0", 67 | "https-proxy-agent": "^7.0.0", 68 | "tslib": "^2.6.2" 69 | }, 70 | "engines": { 71 | "node": ">=18.0.0" 72 | } 73 | }, 74 | "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { 75 | "version": "2.1.1", 76 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.1.tgz", 77 | "integrity": "sha512-NhzeNm5zu2fPlwGXPUjzsRCRuPx5demaZyNcyNYJDqpa/Sbxzvo/RYt9IwUaAOnDW5+r7J9UOE6f22TQnb9nhQ==", 78 | "dependencies": { 79 | "tslib": "^2.6.2" 80 | }, 81 | "engines": { 82 | "node": ">=18.0.0" 83 | } 84 | }, 85 | "node_modules/@azure/core-tracing": { 86 | "version": "1.1.1", 87 | "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.1.tgz", 88 | "integrity": "sha512-qPbYhN1pE5XQ2jPKIHP33x8l3oBu1UqIWnYqZZ3OYnYjzY0xqIHjn49C+ptsPD9yC7uyWI9Zm7iZUZLs2R4DhQ==", 89 | "dependencies": { 90 | "tslib": "^2.6.2" 91 | }, 92 | "engines": { 93 | "node": ">=18.0.0" 94 | } 95 | }, 96 | "node_modules/@azure/core-util": { 97 | "version": "1.8.1", 98 | "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.8.1.tgz", 99 | "integrity": "sha512-L3voj0StUdJ+YKomvwnTv7gHzguJO+a6h30pmmZdRprJCM+RJlGMPxzuh4R7lhQu1jNmEtaHX5wvTgWLDAmbGQ==", 100 | "dependencies": { 101 | "@azure/abort-controller": "^2.0.0", 102 | "tslib": "^2.6.2" 103 | }, 104 | "engines": { 105 | "node": ">=18.0.0" 106 | } 107 | }, 108 | "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { 109 | "version": "2.1.1", 110 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.1.tgz", 111 | "integrity": "sha512-NhzeNm5zu2fPlwGXPUjzsRCRuPx5demaZyNcyNYJDqpa/Sbxzvo/RYt9IwUaAOnDW5+r7J9UOE6f22TQnb9nhQ==", 112 | "dependencies": { 113 | "tslib": "^2.6.2" 114 | }, 115 | "engines": { 116 | "node": ">=18.0.0" 117 | } 118 | }, 119 | "node_modules/@azure/cosmos": { 120 | "version": "4.0.0", 121 | "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.0.tgz", 122 | "integrity": "sha512-/Z27p1+FTkmjmm8jk90zi/HrczPHw2t8WecFnsnTe4xGocWl0Z4clP0YlLUTJPhRLWYa5upwD9rMvKJkS1f1kg==", 123 | "dependencies": { 124 | "@azure/abort-controller": "^1.0.0", 125 | "@azure/core-auth": "^1.3.0", 126 | "@azure/core-rest-pipeline": "^1.2.0", 127 | "@azure/core-tracing": "^1.0.0", 128 | "debug": "^4.1.1", 129 | "fast-json-stable-stringify": "^2.1.0", 130 | "jsbi": "^3.1.3", 131 | "node-abort-controller": "^3.0.0", 132 | "priorityqueuejs": "^1.0.0", 133 | "semaphore": "^1.0.5", 134 | "tslib": "^2.2.0", 135 | "universal-user-agent": "^6.0.0", 136 | "uuid": "^8.3.0" 137 | }, 138 | "engines": { 139 | "node": ">=14.0.0" 140 | } 141 | }, 142 | "node_modules/@azure/logger": { 143 | "version": "1.1.1", 144 | "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.1.tgz", 145 | "integrity": "sha512-/+4TtokaGgC+MnThdf6HyIH9Wrjp+CnCn3Nx3ggevN7FFjjNyjqg0yLlc2i9S+Z2uAzI8GYOo35Nzb1MhQ89MA==", 146 | "dependencies": { 147 | "tslib": "^2.6.2" 148 | }, 149 | "engines": { 150 | "node": ">=18.0.0" 151 | } 152 | }, 153 | "node_modules/@isaacs/cliui": { 154 | "version": "8.0.2", 155 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 156 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 157 | "dev": true, 158 | "dependencies": { 159 | "string-width": "^5.1.2", 160 | "string-width-cjs": "npm:string-width@^4.2.0", 161 | "strip-ansi": "^7.0.1", 162 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 163 | "wrap-ansi": "^8.1.0", 164 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 165 | }, 166 | "engines": { 167 | "node": ">=12" 168 | } 169 | }, 170 | "node_modules/@pkgjs/parseargs": { 171 | "version": "0.11.0", 172 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 173 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 174 | "dev": true, 175 | "optional": true, 176 | "engines": { 177 | "node": ">=14" 178 | } 179 | }, 180 | "node_modules/@types/node": { 181 | "version": "20.12.7", 182 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", 183 | "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", 184 | "dev": true, 185 | "dependencies": { 186 | "undici-types": "~5.26.4" 187 | } 188 | }, 189 | "node_modules/agent-base": { 190 | "version": "7.1.1", 191 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", 192 | "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", 193 | "dependencies": { 194 | "debug": "^4.3.4" 195 | }, 196 | "engines": { 197 | "node": ">= 14" 198 | } 199 | }, 200 | "node_modules/ansi-regex": { 201 | "version": "6.0.1", 202 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", 203 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", 204 | "dev": true, 205 | "engines": { 206 | "node": ">=12" 207 | }, 208 | "funding": { 209 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 210 | } 211 | }, 212 | "node_modules/ansi-styles": { 213 | "version": "6.2.1", 214 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 215 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 216 | "dev": true, 217 | "engines": { 218 | "node": ">=12" 219 | }, 220 | "funding": { 221 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 222 | } 223 | }, 224 | "node_modules/balanced-match": { 225 | "version": "1.0.2", 226 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 227 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 228 | "dev": true 229 | }, 230 | "node_modules/brace-expansion": { 231 | "version": "2.0.1", 232 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 233 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 234 | "dev": true, 235 | "dependencies": { 236 | "balanced-match": "^1.0.0" 237 | } 238 | }, 239 | "node_modules/color-convert": { 240 | "version": "2.0.1", 241 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 242 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 243 | "dev": true, 244 | "dependencies": { 245 | "color-name": "~1.1.4" 246 | }, 247 | "engines": { 248 | "node": ">=7.0.0" 249 | } 250 | }, 251 | "node_modules/color-name": { 252 | "version": "1.1.4", 253 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 254 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 255 | "dev": true 256 | }, 257 | "node_modules/cross-spawn": { 258 | "version": "7.0.3", 259 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 260 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 261 | "dev": true, 262 | "dependencies": { 263 | "path-key": "^3.1.0", 264 | "shebang-command": "^2.0.0", 265 | "which": "^2.0.1" 266 | }, 267 | "engines": { 268 | "node": ">= 8" 269 | } 270 | }, 271 | "node_modules/debug": { 272 | "version": "4.3.4", 273 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 274 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 275 | "dependencies": { 276 | "ms": "2.1.2" 277 | }, 278 | "engines": { 279 | "node": ">=6.0" 280 | }, 281 | "peerDependenciesMeta": { 282 | "supports-color": { 283 | "optional": true 284 | } 285 | } 286 | }, 287 | "node_modules/dotenv": { 288 | "version": "16.4.5", 289 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 290 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", 291 | "engines": { 292 | "node": ">=12" 293 | }, 294 | "funding": { 295 | "url": "https://dotenvx.com" 296 | } 297 | }, 298 | "node_modules/eastasianwidth": { 299 | "version": "0.2.0", 300 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 301 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 302 | "dev": true 303 | }, 304 | "node_modules/emoji-regex": { 305 | "version": "9.2.2", 306 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 307 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 308 | "dev": true 309 | }, 310 | "node_modules/fast-json-stable-stringify": { 311 | "version": "2.1.0", 312 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 313 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 314 | }, 315 | "node_modules/foreground-child": { 316 | "version": "3.1.1", 317 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", 318 | "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", 319 | "dev": true, 320 | "dependencies": { 321 | "cross-spawn": "^7.0.0", 322 | "signal-exit": "^4.0.1" 323 | }, 324 | "engines": { 325 | "node": ">=14" 326 | }, 327 | "funding": { 328 | "url": "https://github.com/sponsors/isaacs" 329 | } 330 | }, 331 | "node_modules/glob": { 332 | "version": "10.3.12", 333 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", 334 | "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", 335 | "dev": true, 336 | "dependencies": { 337 | "foreground-child": "^3.1.0", 338 | "jackspeak": "^2.3.6", 339 | "minimatch": "^9.0.1", 340 | "minipass": "^7.0.4", 341 | "path-scurry": "^1.10.2" 342 | }, 343 | "bin": { 344 | "glob": "dist/esm/bin.mjs" 345 | }, 346 | "engines": { 347 | "node": ">=16 || 14 >=14.17" 348 | }, 349 | "funding": { 350 | "url": "https://github.com/sponsors/isaacs" 351 | } 352 | }, 353 | "node_modules/http-proxy-agent": { 354 | "version": "7.0.2", 355 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", 356 | "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", 357 | "dependencies": { 358 | "agent-base": "^7.1.0", 359 | "debug": "^4.3.4" 360 | }, 361 | "engines": { 362 | "node": ">= 14" 363 | } 364 | }, 365 | "node_modules/https-proxy-agent": { 366 | "version": "7.0.4", 367 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", 368 | "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", 369 | "dependencies": { 370 | "agent-base": "^7.0.2", 371 | "debug": "4" 372 | }, 373 | "engines": { 374 | "node": ">= 14" 375 | } 376 | }, 377 | "node_modules/is-fullwidth-code-point": { 378 | "version": "3.0.0", 379 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 380 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 381 | "dev": true, 382 | "engines": { 383 | "node": ">=8" 384 | } 385 | }, 386 | "node_modules/isexe": { 387 | "version": "2.0.0", 388 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 389 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 390 | "dev": true 391 | }, 392 | "node_modules/jackspeak": { 393 | "version": "2.3.6", 394 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", 395 | "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", 396 | "dev": true, 397 | "dependencies": { 398 | "@isaacs/cliui": "^8.0.2" 399 | }, 400 | "engines": { 401 | "node": ">=14" 402 | }, 403 | "funding": { 404 | "url": "https://github.com/sponsors/isaacs" 405 | }, 406 | "optionalDependencies": { 407 | "@pkgjs/parseargs": "^0.11.0" 408 | } 409 | }, 410 | "node_modules/jsbi": { 411 | "version": "3.2.5", 412 | "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.2.5.tgz", 413 | "integrity": "sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==" 414 | }, 415 | "node_modules/lru-cache": { 416 | "version": "10.2.0", 417 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", 418 | "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", 419 | "dev": true, 420 | "engines": { 421 | "node": "14 || >=16.14" 422 | } 423 | }, 424 | "node_modules/minimatch": { 425 | "version": "9.0.4", 426 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", 427 | "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", 428 | "dev": true, 429 | "dependencies": { 430 | "brace-expansion": "^2.0.1" 431 | }, 432 | "engines": { 433 | "node": ">=16 || 14 >=14.17" 434 | }, 435 | "funding": { 436 | "url": "https://github.com/sponsors/isaacs" 437 | } 438 | }, 439 | "node_modules/minipass": { 440 | "version": "7.0.4", 441 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", 442 | "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", 443 | "dev": true, 444 | "engines": { 445 | "node": ">=16 || 14 >=14.17" 446 | } 447 | }, 448 | "node_modules/ms": { 449 | "version": "2.1.2", 450 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 451 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 452 | }, 453 | "node_modules/node-abort-controller": { 454 | "version": "3.1.1", 455 | "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", 456 | "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" 457 | }, 458 | "node_modules/path-key": { 459 | "version": "3.1.1", 460 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 461 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 462 | "dev": true, 463 | "engines": { 464 | "node": ">=8" 465 | } 466 | }, 467 | "node_modules/path-scurry": { 468 | "version": "1.10.2", 469 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", 470 | "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", 471 | "dev": true, 472 | "dependencies": { 473 | "lru-cache": "^10.2.0", 474 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 475 | }, 476 | "engines": { 477 | "node": ">=16 || 14 >=14.17" 478 | }, 479 | "funding": { 480 | "url": "https://github.com/sponsors/isaacs" 481 | } 482 | }, 483 | "node_modules/priorityqueuejs": { 484 | "version": "1.0.0", 485 | "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", 486 | "integrity": "sha512-lg++21mreCEOuGWTbO5DnJKAdxfjrdN0S9ysoW9SzdSJvbkWpkaDdpG/cdsPCsEnoLUwmd9m3WcZhngW7yKA2g==" 487 | }, 488 | "node_modules/rimraf": { 489 | "version": "5.0.5", 490 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", 491 | "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", 492 | "dev": true, 493 | "dependencies": { 494 | "glob": "^10.3.7" 495 | }, 496 | "bin": { 497 | "rimraf": "dist/esm/bin.mjs" 498 | }, 499 | "engines": { 500 | "node": ">=14" 501 | }, 502 | "funding": { 503 | "url": "https://github.com/sponsors/isaacs" 504 | } 505 | }, 506 | "node_modules/semaphore": { 507 | "version": "1.1.0", 508 | "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", 509 | "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==", 510 | "engines": { 511 | "node": ">=0.8.0" 512 | } 513 | }, 514 | "node_modules/shebang-command": { 515 | "version": "2.0.0", 516 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 517 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 518 | "dev": true, 519 | "dependencies": { 520 | "shebang-regex": "^3.0.0" 521 | }, 522 | "engines": { 523 | "node": ">=8" 524 | } 525 | }, 526 | "node_modules/shebang-regex": { 527 | "version": "3.0.0", 528 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 529 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 530 | "dev": true, 531 | "engines": { 532 | "node": ">=8" 533 | } 534 | }, 535 | "node_modules/signal-exit": { 536 | "version": "4.1.0", 537 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 538 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 539 | "dev": true, 540 | "engines": { 541 | "node": ">=14" 542 | }, 543 | "funding": { 544 | "url": "https://github.com/sponsors/isaacs" 545 | } 546 | }, 547 | "node_modules/string-width": { 548 | "version": "5.1.2", 549 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 550 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 551 | "dev": true, 552 | "dependencies": { 553 | "eastasianwidth": "^0.2.0", 554 | "emoji-regex": "^9.2.2", 555 | "strip-ansi": "^7.0.1" 556 | }, 557 | "engines": { 558 | "node": ">=12" 559 | }, 560 | "funding": { 561 | "url": "https://github.com/sponsors/sindresorhus" 562 | } 563 | }, 564 | "node_modules/string-width-cjs": { 565 | "name": "string-width", 566 | "version": "4.2.3", 567 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 568 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 569 | "dev": true, 570 | "dependencies": { 571 | "emoji-regex": "^8.0.0", 572 | "is-fullwidth-code-point": "^3.0.0", 573 | "strip-ansi": "^6.0.1" 574 | }, 575 | "engines": { 576 | "node": ">=8" 577 | } 578 | }, 579 | "node_modules/string-width-cjs/node_modules/ansi-regex": { 580 | "version": "5.0.1", 581 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 582 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 583 | "dev": true, 584 | "engines": { 585 | "node": ">=8" 586 | } 587 | }, 588 | "node_modules/string-width-cjs/node_modules/emoji-regex": { 589 | "version": "8.0.0", 590 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 591 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 592 | "dev": true 593 | }, 594 | "node_modules/string-width-cjs/node_modules/strip-ansi": { 595 | "version": "6.0.1", 596 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 597 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 598 | "dev": true, 599 | "dependencies": { 600 | "ansi-regex": "^5.0.1" 601 | }, 602 | "engines": { 603 | "node": ">=8" 604 | } 605 | }, 606 | "node_modules/strip-ansi": { 607 | "version": "7.1.0", 608 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 609 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 610 | "dev": true, 611 | "dependencies": { 612 | "ansi-regex": "^6.0.1" 613 | }, 614 | "engines": { 615 | "node": ">=12" 616 | }, 617 | "funding": { 618 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 619 | } 620 | }, 621 | "node_modules/strip-ansi-cjs": { 622 | "name": "strip-ansi", 623 | "version": "6.0.1", 624 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 625 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 626 | "dev": true, 627 | "dependencies": { 628 | "ansi-regex": "^5.0.1" 629 | }, 630 | "engines": { 631 | "node": ">=8" 632 | } 633 | }, 634 | "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 635 | "version": "5.0.1", 636 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 637 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 638 | "dev": true, 639 | "engines": { 640 | "node": ">=8" 641 | } 642 | }, 643 | "node_modules/tslib": { 644 | "version": "2.6.2", 645 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 646 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 647 | }, 648 | "node_modules/typescript": { 649 | "version": "5.4.5", 650 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", 651 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", 652 | "dev": true, 653 | "bin": { 654 | "tsc": "bin/tsc", 655 | "tsserver": "bin/tsserver" 656 | }, 657 | "engines": { 658 | "node": ">=14.17" 659 | } 660 | }, 661 | "node_modules/undici-types": { 662 | "version": "5.26.5", 663 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 664 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 665 | "dev": true 666 | }, 667 | "node_modules/universal-user-agent": { 668 | "version": "6.0.1", 669 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", 670 | "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" 671 | }, 672 | "node_modules/uuid": { 673 | "version": "8.3.2", 674 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 675 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 676 | "bin": { 677 | "uuid": "dist/bin/uuid" 678 | } 679 | }, 680 | "node_modules/which": { 681 | "version": "2.0.2", 682 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 683 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 684 | "dev": true, 685 | "dependencies": { 686 | "isexe": "^2.0.0" 687 | }, 688 | "bin": { 689 | "node-which": "bin/node-which" 690 | }, 691 | "engines": { 692 | "node": ">= 8" 693 | } 694 | }, 695 | "node_modules/wrap-ansi": { 696 | "version": "8.1.0", 697 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 698 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 699 | "dev": true, 700 | "dependencies": { 701 | "ansi-styles": "^6.1.0", 702 | "string-width": "^5.0.1", 703 | "strip-ansi": "^7.0.1" 704 | }, 705 | "engines": { 706 | "node": ">=12" 707 | }, 708 | "funding": { 709 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 710 | } 711 | }, 712 | "node_modules/wrap-ansi-cjs": { 713 | "name": "wrap-ansi", 714 | "version": "7.0.0", 715 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 716 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 717 | "dev": true, 718 | "dependencies": { 719 | "ansi-styles": "^4.0.0", 720 | "string-width": "^4.1.0", 721 | "strip-ansi": "^6.0.0" 722 | }, 723 | "engines": { 724 | "node": ">=10" 725 | }, 726 | "funding": { 727 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 728 | } 729 | }, 730 | "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 731 | "version": "5.0.1", 732 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 733 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 734 | "dev": true, 735 | "engines": { 736 | "node": ">=8" 737 | } 738 | }, 739 | "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { 740 | "version": "4.3.0", 741 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 742 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 743 | "dev": true, 744 | "dependencies": { 745 | "color-convert": "^2.0.1" 746 | }, 747 | "engines": { 748 | "node": ">=8" 749 | }, 750 | "funding": { 751 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 752 | } 753 | }, 754 | "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 755 | "version": "8.0.0", 756 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 757 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 758 | "dev": true 759 | }, 760 | "node_modules/wrap-ansi-cjs/node_modules/string-width": { 761 | "version": "4.2.3", 762 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 763 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 764 | "dev": true, 765 | "dependencies": { 766 | "emoji-regex": "^8.0.0", 767 | "is-fullwidth-code-point": "^3.0.0", 768 | "strip-ansi": "^6.0.1" 769 | }, 770 | "engines": { 771 | "node": ">=8" 772 | } 773 | }, 774 | "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 775 | "version": "6.0.1", 776 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 777 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 778 | "dev": true, 779 | "dependencies": { 780 | "ansi-regex": "^5.0.1" 781 | }, 782 | "engines": { 783 | "node": ">=8" 784 | } 785 | } 786 | }, 787 | "dependencies": { 788 | "@azure/abort-controller": { 789 | "version": "1.1.0", 790 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", 791 | "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", 792 | "requires": { 793 | "tslib": "^2.2.0" 794 | } 795 | }, 796 | "@azure/core-auth": { 797 | "version": "1.7.1", 798 | "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.1.tgz", 799 | "integrity": "sha512-dyeQwvgthqs/SlPVQbZQetpslXceHd4i5a7M/7z/lGEAVwnSluabnQOjF2/dk/hhWgMISusv1Ytp4mQ8JNy62A==", 800 | "requires": { 801 | "@azure/abort-controller": "^2.0.0", 802 | "@azure/core-util": "^1.1.0", 803 | "tslib": "^2.6.2" 804 | }, 805 | "dependencies": { 806 | "@azure/abort-controller": { 807 | "version": "2.1.1", 808 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.1.tgz", 809 | "integrity": "sha512-NhzeNm5zu2fPlwGXPUjzsRCRuPx5demaZyNcyNYJDqpa/Sbxzvo/RYt9IwUaAOnDW5+r7J9UOE6f22TQnb9nhQ==", 810 | "requires": { 811 | "tslib": "^2.6.2" 812 | } 813 | } 814 | } 815 | }, 816 | "@azure/core-rest-pipeline": { 817 | "version": "1.15.1", 818 | "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.15.1.tgz", 819 | "integrity": "sha512-ZxS6i3eHxh86u+1eWZJiYywoN2vxvsSoAUx60Mny8cZ4nTwvt7UzVVBJO+m2PW2KIJfNiXMt59xBa59htOWL4g==", 820 | "requires": { 821 | "@azure/abort-controller": "^2.0.0", 822 | "@azure/core-auth": "^1.4.0", 823 | "@azure/core-tracing": "^1.0.1", 824 | "@azure/core-util": "^1.3.0", 825 | "@azure/logger": "^1.0.0", 826 | "http-proxy-agent": "^7.0.0", 827 | "https-proxy-agent": "^7.0.0", 828 | "tslib": "^2.6.2" 829 | }, 830 | "dependencies": { 831 | "@azure/abort-controller": { 832 | "version": "2.1.1", 833 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.1.tgz", 834 | "integrity": "sha512-NhzeNm5zu2fPlwGXPUjzsRCRuPx5demaZyNcyNYJDqpa/Sbxzvo/RYt9IwUaAOnDW5+r7J9UOE6f22TQnb9nhQ==", 835 | "requires": { 836 | "tslib": "^2.6.2" 837 | } 838 | } 839 | } 840 | }, 841 | "@azure/core-tracing": { 842 | "version": "1.1.1", 843 | "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.1.tgz", 844 | "integrity": "sha512-qPbYhN1pE5XQ2jPKIHP33x8l3oBu1UqIWnYqZZ3OYnYjzY0xqIHjn49C+ptsPD9yC7uyWI9Zm7iZUZLs2R4DhQ==", 845 | "requires": { 846 | "tslib": "^2.6.2" 847 | } 848 | }, 849 | "@azure/core-util": { 850 | "version": "1.8.1", 851 | "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.8.1.tgz", 852 | "integrity": "sha512-L3voj0StUdJ+YKomvwnTv7gHzguJO+a6h30pmmZdRprJCM+RJlGMPxzuh4R7lhQu1jNmEtaHX5wvTgWLDAmbGQ==", 853 | "requires": { 854 | "@azure/abort-controller": "^2.0.0", 855 | "tslib": "^2.6.2" 856 | }, 857 | "dependencies": { 858 | "@azure/abort-controller": { 859 | "version": "2.1.1", 860 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.1.tgz", 861 | "integrity": "sha512-NhzeNm5zu2fPlwGXPUjzsRCRuPx5demaZyNcyNYJDqpa/Sbxzvo/RYt9IwUaAOnDW5+r7J9UOE6f22TQnb9nhQ==", 862 | "requires": { 863 | "tslib": "^2.6.2" 864 | } 865 | } 866 | } 867 | }, 868 | "@azure/cosmos": { 869 | "version": "4.0.0", 870 | "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-4.0.0.tgz", 871 | "integrity": "sha512-/Z27p1+FTkmjmm8jk90zi/HrczPHw2t8WecFnsnTe4xGocWl0Z4clP0YlLUTJPhRLWYa5upwD9rMvKJkS1f1kg==", 872 | "requires": { 873 | "@azure/abort-controller": "^1.0.0", 874 | "@azure/core-auth": "^1.3.0", 875 | "@azure/core-rest-pipeline": "^1.2.0", 876 | "@azure/core-tracing": "^1.0.0", 877 | "debug": "^4.1.1", 878 | "fast-json-stable-stringify": "^2.1.0", 879 | "jsbi": "^3.1.3", 880 | "node-abort-controller": "^3.0.0", 881 | "priorityqueuejs": "^1.0.0", 882 | "semaphore": "^1.0.5", 883 | "tslib": "^2.2.0", 884 | "universal-user-agent": "^6.0.0", 885 | "uuid": "^8.3.0" 886 | } 887 | }, 888 | "@azure/logger": { 889 | "version": "1.1.1", 890 | "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.1.tgz", 891 | "integrity": "sha512-/+4TtokaGgC+MnThdf6HyIH9Wrjp+CnCn3Nx3ggevN7FFjjNyjqg0yLlc2i9S+Z2uAzI8GYOo35Nzb1MhQ89MA==", 892 | "requires": { 893 | "tslib": "^2.6.2" 894 | } 895 | }, 896 | "@isaacs/cliui": { 897 | "version": "8.0.2", 898 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 899 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 900 | "dev": true, 901 | "requires": { 902 | "string-width": "^5.1.2", 903 | "string-width-cjs": "npm:string-width@^4.2.0", 904 | "strip-ansi": "^7.0.1", 905 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 906 | "wrap-ansi": "^8.1.0", 907 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 908 | } 909 | }, 910 | "@pkgjs/parseargs": { 911 | "version": "0.11.0", 912 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 913 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 914 | "dev": true, 915 | "optional": true 916 | }, 917 | "@types/node": { 918 | "version": "20.12.7", 919 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", 920 | "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", 921 | "dev": true, 922 | "requires": { 923 | "undici-types": "~5.26.4" 924 | } 925 | }, 926 | "agent-base": { 927 | "version": "7.1.1", 928 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", 929 | "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", 930 | "requires": { 931 | "debug": "^4.3.4" 932 | } 933 | }, 934 | "ansi-regex": { 935 | "version": "6.0.1", 936 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", 937 | "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", 938 | "dev": true 939 | }, 940 | "ansi-styles": { 941 | "version": "6.2.1", 942 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 943 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 944 | "dev": true 945 | }, 946 | "balanced-match": { 947 | "version": "1.0.2", 948 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 949 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 950 | "dev": true 951 | }, 952 | "brace-expansion": { 953 | "version": "2.0.1", 954 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 955 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 956 | "dev": true, 957 | "requires": { 958 | "balanced-match": "^1.0.0" 959 | } 960 | }, 961 | "color-convert": { 962 | "version": "2.0.1", 963 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 964 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 965 | "dev": true, 966 | "requires": { 967 | "color-name": "~1.1.4" 968 | } 969 | }, 970 | "color-name": { 971 | "version": "1.1.4", 972 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 973 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 974 | "dev": true 975 | }, 976 | "cross-spawn": { 977 | "version": "7.0.3", 978 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 979 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 980 | "dev": true, 981 | "requires": { 982 | "path-key": "^3.1.0", 983 | "shebang-command": "^2.0.0", 984 | "which": "^2.0.1" 985 | } 986 | }, 987 | "debug": { 988 | "version": "4.3.4", 989 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 990 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 991 | "requires": { 992 | "ms": "2.1.2" 993 | } 994 | }, 995 | "dotenv": { 996 | "version": "16.4.5", 997 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", 998 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" 999 | }, 1000 | "eastasianwidth": { 1001 | "version": "0.2.0", 1002 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 1003 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 1004 | "dev": true 1005 | }, 1006 | "emoji-regex": { 1007 | "version": "9.2.2", 1008 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 1009 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 1010 | "dev": true 1011 | }, 1012 | "fast-json-stable-stringify": { 1013 | "version": "2.1.0", 1014 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 1015 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 1016 | }, 1017 | "foreground-child": { 1018 | "version": "3.1.1", 1019 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", 1020 | "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", 1021 | "dev": true, 1022 | "requires": { 1023 | "cross-spawn": "^7.0.0", 1024 | "signal-exit": "^4.0.1" 1025 | } 1026 | }, 1027 | "glob": { 1028 | "version": "10.3.12", 1029 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", 1030 | "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", 1031 | "dev": true, 1032 | "requires": { 1033 | "foreground-child": "^3.1.0", 1034 | "jackspeak": "^2.3.6", 1035 | "minimatch": "^9.0.1", 1036 | "minipass": "^7.0.4", 1037 | "path-scurry": "^1.10.2" 1038 | } 1039 | }, 1040 | "http-proxy-agent": { 1041 | "version": "7.0.2", 1042 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", 1043 | "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", 1044 | "requires": { 1045 | "agent-base": "^7.1.0", 1046 | "debug": "^4.3.4" 1047 | } 1048 | }, 1049 | "https-proxy-agent": { 1050 | "version": "7.0.4", 1051 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", 1052 | "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", 1053 | "requires": { 1054 | "agent-base": "^7.0.2", 1055 | "debug": "4" 1056 | } 1057 | }, 1058 | "is-fullwidth-code-point": { 1059 | "version": "3.0.0", 1060 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1061 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1062 | "dev": true 1063 | }, 1064 | "isexe": { 1065 | "version": "2.0.0", 1066 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1067 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1068 | "dev": true 1069 | }, 1070 | "jackspeak": { 1071 | "version": "2.3.6", 1072 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", 1073 | "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", 1074 | "dev": true, 1075 | "requires": { 1076 | "@isaacs/cliui": "^8.0.2", 1077 | "@pkgjs/parseargs": "^0.11.0" 1078 | } 1079 | }, 1080 | "jsbi": { 1081 | "version": "3.2.5", 1082 | "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.2.5.tgz", 1083 | "integrity": "sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ==" 1084 | }, 1085 | "lru-cache": { 1086 | "version": "10.2.0", 1087 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", 1088 | "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", 1089 | "dev": true 1090 | }, 1091 | "minimatch": { 1092 | "version": "9.0.4", 1093 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", 1094 | "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", 1095 | "dev": true, 1096 | "requires": { 1097 | "brace-expansion": "^2.0.1" 1098 | } 1099 | }, 1100 | "minipass": { 1101 | "version": "7.0.4", 1102 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", 1103 | "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", 1104 | "dev": true 1105 | }, 1106 | "ms": { 1107 | "version": "2.1.2", 1108 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1109 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 1110 | }, 1111 | "node-abort-controller": { 1112 | "version": "3.1.1", 1113 | "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", 1114 | "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" 1115 | }, 1116 | "path-key": { 1117 | "version": "3.1.1", 1118 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1119 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1120 | "dev": true 1121 | }, 1122 | "path-scurry": { 1123 | "version": "1.10.2", 1124 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", 1125 | "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", 1126 | "dev": true, 1127 | "requires": { 1128 | "lru-cache": "^10.2.0", 1129 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 1130 | } 1131 | }, 1132 | "priorityqueuejs": { 1133 | "version": "1.0.0", 1134 | "resolved": "https://registry.npmjs.org/priorityqueuejs/-/priorityqueuejs-1.0.0.tgz", 1135 | "integrity": "sha512-lg++21mreCEOuGWTbO5DnJKAdxfjrdN0S9ysoW9SzdSJvbkWpkaDdpG/cdsPCsEnoLUwmd9m3WcZhngW7yKA2g==" 1136 | }, 1137 | "rimraf": { 1138 | "version": "5.0.5", 1139 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", 1140 | "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", 1141 | "dev": true, 1142 | "requires": { 1143 | "glob": "^10.3.7" 1144 | } 1145 | }, 1146 | "semaphore": { 1147 | "version": "1.1.0", 1148 | "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", 1149 | "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==" 1150 | }, 1151 | "shebang-command": { 1152 | "version": "2.0.0", 1153 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1154 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1155 | "dev": true, 1156 | "requires": { 1157 | "shebang-regex": "^3.0.0" 1158 | } 1159 | }, 1160 | "shebang-regex": { 1161 | "version": "3.0.0", 1162 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1163 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1164 | "dev": true 1165 | }, 1166 | "signal-exit": { 1167 | "version": "4.1.0", 1168 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 1169 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 1170 | "dev": true 1171 | }, 1172 | "string-width": { 1173 | "version": "5.1.2", 1174 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 1175 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 1176 | "dev": true, 1177 | "requires": { 1178 | "eastasianwidth": "^0.2.0", 1179 | "emoji-regex": "^9.2.2", 1180 | "strip-ansi": "^7.0.1" 1181 | } 1182 | }, 1183 | "string-width-cjs": { 1184 | "version": "npm:string-width@4.2.3", 1185 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1186 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1187 | "dev": true, 1188 | "requires": { 1189 | "emoji-regex": "^8.0.0", 1190 | "is-fullwidth-code-point": "^3.0.0", 1191 | "strip-ansi": "^6.0.1" 1192 | }, 1193 | "dependencies": { 1194 | "ansi-regex": { 1195 | "version": "5.0.1", 1196 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1197 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1198 | "dev": true 1199 | }, 1200 | "emoji-regex": { 1201 | "version": "8.0.0", 1202 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1203 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1204 | "dev": true 1205 | }, 1206 | "strip-ansi": { 1207 | "version": "6.0.1", 1208 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1209 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1210 | "dev": true, 1211 | "requires": { 1212 | "ansi-regex": "^5.0.1" 1213 | } 1214 | } 1215 | } 1216 | }, 1217 | "strip-ansi": { 1218 | "version": "7.1.0", 1219 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 1220 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 1221 | "dev": true, 1222 | "requires": { 1223 | "ansi-regex": "^6.0.1" 1224 | } 1225 | }, 1226 | "strip-ansi-cjs": { 1227 | "version": "npm:strip-ansi@6.0.1", 1228 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1229 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1230 | "dev": true, 1231 | "requires": { 1232 | "ansi-regex": "^5.0.1" 1233 | }, 1234 | "dependencies": { 1235 | "ansi-regex": { 1236 | "version": "5.0.1", 1237 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1238 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1239 | "dev": true 1240 | } 1241 | } 1242 | }, 1243 | "tslib": { 1244 | "version": "2.6.2", 1245 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 1246 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 1247 | }, 1248 | "typescript": { 1249 | "version": "5.4.5", 1250 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", 1251 | "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", 1252 | "dev": true 1253 | }, 1254 | "undici-types": { 1255 | "version": "5.26.5", 1256 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 1257 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 1258 | "dev": true 1259 | }, 1260 | "universal-user-agent": { 1261 | "version": "6.0.1", 1262 | "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", 1263 | "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" 1264 | }, 1265 | "uuid": { 1266 | "version": "8.3.2", 1267 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 1268 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" 1269 | }, 1270 | "which": { 1271 | "version": "2.0.2", 1272 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1273 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1274 | "dev": true, 1275 | "requires": { 1276 | "isexe": "^2.0.0" 1277 | } 1278 | }, 1279 | "wrap-ansi": { 1280 | "version": "8.1.0", 1281 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 1282 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 1283 | "dev": true, 1284 | "requires": { 1285 | "ansi-styles": "^6.1.0", 1286 | "string-width": "^5.0.1", 1287 | "strip-ansi": "^7.0.1" 1288 | } 1289 | }, 1290 | "wrap-ansi-cjs": { 1291 | "version": "npm:wrap-ansi@7.0.0", 1292 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1293 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1294 | "dev": true, 1295 | "requires": { 1296 | "ansi-styles": "^4.0.0", 1297 | "string-width": "^4.1.0", 1298 | "strip-ansi": "^6.0.0" 1299 | }, 1300 | "dependencies": { 1301 | "ansi-regex": { 1302 | "version": "5.0.1", 1303 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1304 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1305 | "dev": true 1306 | }, 1307 | "ansi-styles": { 1308 | "version": "4.3.0", 1309 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 1310 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 1311 | "dev": true, 1312 | "requires": { 1313 | "color-convert": "^2.0.1" 1314 | } 1315 | }, 1316 | "emoji-regex": { 1317 | "version": "8.0.0", 1318 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1319 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1320 | "dev": true 1321 | }, 1322 | "string-width": { 1323 | "version": "4.2.3", 1324 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1325 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1326 | "dev": true, 1327 | "requires": { 1328 | "emoji-regex": "^8.0.0", 1329 | "is-fullwidth-code-point": "^3.0.0", 1330 | "strip-ansi": "^6.0.1" 1331 | } 1332 | }, 1333 | "strip-ansi": { 1334 | "version": "6.0.1", 1335 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1336 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1337 | "dev": true, 1338 | "requires": { 1339 | "ansi-regex": "^5.0.1" 1340 | } 1341 | } 1342 | } 1343 | } 1344 | } 1345 | } 1346 | --------------------------------------------------------------------------------