├── etc ├── screen.png └── architecture-diagram.drawio.png ├── .vscode ├── settings.json └── extensions.json ├── api ├── .vscode │ └── extensions.json ├── .funcignore ├── host.json ├── local.settings.sample.json ├── readme.md ├── eslint.config.js ├── .gitignore ├── src │ ├── functions │ │ ├── getChats.js │ │ ├── getUsers.js │ │ ├── getToken.js │ │ └── eventHandler.js │ ├── entra-jwt-validate.js │ └── state.js ├── package.json └── package-lock.json ├── .gitignore ├── .devcontainer ├── post-create.sh └── devcontainer.json ├── .prettierrc ├── client ├── staticwebapp.config.json ├── readme.md ├── js │ ├── utils.js │ ├── components │ │ └── chat.js │ └── app.js ├── css │ └── main.css ├── login.html └── index.html ├── deploy ├── modules │ ├── static-webapp.bicep │ ├── link-backend.bicep │ ├── storage.bicep │ ├── pubsub.bicep │ └── function-app.bicep ├── deploy-live-demo.sh ├── deploy.sh ├── main.bicep └── readme.md ├── .github └── workflows │ └── ci-lint.yaml ├── scripts ├── cleanup.sh └── cleanup.js ├── LICENSE ├── makefile └── readme.md /etc/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benc-uk/chatr/HEAD/etc/screen.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "azureFunctions.showProjectWarning": false 3 | } 4 | -------------------------------------------------------------------------------- /api/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["ms-azuretools.vscode-azurefunctions"] 3 | } 4 | -------------------------------------------------------------------------------- /etc/architecture-diagram.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benc-uk/chatr/HEAD/etc/architecture-diagram.drawio.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | local.settings.json 3 | local.params.json 4 | node_modules 5 | .oryx_prod_node_modules 6 | notes.md 7 | bin/ -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["ms-azuretools.vscode-bicep", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.devcontainer/post-create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install -g @azure/static-web-apps-cli 4 | npm install -g @azure/web-pubsub-tunnel-tool 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "semi": false, 5 | "arrowParens": "always", 6 | "singleQuote": true, 7 | "printWidth": 150, 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /api/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | getting_started.md 8 | node_modules/@types/ 9 | node_modules/azure-functions-core-tools/ 10 | node_modules/typescript/ -------------------------------------------------------------------------------- /client/staticwebapp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "route": "/", 5 | "allowedRoles": ["authenticated"] 6 | } 7 | ], 8 | "responseOverrides": { 9 | "401": { 10 | "redirect": "/login.html", 11 | "statusCode": 302 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/readme.md: -------------------------------------------------------------------------------- 1 | # 😎 Chatr Frontend Client App 2 | 3 | This the Chatr frontend, and is a static standalone pure ES6 JS application (no webpack/bundling), it is written using [Vue.js as a supporting framework](https://vuejs.org/), and [Bulma as a CSS framework](https://bulma.io/). 4 | 5 | [See main readme for details](../readme.md#client--frontend) 6 | -------------------------------------------------------------------------------- /api/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 | } 16 | -------------------------------------------------------------------------------- /api/local.settings.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "node", 5 | "AzureWebJobsStorage": "", 6 | "PUBSUB_ENDPOINT": "https://__CHANGE_ME__", 7 | "PUBSUB_HUB": "chat", 8 | "STORAGE_ACCOUNT_NAME": "__CHANGE_ME__", 9 | "VALIDATION_TENANT_ID": "__OPTIONAL_FOR_TOKEN_VALIDATION_ONLY__" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api/readme.md: -------------------------------------------------------------------------------- 1 | # 🌐 Chatr API and backend service 2 | 3 | This is a Azure Functions app which supports all of Chatr's server side operations, but it is a companion to the client front end, due to the use of managed identity should be hosted in a standalone Azure Function App and [linked to Static Web Apps with API support](https://docs.microsoft.com/en-us/azure/static-web-apps/apis) 4 | 5 | [See main readme for details](../readme.md#server) 6 | -------------------------------------------------------------------------------- /deploy/modules/static-webapp.bicep: -------------------------------------------------------------------------------- 1 | param location string = 'westeurope' 2 | param name string = 'chatr' 3 | param sku string = 'Standard' 4 | 5 | resource staticApp 'Microsoft.Web/staticSites@2024-04-01' = { 6 | name: name 7 | location: location 8 | 9 | sku: { 10 | name: sku 11 | } 12 | 13 | // No longer link to GitHub, we will deploy the app using the SWA CLI 14 | properties: {} 15 | } 16 | 17 | output appHostname string = staticApp.properties.defaultHostname 18 | -------------------------------------------------------------------------------- /api/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import { defineConfig } from 'eslint/config' 4 | 5 | export default defineConfig([ 6 | { 7 | files: ['api/**/*.{js,mjs,cjs}'], 8 | plugins: { js }, 9 | extends: ['js/recommended'], 10 | rules: { 11 | 'prefer-const': 'error', 12 | 'no-unused-vars': [ 13 | 'warn', 14 | { 15 | caughtErrors: 'none', 16 | }, 17 | ], 18 | }, 19 | }, 20 | ]) 21 | -------------------------------------------------------------------------------- /deploy/modules/link-backend.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'resourceGroup' 2 | 3 | param staticWebAppName string 4 | param functionsResourceId string 5 | 6 | resource staticWebApp 'Microsoft.Web/staticSites@2023-12-01' existing = { 7 | name: staticWebAppName 8 | } 9 | 10 | resource linkedStaticWebAppBackend 'Microsoft.Web/staticSites/linkedBackends@2022-09-01' = { 11 | name: 'linkedBackend' 12 | parent: staticWebApp 13 | properties: { 14 | backendResourceId: functionsResourceId 15 | region: staticWebApp.location 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | csx 4 | .vs 5 | edge 6 | Publish 7 | 8 | *.user 9 | *.suo 10 | *.cscfg 11 | *.Cache 12 | project.lock.json 13 | 14 | /packages 15 | /TestResults 16 | 17 | /tools/NuGet.exe 18 | /App_Data 19 | /secrets 20 | /data 21 | .secrets 22 | appsettings.json 23 | local.settings.json 24 | 25 | node_modules 26 | dist 27 | 28 | # Local python packages 29 | .python_packages/ 30 | 31 | # Python Environments 32 | .env 33 | .venv 34 | env/ 35 | venv/ 36 | ENV/ 37 | env.bak/ 38 | venv.bak/ 39 | 40 | # Byte-compiled / optimized / DLL files 41 | __pycache__/ 42 | *.py[cod] 43 | *$py.class -------------------------------------------------------------------------------- /.github/workflows/ci-lint.yaml: -------------------------------------------------------------------------------- 1 | name: CI Lint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | paths: 8 | - 'api/**' 9 | - 'client/**' 10 | push: 11 | branches: 12 | - main 13 | paths: 14 | - 'api/**' 15 | - 'client/**' 16 | 17 | jobs: 18 | lint: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: '22' 29 | 30 | - name: Run lint 31 | run: make lint 32 | -------------------------------------------------------------------------------- /api/src/functions/getChats.js: -------------------------------------------------------------------------------- 1 | // 2 | // Chatr - API 3 | // REST API to return all chats, the route for this function is just `/api/chats` 4 | // Ben Coleman, 2021 - 2025 5 | // 6 | 7 | import { app } from '@azure/functions' 8 | import { listChats } from '../state.js' 9 | 10 | app.http('chats', { 11 | methods: ['GET'], 12 | authLevel: 'anonymous', 13 | 14 | handler: async () => { 15 | try { 16 | const chats = await listChats() 17 | 18 | return { jsonBody: { chats } } 19 | } catch (error) { 20 | console.error('Error fetching chats:', error.statusCode ?? 0, error.message ?? 'Unknown error') 21 | return { status: error.statusCode ?? 500, body: error } 22 | } 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /api/src/functions/getUsers.js: -------------------------------------------------------------------------------- 1 | // 2 | // Chatr - API 3 | // REST API to return all users, the route for this function is just `/api/users` 4 | // Ben Coleman, 2021 - 2025 5 | // 6 | 7 | import { app } from '@azure/functions' 8 | import { listUsers } from '../state.js' 9 | 10 | app.http('users', { 11 | methods: ['GET'], 12 | authLevel: 'anonymous', 13 | 14 | handler: async () => { 15 | try { 16 | const users = await listUsers() 17 | 18 | return { jsonBody: { users } } 19 | } catch (error) { 20 | console.error('Error fetching users:', error.statusCode ?? 0, error.message ?? 'Unknown error') 21 | return { status: error.statusCode ?? 500, body: error } 22 | } 23 | }, 24 | }) 25 | -------------------------------------------------------------------------------- /scripts/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | which node > /dev/null || { echo "💥 Error! Node not found, please install it https://nodejs.org/en/download/ :)"; exit 1; } 6 | 7 | for varName in STORAGE_ACCOUNT_NAME STORAGE_ACCOUNT_KEY; do 8 | varVal=$(eval echo "\${$varName}") 9 | [ -z "$varVal" ] && { echo "💥 Error! Required variable '$varName' is unset!"; varUnset=true; } 10 | done 11 | [ $varUnset ] && exit 1 12 | 13 | # Change this as required or pass in as an argument 14 | MAX_AGE=${1:-24} 15 | 16 | echo -e "\e[31m🧹 Warning! You are about to run the cleanup script\nThis will remove old chats and users older than $MAX_AGE hours!\e[0m"; 17 | read -n 1 -s -r -p "Press any key to continue, or ctrl+c to exit..."; 18 | echo 19 | 20 | node "$SCRIPT_DIR/cleanup.js" "$MAX_AGE" -------------------------------------------------------------------------------- /deploy/modules/storage.bicep: -------------------------------------------------------------------------------- 1 | param location string = 'westeurope' 2 | param name string = 'chatrstore' 3 | 4 | resource storageAcct 'Microsoft.Storage/storageAccounts@2021-02-01' = { 5 | name: name 6 | location: location 7 | kind: 'StorageV2' 8 | 9 | sku: { 10 | name: 'Standard_LRS' 11 | } 12 | } 13 | 14 | resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2024-01-01' = { 15 | parent: storageAcct 16 | name: 'default' 17 | } 18 | 19 | // Add a container for the deploy-packages 20 | resource deployPackagesContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01' = { 21 | parent: blobService 22 | name: 'deployment-packages' 23 | properties: { 24 | publicAccess: 'None' 25 | } 26 | } 27 | 28 | output resourceId string = storageAcct.id 29 | output storageAccountName string = storageAcct.name 30 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatr-serverless-api", 3 | "version": "0.2.0", 4 | "description": "Backend API and event handler for Chatr", 5 | "type": "module", 6 | "main": "src/functions/*.js", 7 | "scripts": { 8 | "start": "swa start ../client --api-location . --swa-config-location ../client", 9 | "lint": "eslint . && prettier --check *.js", 10 | "lint-client": "eslint ../client/**/*.js && prettier --check ../client/**/*.js", 11 | "lint-fix": "eslint . --fix && prettier --write *.js" 12 | }, 13 | "author": "", 14 | "dependencies": { 15 | "@azure/data-tables": "^13.3.0", 16 | "@azure/functions": "^4.0.0", 17 | "@azure/identity": "^4.3.0", 18 | "@azure/web-pubsub": "^1.1.4", 19 | "jsonwebtoken": "^9.0.2", 20 | "jwks-rsa": "^3.2.0" 21 | }, 22 | "devDependencies": { 23 | "@eslint/js": "^9.26.0", 24 | "eslint": "^9.26.0", 25 | "globals": "^16.1.0", 26 | "prettier": "^3.5.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /deploy/modules/pubsub.bicep: -------------------------------------------------------------------------------- 1 | param location string = 'westeurope' 2 | param name string = 'chatr' 3 | param sku string = 'Free_F1' 4 | param eventHandlerUrl string 5 | 6 | resource pubSub 'Microsoft.SignalRService/webPubSub@2024-10-01-preview' = { 7 | name: name 8 | location: location 9 | 10 | sku: { 11 | name: sku 12 | capacity: 1 13 | } 14 | 15 | properties: { 16 | disableLocalAuth: true 17 | publicNetworkAccess: 'Enabled' 18 | } 19 | } 20 | 21 | // Add a hub for the chat application 22 | resource chatHub 'Microsoft.SignalRService/webPubSub/hubs@2024-10-01-preview' = { 23 | name: 'chat' 24 | parent: pubSub 25 | properties: { 26 | eventHandlers: [ 27 | { 28 | urlTemplate: eventHandlerUrl 29 | userEventPattern: '*' 30 | systemEvents: [ 31 | 'Connected' 32 | 'Disconnected' 33 | ] 34 | } 35 | ] 36 | } 37 | } 38 | 39 | output hostName string = pubSub.properties.hostName 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Ben Coleman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /deploy/deploy-live-demo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | # ================================================================================================ 6 | # This script is only used to deploy the live demo instance, 99.99% of people can ignore this :) 7 | # ================================================================================================ 8 | 9 | echo "😲 Warning! You are about to deploy to the live demo environment"; 10 | read -n 1 -s -r -p "Press any key to continue, or ctrl+c to exit..."; 11 | echo 12 | 13 | export AZURE_TENANT="72f988bf-86f1-41af-91ab-2d7cd011db47" 14 | export AZURE_SUB="62f51b53-9e7e-4673-8fcf-e312c82e113b" 15 | export GITHUB_REPO=https://github.com/benc-uk/chatr.git 16 | export AZURE_REGION=centralus 17 | export AZURE_PREFIX=chatrlive 18 | export AZURE_RESGRP=chatr-live 19 | 20 | az account show > /dev/null 2>&1 || { echo "🔐 Not logged into Azure, logging into Microsoft tenant..."; az login --tenant ${AZURE_TENANT} > /dev/null ; } 21 | 22 | echo "⛅ Switching to Azure subscription ${AZURE_SUB}..." 23 | az account set -s ${AZURE_SUB} -o table 24 | 25 | "${SCRIPT_DIR}"/deploy.sh -------------------------------------------------------------------------------- /deploy/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | which az > /dev/null || { echo "💥 Error! Azure CLI not found, please install https://aka.ms/azure-cli"; exit 1; } 5 | 6 | for varName in AZURE_REGION AZURE_PREFIX AZURE_RESGRP; do 7 | varVal=$(eval echo "\${$varName}") 8 | [ -z "$varVal" ] && { echo "💥 Error! Required variable '$varName' is unset!"; varUnset=true; } 9 | done 10 | [ "$varUnset" ] && exit 1 11 | 12 | # Deiplay details and pause for confirmation 13 | echo -e "\e[32m✨ This will deploy the Chatr app to Azure with the following details:\e[33m 14 | - Region: $AZURE_REGION 15 | - Prefix: $AZURE_PREFIX 16 | - Resource Group: $AZURE_RESGRP\e[0m" 17 | read -n 1 -s -r -p "Press any key to continue, or ctrl+c to exit..."; 18 | 19 | echo -e "\n\n🚀 Starting Bicep deployment..." 20 | az deployment sub create \ 21 | --template-file deploy/main.bicep \ 22 | --location "$AZURE_REGION" \ 23 | --name chatr \ 24 | --parameters resPrefix="$AZURE_PREFIX" \ 25 | resGroupName="$AZURE_RESGRP" \ 26 | location="$AZURE_REGION" -o table 27 | 28 | echo -e "\n✨ Deployment complete!\n🌐 The URL to access the app is: $(az deployment sub show --name chatr --query 'properties.outputs.appUrl.value' -o tsv)\n" 29 | -------------------------------------------------------------------------------- /scripts/cleanup.js: -------------------------------------------------------------------------------- 1 | #!node 2 | const state = require('../api/state') 3 | 4 | const DELETE_AGE = process.argv[2] !== undefined ? parseInt(process.argv[2]) : 24 5 | console.log(`### Deleting data older than ${DELETE_AGE} hours`) 6 | 7 | console.log('### Cleaning up old users') 8 | state.listUsers().then((users) => { 9 | for (let userId in users) { 10 | const user = users[userId] 11 | 12 | const timestamp = new Date(user.timestamp) 13 | const now = new Date() 14 | const ageInHours = (now.getTime() - timestamp.getTime()) / (1000 * 60 * 60) 15 | 16 | if (ageInHours > DELETE_AGE) { 17 | console.log(`### Deleting user ${user.userName} with age of ${ageInHours} hours`) 18 | state.removeUser(userId) 19 | } 20 | } 21 | }) 22 | 23 | console.log('### Cleaning up old chats') 24 | state.listChats().then((chats) => { 25 | for (let chatId in chats) { 26 | const chat = chats[chatId] 27 | 28 | const timestamp = new Date(chat.timestamp) 29 | const now = new Date() 30 | const ageInHours = (now.getTime() - timestamp.getTime()) / (1000 * 60 * 60) 31 | 32 | if (ageInHours > DELETE_AGE) { 33 | console.log(`### Deleting chat ${chat.name} with age of ${ageInHours} hours`) 34 | state.removeChat(chatId) 35 | } 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /client/js/utils.js: -------------------------------------------------------------------------------- 1 | import { toast } from 'https://cdn.jsdelivr.net/npm/bulma-toast@2.4.1/dist/bulma-toast.esm.js' 2 | 3 | export default { 4 | uuidv4() { 5 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 6 | const r = (Math.random() * 16) | 0, 7 | v = c == 'x' ? r : (r & 0x3) | 0x8 8 | return v.toString(16) 9 | }) 10 | }, 11 | 12 | toastMessage(msg, type) { 13 | toast({ 14 | message: msg, 15 | type: `is-${type}`, 16 | duration: 1500, 17 | position: 'bottom-center', 18 | animate: { in: 'fadeIn', out: 'fadeOut' }, 19 | }) 20 | }, 21 | 22 | hashString(str, seed = 0) { 23 | let h1 = 0xdeadbeef ^ seed, 24 | h2 = 0x41c6ce57 ^ seed 25 | for (let i = 0, ch; i < str.length; i++) { 26 | ch = str.charCodeAt(i) 27 | h1 = Math.imul(h1 ^ ch, 2654435761) 28 | h2 = Math.imul(h2 ^ ch, 1597334677) 29 | } 30 | h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909) 31 | h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909) 32 | return 4294967296 * (2097151 & h2) + (h1 >>> 0) 33 | }, 34 | 35 | getCookie(name) { 36 | const value = `; ${document.cookie}` 37 | const parts = value.split(`; ${name}=`) 38 | if (parts.length === 2) return parts.pop().split(';').shift() 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /.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/ubuntu 3 | { 4 | "name": "Chatr Devcontainer", 5 | 6 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 7 | "image": "mcr.microsoft.com/devcontainers/base:jammy", 8 | "features": { 9 | "ghcr.io/devcontainers/features/azure-cli:1": { 10 | "version": "latest" 11 | }, 12 | "ghcr.io/devcontainers/features/node:1": { 13 | "version": "lts" 14 | }, 15 | "ghcr.io/jlaundry/devcontainer-features/azure-functions-core-tools:1": {} 16 | }, 17 | 18 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 19 | // "forwardPorts": [], 20 | 21 | // Use 'postCreateCommand' to run commands after the container is created. 22 | "postCreateCommand": "./.devcontainer/post-create.sh", 23 | 24 | // Configure tool-specific properties. 25 | "customizations": { 26 | "vscode": { 27 | "extensions": [ 28 | "ms-azuretools.vscode-azurefunctions", 29 | "ms-azuretools.vscode-azurestorage", 30 | "ms-vscode.azurecli", 31 | "ms-azuretools.vscode-bicep", 32 | "dbaeumer.vscode-eslint", 33 | "esbenp.prettier-vscode" 34 | ] 35 | } 36 | } 37 | 38 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 39 | // "remoteUser": "root" 40 | } 41 | -------------------------------------------------------------------------------- /api/src/functions/getToken.js: -------------------------------------------------------------------------------- 1 | // 2 | // Chatr - API 3 | // REST API to get an access token and URL so that clients can connect to Azure PubSub websocket 4 | // Ben Coleman, 2021 - 2025 5 | // 6 | 7 | import { WebPubSubServiceClient } from '@azure/web-pubsub' 8 | import { DefaultAzureCredential } from '@azure/identity' 9 | import { app } from '@azure/functions' 10 | 11 | const hubName = process.env.PUBSUB_HUB 12 | const endpoint = process.env.PUBSUB_ENDPOINT 13 | 14 | app.http('getToken', { 15 | methods: ['GET'], 16 | authLevel: 'anonymous', 17 | 18 | handler: async (req, context) => { 19 | if (!hubName || !endpoint) { 20 | context.log('### ERROR! Must set PUBSUB_HUB & PUBSUB_ENDPOINT in app settings / env vars') 21 | return { status: 500, body: 'ERROR! Must set PUBSUB_HUB & PUBSUB_ENDPOINT in app settings / env vars' } 22 | } 23 | 24 | const credentials = new DefaultAzureCredential() 25 | const client = new WebPubSubServiceClient(endpoint, credentials, hubName) 26 | const userId = req.query.get('userId') 27 | if (!userId) { 28 | return { status: 400, body: 'Must pass userId on query string' } 29 | } 30 | 31 | context.log(`### User: ${userId} requested a token to access hub: ${hubName}`) 32 | const token = await client.getClientAccessToken({ 33 | userId: userId, 34 | roles: ['webpubsub.sendToGroup', 'webpubsub.joinLeaveGroup'], 35 | }) 36 | 37 | if (token) context.log(`### Token obtained for user: ${userId}`) 38 | 39 | return { jsonBody: token } 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /api/src/entra-jwt-validate.js: -------------------------------------------------------------------------------- 1 | // 2 | // Chatr - API 3 | // Token validation for JWT issued from Microsoft Entra ID 4 | // Ben Coleman, 2025 5 | // 6 | 7 | import jwt from 'jsonwebtoken' 8 | import jwksClient from 'jwks-rsa' 9 | 10 | // Singleton memoized client 11 | let client = null 12 | 13 | async function getKey(kid, tenantId) { 14 | if (!client) { 15 | client = jwksClient({ 16 | jwksUri: `https://login.microsoftonline.com/${tenantId}/discovery/keys`, 17 | cache: true, 18 | rateLimit: false, 19 | cacheMaxEntries: 5, 20 | cacheTTL: 60 * 60 * 24, 21 | }) 22 | } 23 | 24 | try { 25 | const key = await client.getSigningKey(kid) 26 | return key.getPublicKey() 27 | } catch (err) { 28 | console.error(`Error getting signing key from https://login.microsoftonline.com/${tenantId}/discovery/keys`) 29 | console.error('Check the tenantId is correct and login.microsoftonline.com is reachable') 30 | throw new Error(`Error getting signing key: ${err.message}`) 31 | } 32 | } 33 | 34 | export async function validate(token, tenantId) { 35 | if (!token) { 36 | throw new Error('Missing token') 37 | } 38 | 39 | const decoded = jwt.decode(token, { complete: true }) 40 | if (!decoded) { 41 | throw new Error('Invalid token') 42 | } 43 | 44 | const kid = decoded.header.kid 45 | if (!kid) { 46 | throw new Error('Missing kid in token header') 47 | } 48 | 49 | const key = await getKey(kid, tenantId) 50 | if (!key) { 51 | throw new Error('Key not found') 52 | } 53 | 54 | jwt.verify(token, key) 55 | } 56 | -------------------------------------------------------------------------------- /deploy/main.bicep: -------------------------------------------------------------------------------- 1 | targetScope = 'subscription' 2 | 3 | param resPrefix string = 'chatr' 4 | param resGroupName string = 'chatr' 5 | param location string = 'westeurope' 6 | 7 | var appInsightsName = '${resPrefix}-insights' 8 | var swaName = '${resPrefix}-swa' 9 | var wpsName = '${resPrefix}-wps' 10 | var storageName = '${resPrefix}store' 11 | var functionAppName = '${resPrefix}-func' 12 | 13 | resource resGroup 'Microsoft.Resources/resourceGroups@2024-11-01' = { 14 | name: resGroupName 15 | location: location 16 | } 17 | 18 | module staticApp 'modules/static-webapp.bicep' = { 19 | scope: resGroup 20 | params: { 21 | name: swaName 22 | location: location 23 | } 24 | } 25 | 26 | module storage 'modules/storage.bicep' = { 27 | scope: resGroup 28 | params: { 29 | name: storageName 30 | location: location 31 | } 32 | } 33 | 34 | module pubsub 'modules/pubsub.bicep' = { 35 | scope: resGroup 36 | params: { 37 | name: wpsName 38 | location: location 39 | eventHandlerUrl: 'https://${staticApp.outputs.appHostname}/api/eventHandler' 40 | } 41 | } 42 | 43 | module functionApp 'modules/function-app.bicep' = { 44 | scope: resGroup 45 | params: { 46 | name: functionAppName 47 | location: location 48 | storageAccountName: storage.outputs.storageAccountName 49 | appInsightsName: appInsightsName 50 | pubSubName: wpsName 51 | } 52 | } 53 | 54 | module link 'modules/link-backend.bicep' = { 55 | scope: resGroup 56 | params: { 57 | functionsResourceId: functionApp.outputs.functionAppId 58 | staticWebAppName: swaName 59 | } 60 | } 61 | 62 | output appUrl string = 'https://${staticApp.outputs.appHostname}' 63 | -------------------------------------------------------------------------------- /deploy/readme.md: -------------------------------------------------------------------------------- 1 | # 🚀 Deploying Chatr 2 | 3 | This document describes how to deploy the Chatr application to Azure using Bicep and the Azure CLI. 4 | The Chatr application is a full-stack web application that consists of a frontend built with React and a backend API built with Azure Functions. The deployment process involves creating the necessary Azure resources, deploying the API to Azure Functions, and deploying the frontend to Azure Static Web Apps. 5 | 6 | ### Pre-reqs: 7 | 8 | - Bash & make 9 | - Azure CLI v2.22+ 10 | - [Static Web Apps CLI](https://azure.github.io/static-web-apps-cli/) 11 | - [Azure Function Core Tools](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=linux%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-javascript#install-the-azure-functions-core-tools) 12 | 13 | ### Deployment Notes 14 | 15 | - The deployment is defined in an Azure Bicep template `main.bicep` and uses modules for the various child resources. 16 | - To aid deployment a bash script is used `deploy.sh` this does various checks and configuration handling. 17 | - The Bicep template is deployed using `az deployment sub create` as a subscription level template, that way the resource group can also be defined in the template. 18 | 19 | A number of Azure resources are created as part of the deployment: 20 | 21 | - Azure Web PubSub 22 | - Azure Static Web App 23 | - Azure Function App 24 | - Azure Storage Account 25 | - Azure Application Insights 26 | 27 | Roles are assigned from the Azure Function App's system managed identity as follows: 28 | 29 | - Web PubSub - `Web PubSub Service Owner` 30 | - Storage Account - `Storage Blob Data Contributor` 31 | - Storage Account - `Storage Table Data Contributor` 32 | - Application Insights - `Monitoring Metrics Publisher` 33 | 34 | # Deploy using make 35 | 36 | From the root of the project run 37 | 38 | ``` 39 | make deploy 40 | AZURE_RESGRP={Name of Azure resource group, will be created} \ 41 | AZURE_REGION={Azure region to deploy to} \ 42 | AZURE_PREFIX={Resource name prefix, e.g. mychatr} 43 | ``` 44 | 45 | You can also run: 46 | 47 | - `make deploy-infra` - Deploy just the Azure resources 48 | - `make deploy-api` - Deploy the API to the Function App (needs deploy-infra to have run) 49 | - `make deploy-client` - Deploy the frontend app to Static Web App (needs deploy-infra to have run) 50 | 51 | > Note when picking a value for AZURE_PREFIX, use something other than "chatr" as that will result in resource name clash! 52 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Used by `deploy` & `tunnel` targets 2 | AZURE_PREFIX ?= chatrapp 3 | AZURE_RESGRP ?= projects 4 | AZURE_REGION ?= westeurope 5 | deploy-client tunnel: AZURE_SUB ?= $(shell az account show --query id -o tsv) 6 | 7 | # Don't change :) 8 | API_DIR := api 9 | CLIENT_DIR := client 10 | 11 | .PHONY: help lint lint-fix run clean deploy-infra deploy-api deploy-client deploy tunnel 12 | .DEFAULT_GOAL := help 13 | .EXPORT_ALL_VARIABLES: 14 | 15 | help: ## 💬 This help message 16 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' 17 | 18 | lint: $(API_DIR)/node_modules ## 🔎 Lint & format, will not fix but sets exit code on error 19 | cd $(API_DIR); npm run lint 20 | 21 | lint-fix: $(API_DIR)/node_modules ## 📜 Lint & format, will try to fix errors and modify code 22 | cd $(API_DIR); npm run lint-fix 23 | 24 | run: $(API_DIR)/node_modules ## 🏃 Run client and API locally using SWA CLI 25 | @which swa > /dev/null || { echo "👋 Must install the SWA CLI https://aka.ms/swa-cli"; exit 1; } 26 | swa start ./client --api-location ./api --swa-config-location ./client 27 | 28 | clean: ## 🧹 Clean up project 29 | rm -rf $(API_DIR)/node_modules 30 | 31 | deploy-infra: ## 🧱 Deploy required infra in Azure using Bicep 32 | @echo "🧱 Deploying infrastructure to Azure..." 33 | @./deploy/deploy.sh 34 | 35 | deploy-api: ## 🌐 Deploy API to Azure using Function Core Tools 36 | @echo "🌐 Deploying API to Azure Function App..." 37 | @which func > /dev/null || { echo "👋 Must install the Azure Functions Core Tools https://aka.ms/azure-functions-core-tools"; exit 1; } 38 | cd $(API_DIR); func azure functionapp publish $(AZURE_PREFIX)-func 39 | 40 | deploy-client: ## 🧑 Deploy client to Azure using SWA CLI 41 | @echo "🧑 Deploying client frontend to Azure Static Web App..." 42 | @which swa > /dev/null || { echo "👋 Must install the SWA CLI https://aka.ms/swa-cli"; exit 1; } 43 | swa deploy -a ./client -n $(AZURE_PREFIX)-swa -S $(AZURE_SUB) -R $(AZURE_RESGRP) --env production 44 | 45 | deploy: deploy-infra deploy-api deploy-client ## 🚀 Deploy everything! 46 | 47 | tunnel: ## 🚇 Start AWPS local tunnel tool for local development 48 | @which awps-tunnel > /dev/null || { echo "👋 Must install the AWPS Tunnel Tool"; exit 1; } 49 | awps-tunnel run --hub chat -s $(AZURE_SUB) -g $(AZURE_RESGRP) -u http://localhost:7071 -e https://$(AZURE_PREFIX).webpubsub.azure.com 50 | 51 | # ============================================================================ 52 | 53 | $(API_DIR)/node_modules: $(API_DIR)/package.json 54 | cd $(API_DIR); npm install --silent 55 | touch -m $(API_DIR)/node_modules 56 | 57 | $(API_DIR)/package.json: 58 | @echo "package.json was modified" 59 | -------------------------------------------------------------------------------- /client/js/components/chat.js: -------------------------------------------------------------------------------- 1 | import { nextTick } from 'https://unpkg.com/vue@3.5.13/dist/vue.esm-browser.js' 2 | 3 | export default { 4 | data() { 5 | return { 6 | message: '', 7 | connected: false, 8 | chats: [], 9 | } 10 | }, 11 | 12 | props: { 13 | name: String, 14 | id: String, 15 | active: Boolean, 16 | user: Object, 17 | ws: WebSocket, // This is shared with the parent app component 18 | }, 19 | 20 | async mounted() { 21 | // Use addEventListener to not overwrite the existing listeners 22 | this.ws.addEventListener('message', (evt) => { 23 | const msg = JSON.parse(evt.data) 24 | 25 | switch (msg.type) { 26 | case 'message': { 27 | // Pretty important otherwise we show messages from all chats ! 28 | if (msg.group !== this.id) break 29 | 30 | // User sent messages, i.e. from sendMessage() below 31 | if (msg.data.message && msg.data.fromUserName) { 32 | this.appendChat(msg.data.message, msg.data.fromUserName) 33 | break 34 | } 35 | 36 | // Other messages from the server etc 37 | this.appendChat(msg.data) 38 | break 39 | } 40 | } 41 | }) 42 | }, 43 | 44 | updated() { 45 | if (this.active) { 46 | this.$refs.chatInput.focus() 47 | } 48 | }, 49 | 50 | methods: { 51 | appendChat(text, from = null) { 52 | this.chats.push({ 53 | text, 54 | from, 55 | time: new Date(), 56 | }) 57 | 58 | nextTick(() => { 59 | if (this.$refs['chatBox']) { 60 | this.$refs['chatBox'].scrollTop = this.$refs['chatBox'].scrollHeight 61 | } 62 | }) 63 | 64 | if (!this.active) this.$emit('unread', this.id) 65 | }, 66 | 67 | sendMessage() { 68 | if (!this.message) return 69 | this.ws.send( 70 | JSON.stringify({ 71 | type: 'sendToGroup', 72 | group: this.id, 73 | dataType: 'json', 74 | data: { 75 | message: this.message, 76 | fromUserId: this.user.userId, 77 | fromUserName: this.user.userDetails, 78 | }, 79 | }) 80 | ) 81 | this.message = '' 82 | }, 83 | }, 84 | 85 | template: ` 86 |
87 |
88 | 89 |   90 | 91 |     92 | 93 |
94 | 95 |
96 |
97 |
98 |
{{ chat.from }}
99 |
100 |
101 |
102 |
103 |
`, 104 | } 105 | -------------------------------------------------------------------------------- /client/css/main.css: -------------------------------------------------------------------------------- 1 | ::backdrop { 2 | background-color: rgba(0, 0, 0, 0.6); 3 | } 4 | 5 | .logo { 6 | height: 128px !important; 7 | margin-right: 20px; 8 | } 9 | 10 | .chatBox { 11 | width: 100%; 12 | height: 55vh; 13 | margin-top: 1rem; 14 | background-color: #eeeeee; 15 | font-size: 1.1rem; 16 | padding: 0.5rem; 17 | overflow-y: scroll; 18 | } 19 | 20 | .chatMenu { 21 | height: 20vh; 22 | max-height: 20vh; 23 | overflow-y: scroll; 24 | } 25 | 26 | .chatComponent { 27 | padding-top: 0.5rem; 28 | } 29 | 30 | footer { 31 | float: right; 32 | padding-right: 1rem; 33 | font-size: 0.9rem; 34 | color: #888888; 35 | } 36 | 37 | #app { 38 | padding-top: 1rem; 39 | } 40 | 41 | .userSelf { 42 | color: #467ccc !important; 43 | } 44 | 45 | .userAway { 46 | color: #c0bdba !important; 47 | } 48 | 49 | .chatMsgRow { 50 | width: 100%; 51 | display: flex; 52 | } 53 | 54 | .chatMsg { 55 | max-width: 70%; 56 | } 57 | 58 | .chatMsgTitle { 59 | font-size: 0.9rem; 60 | font-weight: bold; 61 | color: hsl(204, 86%, 53%); 62 | } 63 | 64 | .chatMsgBody { 65 | font-size: 1.1rem; 66 | } 67 | 68 | .chatRight { 69 | justify-content: flex-end; 70 | } 71 | 72 | .loaderOverlay { 73 | position: absolute; 74 | top: 0; 75 | width: 100%; 76 | height: 100%; 77 | background-color: rgba(255, 255, 255, 0.7); 78 | z-index: 10; 79 | } 80 | 81 | .loader, 82 | .loader:after { 83 | border-radius: 50%; 84 | width: 10em; 85 | height: 10em; 86 | } 87 | .loader { 88 | margin: 60px auto; 89 | font-size: 15px; 90 | position: relative; 91 | text-indent: -9999em; 92 | 93 | border-top: 1.1em solid rgba(39, 147, 218, 0.2); 94 | border-right: 1.1em solid rgba(39, 147, 218, 0.2); 95 | border-bottom: 1.1em solid rgba(39, 147, 218, 0.2); 96 | border-left: 1.1em solid #2793da; 97 | -webkit-transform: translateZ(0); 98 | -ms-transform: translateZ(0); 99 | transform: translateZ(0); 100 | -webkit-animation: load8 1.1s infinite linear; 101 | animation: load8 1.1s infinite linear; 102 | } 103 | @-webkit-keyframes load8 { 104 | 0% { 105 | -webkit-transform: rotate(0deg); 106 | transform: rotate(0deg); 107 | } 108 | 100% { 109 | -webkit-transform: rotate(360deg); 110 | transform: rotate(360deg); 111 | } 112 | } 113 | @keyframes load8 { 114 | 0% { 115 | -webkit-transform: rotate(0deg); 116 | transform: rotate(0deg); 117 | } 118 | 100% { 119 | -webkit-transform: rotate(360deg); 120 | transform: rotate(360deg); 121 | } 122 | } 123 | 124 | /* Hacky but I had to fight against Bulma CSS */ 125 | .deleteButton { 126 | background-color: #d35252 !important; 127 | color: white !important; 128 | border: none !important; 129 | padding: 0.1rem 0.1rem !important; 130 | margin: 0 !important; 131 | border-radius: 5px !important; 132 | cursor: pointer !important; 133 | max-width: 2rem !important; 134 | float: right !important; 135 | font-size: 0.9rem !important; 136 | text-align: center !important; 137 | } 138 | 139 | [v-cloak] { 140 | display: none; 141 | } 142 | 143 | .errorMessage { 144 | color: #332020 !important; 145 | font-size: 1.1rem !important; 146 | margin-top: 0.5rem !important; 147 | font-family: monospace !important; 148 | white-space: pre-wrap !important; 149 | } 150 | 151 | .debug { 152 | display: block; 153 | margin: 0.5rem; 154 | padding: 0.5rem; 155 | border: 1px solid blue; 156 | } 157 | 158 | dialog { 159 | background: none; 160 | border: none; 161 | padding: 40px; 162 | } 163 | 164 | dialog .card { 165 | box-shadow: 3px 4px 12px rgba(0, 0, 0, 0.3); 166 | border: 2px solid #3b4fe5; 167 | } 168 | -------------------------------------------------------------------------------- /client/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Chatr Login 9 | 10 | 11 | 12 | 13 | 14 | 15 | 26 | 27 | 28 | 29 |
30 |

31 | 32 | Chatr 33 |

34 |

35 | Azure Web PubSub - Demo & Reference App 36 |

37 |
38 |
39 |

40 | Login to start using Chatr 41 |

42 |
43 | 61 | 62 |
63 | 64 | 65 | 66 |
67 |
68 |

69 | Join as a guest user 70 |

71 |
72 |
73 |
74 | 75 |
76 | 79 |
80 |
81 |
82 |
83 | 84 | 85 |
86 |
87 | 88 |
89 |
90 | 91 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /api/src/state.js: -------------------------------------------------------------------------------- 1 | // 2 | // Chatr - API 3 | // State management and persistence backed with Azure Table storage 4 | // Ben Coleman, 2021 - 2025 5 | // 6 | 7 | import { TableServiceClient, TableClient } from '@azure/data-tables' 8 | import { DefaultAzureCredential } from '@azure/identity' 9 | 10 | const account = process.env.STORAGE_ACCOUNT_NAME 11 | const chatsTable = 'chats' 12 | const usersTable = 'users' 13 | const partitionKey = 'chatr' 14 | 15 | if (!account) { 16 | console.log('### 💥 Fatal! STORAGE_ACCOUNT_NAME is not set') 17 | } 18 | 19 | const credential = new DefaultAzureCredential() 20 | const serviceClient = new TableServiceClient(`https://${account}.table.core.windows.net`, credential) 21 | const userTableClient = new TableClient(`https://${account}.table.core.windows.net`, usersTable, credential) 22 | const chatTableClient = new TableClient(`https://${account}.table.core.windows.net`, chatsTable, credential) 23 | 24 | // ============================================================== 25 | // Create tables and absorb errors if they already exist 26 | // ============================================================== 27 | async function initTables() { 28 | console.log(`### 📭 Connected to Azure table storage: ${account}`) 29 | 30 | try { 31 | console.log(`### 📭 Creating table ${chatsTable} (it might already exist, that's OK)`) 32 | await serviceClient.createTable(chatsTable) 33 | } catch (err) { 34 | console.log(`### Error ${err} creating table ${chatsTable}`) 35 | } 36 | try { 37 | console.log(`### 📭 Creating table ${usersTable} (it might already exist, that's OK)`) 38 | await serviceClient.createTable(usersTable) 39 | } catch (err) { 40 | console.log(`### Error ${err} creating table ${usersTable}`) 41 | } 42 | } 43 | 44 | // ============================================================== 45 | // Called when module is imported 46 | // ============================================================== 47 | initTables() 48 | 49 | // ============================================================== 50 | // Chat state functions 51 | // ============================================================== 52 | export async function upsertChat(id, chat) { 53 | const chatEntity = { 54 | partitionKey: partitionKey, 55 | rowKey: id, 56 | data: JSON.stringify(chat), 57 | } 58 | await chatTableClient.upsertEntity(chatEntity, 'Replace') 59 | } 60 | 61 | export async function removeChat(id) { 62 | try { 63 | await chatTableClient.deleteEntity(partitionKey, id) 64 | } catch (e) { 65 | console.log('### Delete chat failed') 66 | } 67 | } 68 | 69 | export async function getChat(id) { 70 | try { 71 | const chatEntity = await chatTableClient.getEntity(partitionKey, id) 72 | 73 | return JSON.parse(chatEntity.data) 74 | } catch (err) { 75 | return null 76 | } 77 | } 78 | 79 | export async function listChats() { 80 | const chatsResp = {} 81 | const chatList = chatTableClient.listEntities() 82 | 83 | for await (const chat of chatList) { 84 | const chatObj = JSON.parse(chat.data) 85 | // Timestamp only used by cleanup script 86 | chatObj.timestamp = chat.timestamp 87 | chatsResp[chat.rowKey] = chatObj 88 | } 89 | 90 | return chatsResp 91 | } 92 | 93 | // ============================================================== 94 | // User state functions 95 | // ============================================================== 96 | export async function upsertUser(id, user) { 97 | const userEntity = { 98 | partitionKey: partitionKey, 99 | rowKey: id, 100 | ...user, 101 | } 102 | await userTableClient.upsertEntity(userEntity, 'Replace') 103 | } 104 | 105 | export async function removeUser(id) { 106 | try { 107 | await userTableClient.deleteEntity(partitionKey, id) 108 | } catch (e) { 109 | console.log('### Delete user failed') 110 | } 111 | } 112 | 113 | export async function listUsers() { 114 | const usersResp = {} 115 | const userList = userTableClient.listEntities() 116 | 117 | for await (const user of userList) { 118 | usersResp[user.rowKey] = user 119 | } 120 | return usersResp 121 | } 122 | 123 | export async function getUser(id) { 124 | try { 125 | const user = await userTableClient.getEntity(partitionKey, id) 126 | return user 127 | } catch (err) { 128 | return null 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /deploy/modules/function-app.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | param storageAccountName string 4 | param appInsightsName string 5 | param pubSubName string 6 | 7 | var storeSuffix = environment().suffixes.storage 8 | 9 | resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' existing = { 10 | name: storageAccountName 11 | } 12 | 13 | resource webPubSub 'Microsoft.SignalRService/webPubSub@2024-10-01-preview' existing = { 14 | name: pubSubName 15 | } 16 | 17 | resource appInsights 'Microsoft.Insights/components@2020-02-02' = { 18 | name: appInsightsName 19 | location: location 20 | kind: 'web' 21 | properties: { 22 | Application_Type: 'web' 23 | } 24 | } 25 | 26 | resource flexFuncPlan 'Microsoft.Web/serverfarms@2024-04-01' = { 27 | name: '${name}-plan' 28 | location: location 29 | kind: 'functionapp' 30 | sku: { 31 | name: 'FC1' // Flex Consumption SKU 32 | tier: 'FlexConsumption' 33 | } 34 | properties: { 35 | reserved: true // Required for Linux plans 36 | } 37 | } 38 | 39 | resource functionApp 'Microsoft.Web/sites@2024-04-01' = { 40 | name: name 41 | location: location 42 | kind: 'functionapp,linux' 43 | identity: { 44 | type: 'SystemAssigned' 45 | } 46 | properties: { 47 | serverFarmId: flexFuncPlan.id 48 | siteConfig: { 49 | appSettings: [ 50 | { 51 | name: 'APPLICATIONINSIGHTS_AUTHENTICATION_STRING' 52 | value: 'Authorization=AAD' 53 | } 54 | { 55 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' 56 | value: appInsights.properties.ConnectionString 57 | } 58 | { 59 | name: 'AzureWebJobsStorage__blobServiceUri' 60 | value: 'https://${storageAccountName}.blob.${storeSuffix}' 61 | } 62 | { 63 | name: 'AzureWebJobsStorage__credential' 64 | value: 'managedidentity' 65 | } 66 | { 67 | name: 'AzureWebJobsStorage__queueServiceUri' 68 | value: 'https://${storageAccountName}.queue.${storeSuffix}' 69 | } 70 | { 71 | name: 'AzureWebJobsStorage__tableServiceUri' 72 | value: 'https://${storageAccountName}.table.${storeSuffix}' 73 | } 74 | { name: 'PUBSUB_ENDPOINT', value: 'https://${pubSubName}.webpubsub.azure.com' } 75 | { name: 'PUBSUB_HUB', value: 'chat' } 76 | { name: 'STORAGE_ACCOUNT_NAME', value: storageAccountName } 77 | ] 78 | minTlsVersion: '1.2' 79 | } 80 | httpsOnly: true 81 | functionAppConfig: { 82 | runtime: { 83 | name: 'node' 84 | version: '22' 85 | } 86 | deployment: { 87 | storage: { 88 | authentication: { 89 | type: 'SystemAssignedIdentity' 90 | } 91 | type: 'blobContainer' 92 | value: 'https://${storageAccountName}.blob.${storeSuffix}/deployment-packages' 93 | } 94 | } 95 | scaleAndConcurrency: { instanceMemoryMB: 512, maximumInstanceCount: 40 } 96 | } 97 | } 98 | } 99 | 100 | // Role assignment for the function app to access the storage account 101 | resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 102 | name: guid(functionApp.id, 'Storage Blob Data Contributor', storage.id) 103 | properties: { 104 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') 105 | principalId: functionApp.identity.principalId 106 | principalType: 'ServicePrincipal' 107 | } 108 | scope: storage 109 | } 110 | 111 | // Role assignment for the function app to access the storage account table 112 | resource tableRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 113 | name: guid(functionApp.id, 'Storage Table Data Contributor', storage.id) 114 | properties: { 115 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3') 116 | principalId: functionApp.identity.principalId 117 | principalType: 'ServicePrincipal' 118 | } 119 | scope: storage 120 | } 121 | 122 | // Add role assignment for the function app to access app insights 123 | resource appInsightsRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 124 | name: guid(functionApp.id, 'Monitoring Metrics Publisher', appInsights.id) 125 | properties: { 126 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '3913510d-42f4-4e42-8a64-420c390055eb') 127 | principalId: functionApp.identity.principalId 128 | principalType: 'ServicePrincipal' 129 | } 130 | scope: appInsights 131 | } 132 | 133 | // Add role assignment for the function app to access the web pub sub 134 | resource webPubSubRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 135 | name: guid(functionApp.id, 'Web PubSub Service Owner', functionApp.id) 136 | properties: { 137 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', '12cf5a90-567b-43ae-8102-96cf46c7d9b4') 138 | principalId: functionApp.identity.principalId 139 | principalType: 'ServicePrincipal' 140 | } 141 | scope: webPubSub 142 | } 143 | 144 | output principalId string = functionApp.identity.principalId 145 | output functionAppName string = functionApp.name 146 | output functionAppHostname string = functionApp.properties.defaultHostName 147 | output functionAppId string = functionApp.id // Added: Output function app resource ID 148 | output location string = location 149 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Chatr App 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 | 29 |
30 |
31 |
ONLINE: {{ online }}
32 |
WS: {{ ws }}
33 |
USER: {{ user }}
34 |
CHATS: {{ allChats }}
35 |
USERS: {{ allUsers }}
36 |
ERROR: {{ error }}
37 |
38 |
39 |
40 | 41 | 42 |
43 |
Loading...
44 |
45 |

Please wait while we connect to the server.

46 |
47 |
48 |
  ACCOUNT
49 |
  ONLINE
50 |
  CHATS
51 |
  USERS
52 |
  PUBSUB
53 |
54 |
55 | 56 |
57 |

💩 Application Error ☠️

58 |
59 | {{ error }} 60 |
61 |
62 | 63 |
64 | 65 |
66 |
67 | Group Chats 68 | 70 |
71 | 72 | 90 |
91 | 92 | 93 |
94 |
95 | Online Users 96 | 97 |   Logout 98 | 99 |
100 | 101 | 117 |
118 |
119 | 120 |
121 | 122 | 135 |
136 | 137 | 138 | 140 | 141 | 142 | 143 | 161 | 162 |
163 | 164 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /api/src/functions/eventHandler.js: -------------------------------------------------------------------------------- 1 | // 2 | // Chatr - API 3 | // Webhook event handler for receiving upstream events from Azure Web PubSub 4 | // 5 | // NOTE. This function DOES NOT use the Web PubSub Trigger binding as you might expect 6 | // We handle the HTTP webhooks manually, it's not hard :) 7 | // Ben Coleman, 2021 - 2025 8 | // 9 | 10 | import { WebPubSubServiceClient } from '@azure/web-pubsub' 11 | import { DefaultAzureCredential } from '@azure/identity' 12 | import { app } from '@azure/functions' 13 | import * as state from '../state.js' 14 | import { validate } from '../entra-jwt-validate.js' 15 | 16 | const hubName = process.env.PUBSUB_HUB 17 | const endpoint = process.env.PUBSUB_ENDPOINT 18 | 19 | app.http('eventHandler', { 20 | methods: ['GET', 'POST', 'OPTIONS'], 21 | authLevel: 'anonymous', 22 | 23 | handler: async (req, context) => { 24 | if (!hubName || !endpoint) { 25 | const errorMsg = 'ERROR! Must set PUBSUB_HUB, PUBSUB_ENDPOINT in app settings / env vars' 26 | context.log(errorMsg) 27 | return { status: 500, body: errorMsg } 28 | } 29 | 30 | // OPTIONAL: Validate the JWT token if TENANT_ID was set 31 | if (process.env.VALIDATION_TENANT_ID) { 32 | let accessToken = req.headers.get('authorization') 33 | if (accessToken && accessToken.startsWith('Bearer ')) { 34 | accessToken = accessToken.substring(7) 35 | } 36 | if (!accessToken) { 37 | return { status: 401, body: 'Missing authorization header' } 38 | } 39 | 40 | // Validate the token 41 | await validate(accessToken, process.env.TENANT_ID) 42 | } 43 | 44 | // We have to handle cloud event webhook validation 45 | // https://learn.microsoft.com/en-us/azure/azure-web-pubsub/howto-develop-eventhandler#upstream-and-validation 46 | if (req.method === 'OPTIONS') { 47 | context.log(`### Webhook validation was called!`) 48 | return { 49 | headers: { 50 | 'webhook-allowed-origin': req.headers.get('webhook-request-origin'), 51 | }, 52 | status: 200, 53 | } 54 | } 55 | 56 | const credentials = new DefaultAzureCredential() 57 | const serviceClient = new WebPubSubServiceClient(endpoint, credentials, hubName) 58 | const userId = req.headers.get('ce-userid') 59 | const eventName = req.headers.get('ce-eventname') 60 | 61 | // System event for disconnected user, logoff or tab closed 62 | if (eventName === 'disconnected') { 63 | context.log(`### User ${userId} has disconnected`) 64 | await removeChatUser(serviceClient, userId) 65 | await serviceClient.sendToAll({ 66 | chatEvent: 'userOffline', 67 | data: userId, 68 | }) 69 | } 70 | 71 | // Use a custom event here rather than the system connected event 72 | // This means we can pass extra data, not just a userId 73 | if (eventName === 'userConnected') { 74 | const body = await req.json() 75 | const userName = body.userName 76 | const userProvider = body.userProvider 77 | console.log(`### User ${userId} ${userName} ${userProvider} connected`) 78 | state.upsertUser(userId, { userName, userProvider, idle: false }) 79 | await serviceClient.sendToAll({ 80 | chatEvent: 'userOnline', 81 | data: JSON.stringify({ 82 | userId, 83 | userName, 84 | userProvider, 85 | idle: false, 86 | }), 87 | }) 88 | } 89 | 90 | if (eventName === 'createChat') { 91 | const body = await req.json() 92 | const chatName = body.name 93 | const chatId = body.id 94 | const chatEntity = { id: chatId, name: chatName, members: {}, owner: userId } 95 | state.upsertChat(chatId, chatEntity) 96 | 97 | serviceClient.sendToAll({ 98 | chatEvent: 'chatCreated', 99 | data: JSON.stringify(chatEntity), 100 | }) 101 | 102 | context.log(`### New chat ${chatName} was created by ${userId}`) 103 | } 104 | 105 | if (eventName === 'joinChat') { 106 | const chatId = await req.text() 107 | const chat = await state.getChat(chatId) 108 | 109 | if (!chat) { 110 | context.log(`### Attempt to join chat with ID ${chatId} failed, it doesn't exist`) 111 | return 112 | } 113 | 114 | // Chat id used as the group name 115 | serviceClient.group(chatId).addUser(userId) 116 | 117 | // Need to call state to get the users name 118 | const user = await state.getUser(userId) 119 | 120 | // Add user to members of the chat (members is a map/dict) and push back into the DB 121 | chat.members[userId] = { userId, userName: user.userName } 122 | await state.upsertChat(chatId, chat) 123 | context.log(`### User ${user.userName} has joined chat ${chat.name}`) 124 | 125 | setTimeout(() => { 126 | serviceClient.group(chatId).sendToAll(`💬 ${user.userName} has joined the chat`) 127 | }, 1000) 128 | } 129 | 130 | if (eventName === 'leaveChat') { 131 | const body = await req.json() 132 | const chatId = body.chatId 133 | const userName = body.userName 134 | context.log(`### User ${userName} has left chat ${chatId}`) 135 | 136 | await serviceClient.group(chatId).removeUser(userId) 137 | await serviceClient.group(chatId).sendToAll(`💨 ${userName} has left the chat`) 138 | 139 | leaveChat(userId, chatId) 140 | } 141 | 142 | if (eventName === 'createPrivateChat') { 143 | const body = await req.json() 144 | const initiator = body.initiatorUserId 145 | const target = body.targetUserId 146 | context.log(`### Starting private chat ${initiator} -> ${target}`) 147 | 148 | let chatId = '' 149 | // This strangeness means we get a unique pair ID no matter who starts the chat 150 | if (target < initiator) { 151 | chatId = `private_${target}_${initiator}` 152 | } else { 153 | chatId = `private_${initiator}_${target}` 154 | } 155 | 156 | // Need to call state to get the users name 157 | const initiatorUser = await state.getUser(initiator) 158 | const targetUser = await state.getUser(target) 159 | 160 | try { 161 | await serviceClient.group(chatId).addUser(target) 162 | 163 | await serviceClient.sendToUser(target, { 164 | chatEvent: 'joinPrivateChat', 165 | data: JSON.stringify({ id: chatId, name: `Chat with ${initiatorUser.userName}`, grabFocus: false }), 166 | }) 167 | } catch (err) { 168 | // This can happen with orphaned disconnected users 169 | context.log(`### Target user for private chat not found, will remove them!`) 170 | await removeChatUser(serviceClient, target) 171 | return { status: 200 } 172 | } 173 | 174 | try { 175 | await serviceClient.group(chatId).addUser(initiator) 176 | 177 | await serviceClient.sendToUser(initiator, { 178 | chatEvent: 'joinPrivateChat', 179 | data: JSON.stringify({ id: chatId, name: `Chat with ${targetUser.userName}`, grabFocus: true }), 180 | }) 181 | } catch (err) { 182 | // This should never happen! 183 | context.log(`### Source user for private chat not found, will remove them!`) 184 | await removeChatUser(serviceClient, initiator) 185 | return { status: 200 } 186 | } 187 | 188 | setTimeout(async () => { 189 | await serviceClient.group(chatId).sendToAll(`💬 ${initiatorUser.userName} wants to chat`) 190 | }, 500) 191 | } 192 | 193 | if (eventName === 'userEnterIdle') { 194 | const userId = await req.text() 195 | context.log(`### User ${userId} has gone idle`) 196 | await serviceClient.sendToAll({ 197 | chatEvent: 'userIsIdle', 198 | data: userId, 199 | }) 200 | } 201 | 202 | if (eventName === 'userExitIdle') { 203 | const userId = await req.text() 204 | context.log(`### User ${userId} has returned`) 205 | await serviceClient.sendToAll({ 206 | chatEvent: 'userNotIdle', 207 | data: userId, 208 | }) 209 | } 210 | 211 | if (eventName === 'deleteChat') { 212 | const chatId = await req.text() 213 | context.log(`### Chat ${chatId} has been deleted`) 214 | await state.removeChat(chatId) 215 | await serviceClient.sendToAll({ 216 | chatEvent: 'chatDeleted', 217 | data: chatId, 218 | }) 219 | } 220 | 221 | // Respond with a 200 to the webhook 222 | return { status: 200 } 223 | }, 224 | }) 225 | 226 | // 227 | // Helper to remove user from a chat 228 | // 229 | async function leaveChat(userId, chatId) { 230 | const chat = await state.getChat(chatId) 231 | if (!chat) { 232 | return 233 | } 234 | 235 | // Find & remove user from chat's member list 236 | for (const memberUserId in chat.members) { 237 | if (memberUserId === userId) { 238 | delete chat.members[userId] 239 | } 240 | } 241 | 242 | state.upsertChat(chatId, chat) 243 | } 244 | 245 | // 246 | // Helper to remove a user, syncs all users and the DB 247 | // 248 | async function removeChatUser(serviceClient, userId) { 249 | console.log(`### User ${userId} is being removed`) 250 | state.removeUser(userId) 251 | 252 | // Notify everyone 253 | serviceClient.sendToAll({ 254 | chatEvent: 'userOffline', 255 | data: userId, 256 | }) 257 | 258 | // Leave all chats 259 | for (const chatId in await state.listChats()) { 260 | console.log('### Calling leaveChat', userId, chatId) 261 | await leaveChat(userId, chatId) 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /client/js/app.js: -------------------------------------------------------------------------------- 1 | // ============================================================================================ 2 | // Chatr App - Client side frontend code 3 | // ============================================================================================ 4 | import chat from './components/chat.js' 5 | import utils from './utils.js' 6 | import { createApp } from 'https://unpkg.com/vue@3.5.13/dist/vue.esm-browser.js' 7 | 8 | const MAX_IDLE_TIME = 60 9 | 10 | createApp({ 11 | components: { 12 | chat, 13 | }, 14 | 15 | data() { 16 | return { 17 | // Map of joined chats, using id as key 18 | // values are client side chat objects -> { id: string, name: string, active: bool, unreadCount: int } 19 | joinedChats: {}, 20 | // Main WebSocket instance 21 | ws: null, 22 | // Toggle to see if user is online 23 | online: false, 24 | // User object which is an instance of SWA clientPrincipal 25 | // See https://docs.microsoft.com/en-us/azure/static-web-apps/user-information?tabs=javascript#client-principal-data 26 | user: null, 27 | // Map of chat id to server chat objects, synced with the server 28 | allChats: null, 29 | // Map of users to server user objects, synced with the server 30 | allUsers: null, 31 | // Used to handle idle detection 32 | idle: false, 33 | idleTime: 0, 34 | // Used by the new chat modal dialog 35 | openNewChatDialog: false, 36 | newChatName: '', 37 | error: '', 38 | debug: false, 39 | } 40 | }, 41 | 42 | async beforeMount() { 43 | // Set up handlers and timer interval for idle time detection 44 | document.onmousemove = this.resetIdle 45 | document.onkeydown = this.resetIdle 46 | setInterval(this.idleChecker, 1000) 47 | 48 | // Special case for guest users 49 | const guestUser = localStorage.getItem('guestUser') 50 | if (guestUser) { 51 | if (guestUser) { 52 | this.user = { 53 | userId: `${utils.hashString(guestUser)}`, 54 | userRoles: ['anonymous', 'authenticated'], 55 | claims: [], 56 | identityProvider: 'guest', // I just invented a provider here! 57 | userDetails: guestUser, 58 | } 59 | console.log('### Will use fake guest user:', this.user) 60 | } 61 | } else { 62 | // If not guest - Get user details from special SWA auth endpoint 63 | try { 64 | const userRes = await fetch(`/.auth/me`) 65 | if (!userRes.ok) { 66 | throw 'Failed to call /.auth/me endpoint' 67 | } else { 68 | // Get user details from clientPrincipal returned from SWA 69 | const userData = await userRes.json() 70 | this.user = userData.clientPrincipal 71 | } 72 | 73 | if (!this.user) { 74 | // redirect to login page 75 | window.location.href = '/login.html' 76 | return 77 | } 78 | console.log('### User details obtained:', JSON.stringify(this.user)) 79 | } catch (err) { 80 | this.error = `Error getting user: ${err}` 81 | } 82 | } 83 | 84 | let res = null 85 | try { 86 | // Get all existing chats from server 87 | res = await fetch(`/api/chats`) 88 | if (!res.ok) throw `chats API error: ${await res.text()}` 89 | let data = await res.json() 90 | this.allChats = data.chats 91 | 92 | // Get all existing users from server 93 | res = await fetch(`/api/users`) 94 | if (!res.ok) throw `users API error: ${await res.text()}` 95 | data = await res.json() 96 | this.allUsers = data.users 97 | 98 | // Get URL & token to connect to Azure Web Pubsub 99 | res = await fetch(`/api/getToken?userId=${this.user.userId}`) 100 | if (!res.ok) throw `getToken API error: ${await res.text()}` 101 | const token = await res.json() 102 | 103 | console.log('### Data fetched from API') 104 | console.log('### PubSub token obtained, creating WS connection to', token.baseUrl) 105 | 106 | // Now connect to Azure Web PubSub using the URL we got 107 | this.ws = new WebSocket(token.url, 'json.webpubsub.azure.v1') 108 | 109 | // Both of these handle error situations 110 | this.ws.onerror = (evt) => { 111 | this.error = `WebSocket error ${evt.message}` 112 | } 113 | this.ws.onclose = (evt) => { 114 | this.error = `WebSocket closed, code: ${evt.code}` 115 | } 116 | 117 | // Custom notification event, rather that relying on the system connected event 118 | this.ws.onopen = () => { 119 | console.log('### WebSocket connection opened') 120 | this.ws.send( 121 | JSON.stringify({ 122 | type: 'event', 123 | event: 'userConnected', 124 | dataType: 'json', 125 | data: { userName: this.user.userDetails, userProvider: this.user.identityProvider }, 126 | }) 127 | ) 128 | } 129 | 130 | console.log('### App startup complete, waiting for WebSocket messages') 131 | } catch (err) { 132 | this.error = `Backend error: ${res.status ?? 'Unknown'}\n${err.replaceAll('\\n', '\n')}` 133 | return 134 | } 135 | 136 | // Handle messages from server 137 | this.ws.addEventListener('message', (evt) => { 138 | if (this.debug) console.log('### WebSocket message', evt.data) 139 | 140 | const msg = JSON.parse(evt.data) 141 | 142 | // System events 143 | if (msg.type === 'system' && msg.event === 'connected') { 144 | utils.toastMessage(`🔌 Connected to ${evt.origin.replace('wss://', '')}`, 'success') 145 | } 146 | 147 | // Server events 148 | if (msg.from === 'server' && msg.data.chatEvent === 'chatCreated') { 149 | const chat = JSON.parse(msg.data.data) 150 | this.allChats[chat.id] = chat 151 | 152 | this.$nextTick(() => { 153 | const chatList = this.$refs.chatList 154 | chatList.scrollTop = chatList.scrollHeight 155 | }) 156 | } 157 | 158 | if (msg.from === 'server' && msg.data.chatEvent === 'chatDeleted') { 159 | const chatId = msg.data.data 160 | this.allChats[chatId] = null 161 | delete this.allChats[chatId] 162 | if (this.joinedChats[chatId]) { 163 | utils.toastMessage(`💥 Chat deleted by owner, you have been removed!`, 'danger') 164 | this.onLeaveEvent(chatId) 165 | } 166 | } 167 | 168 | if (msg.from === 'server' && msg.data.chatEvent === 'userOnline') { 169 | const newUser = JSON.parse(msg.data.data) 170 | 171 | // If the new user is ourselves, that means we're connected and online 172 | if (newUser.userId == this.user.userId) { 173 | console.log('### User is online') 174 | this.online = true 175 | } else { 176 | utils.toastMessage(`🤩 ${newUser.userName} has just joined`, 'success') 177 | } 178 | this.allUsers[newUser.userId] = newUser 179 | } 180 | 181 | if (msg.from === 'server' && msg.data.chatEvent === 'userOffline') { 182 | const userId = msg.data.data 183 | if (msg.data && this.allUsers[userId]) { 184 | const userName = this.allUsers[userId].userName 185 | this.allUsers[userId] = null 186 | delete this.allUsers[userId] 187 | utils.toastMessage(`💨 ${userName} has left or logged off`, 'warning') 188 | } 189 | } 190 | 191 | if (msg.from === 'server' && msg.data.chatEvent === 'joinPrivateChat') { 192 | const chat = JSON.parse(msg.data.data) 193 | if (!chat.grabFocus) { 194 | utils.toastMessage(`💬 Incoming: ${chat.name}`, 'warning') 195 | } 196 | this.joinPrivateChat(chat.id, chat.name, chat.grabFocus) 197 | } 198 | 199 | if (msg.from === 'server' && msg.data.chatEvent === 'userIsIdle') { 200 | const userId = msg.data.data 201 | this.allUsers[userId] = { ...this.allUsers[userId], idle: true } 202 | utils.toastMessage(`💤 User ${this.allUsers[userId].userName} is now idle`, 'link') 203 | } 204 | 205 | if (msg.from === 'server' && msg.data.chatEvent === 'userNotIdle') { 206 | const userId = msg.data.data 207 | this.allUsers[userId] = { ...this.allUsers[userId], idle: false } 208 | 209 | utils.toastMessage(`🤸‍♂️ User ${this.allUsers[userId].userName} has returned`, 'link') 210 | } 211 | }) 212 | }, 213 | 214 | methods: { 215 | // 216 | // Initiate a new group chat, opens the prompt 217 | // 218 | async newChat() { 219 | this.openNewChatDialog = true 220 | this.$nextTick(() => { 221 | this.$refs.newChatInput.focus() 222 | }) 223 | }, 224 | 225 | // 226 | // Called when new group chat dialog is accepted 227 | // 228 | newChatCreate() { 229 | this.openNewChatDialog = false 230 | const chatName = this.newChatName 231 | if (!chatName) return 232 | 233 | const chatId = utils.uuidv4() 234 | this.ws.send( 235 | JSON.stringify({ 236 | type: 'event', 237 | event: 'createChat', 238 | dataType: 'json', 239 | data: { name: chatName, id: chatId }, 240 | }) 241 | ) 242 | this.newChatName = '' 243 | this.deactivateChats() 244 | this.joinChat(chatId, chatName) 245 | }, 246 | 247 | newChatCancel() { 248 | this.openNewChatDialog = false 249 | this.newChatName = '' 250 | }, 251 | 252 | // 253 | // Initiate a private chat with a remote user 254 | // 255 | async newPrivateChat(targetUser) { 256 | // Can't talk to yourself 257 | if (targetUser == this.user.userId) return 258 | 259 | // Prevent starting chats with users if they are already open 260 | const openPrivateChats = Object.keys(this.joinedChats).filter((c) => c.startsWith('private_')) 261 | if (openPrivateChats.length > 0) { 262 | if (openPrivateChats.find((c) => c.includes(targetUser))) return 263 | } 264 | 265 | // Tell the server to notify both users of the chat request 266 | this.ws.send( 267 | JSON.stringify({ 268 | type: 'event', 269 | event: 'createPrivateChat', 270 | dataType: 'json', 271 | data: { initiatorUserId: this.user.userId, targetUserId: targetUser }, 272 | }) 273 | ) 274 | }, 275 | 276 | // 277 | // Join a group chat 278 | // 279 | async joinChat(chatId, chatName) { 280 | // Skip if we are already joined 281 | if (this.joinedChats[chatId]) return 282 | 283 | this.deactivateChats() 284 | this.joinedChats[chatId] = { id: chatId, name: chatName, active: true, unreadCount: 0 } 285 | this.ws.send( 286 | JSON.stringify({ 287 | type: 'event', 288 | event: 'joinChat', 289 | dataType: 'text', 290 | data: chatId, 291 | }) 292 | ) 293 | }, 294 | 295 | // 296 | // On receipt of joinPrivateChat event, open the chat 297 | // 298 | joinPrivateChat(chatId, chatName, grabFocus) { 299 | // Skip if we are already joined 300 | if (this.joinedChats[chatId]) return 301 | 302 | // If grabbing focus means we should deactivate current chat 303 | if (grabFocus) this.deactivateChats() 304 | this.joinedChats[chatId] = { id: chatId, name: chatName, active: grabFocus, unreadCount: 0 } 305 | }, 306 | 307 | // 308 | // Switch chat tab 309 | // 310 | switchChat(evt) { 311 | const chatId = evt.target.getAttribute('data-chat-id') 312 | if (!this.joinedChats[chatId]) return 313 | this.deactivateChats() 314 | this.joinedChats[chatId].active = true 315 | this.joinedChats[chatId].unreadCount = 0 316 | }, 317 | 318 | // 319 | // Deactivate all tabs 320 | // 321 | deactivateChats() { 322 | for (const chatId in this.joinedChats) { 323 | this.joinedChats[chatId].active = false 324 | } 325 | }, 326 | 327 | // 328 | // Vue event handler when child chat component gets a message and is unfocused 329 | // 330 | onUnreadEvent(chatId) { 331 | this.joinedChats[chatId].unreadCount++ 332 | }, 333 | 334 | // 335 | // Vue event handler for when leave is clicked in child chat component 336 | // 337 | onLeaveEvent(chatId) { 338 | this.joinedChats[chatId] = null 339 | delete this.joinedChats[chatId] 340 | 341 | this.ws.send( 342 | JSON.stringify({ 343 | type: 'event', 344 | event: 'leaveChat', 345 | dataType: 'json', 346 | data: { chatId, userName: this.user.userDetails }, 347 | }) 348 | ) 349 | 350 | const firstChat = this.joinedChats[Object.keys(this.joinedChats)[0]] 351 | if (firstChat) { 352 | firstChat.active = true 353 | } 354 | }, 355 | 356 | // 357 | // Used to detect idle time and reset it on any activity 358 | // 359 | resetIdle() { 360 | if (this.idle) { 361 | this.ws.send( 362 | JSON.stringify({ 363 | type: 'event', 364 | event: 'userExitIdle', 365 | dataType: 'text', 366 | data: this.user.userId, 367 | }) 368 | ) 369 | } 370 | this.idle = false 371 | this.idleTime = 0 372 | }, 373 | 374 | // 375 | // Called every 1 second to check for idle time 376 | // 377 | idleChecker() { 378 | this.idleTime += 1 379 | if (this.idleTime > MAX_IDLE_TIME && !this.idle) { 380 | this.idle = true 381 | this.ws.send( 382 | JSON.stringify({ 383 | type: 'event', 384 | event: 'userEnterIdle', 385 | dataType: 'text', 386 | data: this.user.userId, 387 | }) 388 | ) 389 | } 390 | }, 391 | 392 | // 393 | // Remove a chat if you are the owner 394 | // 395 | deleteChat(chatId) { 396 | const ok = confirm('Are you sure you want to delete this chat? This will remove it for all users.') 397 | if (!ok) return 398 | 399 | this.allChats[chatId] = { ...this.allChats[chatId], name: 'DELETING...' } 400 | 401 | this.ws.send( 402 | JSON.stringify({ 403 | type: 'event', 404 | event: 'deleteChat', 405 | dataType: 'text', 406 | data: chatId, 407 | }) 408 | ) 409 | }, 410 | 411 | // 412 | // Logout and remove the guest user cookie 413 | // 414 | logout() { 415 | if (this.user.identityProvider === 'guest') { 416 | localStorage.removeItem('guestUser') 417 | window.location.href = '/login.html' 418 | return 419 | } 420 | 421 | window.location.href = '/.auth/logout?post_logout_redirect_uri=/login.html' 422 | }, 423 | }, 424 | }).mount('#app') 425 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Chatr - Azure Web PubSub Sample App 2 | 3 | This is a demonstration & sample application designed to be a simple multi-user web based chat system. 4 | It provides persistent group chats, user to user private chats, a user list, idle (away from keyboard) detection and several other features. 5 | 6 | It is built on several Azure technologies, including: _Web PubSub, Functions, Static Web Apps_ and _Table Storage_ 7 | 8 | > 👀 Note. This was created as a personal project, created to aid learning while building something interesting. The code comes with all the caveats you might expect from such a project. 9 | 10 | ![](https://img.shields.io/github/license/benc-uk/chatr) 11 | ![](https://img.shields.io/github/last-commit/benc-uk/chatr) 12 | ![](https://img.shields.io/github/checks-status/benc-uk/chatr/main) 13 | 14 | Goals: 15 | 16 | - Learn about using websockets 17 | - Write a 'fun' thing 18 | - Try out the new _Azure Web PubSub_ service 19 | - Use the authentication features of _Azure Static Web Apps_ 20 | - Deploy everything using _Azure Bicep_ 21 | - No connection strings, managed identity for everything 22 | 23 | Use cases & key features: 24 | 25 | - Sign-in with Microsoft, Twitter or GitHub accounts 26 | - Realtime chat with users 27 | - Shared group chats, only the creator can remove the chat 28 | - Detects where users are idle and away from keyboard (default is one minute) 29 | - Private 'user to user' chats, with notifications and popups 30 | 31 | # 🌐 Live Version 32 | 33 | Try the application out here: 34 | [chatr.benco.io](https://chatr.benco.io) 35 | 36 | # 🖼️ Screenshot 37 | 38 | ![](./etc/screen.png) 39 | 40 | # 🗺️ Architecture 41 | 42 | ![](./etc/architecture-diagram.drawio.png) 43 | 44 | # 🧑‍💻 Client / Frontend 45 | 46 | This is the main web frontend as used by end users via the browser. 47 | 48 | The source for this is found in **client/** and consists of a static standalone pure ES6 JS application, no bundling or Node.js is required. It is written using [Vue.js as a supporting framework](https://vuejs.org/), and [Bulma as a CSS framework](https://bulma.io/). 49 | 50 | Some notes: 51 | 52 | - Vue.js is used as a browser side library loaded from CDN as a ESM module, this is an elegant & lightweight approach supported by modern browsers, rather than the usual style of SPA app which requires Node and webpack etc. 53 | - No bundling/build. ES6 modules are used so the various JS files can use import/export without the need to bundle. 54 | - `client/js/app.js` shows how to create a Vue.js app with child components using this approach. The majority of client logic is here. 55 | - `client/js/components/chat.js` is a Vue.js component used to host each chat tab in the application 56 | - The special `.auth/` endpoint provided by Static Web Apps is used to sign users in and fetch their user details, such as userId. 57 | 58 | # ⛽ Server 59 | 60 | This is the backend, handling websocket events to and from Azure Web PubSub, and providing REST API for some operations. 61 | 62 | The source for this is found in **api/** and consists of a Node.js Azure Function App using the v4 programming model. It connects to Azure Table Storage to persist group chat and user data (Table Storage was picked as it's simple & cheap). This is not hosted in a standalone Azure Function App but instead [deployed into the Static Web App as part of it's serverless API support](https://docs.microsoft.com/en-us/azure/static-web-apps/apis) 63 | 64 | There are four HTTP functions all served from the default `/api/` path 65 | 66 | - `eventHandler` - Webhook receiver for "upstream" events sent from Azure Web PubSub service, contains the majority of application logic. Not called directly by the client, only Azure WebPub Sub. 67 | - `getToken` - Called by the client to get an access token and URL to connect via WebSockets to the Azure Web PubSub service. Must be called with userId in the URL query, e.g. GET `/api/getToken?userId={user}` 68 | - `users` - Returns a list of signed in users from state 69 | - `chats` - Returns a list of active group chats from state 70 | 71 | State is handled with `state.js` which is an ES6 module exporting functions supporting state CRUD for users and chats. This module carries out all the interaction with Azure Tables, and provides a relatively transparent interface, so a different storage backend could be swapped in. 72 | 73 | ## WebSocket & API Message Flows 74 | 75 | There is two way message flow between clients and the server via [Azure Web PubSub and event handlers](https://learn.microsoft.com/en-gb/azure/azure-web-pubsub/howto-develop-eventhandler) 76 | 77 | [The json.webpubsub.azure.v1 subprotocol is used](https://learn.microsoft.com/en-gb/azure/azure-web-pubsub/reference-json-webpubsub-subprotocol) rather than basic WebSockets, this provides a number of features: users can be added to groups, clients can send custom events (using `type: event`), and also send messages direct to other clients without going via the server (using `type: sendToGroup`) 78 | 79 | Notes: 80 | 81 | - Chat IDs are simply randomly generated GUIDs, and these correspond 1:1 with "groups" in the subprotocol. 82 | - Private chats are a special case, they are not persisted in state, and they do not trigger **chatCreated** events. Also the user doesn't issue a **joinChat** event to join them, that is handled by the server as a kind of "push" to the clients. 83 | - User IDs are simply strings which are considered to be unique, this could be improved, e.g. with prefixing. 84 | 85 | ### Client Messaging 86 | 87 | Events & chat are sent using the _json.webpubsub.azure.v1_ subprotocol 88 | 89 | Chat messages sent from the client use `sendToGroup` and a custom JSON payload with three fields `message`, `fromUserId` & `fromUserName`, these messages are relayed client to client by Azure Web PubSub, the server is never notified of them: 90 | 91 | ```go 92 | { 93 | type: 'sendToGroup', 94 | group: , 95 | dataType: 'json', 96 | data: { 97 | message: , 98 | fromUserId: , 99 | fromUserName: , 100 | }, 101 | } 102 | ``` 103 | 104 | Events destined for the backend server are sent as WebSocket messages from the client via the same subprotocol with the `event` type, and an application specific sub-type, e.g. 105 | 106 | ```go 107 | { 108 | type: 'event', 109 | event: 'joinChat', 110 | dataType: 'text', 111 | data: , 112 | } 113 | ``` 114 | 115 | The types of events are: 116 | 117 | - **createChat** - Request the server you want to create a group chat 118 | - **createPrivateChat** - Request the server you want to create a private chat 119 | - **joinChat** - To join a chat, the server will add user to the group for that chatId 120 | - **leaveChat** - To leave a group chat 121 | - **deleteChat** - Called from a chat owner to delete a chat 122 | - **userEnterIdle** - Let the server know user is now idle 123 | - **userExitIdle** - Let the server know user is no longer idle 124 | 125 | The backend API `eventHandler` function has cases for each of these user events, along with handlers for connection & disconnection system events. 126 | 127 | ### Server Messaging 128 | 129 | Messages sent from the server have a custom Chatr app specific payload as follows: 130 | 131 | ```go 132 | { 133 | chatEvent: , 134 | data: 135 | } 136 | ``` 137 | 138 | Where `eventType` is one of: 139 | 140 | - **chatCreated** - Let all users know a new group chat has been created 141 | - **chatDeleted** - Let all users know a group chat has been removed 142 | - **userOnline** - Let all users know a user has come online 143 | - **userOffline** - Let all users know a user has left 144 | - **joinPrivateChat** - Sent to both the initiator and recipient of a private chat 145 | - **userIsIdle** - Sent to all users when a user enters idle state 146 | - **userNotIdle** - Sent to all users when a user exits idle state 147 | 148 | The client code in `client/js/app.js` handles these messages as they are received by the client, and reacts accordingly. 149 | 150 | # ✨ Some Notes on Design and Service Choice 151 | 152 | The plan of this project was to use _Azure Web PubSub_ and _Azure Static Web Apps_, and to host the server side component as a set of serverless HTTP functions in _Azure Functions_. _Azure Static Web Apps_ was selected because it has [amazing support for codeless and config-less user sign-in and auth](https://docs.microsoft.com/en-us/azure/static-web-apps/authentication-authorization), which I wanted to leverage. 153 | 154 | Some comments on this approach: 155 | 156 | - New: Originally the fully managed API support in Static Web Apps was used, however as the project switched to using managed identity for everything, a side effect of this was a need to move to a standalone external Function App, [as managed identity is not supported in managed mode](https://learn.microsoft.com/en-gb/azure/static-web-apps/apis-functions). 157 | - A decision was made to create a HTTP function to act as a webhook event handler instead of using the provided `webPubSubConnection` binding. This is partly historical now (see above bullet), but it sill remains a valid approach. For sending messages back to Web PubSub, the server SDK can simply be used within the function code rather than using the `webPubSub` output binding. 158 | - Table Storage was picked for persisting state as it has a good JS SDK (the new SDK in @azure/data-table was used), it's extremely lightweight and cheap and was good enough for this project, see details below 159 | 160 | # 📚 State & Entity Design 161 | 162 | State in Azure Tables consists of two tables (collections) named `chats` and `users` 163 | 164 | ### Chats Table 165 | 166 | As each chat contains nested objects inside the members field, each chat is stored as a JSON string in a field called `data`. The PartitionKey is not used and hardcoded to a string "chatr". The RowKey and the id field inside the data object are the same. 167 | 168 | - **PartitionKey**: "chatr" 169 | - **RowKey**: The chatId (random GUID created client side) 170 | - **data**: JSON stringified chat entity 171 | 172 | Example of a chat data entity 173 | 174 | ```json 175 | { 176 | "id": "eab4b030-1a3d-499a-bd89-191578395910", 177 | "name": "This is a group chat", 178 | "members": { 179 | "0987654321": { 180 | "userId": "0987654321", 181 | "userName": "Another Guy" 182 | }, 183 | "1234567890": { 184 | "userId": "1234567890", 185 | "userName": "Ben" 186 | } 187 | }, 188 | "owner": "1234567890" 189 | } 190 | ``` 191 | 192 | ### Users Table 193 | 194 | Users are stored as entities with the fields (columns) described below. As there are no nested fields, there is no need to encode as a JSON string. Again the PartitionKey is not used and hardcoded to a string "chatr". 195 | 196 | - **PartitionKey**: "chatr" 197 | - **RowKey**: The `userId` field returned from Static Web Apps auth endpoint 198 | - **userName**: The username (could be email address or handle) of the user 199 | - **userProvider**: Which auth provided the user signed in with `twitter`, `aad` or `github` 200 | - **idle**: Boolean, indicating if the user us currently idle 201 | 202 | # 🛠️ Running and Deploying the App 203 | 204 | ## Working Locally 205 | 206 | See makefile 207 | 208 | ```text 209 | $ make 210 | help 💬 This help message 211 | lint 🔎 Lint & format, will not fix but sets exit code on error 212 | lint-fix 📜 Lint & format, will try to fix errors and modify code 213 | run 🏃 Run server locally using SWA CLI 214 | clean 🧹 Clean up project 215 | deploy-infra 🚀 Deploy required infra in Azure using Bicep 216 | deploy-api 🚀 Deploy API to Azure using Function Core Tools 217 | deploy-client 🚀 Deploy client to Azure using SWA CLI 218 | deploy 🚀 Deploy everything! 219 | tunnel 🚇 Start AWPS local tunnel tool for local development 220 | ``` 221 | 222 | ## Deploying to Azure 223 | 224 | Deployment is slightly complex due to the number of components and the configuration between them. The makefile target `deploy` should deploy everything for you in a single step using Bicep templates in the **deploy/** folder 225 | 226 | [See readme in deploy folder for details and instructions](./deploy) 227 | 228 | ## Running Locally 229 | 230 | This requires a little effort as the Azure Web PubSub service needs to be able call the HTTP endpoint on your local machine, plus several role assignments & configs need to be setup, see below. The fabulous Azure Web PubSub local tunnel tool does a great job of providing a way to to tunnel the Azure Web PubSub service to your local machine, so you can run the app locally and test it. 231 | 232 | When running locally the Static Web Apps CLI is used and this provides a nice fake user authentication endpoint for us and will also run the API. 233 | 234 | ### Pre-Reqs 235 | 236 | If these pre-reqs look a little daunting, don't worry, just use the dev container in the repo, this has everything you need to run and deploy the app. 237 | 238 | - Linux / MacOS / WSL with bash make etc 239 | - Node.js & npm 240 | - [Static Web Apps CLI](https://azure.github.io/static-web-apps-cli/) 241 | - [Azure Web PubSub local tunnel tool](https://learn.microsoft.com/en-gb/azure/azure-web-pubsub/howto-web-pubsub-tunnel-tool?tabs=bash) 242 | - _Optional, deployment only_: [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) 243 | - _Optional, deployment only_: [Function Core Tools](https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local) 244 | 245 | ### Summary 246 | 247 | A short summary of the steps to getting it running: 248 | 249 | - Deploy an _Azure Storage_ account, ensure it has public network access. 250 | - Deploy an _Azure Web Pub Sub_ instance into the same resource group, also ensure it has public access. 251 | - Role assignments: 252 | - Assign yourself the 'Web PubSub Service Owner' role on the Web Pub Sub resource 253 | - Assign yourself the 'Storage Table Data Contributor' role on the Storage Account 254 | - Copy `api/local.settings.sample.json` to `api/local.settings.json` and edit the required settings values. 255 | - In _Azure Web Pub Sub_ settings. Go into the 'Settings' section 256 | - Add a hub named **"chat"** (don't pick a different name, and don't include the quotes) 257 | - Add an event handler, by clicking 'Add': 258 | - In 'URL Template Type' select "Tunnel traffic to local" 259 | - For the URL template set it to **"api/eventHandler"** (no quotes) 260 | - Under system events tick **connected** and **disconnected** 261 | - Leave everything else alone :) 262 | - Check the values at the top of the `makefile` 263 | - `AZURE_PREFIX` should be the name of the _Azure Web Pub Sub_ resource 264 | - `AZURE_RESGRP` should be the resource group you deployed into 265 | - Rather than edit the `makefile` you can pass these values in after the make command or set them as env vars. 266 | - Make sure you have the Azure CLI logged in to your account, and the correct subscription selected. 267 | - Run `make run` 268 | - Open a second terminal/shell and run `make tunnel` 269 | - Open `http://localhost:4280/` 270 | 271 | # 🥲 Known Issues 272 | 273 | - Won't run in Firefox as top level await is not yet supported 274 | -------------------------------------------------------------------------------- /api/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatr-serverless-api", 3 | "version": "0.2.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "chatr-serverless-api", 9 | "version": "0.2.0", 10 | "dependencies": { 11 | "@azure/data-tables": "^13.3.0", 12 | "@azure/functions": "^4.0.0", 13 | "@azure/identity": "^4.3.0", 14 | "@azure/web-pubsub": "^1.1.4", 15 | "jsonwebtoken": "^9.0.2", 16 | "jwks-rsa": "^3.2.0" 17 | }, 18 | "devDependencies": { 19 | "@eslint/js": "^9.26.0", 20 | "eslint": "^9.26.0", 21 | "globals": "^16.1.0", 22 | "prettier": "^3.5.3" 23 | } 24 | }, 25 | "node_modules/@azure/abort-controller": { 26 | "version": "2.1.2", 27 | "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", 28 | "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", 29 | "license": "MIT", 30 | "dependencies": { 31 | "tslib": "^2.6.2" 32 | }, 33 | "engines": { 34 | "node": ">=18.0.0" 35 | } 36 | }, 37 | "node_modules/@azure/core-auth": { 38 | "version": "1.9.0", 39 | "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", 40 | "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", 41 | "license": "MIT", 42 | "dependencies": { 43 | "@azure/abort-controller": "^2.0.0", 44 | "@azure/core-util": "^1.11.0", 45 | "tslib": "^2.6.2" 46 | }, 47 | "engines": { 48 | "node": ">=18.0.0" 49 | } 50 | }, 51 | "node_modules/@azure/core-client": { 52 | "version": "1.9.4", 53 | "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.4.tgz", 54 | "integrity": "sha512-f7IxTD15Qdux30s2qFARH+JxgwxWLG2Rlr4oSkPGuLWm+1p5y1+C04XGLA0vmX6EtqfutmjvpNmAfgwVIS5hpw==", 55 | "license": "MIT", 56 | "dependencies": { 57 | "@azure/abort-controller": "^2.0.0", 58 | "@azure/core-auth": "^1.4.0", 59 | "@azure/core-rest-pipeline": "^1.20.0", 60 | "@azure/core-tracing": "^1.0.0", 61 | "@azure/core-util": "^1.6.1", 62 | "@azure/logger": "^1.0.0", 63 | "tslib": "^2.6.2" 64 | }, 65 | "engines": { 66 | "node": ">=18.0.0" 67 | } 68 | }, 69 | "node_modules/@azure/core-paging": { 70 | "version": "1.6.2", 71 | "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.2.tgz", 72 | "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", 73 | "license": "MIT", 74 | "dependencies": { 75 | "tslib": "^2.6.2" 76 | }, 77 | "engines": { 78 | "node": ">=18.0.0" 79 | } 80 | }, 81 | "node_modules/@azure/core-rest-pipeline": { 82 | "version": "1.20.0", 83 | "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz", 84 | "integrity": "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==", 85 | "license": "MIT", 86 | "dependencies": { 87 | "@azure/abort-controller": "^2.0.0", 88 | "@azure/core-auth": "^1.8.0", 89 | "@azure/core-tracing": "^1.0.1", 90 | "@azure/core-util": "^1.11.0", 91 | "@azure/logger": "^1.0.0", 92 | "@typespec/ts-http-runtime": "^0.2.2", 93 | "tslib": "^2.6.2" 94 | }, 95 | "engines": { 96 | "node": ">=18.0.0" 97 | } 98 | }, 99 | "node_modules/@azure/core-tracing": { 100 | "version": "1.2.0", 101 | "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", 102 | "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", 103 | "license": "MIT", 104 | "dependencies": { 105 | "tslib": "^2.6.2" 106 | }, 107 | "engines": { 108 | "node": ">=18.0.0" 109 | } 110 | }, 111 | "node_modules/@azure/core-util": { 112 | "version": "1.12.0", 113 | "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.12.0.tgz", 114 | "integrity": "sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==", 115 | "license": "MIT", 116 | "dependencies": { 117 | "@azure/abort-controller": "^2.0.0", 118 | "@typespec/ts-http-runtime": "^0.2.2", 119 | "tslib": "^2.6.2" 120 | }, 121 | "engines": { 122 | "node": ">=18.0.0" 123 | } 124 | }, 125 | "node_modules/@azure/core-xml": { 126 | "version": "1.4.5", 127 | "resolved": "https://registry.npmjs.org/@azure/core-xml/-/core-xml-1.4.5.tgz", 128 | "integrity": "sha512-gT4H8mTaSXRz7eGTuQyq1aIJnJqeXzpOe9Ay7Z3FrCouer14CbV3VzjnJrNrQfbBpGBLO9oy8BmrY75A0p53cA==", 129 | "license": "MIT", 130 | "dependencies": { 131 | "fast-xml-parser": "^5.0.7", 132 | "tslib": "^2.8.1" 133 | }, 134 | "engines": { 135 | "node": ">=18.0.0" 136 | } 137 | }, 138 | "node_modules/@azure/data-tables": { 139 | "version": "13.3.0", 140 | "resolved": "https://registry.npmjs.org/@azure/data-tables/-/data-tables-13.3.0.tgz", 141 | "integrity": "sha512-g5dbhURt151j1sEbAaAkQ5eVXiZeyZxzrmIO2q9qpvzdOiijRt+jmo8Nmy0QaMb1uGgPeEHtBaKjgS6lxN3/NA==", 142 | "license": "MIT", 143 | "dependencies": { 144 | "@azure/core-auth": "^1.3.0", 145 | "@azure/core-client": "^1.0.0", 146 | "@azure/core-paging": "^1.1.1", 147 | "@azure/core-rest-pipeline": "^1.1.0", 148 | "@azure/core-tracing": "^1.0.0", 149 | "@azure/core-util": "^1.6.1", 150 | "@azure/core-xml": "^1.0.0", 151 | "@azure/logger": "^1.0.0", 152 | "tslib": "^2.2.0" 153 | }, 154 | "engines": { 155 | "node": ">=18.0.0" 156 | } 157 | }, 158 | "node_modules/@azure/functions": { 159 | "version": "4.7.0", 160 | "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.7.0.tgz", 161 | "integrity": "sha512-y1caGX6LYrA7msAckQVb/quFOhWHSPiGzWfIML17t0ee2ydpinJTBF+Sti+URfLAH63dtmXJR4meraZkhhK9tw==", 162 | "license": "MIT", 163 | "dependencies": { 164 | "cookie": "^0.7.0", 165 | "long": "^4.0.0", 166 | "undici": "^5.13.0" 167 | }, 168 | "engines": { 169 | "node": ">=18.0" 170 | } 171 | }, 172 | "node_modules/@azure/identity": { 173 | "version": "4.9.1", 174 | "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.9.1.tgz", 175 | "integrity": "sha512-986D7Cf1AOwYqSDtO/FnMAyk/Jc8qpftkGsxuehoh4F85MhQ4fICBGX/44+X1y78lN4Sqib3Bsoaoh/FvOGgmg==", 176 | "license": "MIT", 177 | "dependencies": { 178 | "@azure/abort-controller": "^2.0.0", 179 | "@azure/core-auth": "^1.9.0", 180 | "@azure/core-client": "^1.9.2", 181 | "@azure/core-rest-pipeline": "^1.17.0", 182 | "@azure/core-tracing": "^1.0.0", 183 | "@azure/core-util": "^1.11.0", 184 | "@azure/logger": "^1.0.0", 185 | "@azure/msal-browser": "^4.2.0", 186 | "@azure/msal-node": "^3.5.0", 187 | "open": "^10.1.0", 188 | "tslib": "^2.2.0" 189 | }, 190 | "engines": { 191 | "node": ">=18.0.0" 192 | } 193 | }, 194 | "node_modules/@azure/logger": { 195 | "version": "1.2.0", 196 | "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.2.0.tgz", 197 | "integrity": "sha512-0hKEzLhpw+ZTAfNJyRrn6s+V0nDWzXk9OjBr2TiGIu0OfMr5s2V4FpKLTAK3Ca5r5OKLbf4hkOGDPyiRjie/jA==", 198 | "license": "MIT", 199 | "dependencies": { 200 | "@typespec/ts-http-runtime": "^0.2.2", 201 | "tslib": "^2.6.2" 202 | }, 203 | "engines": { 204 | "node": ">=18.0.0" 205 | } 206 | }, 207 | "node_modules/@azure/msal-browser": { 208 | "version": "4.12.0", 209 | "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.12.0.tgz", 210 | "integrity": "sha512-WD1lmVWchg7wn1mI7Tr4v7QPyTwK+8Nuyje3jRpOFENLRLEBsdK8VVdTw3C+TypZmYn4cOAdj3zREnuFXgvfIA==", 211 | "license": "MIT", 212 | "dependencies": { 213 | "@azure/msal-common": "15.6.0" 214 | }, 215 | "engines": { 216 | "node": ">=0.8.0" 217 | } 218 | }, 219 | "node_modules/@azure/msal-common": { 220 | "version": "15.6.0", 221 | "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.6.0.tgz", 222 | "integrity": "sha512-EotmBz42apYGjqiIV9rDUdptaMptpTn4TdGf3JfjLvFvinSe9BJ6ywU92K9ky+t/b0ghbeTSe9RfqlgLh8f2jA==", 223 | "license": "MIT", 224 | "engines": { 225 | "node": ">=0.8.0" 226 | } 227 | }, 228 | "node_modules/@azure/msal-node": { 229 | "version": "3.5.3", 230 | "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.5.3.tgz", 231 | "integrity": "sha512-c5mifzHX5mwm5JqMIlURUyp6LEEdKF1a8lmcNRLBo0lD7zpSYPHupa4jHyhJyg9ccLwszLguZJdk2h3ngnXwNw==", 232 | "license": "MIT", 233 | "dependencies": { 234 | "@azure/msal-common": "15.6.0", 235 | "jsonwebtoken": "^9.0.0", 236 | "uuid": "^8.3.0" 237 | }, 238 | "engines": { 239 | "node": ">=16" 240 | } 241 | }, 242 | "node_modules/@azure/web-pubsub": { 243 | "version": "1.1.4", 244 | "resolved": "https://registry.npmjs.org/@azure/web-pubsub/-/web-pubsub-1.1.4.tgz", 245 | "integrity": "sha512-ePRe4OtKmexm5ZJd3D9+JhSuP4XovNaZ8eJftUBoYJca6msl5Qna99dcq+eKJc/pPMXHEMSw+3+FWnSbTDMkkA==", 246 | "license": "MIT", 247 | "dependencies": { 248 | "@azure/core-auth": "^1.9.0", 249 | "@azure/core-client": "^1.9.2", 250 | "@azure/core-rest-pipeline": "^1.19.0", 251 | "@azure/core-tracing": "^1.2.0", 252 | "@azure/logger": "^1.1.4", 253 | "jsonwebtoken": "^9.0.2", 254 | "tslib": "^2.8.1" 255 | }, 256 | "engines": { 257 | "node": ">=18.0.0" 258 | } 259 | }, 260 | "node_modules/@eslint-community/eslint-utils": { 261 | "version": "4.7.0", 262 | "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", 263 | "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", 264 | "dev": true, 265 | "license": "MIT", 266 | "dependencies": { 267 | "eslint-visitor-keys": "^3.4.3" 268 | }, 269 | "engines": { 270 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 271 | }, 272 | "funding": { 273 | "url": "https://opencollective.com/eslint" 274 | }, 275 | "peerDependencies": { 276 | "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 277 | } 278 | }, 279 | "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 280 | "version": "3.4.3", 281 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 282 | "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 283 | "dev": true, 284 | "license": "Apache-2.0", 285 | "engines": { 286 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 287 | }, 288 | "funding": { 289 | "url": "https://opencollective.com/eslint" 290 | } 291 | }, 292 | "node_modules/@eslint-community/regexpp": { 293 | "version": "4.12.1", 294 | "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", 295 | "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", 296 | "dev": true, 297 | "license": "MIT", 298 | "engines": { 299 | "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 300 | } 301 | }, 302 | "node_modules/@eslint/config-array": { 303 | "version": "0.20.0", 304 | "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", 305 | "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", 306 | "dev": true, 307 | "license": "Apache-2.0", 308 | "dependencies": { 309 | "@eslint/object-schema": "^2.1.6", 310 | "debug": "^4.3.1", 311 | "minimatch": "^3.1.2" 312 | }, 313 | "engines": { 314 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 315 | } 316 | }, 317 | "node_modules/@eslint/config-helpers": { 318 | "version": "0.2.2", 319 | "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", 320 | "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", 321 | "dev": true, 322 | "license": "Apache-2.0", 323 | "engines": { 324 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 325 | } 326 | }, 327 | "node_modules/@eslint/core": { 328 | "version": "0.13.0", 329 | "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", 330 | "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", 331 | "dev": true, 332 | "license": "Apache-2.0", 333 | "dependencies": { 334 | "@types/json-schema": "^7.0.15" 335 | }, 336 | "engines": { 337 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 338 | } 339 | }, 340 | "node_modules/@eslint/eslintrc": { 341 | "version": "3.3.1", 342 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", 343 | "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", 344 | "dev": true, 345 | "license": "MIT", 346 | "dependencies": { 347 | "ajv": "^6.12.4", 348 | "debug": "^4.3.2", 349 | "espree": "^10.0.1", 350 | "globals": "^14.0.0", 351 | "ignore": "^5.2.0", 352 | "import-fresh": "^3.2.1", 353 | "js-yaml": "^4.1.0", 354 | "minimatch": "^3.1.2", 355 | "strip-json-comments": "^3.1.1" 356 | }, 357 | "engines": { 358 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 359 | }, 360 | "funding": { 361 | "url": "https://opencollective.com/eslint" 362 | } 363 | }, 364 | "node_modules/@eslint/eslintrc/node_modules/globals": { 365 | "version": "14.0.0", 366 | "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 367 | "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 368 | "dev": true, 369 | "license": "MIT", 370 | "engines": { 371 | "node": ">=18" 372 | }, 373 | "funding": { 374 | "url": "https://github.com/sponsors/sindresorhus" 375 | } 376 | }, 377 | "node_modules/@eslint/js": { 378 | "version": "9.26.0", 379 | "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", 380 | "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", 381 | "dev": true, 382 | "license": "MIT", 383 | "engines": { 384 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 385 | } 386 | }, 387 | "node_modules/@eslint/object-schema": { 388 | "version": "2.1.6", 389 | "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", 390 | "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", 391 | "dev": true, 392 | "license": "Apache-2.0", 393 | "engines": { 394 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 395 | } 396 | }, 397 | "node_modules/@eslint/plugin-kit": { 398 | "version": "0.2.8", 399 | "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", 400 | "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", 401 | "dev": true, 402 | "license": "Apache-2.0", 403 | "dependencies": { 404 | "@eslint/core": "^0.13.0", 405 | "levn": "^0.4.1" 406 | }, 407 | "engines": { 408 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 409 | } 410 | }, 411 | "node_modules/@fastify/busboy": { 412 | "version": "2.1.1", 413 | "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", 414 | "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", 415 | "license": "MIT", 416 | "engines": { 417 | "node": ">=14" 418 | } 419 | }, 420 | "node_modules/@humanfs/core": { 421 | "version": "0.19.1", 422 | "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 423 | "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 424 | "dev": true, 425 | "license": "Apache-2.0", 426 | "engines": { 427 | "node": ">=18.18.0" 428 | } 429 | }, 430 | "node_modules/@humanfs/node": { 431 | "version": "0.16.6", 432 | "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", 433 | "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", 434 | "dev": true, 435 | "license": "Apache-2.0", 436 | "dependencies": { 437 | "@humanfs/core": "^0.19.1", 438 | "@humanwhocodes/retry": "^0.3.0" 439 | }, 440 | "engines": { 441 | "node": ">=18.18.0" 442 | } 443 | }, 444 | "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { 445 | "version": "0.3.1", 446 | "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", 447 | "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", 448 | "dev": true, 449 | "license": "Apache-2.0", 450 | "engines": { 451 | "node": ">=18.18" 452 | }, 453 | "funding": { 454 | "type": "github", 455 | "url": "https://github.com/sponsors/nzakas" 456 | } 457 | }, 458 | "node_modules/@humanwhocodes/module-importer": { 459 | "version": "1.0.1", 460 | "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 461 | "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 462 | "dev": true, 463 | "license": "Apache-2.0", 464 | "engines": { 465 | "node": ">=12.22" 466 | }, 467 | "funding": { 468 | "type": "github", 469 | "url": "https://github.com/sponsors/nzakas" 470 | } 471 | }, 472 | "node_modules/@humanwhocodes/retry": { 473 | "version": "0.4.3", 474 | "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 475 | "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 476 | "dev": true, 477 | "license": "Apache-2.0", 478 | "engines": { 479 | "node": ">=18.18" 480 | }, 481 | "funding": { 482 | "type": "github", 483 | "url": "https://github.com/sponsors/nzakas" 484 | } 485 | }, 486 | "node_modules/@modelcontextprotocol/sdk": { 487 | "version": "1.11.0", 488 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.0.tgz", 489 | "integrity": "sha512-k/1pb70eD638anoi0e8wUGAlbMJXyvdV4p62Ko+EZ7eBe1xMx8Uhak1R5DgfoofsK5IBBnRwsYGTaLZl+6/+RQ==", 490 | "dev": true, 491 | "license": "MIT", 492 | "dependencies": { 493 | "content-type": "^1.0.5", 494 | "cors": "^2.8.5", 495 | "cross-spawn": "^7.0.3", 496 | "eventsource": "^3.0.2", 497 | "express": "^5.0.1", 498 | "express-rate-limit": "^7.5.0", 499 | "pkce-challenge": "^5.0.0", 500 | "raw-body": "^3.0.0", 501 | "zod": "^3.23.8", 502 | "zod-to-json-schema": "^3.24.1" 503 | }, 504 | "engines": { 505 | "node": ">=18" 506 | } 507 | }, 508 | "node_modules/@types/body-parser": { 509 | "version": "1.19.5", 510 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", 511 | "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", 512 | "license": "MIT", 513 | "dependencies": { 514 | "@types/connect": "*", 515 | "@types/node": "*" 516 | } 517 | }, 518 | "node_modules/@types/connect": { 519 | "version": "3.4.38", 520 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", 521 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", 522 | "license": "MIT", 523 | "dependencies": { 524 | "@types/node": "*" 525 | } 526 | }, 527 | "node_modules/@types/estree": { 528 | "version": "1.0.7", 529 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 530 | "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 531 | "dev": true, 532 | "license": "MIT" 533 | }, 534 | "node_modules/@types/express": { 535 | "version": "4.17.21", 536 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", 537 | "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", 538 | "license": "MIT", 539 | "dependencies": { 540 | "@types/body-parser": "*", 541 | "@types/express-serve-static-core": "^4.17.33", 542 | "@types/qs": "*", 543 | "@types/serve-static": "*" 544 | } 545 | }, 546 | "node_modules/@types/express-serve-static-core": { 547 | "version": "4.19.6", 548 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", 549 | "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", 550 | "license": "MIT", 551 | "dependencies": { 552 | "@types/node": "*", 553 | "@types/qs": "*", 554 | "@types/range-parser": "*", 555 | "@types/send": "*" 556 | } 557 | }, 558 | "node_modules/@types/http-errors": { 559 | "version": "2.0.4", 560 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", 561 | "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", 562 | "license": "MIT" 563 | }, 564 | "node_modules/@types/json-schema": { 565 | "version": "7.0.15", 566 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 567 | "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 568 | "dev": true, 569 | "license": "MIT" 570 | }, 571 | "node_modules/@types/jsonwebtoken": { 572 | "version": "9.0.9", 573 | "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", 574 | "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", 575 | "license": "MIT", 576 | "dependencies": { 577 | "@types/ms": "*", 578 | "@types/node": "*" 579 | } 580 | }, 581 | "node_modules/@types/mime": { 582 | "version": "1.3.5", 583 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", 584 | "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", 585 | "license": "MIT" 586 | }, 587 | "node_modules/@types/ms": { 588 | "version": "2.1.0", 589 | "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", 590 | "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", 591 | "license": "MIT" 592 | }, 593 | "node_modules/@types/node": { 594 | "version": "22.15.15", 595 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.15.tgz", 596 | "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", 597 | "license": "MIT", 598 | "dependencies": { 599 | "undici-types": "~6.21.0" 600 | } 601 | }, 602 | "node_modules/@types/qs": { 603 | "version": "6.9.18", 604 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", 605 | "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", 606 | "license": "MIT" 607 | }, 608 | "node_modules/@types/range-parser": { 609 | "version": "1.2.7", 610 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", 611 | "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", 612 | "license": "MIT" 613 | }, 614 | "node_modules/@types/send": { 615 | "version": "0.17.4", 616 | "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", 617 | "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", 618 | "license": "MIT", 619 | "dependencies": { 620 | "@types/mime": "^1", 621 | "@types/node": "*" 622 | } 623 | }, 624 | "node_modules/@types/serve-static": { 625 | "version": "1.15.7", 626 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", 627 | "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", 628 | "license": "MIT", 629 | "dependencies": { 630 | "@types/http-errors": "*", 631 | "@types/node": "*", 632 | "@types/send": "*" 633 | } 634 | }, 635 | "node_modules/@typespec/ts-http-runtime": { 636 | "version": "0.2.2", 637 | "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz", 638 | "integrity": "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==", 639 | "license": "MIT", 640 | "dependencies": { 641 | "http-proxy-agent": "^7.0.0", 642 | "https-proxy-agent": "^7.0.0", 643 | "tslib": "^2.6.2" 644 | }, 645 | "engines": { 646 | "node": ">=18.0.0" 647 | } 648 | }, 649 | "node_modules/accepts": { 650 | "version": "2.0.0", 651 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 652 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 653 | "dev": true, 654 | "license": "MIT", 655 | "dependencies": { 656 | "mime-types": "^3.0.0", 657 | "negotiator": "^1.0.0" 658 | }, 659 | "engines": { 660 | "node": ">= 0.6" 661 | } 662 | }, 663 | "node_modules/acorn": { 664 | "version": "8.14.1", 665 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", 666 | "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", 667 | "dev": true, 668 | "license": "MIT", 669 | "bin": { 670 | "acorn": "bin/acorn" 671 | }, 672 | "engines": { 673 | "node": ">=0.4.0" 674 | } 675 | }, 676 | "node_modules/acorn-jsx": { 677 | "version": "5.3.2", 678 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 679 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 680 | "dev": true, 681 | "license": "MIT", 682 | "peerDependencies": { 683 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 684 | } 685 | }, 686 | "node_modules/agent-base": { 687 | "version": "7.1.3", 688 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", 689 | "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", 690 | "license": "MIT", 691 | "engines": { 692 | "node": ">= 14" 693 | } 694 | }, 695 | "node_modules/ajv": { 696 | "version": "6.12.6", 697 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 698 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 699 | "dev": true, 700 | "license": "MIT", 701 | "dependencies": { 702 | "fast-deep-equal": "^3.1.1", 703 | "fast-json-stable-stringify": "^2.0.0", 704 | "json-schema-traverse": "^0.4.1", 705 | "uri-js": "^4.2.2" 706 | }, 707 | "funding": { 708 | "type": "github", 709 | "url": "https://github.com/sponsors/epoberezkin" 710 | } 711 | }, 712 | "node_modules/ansi-styles": { 713 | "version": "4.3.0", 714 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 715 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 716 | "dev": true, 717 | "license": "MIT", 718 | "dependencies": { 719 | "color-convert": "^2.0.1" 720 | }, 721 | "engines": { 722 | "node": ">=8" 723 | }, 724 | "funding": { 725 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 726 | } 727 | }, 728 | "node_modules/argparse": { 729 | "version": "2.0.1", 730 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 731 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 732 | "dev": true, 733 | "license": "Python-2.0" 734 | }, 735 | "node_modules/balanced-match": { 736 | "version": "1.0.2", 737 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 738 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 739 | "dev": true, 740 | "license": "MIT" 741 | }, 742 | "node_modules/body-parser": { 743 | "version": "2.2.0", 744 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", 745 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 746 | "dev": true, 747 | "license": "MIT", 748 | "dependencies": { 749 | "bytes": "^3.1.2", 750 | "content-type": "^1.0.5", 751 | "debug": "^4.4.0", 752 | "http-errors": "^2.0.0", 753 | "iconv-lite": "^0.6.3", 754 | "on-finished": "^2.4.1", 755 | "qs": "^6.14.0", 756 | "raw-body": "^3.0.0", 757 | "type-is": "^2.0.0" 758 | }, 759 | "engines": { 760 | "node": ">=18" 761 | } 762 | }, 763 | "node_modules/brace-expansion": { 764 | "version": "1.1.11", 765 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 766 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 767 | "dev": true, 768 | "license": "MIT", 769 | "dependencies": { 770 | "balanced-match": "^1.0.0", 771 | "concat-map": "0.0.1" 772 | } 773 | }, 774 | "node_modules/buffer-equal-constant-time": { 775 | "version": "1.0.1", 776 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 777 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", 778 | "license": "BSD-3-Clause" 779 | }, 780 | "node_modules/bundle-name": { 781 | "version": "4.1.0", 782 | "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", 783 | "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", 784 | "license": "MIT", 785 | "dependencies": { 786 | "run-applescript": "^7.0.0" 787 | }, 788 | "engines": { 789 | "node": ">=18" 790 | }, 791 | "funding": { 792 | "url": "https://github.com/sponsors/sindresorhus" 793 | } 794 | }, 795 | "node_modules/bytes": { 796 | "version": "3.1.2", 797 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 798 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 799 | "dev": true, 800 | "license": "MIT", 801 | "engines": { 802 | "node": ">= 0.8" 803 | } 804 | }, 805 | "node_modules/call-bind-apply-helpers": { 806 | "version": "1.0.2", 807 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 808 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 809 | "dev": true, 810 | "license": "MIT", 811 | "dependencies": { 812 | "es-errors": "^1.3.0", 813 | "function-bind": "^1.1.2" 814 | }, 815 | "engines": { 816 | "node": ">= 0.4" 817 | } 818 | }, 819 | "node_modules/call-bound": { 820 | "version": "1.0.4", 821 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 822 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 823 | "dev": true, 824 | "license": "MIT", 825 | "dependencies": { 826 | "call-bind-apply-helpers": "^1.0.2", 827 | "get-intrinsic": "^1.3.0" 828 | }, 829 | "engines": { 830 | "node": ">= 0.4" 831 | }, 832 | "funding": { 833 | "url": "https://github.com/sponsors/ljharb" 834 | } 835 | }, 836 | "node_modules/callsites": { 837 | "version": "3.1.0", 838 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 839 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 840 | "dev": true, 841 | "license": "MIT", 842 | "engines": { 843 | "node": ">=6" 844 | } 845 | }, 846 | "node_modules/chalk": { 847 | "version": "4.1.2", 848 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 849 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 850 | "dev": true, 851 | "license": "MIT", 852 | "dependencies": { 853 | "ansi-styles": "^4.1.0", 854 | "supports-color": "^7.1.0" 855 | }, 856 | "engines": { 857 | "node": ">=10" 858 | }, 859 | "funding": { 860 | "url": "https://github.com/chalk/chalk?sponsor=1" 861 | } 862 | }, 863 | "node_modules/color-convert": { 864 | "version": "2.0.1", 865 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 866 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 867 | "dev": true, 868 | "license": "MIT", 869 | "dependencies": { 870 | "color-name": "~1.1.4" 871 | }, 872 | "engines": { 873 | "node": ">=7.0.0" 874 | } 875 | }, 876 | "node_modules/color-name": { 877 | "version": "1.1.4", 878 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 879 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 880 | "dev": true, 881 | "license": "MIT" 882 | }, 883 | "node_modules/concat-map": { 884 | "version": "0.0.1", 885 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 886 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 887 | "dev": true, 888 | "license": "MIT" 889 | }, 890 | "node_modules/content-disposition": { 891 | "version": "1.0.0", 892 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 893 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 894 | "dev": true, 895 | "license": "MIT", 896 | "dependencies": { 897 | "safe-buffer": "5.2.1" 898 | }, 899 | "engines": { 900 | "node": ">= 0.6" 901 | } 902 | }, 903 | "node_modules/content-type": { 904 | "version": "1.0.5", 905 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 906 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 907 | "dev": true, 908 | "license": "MIT", 909 | "engines": { 910 | "node": ">= 0.6" 911 | } 912 | }, 913 | "node_modules/cookie": { 914 | "version": "0.7.2", 915 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 916 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 917 | "license": "MIT", 918 | "engines": { 919 | "node": ">= 0.6" 920 | } 921 | }, 922 | "node_modules/cookie-signature": { 923 | "version": "1.2.2", 924 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 925 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 926 | "dev": true, 927 | "license": "MIT", 928 | "engines": { 929 | "node": ">=6.6.0" 930 | } 931 | }, 932 | "node_modules/cors": { 933 | "version": "2.8.5", 934 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 935 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 936 | "dev": true, 937 | "license": "MIT", 938 | "dependencies": { 939 | "object-assign": "^4", 940 | "vary": "^1" 941 | }, 942 | "engines": { 943 | "node": ">= 0.10" 944 | } 945 | }, 946 | "node_modules/cross-spawn": { 947 | "version": "7.0.6", 948 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 949 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 950 | "dev": true, 951 | "license": "MIT", 952 | "dependencies": { 953 | "path-key": "^3.1.0", 954 | "shebang-command": "^2.0.0", 955 | "which": "^2.0.1" 956 | }, 957 | "engines": { 958 | "node": ">= 8" 959 | } 960 | }, 961 | "node_modules/debug": { 962 | "version": "4.4.0", 963 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 964 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 965 | "license": "MIT", 966 | "dependencies": { 967 | "ms": "^2.1.3" 968 | }, 969 | "engines": { 970 | "node": ">=6.0" 971 | }, 972 | "peerDependenciesMeta": { 973 | "supports-color": { 974 | "optional": true 975 | } 976 | } 977 | }, 978 | "node_modules/deep-is": { 979 | "version": "0.1.4", 980 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 981 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 982 | "dev": true, 983 | "license": "MIT" 984 | }, 985 | "node_modules/default-browser": { 986 | "version": "5.2.1", 987 | "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", 988 | "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", 989 | "license": "MIT", 990 | "dependencies": { 991 | "bundle-name": "^4.1.0", 992 | "default-browser-id": "^5.0.0" 993 | }, 994 | "engines": { 995 | "node": ">=18" 996 | }, 997 | "funding": { 998 | "url": "https://github.com/sponsors/sindresorhus" 999 | } 1000 | }, 1001 | "node_modules/default-browser-id": { 1002 | "version": "5.0.0", 1003 | "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", 1004 | "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", 1005 | "license": "MIT", 1006 | "engines": { 1007 | "node": ">=18" 1008 | }, 1009 | "funding": { 1010 | "url": "https://github.com/sponsors/sindresorhus" 1011 | } 1012 | }, 1013 | "node_modules/define-lazy-prop": { 1014 | "version": "3.0.0", 1015 | "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", 1016 | "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", 1017 | "license": "MIT", 1018 | "engines": { 1019 | "node": ">=12" 1020 | }, 1021 | "funding": { 1022 | "url": "https://github.com/sponsors/sindresorhus" 1023 | } 1024 | }, 1025 | "node_modules/depd": { 1026 | "version": "2.0.0", 1027 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 1028 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 1029 | "dev": true, 1030 | "license": "MIT", 1031 | "engines": { 1032 | "node": ">= 0.8" 1033 | } 1034 | }, 1035 | "node_modules/dunder-proto": { 1036 | "version": "1.0.1", 1037 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 1038 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 1039 | "dev": true, 1040 | "license": "MIT", 1041 | "dependencies": { 1042 | "call-bind-apply-helpers": "^1.0.1", 1043 | "es-errors": "^1.3.0", 1044 | "gopd": "^1.2.0" 1045 | }, 1046 | "engines": { 1047 | "node": ">= 0.4" 1048 | } 1049 | }, 1050 | "node_modules/ecdsa-sig-formatter": { 1051 | "version": "1.0.11", 1052 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", 1053 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", 1054 | "license": "Apache-2.0", 1055 | "dependencies": { 1056 | "safe-buffer": "^5.0.1" 1057 | } 1058 | }, 1059 | "node_modules/ee-first": { 1060 | "version": "1.1.1", 1061 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 1062 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 1063 | "dev": true, 1064 | "license": "MIT" 1065 | }, 1066 | "node_modules/encodeurl": { 1067 | "version": "2.0.0", 1068 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 1069 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 1070 | "dev": true, 1071 | "license": "MIT", 1072 | "engines": { 1073 | "node": ">= 0.8" 1074 | } 1075 | }, 1076 | "node_modules/es-define-property": { 1077 | "version": "1.0.1", 1078 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 1079 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 1080 | "dev": true, 1081 | "license": "MIT", 1082 | "engines": { 1083 | "node": ">= 0.4" 1084 | } 1085 | }, 1086 | "node_modules/es-errors": { 1087 | "version": "1.3.0", 1088 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 1089 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 1090 | "dev": true, 1091 | "license": "MIT", 1092 | "engines": { 1093 | "node": ">= 0.4" 1094 | } 1095 | }, 1096 | "node_modules/es-object-atoms": { 1097 | "version": "1.1.1", 1098 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 1099 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 1100 | "dev": true, 1101 | "license": "MIT", 1102 | "dependencies": { 1103 | "es-errors": "^1.3.0" 1104 | }, 1105 | "engines": { 1106 | "node": ">= 0.4" 1107 | } 1108 | }, 1109 | "node_modules/escape-html": { 1110 | "version": "1.0.3", 1111 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 1112 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 1113 | "dev": true, 1114 | "license": "MIT" 1115 | }, 1116 | "node_modules/escape-string-regexp": { 1117 | "version": "4.0.0", 1118 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 1119 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 1120 | "dev": true, 1121 | "license": "MIT", 1122 | "engines": { 1123 | "node": ">=10" 1124 | }, 1125 | "funding": { 1126 | "url": "https://github.com/sponsors/sindresorhus" 1127 | } 1128 | }, 1129 | "node_modules/eslint": { 1130 | "version": "9.26.0", 1131 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", 1132 | "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", 1133 | "dev": true, 1134 | "license": "MIT", 1135 | "dependencies": { 1136 | "@eslint-community/eslint-utils": "^4.2.0", 1137 | "@eslint-community/regexpp": "^4.12.1", 1138 | "@eslint/config-array": "^0.20.0", 1139 | "@eslint/config-helpers": "^0.2.1", 1140 | "@eslint/core": "^0.13.0", 1141 | "@eslint/eslintrc": "^3.3.1", 1142 | "@eslint/js": "9.26.0", 1143 | "@eslint/plugin-kit": "^0.2.8", 1144 | "@humanfs/node": "^0.16.6", 1145 | "@humanwhocodes/module-importer": "^1.0.1", 1146 | "@humanwhocodes/retry": "^0.4.2", 1147 | "@modelcontextprotocol/sdk": "^1.8.0", 1148 | "@types/estree": "^1.0.6", 1149 | "@types/json-schema": "^7.0.15", 1150 | "ajv": "^6.12.4", 1151 | "chalk": "^4.0.0", 1152 | "cross-spawn": "^7.0.6", 1153 | "debug": "^4.3.2", 1154 | "escape-string-regexp": "^4.0.0", 1155 | "eslint-scope": "^8.3.0", 1156 | "eslint-visitor-keys": "^4.2.0", 1157 | "espree": "^10.3.0", 1158 | "esquery": "^1.5.0", 1159 | "esutils": "^2.0.2", 1160 | "fast-deep-equal": "^3.1.3", 1161 | "file-entry-cache": "^8.0.0", 1162 | "find-up": "^5.0.0", 1163 | "glob-parent": "^6.0.2", 1164 | "ignore": "^5.2.0", 1165 | "imurmurhash": "^0.1.4", 1166 | "is-glob": "^4.0.0", 1167 | "json-stable-stringify-without-jsonify": "^1.0.1", 1168 | "lodash.merge": "^4.6.2", 1169 | "minimatch": "^3.1.2", 1170 | "natural-compare": "^1.4.0", 1171 | "optionator": "^0.9.3", 1172 | "zod": "^3.24.2" 1173 | }, 1174 | "bin": { 1175 | "eslint": "bin/eslint.js" 1176 | }, 1177 | "engines": { 1178 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1179 | }, 1180 | "funding": { 1181 | "url": "https://eslint.org/donate" 1182 | }, 1183 | "peerDependencies": { 1184 | "jiti": "*" 1185 | }, 1186 | "peerDependenciesMeta": { 1187 | "jiti": { 1188 | "optional": true 1189 | } 1190 | } 1191 | }, 1192 | "node_modules/eslint-scope": { 1193 | "version": "8.3.0", 1194 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", 1195 | "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", 1196 | "dev": true, 1197 | "license": "BSD-2-Clause", 1198 | "dependencies": { 1199 | "esrecurse": "^4.3.0", 1200 | "estraverse": "^5.2.0" 1201 | }, 1202 | "engines": { 1203 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1204 | }, 1205 | "funding": { 1206 | "url": "https://opencollective.com/eslint" 1207 | } 1208 | }, 1209 | "node_modules/eslint-visitor-keys": { 1210 | "version": "4.2.0", 1211 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", 1212 | "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", 1213 | "dev": true, 1214 | "license": "Apache-2.0", 1215 | "engines": { 1216 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1217 | }, 1218 | "funding": { 1219 | "url": "https://opencollective.com/eslint" 1220 | } 1221 | }, 1222 | "node_modules/espree": { 1223 | "version": "10.3.0", 1224 | "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", 1225 | "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", 1226 | "dev": true, 1227 | "license": "BSD-2-Clause", 1228 | "dependencies": { 1229 | "acorn": "^8.14.0", 1230 | "acorn-jsx": "^5.3.2", 1231 | "eslint-visitor-keys": "^4.2.0" 1232 | }, 1233 | "engines": { 1234 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 1235 | }, 1236 | "funding": { 1237 | "url": "https://opencollective.com/eslint" 1238 | } 1239 | }, 1240 | "node_modules/esquery": { 1241 | "version": "1.6.0", 1242 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", 1243 | "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", 1244 | "dev": true, 1245 | "license": "BSD-3-Clause", 1246 | "dependencies": { 1247 | "estraverse": "^5.1.0" 1248 | }, 1249 | "engines": { 1250 | "node": ">=0.10" 1251 | } 1252 | }, 1253 | "node_modules/esrecurse": { 1254 | "version": "4.3.0", 1255 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 1256 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 1257 | "dev": true, 1258 | "license": "BSD-2-Clause", 1259 | "dependencies": { 1260 | "estraverse": "^5.2.0" 1261 | }, 1262 | "engines": { 1263 | "node": ">=4.0" 1264 | } 1265 | }, 1266 | "node_modules/estraverse": { 1267 | "version": "5.3.0", 1268 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 1269 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 1270 | "dev": true, 1271 | "license": "BSD-2-Clause", 1272 | "engines": { 1273 | "node": ">=4.0" 1274 | } 1275 | }, 1276 | "node_modules/esutils": { 1277 | "version": "2.0.3", 1278 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 1279 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 1280 | "dev": true, 1281 | "license": "BSD-2-Clause", 1282 | "engines": { 1283 | "node": ">=0.10.0" 1284 | } 1285 | }, 1286 | "node_modules/etag": { 1287 | "version": "1.8.1", 1288 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 1289 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 1290 | "dev": true, 1291 | "license": "MIT", 1292 | "engines": { 1293 | "node": ">= 0.6" 1294 | } 1295 | }, 1296 | "node_modules/eventsource": { 1297 | "version": "3.0.6", 1298 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", 1299 | "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", 1300 | "dev": true, 1301 | "license": "MIT", 1302 | "dependencies": { 1303 | "eventsource-parser": "^3.0.1" 1304 | }, 1305 | "engines": { 1306 | "node": ">=18.0.0" 1307 | } 1308 | }, 1309 | "node_modules/eventsource-parser": { 1310 | "version": "3.0.1", 1311 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", 1312 | "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", 1313 | "dev": true, 1314 | "license": "MIT", 1315 | "engines": { 1316 | "node": ">=18.0.0" 1317 | } 1318 | }, 1319 | "node_modules/express": { 1320 | "version": "5.1.0", 1321 | "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", 1322 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 1323 | "dev": true, 1324 | "license": "MIT", 1325 | "dependencies": { 1326 | "accepts": "^2.0.0", 1327 | "body-parser": "^2.2.0", 1328 | "content-disposition": "^1.0.0", 1329 | "content-type": "^1.0.5", 1330 | "cookie": "^0.7.1", 1331 | "cookie-signature": "^1.2.1", 1332 | "debug": "^4.4.0", 1333 | "encodeurl": "^2.0.0", 1334 | "escape-html": "^1.0.3", 1335 | "etag": "^1.8.1", 1336 | "finalhandler": "^2.1.0", 1337 | "fresh": "^2.0.0", 1338 | "http-errors": "^2.0.0", 1339 | "merge-descriptors": "^2.0.0", 1340 | "mime-types": "^3.0.0", 1341 | "on-finished": "^2.4.1", 1342 | "once": "^1.4.0", 1343 | "parseurl": "^1.3.3", 1344 | "proxy-addr": "^2.0.7", 1345 | "qs": "^6.14.0", 1346 | "range-parser": "^1.2.1", 1347 | "router": "^2.2.0", 1348 | "send": "^1.1.0", 1349 | "serve-static": "^2.2.0", 1350 | "statuses": "^2.0.1", 1351 | "type-is": "^2.0.1", 1352 | "vary": "^1.1.2" 1353 | }, 1354 | "engines": { 1355 | "node": ">= 18" 1356 | }, 1357 | "funding": { 1358 | "type": "opencollective", 1359 | "url": "https://opencollective.com/express" 1360 | } 1361 | }, 1362 | "node_modules/express-rate-limit": { 1363 | "version": "7.5.0", 1364 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", 1365 | "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", 1366 | "dev": true, 1367 | "license": "MIT", 1368 | "engines": { 1369 | "node": ">= 16" 1370 | }, 1371 | "funding": { 1372 | "url": "https://github.com/sponsors/express-rate-limit" 1373 | }, 1374 | "peerDependencies": { 1375 | "express": "^4.11 || 5 || ^5.0.0-beta.1" 1376 | } 1377 | }, 1378 | "node_modules/fast-deep-equal": { 1379 | "version": "3.1.3", 1380 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 1381 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 1382 | "dev": true, 1383 | "license": "MIT" 1384 | }, 1385 | "node_modules/fast-json-stable-stringify": { 1386 | "version": "2.1.0", 1387 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 1388 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 1389 | "dev": true, 1390 | "license": "MIT" 1391 | }, 1392 | "node_modules/fast-levenshtein": { 1393 | "version": "2.0.6", 1394 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 1395 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 1396 | "dev": true, 1397 | "license": "MIT" 1398 | }, 1399 | "node_modules/fast-xml-parser": { 1400 | "version": "5.2.2", 1401 | "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.2.tgz", 1402 | "integrity": "sha512-ZaCmslH75Jkfowo/x44Uq8KT5SutC5BFxHmY61nmTXPccw11PVuIXKUqC2hembMkJ3nPwTkQESXiUlsKutCbMg==", 1403 | "funding": [ 1404 | { 1405 | "type": "github", 1406 | "url": "https://github.com/sponsors/NaturalIntelligence" 1407 | } 1408 | ], 1409 | "license": "MIT", 1410 | "dependencies": { 1411 | "strnum": "^2.1.0" 1412 | }, 1413 | "bin": { 1414 | "fxparser": "src/cli/cli.js" 1415 | } 1416 | }, 1417 | "node_modules/file-entry-cache": { 1418 | "version": "8.0.0", 1419 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 1420 | "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 1421 | "dev": true, 1422 | "license": "MIT", 1423 | "dependencies": { 1424 | "flat-cache": "^4.0.0" 1425 | }, 1426 | "engines": { 1427 | "node": ">=16.0.0" 1428 | } 1429 | }, 1430 | "node_modules/finalhandler": { 1431 | "version": "2.1.0", 1432 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 1433 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 1434 | "dev": true, 1435 | "license": "MIT", 1436 | "dependencies": { 1437 | "debug": "^4.4.0", 1438 | "encodeurl": "^2.0.0", 1439 | "escape-html": "^1.0.3", 1440 | "on-finished": "^2.4.1", 1441 | "parseurl": "^1.3.3", 1442 | "statuses": "^2.0.1" 1443 | }, 1444 | "engines": { 1445 | "node": ">= 0.8" 1446 | } 1447 | }, 1448 | "node_modules/find-up": { 1449 | "version": "5.0.0", 1450 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 1451 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 1452 | "dev": true, 1453 | "license": "MIT", 1454 | "dependencies": { 1455 | "locate-path": "^6.0.0", 1456 | "path-exists": "^4.0.0" 1457 | }, 1458 | "engines": { 1459 | "node": ">=10" 1460 | }, 1461 | "funding": { 1462 | "url": "https://github.com/sponsors/sindresorhus" 1463 | } 1464 | }, 1465 | "node_modules/flat-cache": { 1466 | "version": "4.0.1", 1467 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 1468 | "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 1469 | "dev": true, 1470 | "license": "MIT", 1471 | "dependencies": { 1472 | "flatted": "^3.2.9", 1473 | "keyv": "^4.5.4" 1474 | }, 1475 | "engines": { 1476 | "node": ">=16" 1477 | } 1478 | }, 1479 | "node_modules/flatted": { 1480 | "version": "3.3.3", 1481 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 1482 | "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 1483 | "dev": true, 1484 | "license": "ISC" 1485 | }, 1486 | "node_modules/forwarded": { 1487 | "version": "0.2.0", 1488 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 1489 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 1490 | "dev": true, 1491 | "license": "MIT", 1492 | "engines": { 1493 | "node": ">= 0.6" 1494 | } 1495 | }, 1496 | "node_modules/fresh": { 1497 | "version": "2.0.0", 1498 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 1499 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 1500 | "dev": true, 1501 | "license": "MIT", 1502 | "engines": { 1503 | "node": ">= 0.8" 1504 | } 1505 | }, 1506 | "node_modules/function-bind": { 1507 | "version": "1.1.2", 1508 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 1509 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 1510 | "dev": true, 1511 | "license": "MIT", 1512 | "funding": { 1513 | "url": "https://github.com/sponsors/ljharb" 1514 | } 1515 | }, 1516 | "node_modules/get-intrinsic": { 1517 | "version": "1.3.0", 1518 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 1519 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 1520 | "dev": true, 1521 | "license": "MIT", 1522 | "dependencies": { 1523 | "call-bind-apply-helpers": "^1.0.2", 1524 | "es-define-property": "^1.0.1", 1525 | "es-errors": "^1.3.0", 1526 | "es-object-atoms": "^1.1.1", 1527 | "function-bind": "^1.1.2", 1528 | "get-proto": "^1.0.1", 1529 | "gopd": "^1.2.0", 1530 | "has-symbols": "^1.1.0", 1531 | "hasown": "^2.0.2", 1532 | "math-intrinsics": "^1.1.0" 1533 | }, 1534 | "engines": { 1535 | "node": ">= 0.4" 1536 | }, 1537 | "funding": { 1538 | "url": "https://github.com/sponsors/ljharb" 1539 | } 1540 | }, 1541 | "node_modules/get-proto": { 1542 | "version": "1.0.1", 1543 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 1544 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 1545 | "dev": true, 1546 | "license": "MIT", 1547 | "dependencies": { 1548 | "dunder-proto": "^1.0.1", 1549 | "es-object-atoms": "^1.0.0" 1550 | }, 1551 | "engines": { 1552 | "node": ">= 0.4" 1553 | } 1554 | }, 1555 | "node_modules/glob-parent": { 1556 | "version": "6.0.2", 1557 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 1558 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 1559 | "dev": true, 1560 | "license": "ISC", 1561 | "dependencies": { 1562 | "is-glob": "^4.0.3" 1563 | }, 1564 | "engines": { 1565 | "node": ">=10.13.0" 1566 | } 1567 | }, 1568 | "node_modules/globals": { 1569 | "version": "16.1.0", 1570 | "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", 1571 | "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", 1572 | "dev": true, 1573 | "license": "MIT", 1574 | "engines": { 1575 | "node": ">=18" 1576 | }, 1577 | "funding": { 1578 | "url": "https://github.com/sponsors/sindresorhus" 1579 | } 1580 | }, 1581 | "node_modules/gopd": { 1582 | "version": "1.2.0", 1583 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 1584 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 1585 | "dev": true, 1586 | "license": "MIT", 1587 | "engines": { 1588 | "node": ">= 0.4" 1589 | }, 1590 | "funding": { 1591 | "url": "https://github.com/sponsors/ljharb" 1592 | } 1593 | }, 1594 | "node_modules/has-flag": { 1595 | "version": "4.0.0", 1596 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1597 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1598 | "dev": true, 1599 | "license": "MIT", 1600 | "engines": { 1601 | "node": ">=8" 1602 | } 1603 | }, 1604 | "node_modules/has-symbols": { 1605 | "version": "1.1.0", 1606 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 1607 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 1608 | "dev": true, 1609 | "license": "MIT", 1610 | "engines": { 1611 | "node": ">= 0.4" 1612 | }, 1613 | "funding": { 1614 | "url": "https://github.com/sponsors/ljharb" 1615 | } 1616 | }, 1617 | "node_modules/hasown": { 1618 | "version": "2.0.2", 1619 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 1620 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 1621 | "dev": true, 1622 | "license": "MIT", 1623 | "dependencies": { 1624 | "function-bind": "^1.1.2" 1625 | }, 1626 | "engines": { 1627 | "node": ">= 0.4" 1628 | } 1629 | }, 1630 | "node_modules/http-errors": { 1631 | "version": "2.0.0", 1632 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 1633 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 1634 | "dev": true, 1635 | "license": "MIT", 1636 | "dependencies": { 1637 | "depd": "2.0.0", 1638 | "inherits": "2.0.4", 1639 | "setprototypeof": "1.2.0", 1640 | "statuses": "2.0.1", 1641 | "toidentifier": "1.0.1" 1642 | }, 1643 | "engines": { 1644 | "node": ">= 0.8" 1645 | } 1646 | }, 1647 | "node_modules/http-proxy-agent": { 1648 | "version": "7.0.2", 1649 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", 1650 | "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", 1651 | "license": "MIT", 1652 | "dependencies": { 1653 | "agent-base": "^7.1.0", 1654 | "debug": "^4.3.4" 1655 | }, 1656 | "engines": { 1657 | "node": ">= 14" 1658 | } 1659 | }, 1660 | "node_modules/https-proxy-agent": { 1661 | "version": "7.0.6", 1662 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", 1663 | "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", 1664 | "license": "MIT", 1665 | "dependencies": { 1666 | "agent-base": "^7.1.2", 1667 | "debug": "4" 1668 | }, 1669 | "engines": { 1670 | "node": ">= 14" 1671 | } 1672 | }, 1673 | "node_modules/iconv-lite": { 1674 | "version": "0.6.3", 1675 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 1676 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 1677 | "dev": true, 1678 | "license": "MIT", 1679 | "dependencies": { 1680 | "safer-buffer": ">= 2.1.2 < 3.0.0" 1681 | }, 1682 | "engines": { 1683 | "node": ">=0.10.0" 1684 | } 1685 | }, 1686 | "node_modules/ignore": { 1687 | "version": "5.3.2", 1688 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 1689 | "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 1690 | "dev": true, 1691 | "license": "MIT", 1692 | "engines": { 1693 | "node": ">= 4" 1694 | } 1695 | }, 1696 | "node_modules/import-fresh": { 1697 | "version": "3.3.1", 1698 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 1699 | "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 1700 | "dev": true, 1701 | "license": "MIT", 1702 | "dependencies": { 1703 | "parent-module": "^1.0.0", 1704 | "resolve-from": "^4.0.0" 1705 | }, 1706 | "engines": { 1707 | "node": ">=6" 1708 | }, 1709 | "funding": { 1710 | "url": "https://github.com/sponsors/sindresorhus" 1711 | } 1712 | }, 1713 | "node_modules/imurmurhash": { 1714 | "version": "0.1.4", 1715 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 1716 | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 1717 | "dev": true, 1718 | "license": "MIT", 1719 | "engines": { 1720 | "node": ">=0.8.19" 1721 | } 1722 | }, 1723 | "node_modules/inherits": { 1724 | "version": "2.0.4", 1725 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1726 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1727 | "dev": true, 1728 | "license": "ISC" 1729 | }, 1730 | "node_modules/ipaddr.js": { 1731 | "version": "1.9.1", 1732 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 1733 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 1734 | "dev": true, 1735 | "license": "MIT", 1736 | "engines": { 1737 | "node": ">= 0.10" 1738 | } 1739 | }, 1740 | "node_modules/is-docker": { 1741 | "version": "3.0.0", 1742 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", 1743 | "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", 1744 | "license": "MIT", 1745 | "bin": { 1746 | "is-docker": "cli.js" 1747 | }, 1748 | "engines": { 1749 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 1750 | }, 1751 | "funding": { 1752 | "url": "https://github.com/sponsors/sindresorhus" 1753 | } 1754 | }, 1755 | "node_modules/is-extglob": { 1756 | "version": "2.1.1", 1757 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1758 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1759 | "dev": true, 1760 | "license": "MIT", 1761 | "engines": { 1762 | "node": ">=0.10.0" 1763 | } 1764 | }, 1765 | "node_modules/is-glob": { 1766 | "version": "4.0.3", 1767 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1768 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1769 | "dev": true, 1770 | "license": "MIT", 1771 | "dependencies": { 1772 | "is-extglob": "^2.1.1" 1773 | }, 1774 | "engines": { 1775 | "node": ">=0.10.0" 1776 | } 1777 | }, 1778 | "node_modules/is-inside-container": { 1779 | "version": "1.0.0", 1780 | "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", 1781 | "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", 1782 | "license": "MIT", 1783 | "dependencies": { 1784 | "is-docker": "^3.0.0" 1785 | }, 1786 | "bin": { 1787 | "is-inside-container": "cli.js" 1788 | }, 1789 | "engines": { 1790 | "node": ">=14.16" 1791 | }, 1792 | "funding": { 1793 | "url": "https://github.com/sponsors/sindresorhus" 1794 | } 1795 | }, 1796 | "node_modules/is-promise": { 1797 | "version": "4.0.0", 1798 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 1799 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", 1800 | "dev": true, 1801 | "license": "MIT" 1802 | }, 1803 | "node_modules/is-wsl": { 1804 | "version": "3.1.0", 1805 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", 1806 | "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", 1807 | "license": "MIT", 1808 | "dependencies": { 1809 | "is-inside-container": "^1.0.0" 1810 | }, 1811 | "engines": { 1812 | "node": ">=16" 1813 | }, 1814 | "funding": { 1815 | "url": "https://github.com/sponsors/sindresorhus" 1816 | } 1817 | }, 1818 | "node_modules/isexe": { 1819 | "version": "2.0.0", 1820 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1821 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 1822 | "dev": true, 1823 | "license": "ISC" 1824 | }, 1825 | "node_modules/jose": { 1826 | "version": "4.15.9", 1827 | "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", 1828 | "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", 1829 | "license": "MIT", 1830 | "funding": { 1831 | "url": "https://github.com/sponsors/panva" 1832 | } 1833 | }, 1834 | "node_modules/js-yaml": { 1835 | "version": "4.1.0", 1836 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 1837 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 1838 | "dev": true, 1839 | "license": "MIT", 1840 | "dependencies": { 1841 | "argparse": "^2.0.1" 1842 | }, 1843 | "bin": { 1844 | "js-yaml": "bin/js-yaml.js" 1845 | } 1846 | }, 1847 | "node_modules/json-buffer": { 1848 | "version": "3.0.1", 1849 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 1850 | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 1851 | "dev": true, 1852 | "license": "MIT" 1853 | }, 1854 | "node_modules/json-schema-traverse": { 1855 | "version": "0.4.1", 1856 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1857 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 1858 | "dev": true, 1859 | "license": "MIT" 1860 | }, 1861 | "node_modules/json-stable-stringify-without-jsonify": { 1862 | "version": "1.0.1", 1863 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 1864 | "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 1865 | "dev": true, 1866 | "license": "MIT" 1867 | }, 1868 | "node_modules/jsonwebtoken": { 1869 | "version": "9.0.2", 1870 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", 1871 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", 1872 | "license": "MIT", 1873 | "dependencies": { 1874 | "jws": "^3.2.2", 1875 | "lodash.includes": "^4.3.0", 1876 | "lodash.isboolean": "^3.0.3", 1877 | "lodash.isinteger": "^4.0.4", 1878 | "lodash.isnumber": "^3.0.3", 1879 | "lodash.isplainobject": "^4.0.6", 1880 | "lodash.isstring": "^4.0.1", 1881 | "lodash.once": "^4.0.0", 1882 | "ms": "^2.1.1", 1883 | "semver": "^7.5.4" 1884 | }, 1885 | "engines": { 1886 | "node": ">=12", 1887 | "npm": ">=6" 1888 | } 1889 | }, 1890 | "node_modules/jwa": { 1891 | "version": "1.4.2", 1892 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", 1893 | "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", 1894 | "license": "MIT", 1895 | "dependencies": { 1896 | "buffer-equal-constant-time": "^1.0.1", 1897 | "ecdsa-sig-formatter": "1.0.11", 1898 | "safe-buffer": "^5.0.1" 1899 | } 1900 | }, 1901 | "node_modules/jwks-rsa": { 1902 | "version": "3.2.0", 1903 | "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", 1904 | "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", 1905 | "license": "MIT", 1906 | "dependencies": { 1907 | "@types/express": "^4.17.20", 1908 | "@types/jsonwebtoken": "^9.0.4", 1909 | "debug": "^4.3.4", 1910 | "jose": "^4.15.4", 1911 | "limiter": "^1.1.5", 1912 | "lru-memoizer": "^2.2.0" 1913 | }, 1914 | "engines": { 1915 | "node": ">=14" 1916 | } 1917 | }, 1918 | "node_modules/jws": { 1919 | "version": "3.2.2", 1920 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", 1921 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", 1922 | "license": "MIT", 1923 | "dependencies": { 1924 | "jwa": "^1.4.1", 1925 | "safe-buffer": "^5.0.1" 1926 | } 1927 | }, 1928 | "node_modules/keyv": { 1929 | "version": "4.5.4", 1930 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 1931 | "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 1932 | "dev": true, 1933 | "license": "MIT", 1934 | "dependencies": { 1935 | "json-buffer": "3.0.1" 1936 | } 1937 | }, 1938 | "node_modules/levn": { 1939 | "version": "0.4.1", 1940 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 1941 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 1942 | "dev": true, 1943 | "license": "MIT", 1944 | "dependencies": { 1945 | "prelude-ls": "^1.2.1", 1946 | "type-check": "~0.4.0" 1947 | }, 1948 | "engines": { 1949 | "node": ">= 0.8.0" 1950 | } 1951 | }, 1952 | "node_modules/limiter": { 1953 | "version": "1.1.5", 1954 | "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", 1955 | "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" 1956 | }, 1957 | "node_modules/locate-path": { 1958 | "version": "6.0.0", 1959 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1960 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1961 | "dev": true, 1962 | "license": "MIT", 1963 | "dependencies": { 1964 | "p-locate": "^5.0.0" 1965 | }, 1966 | "engines": { 1967 | "node": ">=10" 1968 | }, 1969 | "funding": { 1970 | "url": "https://github.com/sponsors/sindresorhus" 1971 | } 1972 | }, 1973 | "node_modules/lodash.clonedeep": { 1974 | "version": "4.5.0", 1975 | "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", 1976 | "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", 1977 | "license": "MIT" 1978 | }, 1979 | "node_modules/lodash.includes": { 1980 | "version": "4.3.0", 1981 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 1982 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", 1983 | "license": "MIT" 1984 | }, 1985 | "node_modules/lodash.isboolean": { 1986 | "version": "3.0.3", 1987 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 1988 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", 1989 | "license": "MIT" 1990 | }, 1991 | "node_modules/lodash.isinteger": { 1992 | "version": "4.0.4", 1993 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 1994 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", 1995 | "license": "MIT" 1996 | }, 1997 | "node_modules/lodash.isnumber": { 1998 | "version": "3.0.3", 1999 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 2000 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", 2001 | "license": "MIT" 2002 | }, 2003 | "node_modules/lodash.isplainobject": { 2004 | "version": "4.0.6", 2005 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 2006 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", 2007 | "license": "MIT" 2008 | }, 2009 | "node_modules/lodash.isstring": { 2010 | "version": "4.0.1", 2011 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 2012 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", 2013 | "license": "MIT" 2014 | }, 2015 | "node_modules/lodash.merge": { 2016 | "version": "4.6.2", 2017 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 2018 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 2019 | "dev": true, 2020 | "license": "MIT" 2021 | }, 2022 | "node_modules/lodash.once": { 2023 | "version": "4.1.1", 2024 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 2025 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", 2026 | "license": "MIT" 2027 | }, 2028 | "node_modules/long": { 2029 | "version": "4.0.0", 2030 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 2031 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", 2032 | "license": "Apache-2.0" 2033 | }, 2034 | "node_modules/lru-cache": { 2035 | "version": "6.0.0", 2036 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 2037 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 2038 | "license": "ISC", 2039 | "dependencies": { 2040 | "yallist": "^4.0.0" 2041 | }, 2042 | "engines": { 2043 | "node": ">=10" 2044 | } 2045 | }, 2046 | "node_modules/lru-memoizer": { 2047 | "version": "2.3.0", 2048 | "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", 2049 | "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", 2050 | "license": "MIT", 2051 | "dependencies": { 2052 | "lodash.clonedeep": "^4.5.0", 2053 | "lru-cache": "6.0.0" 2054 | } 2055 | }, 2056 | "node_modules/math-intrinsics": { 2057 | "version": "1.1.0", 2058 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 2059 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 2060 | "dev": true, 2061 | "license": "MIT", 2062 | "engines": { 2063 | "node": ">= 0.4" 2064 | } 2065 | }, 2066 | "node_modules/media-typer": { 2067 | "version": "1.1.0", 2068 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 2069 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 2070 | "dev": true, 2071 | "license": "MIT", 2072 | "engines": { 2073 | "node": ">= 0.8" 2074 | } 2075 | }, 2076 | "node_modules/merge-descriptors": { 2077 | "version": "2.0.0", 2078 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 2079 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 2080 | "dev": true, 2081 | "license": "MIT", 2082 | "engines": { 2083 | "node": ">=18" 2084 | }, 2085 | "funding": { 2086 | "url": "https://github.com/sponsors/sindresorhus" 2087 | } 2088 | }, 2089 | "node_modules/mime-db": { 2090 | "version": "1.54.0", 2091 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 2092 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 2093 | "dev": true, 2094 | "license": "MIT", 2095 | "engines": { 2096 | "node": ">= 0.6" 2097 | } 2098 | }, 2099 | "node_modules/mime-types": { 2100 | "version": "3.0.1", 2101 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", 2102 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 2103 | "dev": true, 2104 | "license": "MIT", 2105 | "dependencies": { 2106 | "mime-db": "^1.54.0" 2107 | }, 2108 | "engines": { 2109 | "node": ">= 0.6" 2110 | } 2111 | }, 2112 | "node_modules/minimatch": { 2113 | "version": "3.1.2", 2114 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 2115 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 2116 | "dev": true, 2117 | "license": "ISC", 2118 | "dependencies": { 2119 | "brace-expansion": "^1.1.7" 2120 | }, 2121 | "engines": { 2122 | "node": "*" 2123 | } 2124 | }, 2125 | "node_modules/ms": { 2126 | "version": "2.1.3", 2127 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 2128 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 2129 | "license": "MIT" 2130 | }, 2131 | "node_modules/natural-compare": { 2132 | "version": "1.4.0", 2133 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 2134 | "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 2135 | "dev": true, 2136 | "license": "MIT" 2137 | }, 2138 | "node_modules/negotiator": { 2139 | "version": "1.0.0", 2140 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 2141 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 2142 | "dev": true, 2143 | "license": "MIT", 2144 | "engines": { 2145 | "node": ">= 0.6" 2146 | } 2147 | }, 2148 | "node_modules/object-assign": { 2149 | "version": "4.1.1", 2150 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 2151 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 2152 | "dev": true, 2153 | "license": "MIT", 2154 | "engines": { 2155 | "node": ">=0.10.0" 2156 | } 2157 | }, 2158 | "node_modules/object-inspect": { 2159 | "version": "1.13.4", 2160 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 2161 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 2162 | "dev": true, 2163 | "license": "MIT", 2164 | "engines": { 2165 | "node": ">= 0.4" 2166 | }, 2167 | "funding": { 2168 | "url": "https://github.com/sponsors/ljharb" 2169 | } 2170 | }, 2171 | "node_modules/on-finished": { 2172 | "version": "2.4.1", 2173 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 2174 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 2175 | "dev": true, 2176 | "license": "MIT", 2177 | "dependencies": { 2178 | "ee-first": "1.1.1" 2179 | }, 2180 | "engines": { 2181 | "node": ">= 0.8" 2182 | } 2183 | }, 2184 | "node_modules/once": { 2185 | "version": "1.4.0", 2186 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 2187 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 2188 | "dev": true, 2189 | "license": "ISC", 2190 | "dependencies": { 2191 | "wrappy": "1" 2192 | } 2193 | }, 2194 | "node_modules/open": { 2195 | "version": "10.1.2", 2196 | "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", 2197 | "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", 2198 | "license": "MIT", 2199 | "dependencies": { 2200 | "default-browser": "^5.2.1", 2201 | "define-lazy-prop": "^3.0.0", 2202 | "is-inside-container": "^1.0.0", 2203 | "is-wsl": "^3.1.0" 2204 | }, 2205 | "engines": { 2206 | "node": ">=18" 2207 | }, 2208 | "funding": { 2209 | "url": "https://github.com/sponsors/sindresorhus" 2210 | } 2211 | }, 2212 | "node_modules/optionator": { 2213 | "version": "0.9.4", 2214 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 2215 | "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 2216 | "dev": true, 2217 | "license": "MIT", 2218 | "dependencies": { 2219 | "deep-is": "^0.1.3", 2220 | "fast-levenshtein": "^2.0.6", 2221 | "levn": "^0.4.1", 2222 | "prelude-ls": "^1.2.1", 2223 | "type-check": "^0.4.0", 2224 | "word-wrap": "^1.2.5" 2225 | }, 2226 | "engines": { 2227 | "node": ">= 0.8.0" 2228 | } 2229 | }, 2230 | "node_modules/p-limit": { 2231 | "version": "3.1.0", 2232 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 2233 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 2234 | "dev": true, 2235 | "license": "MIT", 2236 | "dependencies": { 2237 | "yocto-queue": "^0.1.0" 2238 | }, 2239 | "engines": { 2240 | "node": ">=10" 2241 | }, 2242 | "funding": { 2243 | "url": "https://github.com/sponsors/sindresorhus" 2244 | } 2245 | }, 2246 | "node_modules/p-locate": { 2247 | "version": "5.0.0", 2248 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 2249 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 2250 | "dev": true, 2251 | "license": "MIT", 2252 | "dependencies": { 2253 | "p-limit": "^3.0.2" 2254 | }, 2255 | "engines": { 2256 | "node": ">=10" 2257 | }, 2258 | "funding": { 2259 | "url": "https://github.com/sponsors/sindresorhus" 2260 | } 2261 | }, 2262 | "node_modules/parent-module": { 2263 | "version": "1.0.1", 2264 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 2265 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 2266 | "dev": true, 2267 | "license": "MIT", 2268 | "dependencies": { 2269 | "callsites": "^3.0.0" 2270 | }, 2271 | "engines": { 2272 | "node": ">=6" 2273 | } 2274 | }, 2275 | "node_modules/parseurl": { 2276 | "version": "1.3.3", 2277 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 2278 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 2279 | "dev": true, 2280 | "license": "MIT", 2281 | "engines": { 2282 | "node": ">= 0.8" 2283 | } 2284 | }, 2285 | "node_modules/path-exists": { 2286 | "version": "4.0.0", 2287 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 2288 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 2289 | "dev": true, 2290 | "license": "MIT", 2291 | "engines": { 2292 | "node": ">=8" 2293 | } 2294 | }, 2295 | "node_modules/path-key": { 2296 | "version": "3.1.1", 2297 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 2298 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 2299 | "dev": true, 2300 | "license": "MIT", 2301 | "engines": { 2302 | "node": ">=8" 2303 | } 2304 | }, 2305 | "node_modules/path-to-regexp": { 2306 | "version": "8.2.0", 2307 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 2308 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 2309 | "dev": true, 2310 | "license": "MIT", 2311 | "engines": { 2312 | "node": ">=16" 2313 | } 2314 | }, 2315 | "node_modules/pkce-challenge": { 2316 | "version": "5.0.0", 2317 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", 2318 | "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", 2319 | "dev": true, 2320 | "license": "MIT", 2321 | "engines": { 2322 | "node": ">=16.20.0" 2323 | } 2324 | }, 2325 | "node_modules/prelude-ls": { 2326 | "version": "1.2.1", 2327 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 2328 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 2329 | "dev": true, 2330 | "license": "MIT", 2331 | "engines": { 2332 | "node": ">= 0.8.0" 2333 | } 2334 | }, 2335 | "node_modules/prettier": { 2336 | "version": "3.5.3", 2337 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", 2338 | "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", 2339 | "dev": true, 2340 | "license": "MIT", 2341 | "bin": { 2342 | "prettier": "bin/prettier.cjs" 2343 | }, 2344 | "engines": { 2345 | "node": ">=14" 2346 | }, 2347 | "funding": { 2348 | "url": "https://github.com/prettier/prettier?sponsor=1" 2349 | } 2350 | }, 2351 | "node_modules/proxy-addr": { 2352 | "version": "2.0.7", 2353 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 2354 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 2355 | "dev": true, 2356 | "license": "MIT", 2357 | "dependencies": { 2358 | "forwarded": "0.2.0", 2359 | "ipaddr.js": "1.9.1" 2360 | }, 2361 | "engines": { 2362 | "node": ">= 0.10" 2363 | } 2364 | }, 2365 | "node_modules/punycode": { 2366 | "version": "2.3.1", 2367 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 2368 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 2369 | "dev": true, 2370 | "license": "MIT", 2371 | "engines": { 2372 | "node": ">=6" 2373 | } 2374 | }, 2375 | "node_modules/qs": { 2376 | "version": "6.14.0", 2377 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 2378 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 2379 | "dev": true, 2380 | "license": "BSD-3-Clause", 2381 | "dependencies": { 2382 | "side-channel": "^1.1.0" 2383 | }, 2384 | "engines": { 2385 | "node": ">=0.6" 2386 | }, 2387 | "funding": { 2388 | "url": "https://github.com/sponsors/ljharb" 2389 | } 2390 | }, 2391 | "node_modules/range-parser": { 2392 | "version": "1.2.1", 2393 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 2394 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 2395 | "dev": true, 2396 | "license": "MIT", 2397 | "engines": { 2398 | "node": ">= 0.6" 2399 | } 2400 | }, 2401 | "node_modules/raw-body": { 2402 | "version": "3.0.0", 2403 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 2404 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 2405 | "dev": true, 2406 | "license": "MIT", 2407 | "dependencies": { 2408 | "bytes": "3.1.2", 2409 | "http-errors": "2.0.0", 2410 | "iconv-lite": "0.6.3", 2411 | "unpipe": "1.0.0" 2412 | }, 2413 | "engines": { 2414 | "node": ">= 0.8" 2415 | } 2416 | }, 2417 | "node_modules/resolve-from": { 2418 | "version": "4.0.0", 2419 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 2420 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 2421 | "dev": true, 2422 | "license": "MIT", 2423 | "engines": { 2424 | "node": ">=4" 2425 | } 2426 | }, 2427 | "node_modules/router": { 2428 | "version": "2.2.0", 2429 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 2430 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 2431 | "dev": true, 2432 | "license": "MIT", 2433 | "dependencies": { 2434 | "debug": "^4.4.0", 2435 | "depd": "^2.0.0", 2436 | "is-promise": "^4.0.0", 2437 | "parseurl": "^1.3.3", 2438 | "path-to-regexp": "^8.0.0" 2439 | }, 2440 | "engines": { 2441 | "node": ">= 18" 2442 | } 2443 | }, 2444 | "node_modules/run-applescript": { 2445 | "version": "7.0.0", 2446 | "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", 2447 | "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", 2448 | "license": "MIT", 2449 | "engines": { 2450 | "node": ">=18" 2451 | }, 2452 | "funding": { 2453 | "url": "https://github.com/sponsors/sindresorhus" 2454 | } 2455 | }, 2456 | "node_modules/safe-buffer": { 2457 | "version": "5.2.1", 2458 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 2459 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 2460 | "funding": [ 2461 | { 2462 | "type": "github", 2463 | "url": "https://github.com/sponsors/feross" 2464 | }, 2465 | { 2466 | "type": "patreon", 2467 | "url": "https://www.patreon.com/feross" 2468 | }, 2469 | { 2470 | "type": "consulting", 2471 | "url": "https://feross.org/support" 2472 | } 2473 | ], 2474 | "license": "MIT" 2475 | }, 2476 | "node_modules/safer-buffer": { 2477 | "version": "2.1.2", 2478 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 2479 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 2480 | "dev": true, 2481 | "license": "MIT" 2482 | }, 2483 | "node_modules/semver": { 2484 | "version": "7.7.1", 2485 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 2486 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 2487 | "license": "ISC", 2488 | "bin": { 2489 | "semver": "bin/semver.js" 2490 | }, 2491 | "engines": { 2492 | "node": ">=10" 2493 | } 2494 | }, 2495 | "node_modules/send": { 2496 | "version": "1.2.0", 2497 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", 2498 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 2499 | "dev": true, 2500 | "license": "MIT", 2501 | "dependencies": { 2502 | "debug": "^4.3.5", 2503 | "encodeurl": "^2.0.0", 2504 | "escape-html": "^1.0.3", 2505 | "etag": "^1.8.1", 2506 | "fresh": "^2.0.0", 2507 | "http-errors": "^2.0.0", 2508 | "mime-types": "^3.0.1", 2509 | "ms": "^2.1.3", 2510 | "on-finished": "^2.4.1", 2511 | "range-parser": "^1.2.1", 2512 | "statuses": "^2.0.1" 2513 | }, 2514 | "engines": { 2515 | "node": ">= 18" 2516 | } 2517 | }, 2518 | "node_modules/serve-static": { 2519 | "version": "2.2.0", 2520 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", 2521 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 2522 | "dev": true, 2523 | "license": "MIT", 2524 | "dependencies": { 2525 | "encodeurl": "^2.0.0", 2526 | "escape-html": "^1.0.3", 2527 | "parseurl": "^1.3.3", 2528 | "send": "^1.2.0" 2529 | }, 2530 | "engines": { 2531 | "node": ">= 18" 2532 | } 2533 | }, 2534 | "node_modules/setprototypeof": { 2535 | "version": "1.2.0", 2536 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 2537 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 2538 | "dev": true, 2539 | "license": "ISC" 2540 | }, 2541 | "node_modules/shebang-command": { 2542 | "version": "2.0.0", 2543 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 2544 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 2545 | "dev": true, 2546 | "license": "MIT", 2547 | "dependencies": { 2548 | "shebang-regex": "^3.0.0" 2549 | }, 2550 | "engines": { 2551 | "node": ">=8" 2552 | } 2553 | }, 2554 | "node_modules/shebang-regex": { 2555 | "version": "3.0.0", 2556 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 2557 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 2558 | "dev": true, 2559 | "license": "MIT", 2560 | "engines": { 2561 | "node": ">=8" 2562 | } 2563 | }, 2564 | "node_modules/side-channel": { 2565 | "version": "1.1.0", 2566 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 2567 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 2568 | "dev": true, 2569 | "license": "MIT", 2570 | "dependencies": { 2571 | "es-errors": "^1.3.0", 2572 | "object-inspect": "^1.13.3", 2573 | "side-channel-list": "^1.0.0", 2574 | "side-channel-map": "^1.0.1", 2575 | "side-channel-weakmap": "^1.0.2" 2576 | }, 2577 | "engines": { 2578 | "node": ">= 0.4" 2579 | }, 2580 | "funding": { 2581 | "url": "https://github.com/sponsors/ljharb" 2582 | } 2583 | }, 2584 | "node_modules/side-channel-list": { 2585 | "version": "1.0.0", 2586 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 2587 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 2588 | "dev": true, 2589 | "license": "MIT", 2590 | "dependencies": { 2591 | "es-errors": "^1.3.0", 2592 | "object-inspect": "^1.13.3" 2593 | }, 2594 | "engines": { 2595 | "node": ">= 0.4" 2596 | }, 2597 | "funding": { 2598 | "url": "https://github.com/sponsors/ljharb" 2599 | } 2600 | }, 2601 | "node_modules/side-channel-map": { 2602 | "version": "1.0.1", 2603 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 2604 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 2605 | "dev": true, 2606 | "license": "MIT", 2607 | "dependencies": { 2608 | "call-bound": "^1.0.2", 2609 | "es-errors": "^1.3.0", 2610 | "get-intrinsic": "^1.2.5", 2611 | "object-inspect": "^1.13.3" 2612 | }, 2613 | "engines": { 2614 | "node": ">= 0.4" 2615 | }, 2616 | "funding": { 2617 | "url": "https://github.com/sponsors/ljharb" 2618 | } 2619 | }, 2620 | "node_modules/side-channel-weakmap": { 2621 | "version": "1.0.2", 2622 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 2623 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 2624 | "dev": true, 2625 | "license": "MIT", 2626 | "dependencies": { 2627 | "call-bound": "^1.0.2", 2628 | "es-errors": "^1.3.0", 2629 | "get-intrinsic": "^1.2.5", 2630 | "object-inspect": "^1.13.3", 2631 | "side-channel-map": "^1.0.1" 2632 | }, 2633 | "engines": { 2634 | "node": ">= 0.4" 2635 | }, 2636 | "funding": { 2637 | "url": "https://github.com/sponsors/ljharb" 2638 | } 2639 | }, 2640 | "node_modules/statuses": { 2641 | "version": "2.0.1", 2642 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 2643 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 2644 | "dev": true, 2645 | "license": "MIT", 2646 | "engines": { 2647 | "node": ">= 0.8" 2648 | } 2649 | }, 2650 | "node_modules/strip-json-comments": { 2651 | "version": "3.1.1", 2652 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 2653 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 2654 | "dev": true, 2655 | "license": "MIT", 2656 | "engines": { 2657 | "node": ">=8" 2658 | }, 2659 | "funding": { 2660 | "url": "https://github.com/sponsors/sindresorhus" 2661 | } 2662 | }, 2663 | "node_modules/strnum": { 2664 | "version": "2.1.0", 2665 | "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.0.tgz", 2666 | "integrity": "sha512-w0S//9BqZZGw0L0Y8uLSelFGnDJgTyyNQLmSlPnVz43zPAiqu3w4t8J8sDqqANOGeZIZ/9jWuPguYcEnsoHv4A==", 2667 | "funding": [ 2668 | { 2669 | "type": "github", 2670 | "url": "https://github.com/sponsors/NaturalIntelligence" 2671 | } 2672 | ], 2673 | "license": "MIT" 2674 | }, 2675 | "node_modules/supports-color": { 2676 | "version": "7.2.0", 2677 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 2678 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 2679 | "dev": true, 2680 | "license": "MIT", 2681 | "dependencies": { 2682 | "has-flag": "^4.0.0" 2683 | }, 2684 | "engines": { 2685 | "node": ">=8" 2686 | } 2687 | }, 2688 | "node_modules/toidentifier": { 2689 | "version": "1.0.1", 2690 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 2691 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 2692 | "dev": true, 2693 | "license": "MIT", 2694 | "engines": { 2695 | "node": ">=0.6" 2696 | } 2697 | }, 2698 | "node_modules/tslib": { 2699 | "version": "2.8.1", 2700 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2701 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2702 | "license": "0BSD" 2703 | }, 2704 | "node_modules/type-check": { 2705 | "version": "0.4.0", 2706 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 2707 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 2708 | "dev": true, 2709 | "license": "MIT", 2710 | "dependencies": { 2711 | "prelude-ls": "^1.2.1" 2712 | }, 2713 | "engines": { 2714 | "node": ">= 0.8.0" 2715 | } 2716 | }, 2717 | "node_modules/type-is": { 2718 | "version": "2.0.1", 2719 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 2720 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 2721 | "dev": true, 2722 | "license": "MIT", 2723 | "dependencies": { 2724 | "content-type": "^1.0.5", 2725 | "media-typer": "^1.1.0", 2726 | "mime-types": "^3.0.0" 2727 | }, 2728 | "engines": { 2729 | "node": ">= 0.6" 2730 | } 2731 | }, 2732 | "node_modules/undici": { 2733 | "version": "5.29.0", 2734 | "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", 2735 | "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", 2736 | "license": "MIT", 2737 | "dependencies": { 2738 | "@fastify/busboy": "^2.0.0" 2739 | }, 2740 | "engines": { 2741 | "node": ">=14.0" 2742 | } 2743 | }, 2744 | "node_modules/undici-types": { 2745 | "version": "6.21.0", 2746 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 2747 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 2748 | "license": "MIT" 2749 | }, 2750 | "node_modules/unpipe": { 2751 | "version": "1.0.0", 2752 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 2753 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 2754 | "dev": true, 2755 | "license": "MIT", 2756 | "engines": { 2757 | "node": ">= 0.8" 2758 | } 2759 | }, 2760 | "node_modules/uri-js": { 2761 | "version": "4.4.1", 2762 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 2763 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 2764 | "dev": true, 2765 | "license": "BSD-2-Clause", 2766 | "dependencies": { 2767 | "punycode": "^2.1.0" 2768 | } 2769 | }, 2770 | "node_modules/uuid": { 2771 | "version": "8.3.2", 2772 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 2773 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 2774 | "license": "MIT", 2775 | "bin": { 2776 | "uuid": "dist/bin/uuid" 2777 | } 2778 | }, 2779 | "node_modules/vary": { 2780 | "version": "1.1.2", 2781 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 2782 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 2783 | "dev": true, 2784 | "license": "MIT", 2785 | "engines": { 2786 | "node": ">= 0.8" 2787 | } 2788 | }, 2789 | "node_modules/which": { 2790 | "version": "2.0.2", 2791 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 2792 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 2793 | "dev": true, 2794 | "license": "ISC", 2795 | "dependencies": { 2796 | "isexe": "^2.0.0" 2797 | }, 2798 | "bin": { 2799 | "node-which": "bin/node-which" 2800 | }, 2801 | "engines": { 2802 | "node": ">= 8" 2803 | } 2804 | }, 2805 | "node_modules/word-wrap": { 2806 | "version": "1.2.5", 2807 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 2808 | "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 2809 | "dev": true, 2810 | "license": "MIT", 2811 | "engines": { 2812 | "node": ">=0.10.0" 2813 | } 2814 | }, 2815 | "node_modules/wrappy": { 2816 | "version": "1.0.2", 2817 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2818 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 2819 | "dev": true, 2820 | "license": "ISC" 2821 | }, 2822 | "node_modules/yallist": { 2823 | "version": "4.0.0", 2824 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 2825 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", 2826 | "license": "ISC" 2827 | }, 2828 | "node_modules/yocto-queue": { 2829 | "version": "0.1.0", 2830 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 2831 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 2832 | "dev": true, 2833 | "license": "MIT", 2834 | "engines": { 2835 | "node": ">=10" 2836 | }, 2837 | "funding": { 2838 | "url": "https://github.com/sponsors/sindresorhus" 2839 | } 2840 | }, 2841 | "node_modules/zod": { 2842 | "version": "3.24.4", 2843 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", 2844 | "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", 2845 | "dev": true, 2846 | "license": "MIT", 2847 | "funding": { 2848 | "url": "https://github.com/sponsors/colinhacks" 2849 | } 2850 | }, 2851 | "node_modules/zod-to-json-schema": { 2852 | "version": "3.24.5", 2853 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", 2854 | "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", 2855 | "dev": true, 2856 | "license": "ISC", 2857 | "peerDependencies": { 2858 | "zod": "^3.24.1" 2859 | } 2860 | } 2861 | } 2862 | } 2863 | --------------------------------------------------------------------------------