├── 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 |
63 |
64 |
65 |
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 |
118 |
119 |
120 |
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 | 
11 | 
12 | 
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 | 
39 |
40 | # 🗺️ Architecture
41 |
42 | 
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 |
--------------------------------------------------------------------------------