├── .github └── workflows │ ├── release.yaml │ └── tests.yaml ├── .gitignore ├── .npmignore ├── .npmrc ├── README.md ├── dependabot.yml ├── images ├── local-first-relay.png ├── relay-1.png ├── relay-2.png ├── relay-3.png ├── relay-connection.png ├── relay-introduction.png └── screenshot.png ├── package.json ├── pnpm-lock.yaml ├── prettier.config.cjs ├── src ├── Client.ts ├── Server.ts ├── lib │ ├── EventEmitter.ts │ ├── deduplicate.ts │ ├── eventPromise.ts │ ├── intersection.ts │ ├── isReady.ts │ ├── msgpack.ts │ ├── newid.ts │ ├── pause.ts │ └── pipeSockets.ts ├── start.ts ├── test │ ├── Client.test.ts │ ├── Server.test.ts │ └── helpers │ │ ├── allConnected.ts │ │ ├── allDisconnected.ts │ │ ├── connection.ts │ │ ├── disconnection.ts │ │ ├── factorial.ts │ │ └── permutationsOfTwo.ts └── types.ts ├── tsconfig.build.json ├── tsconfig.json ├── vitest.config.ts └── wallaby.conf.cjs /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | publish: 12 | name: Publish package 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: "18.x" 18 | registry-url: "https://registry.npmjs.org" 19 | - uses: pnpm/action-setup@v2 20 | with: 21 | version: 8 22 | run_install: false 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | ref: ${{ github.ref }} 27 | - name: Install dependencies 28 | id: deps 29 | run: | 30 | pnpm install 31 | - name: Build release 32 | id: build_release 33 | run: | 34 | pnpm build 35 | - name: Run Tests 36 | id: tests 37 | run: | 38 | pnpm test 39 | - name: Publish Release 40 | if: steps.tests.outcome == 'success' 41 | run: | 42 | if [ "$NODE_AUTH_TOKEN" = "" ]; then 43 | echo "You need a NPM_TOKEN secret in order to publish." 44 | false 45 | fi 46 | git config user.name github-actions 47 | git config user.email github-actions@github.com 48 | echo //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} > .npmrc 49 | EXTRA_ARGS="" 50 | if [[ $VERSION == *"alpha."* ]] || [[ $VERSION == *"beta."* ]] || [[ $VERSION == *"rc."* ]]; then 51 | echo "Is pre-release version" 52 | EXTRA_ARGS="$EXTRA_ARGS --dist-tag next" 53 | fi 54 | npm publish ${VERSION} --force-publish $EXTRA_ARGS 55 | env: 56 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | - name: Tag release 59 | if: steps.tests.outcome == 'success' 60 | uses: softprops/action-gh-release@v1 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: [opened, synchronize, reopened, ready_for_review, review_requested] 8 | branches: 9 | - main 10 | jobs: 11 | run-tests: 12 | name: Run tests 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: "18.x" 18 | registry-url: "https://registry.npmjs.org" 19 | - uses: pnpm/action-setup@v2 20 | with: 21 | version: 8 22 | run_install: false 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | ref: ${{ github.ref }} 27 | - name: Install and build 28 | run: | 29 | pnpm install 30 | pnpm build 31 | - name: Run tests 32 | run: pnpm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.tsbuildinfo 3 | 4 | node_modules 5 | dist 6 | coverage 7 | build 8 | 9 | .DS_Store 10 | .rts2_cache_cjs 11 | .rts2_cache_es 12 | .rts2_cache_umd 13 | .env 14 | .vscode/settings.json 15 | 16 | **/cypress/videos 17 | **/cypress/screenshots 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .rts2_cache_cjs 5 | .rts2_cache_es 6 | .rts2_cache_umd 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | enable-pre-post-scripts=true -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | @localfirst/relay logo 3 | 4 | `@localfirst/relay` is a tiny service that helps local-first applications connect with peers on 5 | other devices. It can run in the cloud or on any device with a known address. 6 | 7 | Deploy to: 8 | [Glitch](#deploying-to-glitch) | 9 | [Heroku](#deploying-to-heroku) | 10 | [AWS](#deploying-to-aws-elastic-beanstalk) | 11 | [Google](#deploying-to-google-cloud) | 12 | [Azure](#deploying-to-azure) | 13 | [local server](#server) 14 | 15 | ## Why 16 | 17 | 18 | 19 | Getting two end-user devices to communicate with each other over the internet is 20 | [hard](https://tailscale.com/blog/how-nat-traversal-works/). Most devices don't have stable public 21 | IP addresses, and they're often behind firewalls that turn away attempts to connect from the 22 | outside. This is a **connection** problem. 23 | 24 | Even within a local network, or in other situations where devices can be reached directly, devices 25 | that want to communicate need a way to find each other. This is a problem of **discovery**. 26 | 27 | ## What 28 | 29 | This little server offers a solution to each of these two problems. 30 | 31 | ### 1. Discovery 32 | 33 | Alice can provide a `documentId` (or several) that she's interested in. (A `documentId` 34 | is a unique ID for a topic or channel — it could be a GUID, or just a unique string like 35 | `ambitious-mongoose`.) 36 | 37 | [![diagram](./images/relay-introduction.png)](https://raw.githubusercontent.com/local-first-web/relay/master/images/relay-introduction.png) 38 | 39 | If Bob is interested in the same `documentId`, each will receive an `Introduction` 40 | message with the other's peerId. They can then use that information to connect. 41 | 42 | ### 2. Connection 43 | 44 | Alice can request to connect with Bob on a given documentId. If we get matching connection 45 | requests from Alice and Bob, we pipe their sockets together. 46 | 47 | [![diagram](./images/relay-connection.png)](https://raw.githubusercontent.com/local-first-web/relay/master/images/relay-connection.png) 48 | 49 | ## How 50 | 51 | ### Server 52 | 53 | From this repo, you can run this server as follows: 54 | 55 | ```bash 56 | pnpm 57 | pnpm start 58 | ``` 59 | 60 | You should see something like thsi: 61 | 62 | ```bash 63 | > @localfirst/relay@4.0.0 start local-first-web/relay 64 | > node dist/start.js 65 | 66 | 🐟 Listening at http://localhost:8080 67 | ``` 68 | 69 | You can visit that URL with a web browser to confirm that it's working; you should see something like this: 70 | 71 | 72 | 73 | #### Running server from another package 74 | 75 | From another codebase, you can import the server and run it as follows: 76 | 77 | ```ts 78 | import { Server } from "@localfirst/relay/Server.js" 79 | 80 | const DEFAULT_PORT = 8080 81 | const port = Number(process.env.PORT) || DEFAULT_PORT 82 | 83 | const server = new Server({ port }) 84 | 85 | server.listen() 86 | ``` 87 | 88 | ### Client 89 | 90 | This library includes a lightweight client designed to be used with this server. 91 | 92 | The client keeps track of all peers that the server connects you to, and for each peer it keeps 93 | track of each documentId (aka discoveryKey, aka channel) that you're working with that peer on. 94 | 95 | ```ts 96 | import { Client } from "@localfirst/relay/Client.js" 97 | 98 | client = new Client({ peerId: "alice", url: "myrelay.somedomain.com" }) 99 | .join("ambitious-mongoose") 100 | .on("peer-connect", ({ documentId, peerId, socket }) => { 101 | // `socket` is a WebSocket 102 | 103 | // send a message 104 | socket.write("Hello! 🎉") 105 | 106 | // listen for messages 107 | socket.addEventListener("data", event => { 108 | const message = event.data 109 | console.log(`message from ${peerId} about ${documentId}`, message) 110 | }) 111 | }) 112 | ``` 113 | 114 | ## ⚠ Security 115 | 116 | This server makes no security guarantees. Alice and Bob should probably: 117 | 118 | 1. **Authenticate** each other, to ensure that "Alice" is actually Alice and "Bob" is actually Bob. 119 | 2. **Encrypt** all communications with each other. 120 | 121 | The [@localfirst/auth] library can be used with this relay service. It provides peer-to-peer 122 | authentication and end-to-end encryption, and allows you to treat this relay (and the rest of the 123 | network) as untrusted. 124 | 125 | ## Server API 126 | 127 | > The following documentation might be of interest to anyone working on the @localfirst/relay 128 | > `Client`, or replacing it with a new client. You don't need to know any of this to interact with 129 | > this server if you're using the included client. 130 | 131 | This server has two WebSocket endpoints: `/introduction` and `/connection`. 132 | 133 | In the following examples, Alice is the local peer and Bob is a remote peer. We're using `alice` and `bob` as their `peerId`s; in practice, typically these would be GUIDs that uniquely identify their devices. 134 | 135 | #### `/introduction/:localPeerId` 136 | 137 | - `:localPeerId` is the local peer's unique `peerId`. 138 | 139 | Alice connects to this endpoint, e.g. `wss://myrelay.somedomain.com/introduction/alice`. 140 | 141 | Once a WebSocket connection has been made, Alice sends an introduction request containing one or more `documentId`s that she has or is interested in: 142 | 143 | ```ts 144 | { 145 | type: 'Join', 146 | documentIds: ['ambitious-mongoose', 'frivolous-platypus'], // documents Alice has or is interested in 147 | } 148 | ``` 149 | 150 | If Bob is connected to the same server and interested in one or more of the same documents IDs, the 151 | server sends Alice an introduction message: 152 | 153 | ```ts 154 | { 155 | type: 'Introduction', 156 | peerId: 'bob', // Bob's peerId 157 | documentIds: ['ambitious-mongoose'] // documents we're both interested in 158 | } 159 | ``` 160 | 161 | Alice can now use this information to request a connection to this peer via the `connection` endpoint: 162 | 163 | #### `/connection/:localPeerId/:remotePeerId/:documentId` 164 | 165 | Once Alice has Bob's `peerId`, she makes a new connection to this endpoint, e.g. 166 | `wss://myrelay.somedomain.com/connection/alice/bob/ambitious-mongoose`. 167 | 168 | - `:localPeerId` is the local peer's unique `peerId`. 169 | - `:remotePeerId` is the remote peer's unique `peerId`. 170 | - `:documentId` is the document ID. 171 | 172 | If and when Bob makes a reciprocal connection by connecting to 173 | `wss://myrelay.somedomain.com/connection/bob/alice/ambitious-mongoose`, the server pipes their 174 | sockets together and leaves them to talk. 175 | 176 | The client and server don't communicate with each other via the `connection` endpoint; it's purely a 177 | relay between two peers. 178 | 179 | ## Deployment 180 | 181 | ### Deploying to Glitch 182 | 183 | You can deploy this relay to [Glitch](https://glitch.com) by clicking this button: 184 | 185 | [![Remix on Glitch](https://cdn.glitch.com/2703baf2-b643-4da7-ab91-7ee2a2d00b5b%2Fremix-button.svg)](https://glitch.com/edit/#!/import/github/local-first-web/relay) 186 | 187 | Alternatively, you can remix the [**local-first-relay**](https://glitch.com/edit/#!/local-first-relay) project. 188 | 189 | ### Deploying to Heroku 190 | 191 | This server can be deployed to [Heroku](https://heroku.com). By design, it should only ever run with a single dyno. You can deploy it by clicking on this button: 192 | 193 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) 194 | 195 | Or, you can install using the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) as follows: 196 | 197 | ```bash 198 | heroku create 199 | git push heroku main 200 | heroku open 201 | ``` 202 | 203 | ### Deploying to AWS Elastic Beanstalk 204 | 205 | Install using the [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv1.html): 206 | 207 | ```bash 208 | eb init 209 | eb create 210 | eb open 211 | ``` 212 | 213 | ### Deploying to Google Cloud 214 | 215 | Install using the [Google Cloud SDK](https://cloud.google.com/sdk/docs/): 216 | 217 | ```bash 218 | gcloud projects create my-local-first-relay --set-as-default 219 | gcloud app create 220 | gcloud app deploy 221 | gcloud app browse 222 | ``` 223 | 224 | ### Deploying to Azure 225 | 226 | Install using the [Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest): 227 | 228 | ```bash 229 | az group create --name my-local-first-relay --location eastus 230 | az configure --defaults group=my-local-first-relay location=eastus 231 | az appservice plan create --name my-local-first-relay --sku F1 232 | az webapp create --name my-local-first-relay --plan my-local-first-relay 233 | az webapp deployment user set --user-name PEERID --password PASSWORD 234 | az webapp deployment source config-local-git --name my-local-first-relay 235 | git remote add azure https://PEERID@my-local-first-relay.scm.azurewebsites.net/my-local-first-relay.git 236 | git push azure main 237 | az webapp browse --name my-local-first-relay 238 | ``` 239 | 240 | ### AWS Lambda, Azure Functions, Vercel, Serverless, Cloudwatch Workers, etc. 241 | 242 | Since true serverless functions are stateless and only spun up on demand, they're not a good fit for 243 | this server, which needs to remember information about connected peers and maintain a stable 244 | websocket connection with each one. 245 | 246 | ## License 247 | 248 | MIT 249 | 250 | ## Prior art 251 | 252 | Inspired by https://github.com/orionz/discovery-cloud-server 253 | 254 | Formerly known as 🐟 Cevitxe Signal Server. (Cevitxe is now [@localfirst/state]) 255 | 256 | [@localfirst/state]: https://github.com/local-first-web/state 257 | [@localfirst/auth]: https://github.com/local-first-web/auth 258 | [@localfirst/relay-client]: ./packages/client/ 259 | [server tests]: ./packages/relay/src/Server.test.ts 260 | -------------------------------------------------------------------------------- /dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | -------------------------------------------------------------------------------- /images/local-first-relay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/local-first-web/relay/11f020cc6b48974c1f183a1d349a36d4c3361f64/images/local-first-relay.png -------------------------------------------------------------------------------- /images/relay-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/local-first-web/relay/11f020cc6b48974c1f183a1d349a36d4c3361f64/images/relay-1.png -------------------------------------------------------------------------------- /images/relay-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/local-first-web/relay/11f020cc6b48974c1f183a1d349a36d4c3361f64/images/relay-2.png -------------------------------------------------------------------------------- /images/relay-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/local-first-web/relay/11f020cc6b48974c1f183a1d349a36d4c3361f64/images/relay-3.png -------------------------------------------------------------------------------- /images/relay-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/local-first-web/relay/11f020cc6b48974c1f183a1d349a36d4c3361f64/images/relay-connection.png -------------------------------------------------------------------------------- /images/relay-introduction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/local-first-web/relay/11f020cc6b48974c1f183a1d349a36d4c3361f64/images/relay-introduction.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/local-first-web/relay/11f020cc6b48974c1f183a1d349a36d4c3361f64/images/screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@localfirst/relay", 3 | "version": "4.2.2", 4 | "description": "A tiny service that helps local-first applications connect with peers on other devices", 5 | "repository": "https://github.com/local-first-web/relay", 6 | "author": "herb@devresults.com", 7 | "license": "MIT", 8 | "type": "module", 9 | "private": false, 10 | "exports": { 11 | "./client": { 12 | "types": "./dist/types.ts", 13 | "default": "./dist/Client.js" 14 | }, 15 | "./server": { 16 | "types": "./dist/types.ts", 17 | "default": "./dist/Server.js" 18 | } 19 | }, 20 | "engines": { 21 | "node": ">=18.0.0" 22 | }, 23 | "scripts": { 24 | "prebuild": "rimraf dist", 25 | "build": "tsc -p tsconfig.build.json", 26 | "dev": "cross-env DEBUG='lf*' DEBUG_COLORS=1 ts-node-dev src/start.ts --respawn --transpileOnly", 27 | "start": "cross-env NODE_NO_WARNINGS=1 node dist/start.js", 28 | "start:log": "cross-env DEBUG='lf*' DEBUG_COLORS=1 pnpm start", 29 | "test": "vitest", 30 | "test:log": "cross-env DEBUG='lf*' DEBUG_COLORS=1 pnpm test", 31 | "version:alpha": "npm version prerelease --preid=alpha && git push --follow-tagsxs", 32 | "version:beta": "npm version prerelease --preid=beta && git push --follow-tags", 33 | "version:patch": "npm version patch && git push --follow-tags", 34 | "version:minor": "npm version minor && git push --follow-tags", 35 | "version:major": "npm version major && git push --follow-tags" 36 | }, 37 | "dependencies": { 38 | "cuid": "^3.0.0", 39 | "debug": "^4.3.4", 40 | "eventemitter3": "^5.0.1", 41 | "express": "^4.18.2", 42 | "express-ws": "^5.0.2", 43 | "ws": "^8.15.0", 44 | "isomorphic-ws": "^5.0.0", 45 | "msgpackr": "^1.10.0" 46 | }, 47 | "devDependencies": { 48 | "@types/debug": "^4.1.12", 49 | "@types/express": "^4.17.21", 50 | "@types/express-ws": "^3.0.4", 51 | "@types/node": "^20.10.4", 52 | "@types/ws": "^8.5.10", 53 | "cross-env": "^7.0.3", 54 | "jsdom": "^23.0.1", 55 | "portfinder": "^1.0.32", 56 | "prettier": "^3.1.1", 57 | "rimraf": "^5.0.5", 58 | "ts-node-dev": "^2.0.0", 59 | "typescript": "^5.3.3", 60 | "vitest": "^1.0.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | cuid: 12 | specifier: ^3.0.0 13 | version: 3.0.0 14 | debug: 15 | specifier: ^4.3.4 16 | version: 4.3.4 17 | eventemitter3: 18 | specifier: ^5.0.1 19 | version: 5.0.1 20 | express: 21 | specifier: ^4.18.2 22 | version: 4.18.2 23 | express-ws: 24 | specifier: ^5.0.2 25 | version: 5.0.2(express@4.18.2) 26 | isomorphic-ws: 27 | specifier: ^5.0.0 28 | version: 5.0.0(ws@8.15.0) 29 | msgpackr: 30 | specifier: ^1.10.0 31 | version: 1.10.0 32 | ws: 33 | specifier: ^8.15.0 34 | version: 8.15.0 35 | devDependencies: 36 | '@types/debug': 37 | specifier: ^4.1.12 38 | version: 4.1.12 39 | '@types/express': 40 | specifier: ^4.17.21 41 | version: 4.17.21 42 | '@types/express-ws': 43 | specifier: ^3.0.4 44 | version: 3.0.4 45 | '@types/node': 46 | specifier: ^20.10.4 47 | version: 20.10.4 48 | '@types/ws': 49 | specifier: ^8.5.10 50 | version: 8.5.10 51 | cross-env: 52 | specifier: ^7.0.3 53 | version: 7.0.3 54 | jsdom: 55 | specifier: ^23.0.1 56 | version: 23.0.1 57 | portfinder: 58 | specifier: ^1.0.32 59 | version: 1.0.32 60 | prettier: 61 | specifier: ^3.1.1 62 | version: 3.1.1 63 | rimraf: 64 | specifier: ^5.0.5 65 | version: 5.0.5 66 | ts-node-dev: 67 | specifier: ^2.0.0 68 | version: 2.0.0(@types/node@20.10.4)(typescript@5.3.3) 69 | typescript: 70 | specifier: ^5.3.3 71 | version: 5.3.3 72 | vitest: 73 | specifier: ^1.0.0 74 | version: 1.0.4(@types/node@20.10.4)(jsdom@23.0.1) 75 | 76 | packages/client: 77 | dependencies: 78 | cuid: 79 | specifier: ^3.0.0 80 | version: 3.0.0 81 | debug: 82 | specifier: ^4.3.4 83 | version: 4.3.4 84 | eventemitter3: 85 | specifier: ^5.0.1 86 | version: 5.0.1 87 | isomorphic-ws: 88 | specifier: ^5.0.0 89 | version: 5.0.0(ws@8.15.0) 90 | msgpackr: 91 | specifier: ^1.10.0 92 | version: 1.10.0 93 | devDependencies: 94 | '@localfirst/relay': 95 | specifier: ^3.6.2 96 | version: link:../server 97 | 98 | packages/server: 99 | dependencies: 100 | debug: 101 | specifier: ^4.3.4 102 | version: 4.3.4 103 | eventemitter3: 104 | specifier: ^5.0.1 105 | version: 5.0.1 106 | express: 107 | specifier: ^4.18.2 108 | version: 4.18.2 109 | express-ws: 110 | specifier: ^5.0.2 111 | version: 5.0.2(express@4.18.2) 112 | isomorphic-ws: 113 | specifier: ^5.0.0 114 | version: 5.0.0(ws@8.15.0) 115 | msgpackr: 116 | specifier: ^1.10.0 117 | version: 1.10.0 118 | 119 | packages: 120 | 121 | /@cspotcode/source-map-support@0.8.1: 122 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} 123 | engines: {node: '>=12'} 124 | dependencies: 125 | '@jridgewell/trace-mapping': 0.3.9 126 | dev: true 127 | 128 | /@esbuild/android-arm64@0.19.9: 129 | resolution: {integrity: sha512-q4cR+6ZD0938R19MyEW3jEsMzbb/1rulLXiNAJQADD/XYp7pT+rOS5JGxvpRW8dFDEfjW4wLgC/3FXIw4zYglQ==} 130 | engines: {node: '>=12'} 131 | cpu: [arm64] 132 | os: [android] 133 | requiresBuild: true 134 | dev: true 135 | optional: true 136 | 137 | /@esbuild/android-arm@0.19.9: 138 | resolution: {integrity: sha512-jkYjjq7SdsWuNI6b5quymW0oC83NN5FdRPuCbs9HZ02mfVdAP8B8eeqLSYU3gb6OJEaY5CQabtTFbqBf26H3GA==} 139 | engines: {node: '>=12'} 140 | cpu: [arm] 141 | os: [android] 142 | requiresBuild: true 143 | dev: true 144 | optional: true 145 | 146 | /@esbuild/android-x64@0.19.9: 147 | resolution: {integrity: sha512-KOqoPntWAH6ZxDwx1D6mRntIgZh9KodzgNOy5Ebt9ghzffOk9X2c1sPwtM9P+0eXbefnDhqYfkh5PLP5ULtWFA==} 148 | engines: {node: '>=12'} 149 | cpu: [x64] 150 | os: [android] 151 | requiresBuild: true 152 | dev: true 153 | optional: true 154 | 155 | /@esbuild/darwin-arm64@0.19.9: 156 | resolution: {integrity: sha512-KBJ9S0AFyLVx2E5D8W0vExqRW01WqRtczUZ8NRu+Pi+87opZn5tL4Y0xT0mA4FtHctd0ZgwNoN639fUUGlNIWw==} 157 | engines: {node: '>=12'} 158 | cpu: [arm64] 159 | os: [darwin] 160 | requiresBuild: true 161 | dev: true 162 | optional: true 163 | 164 | /@esbuild/darwin-x64@0.19.9: 165 | resolution: {integrity: sha512-vE0VotmNTQaTdX0Q9dOHmMTao6ObjyPm58CHZr1UK7qpNleQyxlFlNCaHsHx6Uqv86VgPmR4o2wdNq3dP1qyDQ==} 166 | engines: {node: '>=12'} 167 | cpu: [x64] 168 | os: [darwin] 169 | requiresBuild: true 170 | dev: true 171 | optional: true 172 | 173 | /@esbuild/freebsd-arm64@0.19.9: 174 | resolution: {integrity: sha512-uFQyd/o1IjiEk3rUHSwUKkqZwqdvuD8GevWF065eqgYfexcVkxh+IJgwTaGZVu59XczZGcN/YMh9uF1fWD8j1g==} 175 | engines: {node: '>=12'} 176 | cpu: [arm64] 177 | os: [freebsd] 178 | requiresBuild: true 179 | dev: true 180 | optional: true 181 | 182 | /@esbuild/freebsd-x64@0.19.9: 183 | resolution: {integrity: sha512-WMLgWAtkdTbTu1AWacY7uoj/YtHthgqrqhf1OaEWnZb7PQgpt8eaA/F3LkV0E6K/Lc0cUr/uaVP/49iE4M4asA==} 184 | engines: {node: '>=12'} 185 | cpu: [x64] 186 | os: [freebsd] 187 | requiresBuild: true 188 | dev: true 189 | optional: true 190 | 191 | /@esbuild/linux-arm64@0.19.9: 192 | resolution: {integrity: sha512-PiPblfe1BjK7WDAKR1Cr9O7VVPqVNpwFcPWgfn4xu0eMemzRp442hXyzF/fSwgrufI66FpHOEJk0yYdPInsmyQ==} 193 | engines: {node: '>=12'} 194 | cpu: [arm64] 195 | os: [linux] 196 | requiresBuild: true 197 | dev: true 198 | optional: true 199 | 200 | /@esbuild/linux-arm@0.19.9: 201 | resolution: {integrity: sha512-C/ChPohUYoyUaqn1h17m/6yt6OB14hbXvT8EgM1ZWaiiTYz7nWZR0SYmMnB5BzQA4GXl3BgBO1l8MYqL/He3qw==} 202 | engines: {node: '>=12'} 203 | cpu: [arm] 204 | os: [linux] 205 | requiresBuild: true 206 | dev: true 207 | optional: true 208 | 209 | /@esbuild/linux-ia32@0.19.9: 210 | resolution: {integrity: sha512-f37i/0zE0MjDxijkPSQw1CO/7C27Eojqb+r3BbHVxMLkj8GCa78TrBZzvPyA/FNLUMzP3eyHCVkAopkKVja+6Q==} 211 | engines: {node: '>=12'} 212 | cpu: [ia32] 213 | os: [linux] 214 | requiresBuild: true 215 | dev: true 216 | optional: true 217 | 218 | /@esbuild/linux-loong64@0.19.9: 219 | resolution: {integrity: sha512-t6mN147pUIf3t6wUt3FeumoOTPfmv9Cc6DQlsVBpB7eCpLOqQDyWBP1ymXn1lDw4fNUSb/gBcKAmvTP49oIkaA==} 220 | engines: {node: '>=12'} 221 | cpu: [loong64] 222 | os: [linux] 223 | requiresBuild: true 224 | dev: true 225 | optional: true 226 | 227 | /@esbuild/linux-mips64el@0.19.9: 228 | resolution: {integrity: sha512-jg9fujJTNTQBuDXdmAg1eeJUL4Jds7BklOTkkH80ZgQIoCTdQrDaHYgbFZyeTq8zbY+axgptncko3v9p5hLZtw==} 229 | engines: {node: '>=12'} 230 | cpu: [mips64el] 231 | os: [linux] 232 | requiresBuild: true 233 | dev: true 234 | optional: true 235 | 236 | /@esbuild/linux-ppc64@0.19.9: 237 | resolution: {integrity: sha512-tkV0xUX0pUUgY4ha7z5BbDS85uI7ABw3V1d0RNTii7E9lbmV8Z37Pup2tsLV46SQWzjOeyDi1Q7Wx2+QM8WaCQ==} 238 | engines: {node: '>=12'} 239 | cpu: [ppc64] 240 | os: [linux] 241 | requiresBuild: true 242 | dev: true 243 | optional: true 244 | 245 | /@esbuild/linux-riscv64@0.19.9: 246 | resolution: {integrity: sha512-DfLp8dj91cufgPZDXr9p3FoR++m3ZJ6uIXsXrIvJdOjXVREtXuQCjfMfvmc3LScAVmLjcfloyVtpn43D56JFHg==} 247 | engines: {node: '>=12'} 248 | cpu: [riscv64] 249 | os: [linux] 250 | requiresBuild: true 251 | dev: true 252 | optional: true 253 | 254 | /@esbuild/linux-s390x@0.19.9: 255 | resolution: {integrity: sha512-zHbglfEdC88KMgCWpOl/zc6dDYJvWGLiUtmPRsr1OgCViu3z5GncvNVdf+6/56O2Ca8jUU+t1BW261V6kp8qdw==} 256 | engines: {node: '>=12'} 257 | cpu: [s390x] 258 | os: [linux] 259 | requiresBuild: true 260 | dev: true 261 | optional: true 262 | 263 | /@esbuild/linux-x64@0.19.9: 264 | resolution: {integrity: sha512-JUjpystGFFmNrEHQnIVG8hKwvA2DN5o7RqiO1CVX8EN/F/gkCjkUMgVn6hzScpwnJtl2mPR6I9XV1oW8k9O+0A==} 265 | engines: {node: '>=12'} 266 | cpu: [x64] 267 | os: [linux] 268 | requiresBuild: true 269 | dev: true 270 | optional: true 271 | 272 | /@esbuild/netbsd-x64@0.19.9: 273 | resolution: {integrity: sha512-GThgZPAwOBOsheA2RUlW5UeroRfESwMq/guy8uEe3wJlAOjpOXuSevLRd70NZ37ZrpO6RHGHgEHvPg1h3S1Jug==} 274 | engines: {node: '>=12'} 275 | cpu: [x64] 276 | os: [netbsd] 277 | requiresBuild: true 278 | dev: true 279 | optional: true 280 | 281 | /@esbuild/openbsd-x64@0.19.9: 282 | resolution: {integrity: sha512-Ki6PlzppaFVbLnD8PtlVQfsYw4S9n3eQl87cqgeIw+O3sRr9IghpfSKY62mggdt1yCSZ8QWvTZ9jo9fjDSg9uw==} 283 | engines: {node: '>=12'} 284 | cpu: [x64] 285 | os: [openbsd] 286 | requiresBuild: true 287 | dev: true 288 | optional: true 289 | 290 | /@esbuild/sunos-x64@0.19.9: 291 | resolution: {integrity: sha512-MLHj7k9hWh4y1ddkBpvRj2b9NCBhfgBt3VpWbHQnXRedVun/hC7sIyTGDGTfsGuXo4ebik2+3ShjcPbhtFwWDw==} 292 | engines: {node: '>=12'} 293 | cpu: [x64] 294 | os: [sunos] 295 | requiresBuild: true 296 | dev: true 297 | optional: true 298 | 299 | /@esbuild/win32-arm64@0.19.9: 300 | resolution: {integrity: sha512-GQoa6OrQ8G08guMFgeXPH7yE/8Dt0IfOGWJSfSH4uafwdC7rWwrfE6P9N8AtPGIjUzdo2+7bN8Xo3qC578olhg==} 301 | engines: {node: '>=12'} 302 | cpu: [arm64] 303 | os: [win32] 304 | requiresBuild: true 305 | dev: true 306 | optional: true 307 | 308 | /@esbuild/win32-ia32@0.19.9: 309 | resolution: {integrity: sha512-UOozV7Ntykvr5tSOlGCrqU3NBr3d8JqPes0QWN2WOXfvkWVGRajC+Ym0/Wj88fUgecUCLDdJPDF0Nna2UK3Qtg==} 310 | engines: {node: '>=12'} 311 | cpu: [ia32] 312 | os: [win32] 313 | requiresBuild: true 314 | dev: true 315 | optional: true 316 | 317 | /@esbuild/win32-x64@0.19.9: 318 | resolution: {integrity: sha512-oxoQgglOP7RH6iasDrhY+R/3cHrfwIDvRlT4CGChflq6twk8iENeVvMJjmvBb94Ik1Z+93iGO27err7w6l54GQ==} 319 | engines: {node: '>=12'} 320 | cpu: [x64] 321 | os: [win32] 322 | requiresBuild: true 323 | dev: true 324 | optional: true 325 | 326 | /@isaacs/cliui@8.0.2: 327 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 328 | engines: {node: '>=12'} 329 | dependencies: 330 | string-width: 5.1.2 331 | string-width-cjs: /string-width@4.2.3 332 | strip-ansi: 7.1.0 333 | strip-ansi-cjs: /strip-ansi@6.0.1 334 | wrap-ansi: 8.1.0 335 | wrap-ansi-cjs: /wrap-ansi@7.0.0 336 | dev: true 337 | 338 | /@jest/schemas@29.6.3: 339 | resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} 340 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 341 | dependencies: 342 | '@sinclair/typebox': 0.27.8 343 | dev: true 344 | 345 | /@jridgewell/resolve-uri@3.1.1: 346 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 347 | engines: {node: '>=6.0.0'} 348 | dev: true 349 | 350 | /@jridgewell/sourcemap-codec@1.4.15: 351 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 352 | dev: true 353 | 354 | /@jridgewell/trace-mapping@0.3.9: 355 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} 356 | dependencies: 357 | '@jridgewell/resolve-uri': 3.1.1 358 | '@jridgewell/sourcemap-codec': 1.4.15 359 | dev: true 360 | 361 | /@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2: 362 | resolution: {integrity: sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==} 363 | cpu: [arm64] 364 | os: [darwin] 365 | requiresBuild: true 366 | dev: false 367 | optional: true 368 | 369 | /@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2: 370 | resolution: {integrity: sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==} 371 | cpu: [x64] 372 | os: [darwin] 373 | requiresBuild: true 374 | dev: false 375 | optional: true 376 | 377 | /@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2: 378 | resolution: {integrity: sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==} 379 | cpu: [arm64] 380 | os: [linux] 381 | requiresBuild: true 382 | dev: false 383 | optional: true 384 | 385 | /@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2: 386 | resolution: {integrity: sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==} 387 | cpu: [arm] 388 | os: [linux] 389 | requiresBuild: true 390 | dev: false 391 | optional: true 392 | 393 | /@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2: 394 | resolution: {integrity: sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==} 395 | cpu: [x64] 396 | os: [linux] 397 | requiresBuild: true 398 | dev: false 399 | optional: true 400 | 401 | /@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2: 402 | resolution: {integrity: sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==} 403 | cpu: [x64] 404 | os: [win32] 405 | requiresBuild: true 406 | dev: false 407 | optional: true 408 | 409 | /@pkgjs/parseargs@0.11.0: 410 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 411 | engines: {node: '>=14'} 412 | requiresBuild: true 413 | dev: true 414 | optional: true 415 | 416 | /@rollup/rollup-android-arm-eabi@4.8.0: 417 | resolution: {integrity: sha512-zdTObFRoNENrdPpnTNnhOljYIcOX7aI7+7wyrSpPFFIOf/nRdedE6IYsjaBE7tjukphh1tMTojgJ7p3lKY8x6Q==} 418 | cpu: [arm] 419 | os: [android] 420 | requiresBuild: true 421 | dev: true 422 | optional: true 423 | 424 | /@rollup/rollup-android-arm64@4.8.0: 425 | resolution: {integrity: sha512-aiItwP48BiGpMFS9Znjo/xCNQVwTQVcRKkFKsO81m8exrGjHkCBDvm9PHay2kpa8RPnZzzKcD1iQ9KaLY4fPQQ==} 426 | cpu: [arm64] 427 | os: [android] 428 | requiresBuild: true 429 | dev: true 430 | optional: true 431 | 432 | /@rollup/rollup-darwin-arm64@4.8.0: 433 | resolution: {integrity: sha512-zhNIS+L4ZYkYQUjIQUR6Zl0RXhbbA0huvNIWjmPc2SL0cB1h5Djkcy+RZ3/Bwszfb6vgwUvcVJYD6e6Zkpsi8g==} 434 | cpu: [arm64] 435 | os: [darwin] 436 | requiresBuild: true 437 | dev: true 438 | optional: true 439 | 440 | /@rollup/rollup-darwin-x64@4.8.0: 441 | resolution: {integrity: sha512-A/FAHFRNQYrELrb/JHncRWzTTXB2ticiRFztP4ggIUAfa9Up1qfW8aG2w/mN9jNiZ+HB0t0u0jpJgFXG6BfRTA==} 442 | cpu: [x64] 443 | os: [darwin] 444 | requiresBuild: true 445 | dev: true 446 | optional: true 447 | 448 | /@rollup/rollup-linux-arm-gnueabihf@4.8.0: 449 | resolution: {integrity: sha512-JsidBnh3p2IJJA4/2xOF2puAYqbaczB3elZDT0qHxn362EIoIkq7hrR43Xa8RisgI6/WPfvb2umbGsuvf7E37A==} 450 | cpu: [arm] 451 | os: [linux] 452 | requiresBuild: true 453 | dev: true 454 | optional: true 455 | 456 | /@rollup/rollup-linux-arm64-gnu@4.8.0: 457 | resolution: {integrity: sha512-hBNCnqw3EVCkaPB0Oqd24bv8SklETptQWcJz06kb9OtiShn9jK1VuTgi7o4zPSt6rNGWQOTDEAccbk0OqJmS+g==} 458 | cpu: [arm64] 459 | os: [linux] 460 | requiresBuild: true 461 | dev: true 462 | optional: true 463 | 464 | /@rollup/rollup-linux-arm64-musl@4.8.0: 465 | resolution: {integrity: sha512-Fw9ChYfJPdltvi9ALJ9wzdCdxGw4wtq4t1qY028b2O7GwB5qLNSGtqMsAel1lfWTZvf4b6/+4HKp0GlSYg0ahA==} 466 | cpu: [arm64] 467 | os: [linux] 468 | requiresBuild: true 469 | dev: true 470 | optional: true 471 | 472 | /@rollup/rollup-linux-riscv64-gnu@4.8.0: 473 | resolution: {integrity: sha512-BH5xIh7tOzS9yBi8dFrCTG8Z6iNIGWGltd3IpTSKp6+pNWWO6qy8eKoRxOtwFbMrid5NZaidLYN6rHh9aB8bEw==} 474 | cpu: [riscv64] 475 | os: [linux] 476 | requiresBuild: true 477 | dev: true 478 | optional: true 479 | 480 | /@rollup/rollup-linux-x64-gnu@4.8.0: 481 | resolution: {integrity: sha512-PmvAj8k6EuWiyLbkNpd6BLv5XeYFpqWuRvRNRl80xVfpGXK/z6KYXmAgbI4ogz7uFiJxCnYcqyvZVD0dgFog7Q==} 482 | cpu: [x64] 483 | os: [linux] 484 | requiresBuild: true 485 | dev: true 486 | optional: true 487 | 488 | /@rollup/rollup-linux-x64-musl@4.8.0: 489 | resolution: {integrity: sha512-mdxnlW2QUzXwY+95TuxZ+CurrhgrPAMveDWI97EQlA9bfhR8tw3Pt7SUlc/eSlCNxlWktpmT//EAA8UfCHOyXg==} 490 | cpu: [x64] 491 | os: [linux] 492 | requiresBuild: true 493 | dev: true 494 | optional: true 495 | 496 | /@rollup/rollup-win32-arm64-msvc@4.8.0: 497 | resolution: {integrity: sha512-ge7saUz38aesM4MA7Cad8CHo0Fyd1+qTaqoIo+Jtk+ipBi4ATSrHWov9/S4u5pbEQmLjgUjB7BJt+MiKG2kzmA==} 498 | cpu: [arm64] 499 | os: [win32] 500 | requiresBuild: true 501 | dev: true 502 | optional: true 503 | 504 | /@rollup/rollup-win32-ia32-msvc@4.8.0: 505 | resolution: {integrity: sha512-p9E3PZlzurhlsN5h9g7zIP1DnqKXJe8ZUkFwAazqSvHuWfihlIISPxG9hCHCoA+dOOspL/c7ty1eeEVFTE0UTw==} 506 | cpu: [ia32] 507 | os: [win32] 508 | requiresBuild: true 509 | dev: true 510 | optional: true 511 | 512 | /@rollup/rollup-win32-x64-msvc@4.8.0: 513 | resolution: {integrity: sha512-kb4/auKXkYKqlUYTE8s40FcJIj5soOyRLHKd4ugR0dCq0G2EfcF54eYcfQiGkHzjidZ40daB4ulsFdtqNKZtBg==} 514 | cpu: [x64] 515 | os: [win32] 516 | requiresBuild: true 517 | dev: true 518 | optional: true 519 | 520 | /@sinclair/typebox@0.27.8: 521 | resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} 522 | dev: true 523 | 524 | /@tsconfig/node10@1.0.9: 525 | resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} 526 | dev: true 527 | 528 | /@tsconfig/node12@1.0.11: 529 | resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} 530 | dev: true 531 | 532 | /@tsconfig/node14@1.0.3: 533 | resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} 534 | dev: true 535 | 536 | /@tsconfig/node16@1.0.4: 537 | resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} 538 | dev: true 539 | 540 | /@types/body-parser@1.19.5: 541 | resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} 542 | dependencies: 543 | '@types/connect': 3.4.38 544 | '@types/node': 20.10.4 545 | dev: true 546 | 547 | /@types/connect@3.4.38: 548 | resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} 549 | dependencies: 550 | '@types/node': 20.10.4 551 | dev: true 552 | 553 | /@types/debug@4.1.12: 554 | resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} 555 | dependencies: 556 | '@types/ms': 0.7.34 557 | dev: true 558 | 559 | /@types/express-serve-static-core@4.17.41: 560 | resolution: {integrity: sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==} 561 | dependencies: 562 | '@types/node': 20.10.4 563 | '@types/qs': 6.9.10 564 | '@types/range-parser': 1.2.7 565 | '@types/send': 0.17.4 566 | dev: true 567 | 568 | /@types/express-ws@3.0.4: 569 | resolution: {integrity: sha512-Yjj18CaivG5KndgcvzttWe8mPFinPCHJC2wvyQqVzA7hqeufM8EtWMj6mpp5omg3s8XALUexhOu8aXAyi/DyJQ==} 570 | dependencies: 571 | '@types/express': 4.17.21 572 | '@types/express-serve-static-core': 4.17.41 573 | '@types/ws': 8.5.10 574 | dev: true 575 | 576 | /@types/express@4.17.21: 577 | resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} 578 | dependencies: 579 | '@types/body-parser': 1.19.5 580 | '@types/express-serve-static-core': 4.17.41 581 | '@types/qs': 6.9.10 582 | '@types/serve-static': 1.15.5 583 | dev: true 584 | 585 | /@types/http-errors@2.0.4: 586 | resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} 587 | dev: true 588 | 589 | /@types/mime@1.3.5: 590 | resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} 591 | dev: true 592 | 593 | /@types/mime@3.0.4: 594 | resolution: {integrity: sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==} 595 | dev: true 596 | 597 | /@types/ms@0.7.34: 598 | resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} 599 | dev: true 600 | 601 | /@types/node@20.10.4: 602 | resolution: {integrity: sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==} 603 | dependencies: 604 | undici-types: 5.26.5 605 | dev: true 606 | 607 | /@types/qs@6.9.10: 608 | resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==} 609 | dev: true 610 | 611 | /@types/range-parser@1.2.7: 612 | resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} 613 | dev: true 614 | 615 | /@types/send@0.17.4: 616 | resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==} 617 | dependencies: 618 | '@types/mime': 1.3.5 619 | '@types/node': 20.10.4 620 | dev: true 621 | 622 | /@types/serve-static@1.15.5: 623 | resolution: {integrity: sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==} 624 | dependencies: 625 | '@types/http-errors': 2.0.4 626 | '@types/mime': 3.0.4 627 | '@types/node': 20.10.4 628 | dev: true 629 | 630 | /@types/strip-bom@3.0.0: 631 | resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} 632 | dev: true 633 | 634 | /@types/strip-json-comments@0.0.30: 635 | resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} 636 | dev: true 637 | 638 | /@types/ws@8.5.10: 639 | resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} 640 | dependencies: 641 | '@types/node': 20.10.4 642 | dev: true 643 | 644 | /@vitest/expect@1.0.4: 645 | resolution: {integrity: sha512-/NRN9N88qjg3dkhmFcCBwhn/Ie4h064pY3iv7WLRsDJW7dXnEgeoa8W9zy7gIPluhz6CkgqiB3HmpIXgmEY5dQ==} 646 | dependencies: 647 | '@vitest/spy': 1.0.4 648 | '@vitest/utils': 1.0.4 649 | chai: 4.3.10 650 | dev: true 651 | 652 | /@vitest/runner@1.0.4: 653 | resolution: {integrity: sha512-rhOQ9FZTEkV41JWXozFM8YgOqaG9zA7QXbhg5gy6mFOVqh4PcupirIJ+wN7QjeJt8S8nJRYuZH1OjJjsbxAXTQ==} 654 | dependencies: 655 | '@vitest/utils': 1.0.4 656 | p-limit: 5.0.0 657 | pathe: 1.1.1 658 | dev: true 659 | 660 | /@vitest/snapshot@1.0.4: 661 | resolution: {integrity: sha512-vkfXUrNyNRA/Gzsp2lpyJxh94vU2OHT1amoD6WuvUAA12n32xeVZQ0KjjQIf8F6u7bcq2A2k969fMVxEsxeKYA==} 662 | dependencies: 663 | magic-string: 0.30.5 664 | pathe: 1.1.1 665 | pretty-format: 29.7.0 666 | dev: true 667 | 668 | /@vitest/spy@1.0.4: 669 | resolution: {integrity: sha512-9ojTFRL1AJVh0hvfzAQpm0QS6xIS+1HFIw94kl/1ucTfGCaj1LV/iuJU4Y6cdR03EzPDygxTHwE1JOm+5RCcvA==} 670 | dependencies: 671 | tinyspy: 2.2.0 672 | dev: true 673 | 674 | /@vitest/utils@1.0.4: 675 | resolution: {integrity: sha512-gsswWDXxtt0QvtK/y/LWukN7sGMYmnCcv1qv05CsY6cU/Y1zpGX1QuvLs+GO1inczpE6Owixeel3ShkjhYtGfA==} 676 | dependencies: 677 | diff-sequences: 29.6.3 678 | loupe: 2.3.7 679 | pretty-format: 29.7.0 680 | dev: true 681 | 682 | /accepts@1.3.8: 683 | resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 684 | engines: {node: '>= 0.6'} 685 | dependencies: 686 | mime-types: 2.1.35 687 | negotiator: 0.6.3 688 | dev: false 689 | 690 | /acorn-walk@8.3.1: 691 | resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==} 692 | engines: {node: '>=0.4.0'} 693 | dev: true 694 | 695 | /acorn@8.11.2: 696 | resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} 697 | engines: {node: '>=0.4.0'} 698 | hasBin: true 699 | dev: true 700 | 701 | /agent-base@7.1.0: 702 | resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==} 703 | engines: {node: '>= 14'} 704 | dependencies: 705 | debug: 4.3.4 706 | transitivePeerDependencies: 707 | - supports-color 708 | dev: true 709 | 710 | /ansi-regex@5.0.1: 711 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 712 | engines: {node: '>=8'} 713 | dev: true 714 | 715 | /ansi-regex@6.0.1: 716 | resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} 717 | engines: {node: '>=12'} 718 | dev: true 719 | 720 | /ansi-styles@4.3.0: 721 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 722 | engines: {node: '>=8'} 723 | dependencies: 724 | color-convert: 2.0.1 725 | dev: true 726 | 727 | /ansi-styles@5.2.0: 728 | resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} 729 | engines: {node: '>=10'} 730 | dev: true 731 | 732 | /ansi-styles@6.2.1: 733 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 734 | engines: {node: '>=12'} 735 | dev: true 736 | 737 | /anymatch@3.1.3: 738 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 739 | engines: {node: '>= 8'} 740 | dependencies: 741 | normalize-path: 3.0.0 742 | picomatch: 2.3.1 743 | dev: true 744 | 745 | /arg@4.1.3: 746 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} 747 | dev: true 748 | 749 | /array-flatten@1.1.1: 750 | resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 751 | dev: false 752 | 753 | /assertion-error@1.1.0: 754 | resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} 755 | dev: true 756 | 757 | /async@2.6.4: 758 | resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} 759 | dependencies: 760 | lodash: 4.17.21 761 | dev: true 762 | 763 | /asynckit@0.4.0: 764 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 765 | dev: true 766 | 767 | /balanced-match@1.0.2: 768 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 769 | dev: true 770 | 771 | /binary-extensions@2.2.0: 772 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 773 | engines: {node: '>=8'} 774 | dev: true 775 | 776 | /body-parser@1.20.1: 777 | resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} 778 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 779 | dependencies: 780 | bytes: 3.1.2 781 | content-type: 1.0.5 782 | debug: 2.6.9 783 | depd: 2.0.0 784 | destroy: 1.2.0 785 | http-errors: 2.0.0 786 | iconv-lite: 0.4.24 787 | on-finished: 2.4.1 788 | qs: 6.11.0 789 | raw-body: 2.5.1 790 | type-is: 1.6.18 791 | unpipe: 1.0.0 792 | transitivePeerDependencies: 793 | - supports-color 794 | dev: false 795 | 796 | /brace-expansion@1.1.11: 797 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 798 | dependencies: 799 | balanced-match: 1.0.2 800 | concat-map: 0.0.1 801 | dev: true 802 | 803 | /brace-expansion@2.0.1: 804 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 805 | dependencies: 806 | balanced-match: 1.0.2 807 | dev: true 808 | 809 | /braces@3.0.2: 810 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 811 | engines: {node: '>=8'} 812 | dependencies: 813 | fill-range: 7.0.1 814 | dev: true 815 | 816 | /buffer-from@1.1.2: 817 | resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} 818 | dev: true 819 | 820 | /bytes@3.1.2: 821 | resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} 822 | engines: {node: '>= 0.8'} 823 | dev: false 824 | 825 | /cac@6.7.14: 826 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 827 | engines: {node: '>=8'} 828 | dev: true 829 | 830 | /call-bind@1.0.5: 831 | resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} 832 | dependencies: 833 | function-bind: 1.1.2 834 | get-intrinsic: 1.2.2 835 | set-function-length: 1.1.1 836 | dev: false 837 | 838 | /chai@4.3.10: 839 | resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} 840 | engines: {node: '>=4'} 841 | dependencies: 842 | assertion-error: 1.1.0 843 | check-error: 1.0.3 844 | deep-eql: 4.1.3 845 | get-func-name: 2.0.2 846 | loupe: 2.3.7 847 | pathval: 1.1.1 848 | type-detect: 4.0.8 849 | dev: true 850 | 851 | /check-error@1.0.3: 852 | resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} 853 | dependencies: 854 | get-func-name: 2.0.2 855 | dev: true 856 | 857 | /chokidar@3.5.3: 858 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 859 | engines: {node: '>= 8.10.0'} 860 | dependencies: 861 | anymatch: 3.1.3 862 | braces: 3.0.2 863 | glob-parent: 5.1.2 864 | is-binary-path: 2.1.0 865 | is-glob: 4.0.3 866 | normalize-path: 3.0.0 867 | readdirp: 3.6.0 868 | optionalDependencies: 869 | fsevents: 2.3.3 870 | dev: true 871 | 872 | /color-convert@2.0.1: 873 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 874 | engines: {node: '>=7.0.0'} 875 | dependencies: 876 | color-name: 1.1.4 877 | dev: true 878 | 879 | /color-name@1.1.4: 880 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 881 | dev: true 882 | 883 | /combined-stream@1.0.8: 884 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 885 | engines: {node: '>= 0.8'} 886 | dependencies: 887 | delayed-stream: 1.0.0 888 | dev: true 889 | 890 | /concat-map@0.0.1: 891 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 892 | dev: true 893 | 894 | /content-disposition@0.5.4: 895 | resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} 896 | engines: {node: '>= 0.6'} 897 | dependencies: 898 | safe-buffer: 5.2.1 899 | dev: false 900 | 901 | /content-type@1.0.5: 902 | resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 903 | engines: {node: '>= 0.6'} 904 | dev: false 905 | 906 | /cookie-signature@1.0.6: 907 | resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} 908 | dev: false 909 | 910 | /cookie@0.5.0: 911 | resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} 912 | engines: {node: '>= 0.6'} 913 | dev: false 914 | 915 | /create-require@1.1.1: 916 | resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} 917 | dev: true 918 | 919 | /cross-env@7.0.3: 920 | resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} 921 | engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} 922 | hasBin: true 923 | dependencies: 924 | cross-spawn: 7.0.3 925 | dev: true 926 | 927 | /cross-spawn@7.0.3: 928 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 929 | engines: {node: '>= 8'} 930 | dependencies: 931 | path-key: 3.1.1 932 | shebang-command: 2.0.0 933 | which: 2.0.2 934 | dev: true 935 | 936 | /cssstyle@3.0.0: 937 | resolution: {integrity: sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==} 938 | engines: {node: '>=14'} 939 | dependencies: 940 | rrweb-cssom: 0.6.0 941 | dev: true 942 | 943 | /cuid@3.0.0: 944 | resolution: {integrity: sha512-WZYYkHdIDnaxdeP8Misq3Lah5vFjJwGuItJuV+tvMafosMzw0nF297T7mrm8IOWiPJkV6gc7sa8pzx27+w25Zg==} 945 | deprecated: Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead. 946 | dev: false 947 | 948 | /data-urls@5.0.0: 949 | resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} 950 | engines: {node: '>=18'} 951 | dependencies: 952 | whatwg-mimetype: 4.0.0 953 | whatwg-url: 14.0.0 954 | dev: true 955 | 956 | /debug@2.6.9: 957 | resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} 958 | peerDependencies: 959 | supports-color: '*' 960 | peerDependenciesMeta: 961 | supports-color: 962 | optional: true 963 | dependencies: 964 | ms: 2.0.0 965 | dev: false 966 | 967 | /debug@3.2.7: 968 | resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} 969 | peerDependencies: 970 | supports-color: '*' 971 | peerDependenciesMeta: 972 | supports-color: 973 | optional: true 974 | dependencies: 975 | ms: 2.1.3 976 | dev: true 977 | 978 | /debug@4.3.4: 979 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 980 | engines: {node: '>=6.0'} 981 | peerDependencies: 982 | supports-color: '*' 983 | peerDependenciesMeta: 984 | supports-color: 985 | optional: true 986 | dependencies: 987 | ms: 2.1.2 988 | 989 | /decimal.js@10.4.3: 990 | resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} 991 | dev: true 992 | 993 | /deep-eql@4.1.3: 994 | resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} 995 | engines: {node: '>=6'} 996 | dependencies: 997 | type-detect: 4.0.8 998 | dev: true 999 | 1000 | /define-data-property@1.1.1: 1001 | resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} 1002 | engines: {node: '>= 0.4'} 1003 | dependencies: 1004 | get-intrinsic: 1.2.2 1005 | gopd: 1.0.1 1006 | has-property-descriptors: 1.0.1 1007 | dev: false 1008 | 1009 | /delayed-stream@1.0.0: 1010 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 1011 | engines: {node: '>=0.4.0'} 1012 | dev: true 1013 | 1014 | /depd@2.0.0: 1015 | resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 1016 | engines: {node: '>= 0.8'} 1017 | dev: false 1018 | 1019 | /destroy@1.2.0: 1020 | resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 1021 | engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 1022 | dev: false 1023 | 1024 | /diff-sequences@29.6.3: 1025 | resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} 1026 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 1027 | dev: true 1028 | 1029 | /diff@4.0.2: 1030 | resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} 1031 | engines: {node: '>=0.3.1'} 1032 | dev: true 1033 | 1034 | /dynamic-dedupe@0.3.0: 1035 | resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} 1036 | dependencies: 1037 | xtend: 4.0.2 1038 | dev: true 1039 | 1040 | /eastasianwidth@0.2.0: 1041 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 1042 | dev: true 1043 | 1044 | /ee-first@1.1.1: 1045 | resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 1046 | dev: false 1047 | 1048 | /emoji-regex@8.0.0: 1049 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 1050 | dev: true 1051 | 1052 | /emoji-regex@9.2.2: 1053 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 1054 | dev: true 1055 | 1056 | /encodeurl@1.0.2: 1057 | resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} 1058 | engines: {node: '>= 0.8'} 1059 | dev: false 1060 | 1061 | /entities@4.5.0: 1062 | resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} 1063 | engines: {node: '>=0.12'} 1064 | dev: true 1065 | 1066 | /esbuild@0.19.9: 1067 | resolution: {integrity: sha512-U9CHtKSy+EpPsEBa+/A2gMs/h3ylBC0H0KSqIg7tpztHerLi6nrrcoUJAkNCEPumx8yJ+Byic4BVwHgRbN0TBg==} 1068 | engines: {node: '>=12'} 1069 | hasBin: true 1070 | requiresBuild: true 1071 | optionalDependencies: 1072 | '@esbuild/android-arm': 0.19.9 1073 | '@esbuild/android-arm64': 0.19.9 1074 | '@esbuild/android-x64': 0.19.9 1075 | '@esbuild/darwin-arm64': 0.19.9 1076 | '@esbuild/darwin-x64': 0.19.9 1077 | '@esbuild/freebsd-arm64': 0.19.9 1078 | '@esbuild/freebsd-x64': 0.19.9 1079 | '@esbuild/linux-arm': 0.19.9 1080 | '@esbuild/linux-arm64': 0.19.9 1081 | '@esbuild/linux-ia32': 0.19.9 1082 | '@esbuild/linux-loong64': 0.19.9 1083 | '@esbuild/linux-mips64el': 0.19.9 1084 | '@esbuild/linux-ppc64': 0.19.9 1085 | '@esbuild/linux-riscv64': 0.19.9 1086 | '@esbuild/linux-s390x': 0.19.9 1087 | '@esbuild/linux-x64': 0.19.9 1088 | '@esbuild/netbsd-x64': 0.19.9 1089 | '@esbuild/openbsd-x64': 0.19.9 1090 | '@esbuild/sunos-x64': 0.19.9 1091 | '@esbuild/win32-arm64': 0.19.9 1092 | '@esbuild/win32-ia32': 0.19.9 1093 | '@esbuild/win32-x64': 0.19.9 1094 | dev: true 1095 | 1096 | /escape-html@1.0.3: 1097 | resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} 1098 | dev: false 1099 | 1100 | /etag@1.8.1: 1101 | resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} 1102 | engines: {node: '>= 0.6'} 1103 | dev: false 1104 | 1105 | /eventemitter3@5.0.1: 1106 | resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} 1107 | dev: false 1108 | 1109 | /execa@8.0.1: 1110 | resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} 1111 | engines: {node: '>=16.17'} 1112 | dependencies: 1113 | cross-spawn: 7.0.3 1114 | get-stream: 8.0.1 1115 | human-signals: 5.0.0 1116 | is-stream: 3.0.0 1117 | merge-stream: 2.0.0 1118 | npm-run-path: 5.1.0 1119 | onetime: 6.0.0 1120 | signal-exit: 4.1.0 1121 | strip-final-newline: 3.0.0 1122 | dev: true 1123 | 1124 | /express-ws@5.0.2(express@4.18.2): 1125 | resolution: {integrity: sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==} 1126 | engines: {node: '>=4.5.0'} 1127 | peerDependencies: 1128 | express: ^4.0.0 || ^5.0.0-alpha.1 1129 | dependencies: 1130 | express: 4.18.2 1131 | ws: 7.5.9 1132 | transitivePeerDependencies: 1133 | - bufferutil 1134 | - utf-8-validate 1135 | dev: false 1136 | 1137 | /express@4.18.2: 1138 | resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} 1139 | engines: {node: '>= 0.10.0'} 1140 | dependencies: 1141 | accepts: 1.3.8 1142 | array-flatten: 1.1.1 1143 | body-parser: 1.20.1 1144 | content-disposition: 0.5.4 1145 | content-type: 1.0.5 1146 | cookie: 0.5.0 1147 | cookie-signature: 1.0.6 1148 | debug: 2.6.9 1149 | depd: 2.0.0 1150 | encodeurl: 1.0.2 1151 | escape-html: 1.0.3 1152 | etag: 1.8.1 1153 | finalhandler: 1.2.0 1154 | fresh: 0.5.2 1155 | http-errors: 2.0.0 1156 | merge-descriptors: 1.0.1 1157 | methods: 1.1.2 1158 | on-finished: 2.4.1 1159 | parseurl: 1.3.3 1160 | path-to-regexp: 0.1.7 1161 | proxy-addr: 2.0.7 1162 | qs: 6.11.0 1163 | range-parser: 1.2.1 1164 | safe-buffer: 5.2.1 1165 | send: 0.18.0 1166 | serve-static: 1.15.0 1167 | setprototypeof: 1.2.0 1168 | statuses: 2.0.1 1169 | type-is: 1.6.18 1170 | utils-merge: 1.0.1 1171 | vary: 1.1.2 1172 | transitivePeerDependencies: 1173 | - supports-color 1174 | dev: false 1175 | 1176 | /fill-range@7.0.1: 1177 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 1178 | engines: {node: '>=8'} 1179 | dependencies: 1180 | to-regex-range: 5.0.1 1181 | dev: true 1182 | 1183 | /finalhandler@1.2.0: 1184 | resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} 1185 | engines: {node: '>= 0.8'} 1186 | dependencies: 1187 | debug: 2.6.9 1188 | encodeurl: 1.0.2 1189 | escape-html: 1.0.3 1190 | on-finished: 2.4.1 1191 | parseurl: 1.3.3 1192 | statuses: 2.0.1 1193 | unpipe: 1.0.0 1194 | transitivePeerDependencies: 1195 | - supports-color 1196 | dev: false 1197 | 1198 | /foreground-child@3.1.1: 1199 | resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} 1200 | engines: {node: '>=14'} 1201 | dependencies: 1202 | cross-spawn: 7.0.3 1203 | signal-exit: 4.1.0 1204 | dev: true 1205 | 1206 | /form-data@4.0.0: 1207 | resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} 1208 | engines: {node: '>= 6'} 1209 | dependencies: 1210 | asynckit: 0.4.0 1211 | combined-stream: 1.0.8 1212 | mime-types: 2.1.35 1213 | dev: true 1214 | 1215 | /forwarded@0.2.0: 1216 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 1217 | engines: {node: '>= 0.6'} 1218 | dev: false 1219 | 1220 | /fresh@0.5.2: 1221 | resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 1222 | engines: {node: '>= 0.6'} 1223 | dev: false 1224 | 1225 | /fs.realpath@1.0.0: 1226 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 1227 | dev: true 1228 | 1229 | /fsevents@2.3.3: 1230 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 1231 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 1232 | os: [darwin] 1233 | requiresBuild: true 1234 | dev: true 1235 | optional: true 1236 | 1237 | /function-bind@1.1.2: 1238 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 1239 | 1240 | /get-func-name@2.0.2: 1241 | resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} 1242 | dev: true 1243 | 1244 | /get-intrinsic@1.2.2: 1245 | resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} 1246 | dependencies: 1247 | function-bind: 1.1.2 1248 | has-proto: 1.0.1 1249 | has-symbols: 1.0.3 1250 | hasown: 2.0.0 1251 | dev: false 1252 | 1253 | /get-stream@8.0.1: 1254 | resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} 1255 | engines: {node: '>=16'} 1256 | dev: true 1257 | 1258 | /glob-parent@5.1.2: 1259 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 1260 | engines: {node: '>= 6'} 1261 | dependencies: 1262 | is-glob: 4.0.3 1263 | dev: true 1264 | 1265 | /glob@10.3.10: 1266 | resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} 1267 | engines: {node: '>=16 || 14 >=14.17'} 1268 | hasBin: true 1269 | dependencies: 1270 | foreground-child: 3.1.1 1271 | jackspeak: 2.3.6 1272 | minimatch: 9.0.3 1273 | minipass: 7.0.4 1274 | path-scurry: 1.10.1 1275 | dev: true 1276 | 1277 | /glob@7.2.3: 1278 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 1279 | dependencies: 1280 | fs.realpath: 1.0.0 1281 | inflight: 1.0.6 1282 | inherits: 2.0.4 1283 | minimatch: 3.1.2 1284 | once: 1.4.0 1285 | path-is-absolute: 1.0.1 1286 | dev: true 1287 | 1288 | /gopd@1.0.1: 1289 | resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} 1290 | dependencies: 1291 | get-intrinsic: 1.2.2 1292 | dev: false 1293 | 1294 | /has-property-descriptors@1.0.1: 1295 | resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} 1296 | dependencies: 1297 | get-intrinsic: 1.2.2 1298 | dev: false 1299 | 1300 | /has-proto@1.0.1: 1301 | resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} 1302 | engines: {node: '>= 0.4'} 1303 | dev: false 1304 | 1305 | /has-symbols@1.0.3: 1306 | resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} 1307 | engines: {node: '>= 0.4'} 1308 | dev: false 1309 | 1310 | /hasown@2.0.0: 1311 | resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} 1312 | engines: {node: '>= 0.4'} 1313 | dependencies: 1314 | function-bind: 1.1.2 1315 | 1316 | /html-encoding-sniffer@4.0.0: 1317 | resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} 1318 | engines: {node: '>=18'} 1319 | dependencies: 1320 | whatwg-encoding: 3.1.1 1321 | dev: true 1322 | 1323 | /http-errors@2.0.0: 1324 | resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 1325 | engines: {node: '>= 0.8'} 1326 | dependencies: 1327 | depd: 2.0.0 1328 | inherits: 2.0.4 1329 | setprototypeof: 1.2.0 1330 | statuses: 2.0.1 1331 | toidentifier: 1.0.1 1332 | dev: false 1333 | 1334 | /http-proxy-agent@7.0.0: 1335 | resolution: {integrity: sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==} 1336 | engines: {node: '>= 14'} 1337 | dependencies: 1338 | agent-base: 7.1.0 1339 | debug: 4.3.4 1340 | transitivePeerDependencies: 1341 | - supports-color 1342 | dev: true 1343 | 1344 | /https-proxy-agent@7.0.2: 1345 | resolution: {integrity: sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==} 1346 | engines: {node: '>= 14'} 1347 | dependencies: 1348 | agent-base: 7.1.0 1349 | debug: 4.3.4 1350 | transitivePeerDependencies: 1351 | - supports-color 1352 | dev: true 1353 | 1354 | /human-signals@5.0.0: 1355 | resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} 1356 | engines: {node: '>=16.17.0'} 1357 | dev: true 1358 | 1359 | /iconv-lite@0.4.24: 1360 | resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} 1361 | engines: {node: '>=0.10.0'} 1362 | dependencies: 1363 | safer-buffer: 2.1.2 1364 | dev: false 1365 | 1366 | /iconv-lite@0.6.3: 1367 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 1368 | engines: {node: '>=0.10.0'} 1369 | requiresBuild: true 1370 | dependencies: 1371 | safer-buffer: 2.1.2 1372 | dev: true 1373 | 1374 | /inflight@1.0.6: 1375 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 1376 | dependencies: 1377 | once: 1.4.0 1378 | wrappy: 1.0.2 1379 | dev: true 1380 | 1381 | /inherits@2.0.4: 1382 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 1383 | 1384 | /ipaddr.js@1.9.1: 1385 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} 1386 | engines: {node: '>= 0.10'} 1387 | dev: false 1388 | 1389 | /is-binary-path@2.1.0: 1390 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 1391 | engines: {node: '>=8'} 1392 | dependencies: 1393 | binary-extensions: 2.2.0 1394 | dev: true 1395 | 1396 | /is-core-module@2.13.1: 1397 | resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} 1398 | dependencies: 1399 | hasown: 2.0.0 1400 | dev: true 1401 | 1402 | /is-extglob@2.1.1: 1403 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 1404 | engines: {node: '>=0.10.0'} 1405 | dev: true 1406 | 1407 | /is-fullwidth-code-point@3.0.0: 1408 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 1409 | engines: {node: '>=8'} 1410 | dev: true 1411 | 1412 | /is-glob@4.0.3: 1413 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 1414 | engines: {node: '>=0.10.0'} 1415 | dependencies: 1416 | is-extglob: 2.1.1 1417 | dev: true 1418 | 1419 | /is-number@7.0.0: 1420 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 1421 | engines: {node: '>=0.12.0'} 1422 | dev: true 1423 | 1424 | /is-potential-custom-element-name@1.0.1: 1425 | resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} 1426 | dev: true 1427 | 1428 | /is-stream@3.0.0: 1429 | resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} 1430 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1431 | dev: true 1432 | 1433 | /isexe@2.0.0: 1434 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 1435 | dev: true 1436 | 1437 | /isomorphic-ws@5.0.0(ws@8.15.0): 1438 | resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} 1439 | peerDependencies: 1440 | ws: '*' 1441 | dependencies: 1442 | ws: 8.15.0 1443 | dev: false 1444 | 1445 | /jackspeak@2.3.6: 1446 | resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} 1447 | engines: {node: '>=14'} 1448 | dependencies: 1449 | '@isaacs/cliui': 8.0.2 1450 | optionalDependencies: 1451 | '@pkgjs/parseargs': 0.11.0 1452 | dev: true 1453 | 1454 | /jsdom@23.0.1: 1455 | resolution: {integrity: sha512-2i27vgvlUsGEBO9+/kJQRbtqtm+191b5zAZrU/UezVmnC2dlDAFLgDYJvAEi94T4kjsRKkezEtLQTgsNEsW2lQ==} 1456 | engines: {node: '>=18'} 1457 | peerDependencies: 1458 | canvas: ^2.11.2 1459 | peerDependenciesMeta: 1460 | canvas: 1461 | optional: true 1462 | dependencies: 1463 | cssstyle: 3.0.0 1464 | data-urls: 5.0.0 1465 | decimal.js: 10.4.3 1466 | form-data: 4.0.0 1467 | html-encoding-sniffer: 4.0.0 1468 | http-proxy-agent: 7.0.0 1469 | https-proxy-agent: 7.0.2 1470 | is-potential-custom-element-name: 1.0.1 1471 | nwsapi: 2.2.7 1472 | parse5: 7.1.2 1473 | rrweb-cssom: 0.6.0 1474 | saxes: 6.0.0 1475 | symbol-tree: 3.2.4 1476 | tough-cookie: 4.1.3 1477 | w3c-xmlserializer: 5.0.0 1478 | webidl-conversions: 7.0.0 1479 | whatwg-encoding: 3.1.1 1480 | whatwg-mimetype: 4.0.0 1481 | whatwg-url: 14.0.0 1482 | ws: 8.15.0 1483 | xml-name-validator: 5.0.0 1484 | transitivePeerDependencies: 1485 | - bufferutil 1486 | - supports-color 1487 | - utf-8-validate 1488 | dev: true 1489 | 1490 | /jsonc-parser@3.2.0: 1491 | resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} 1492 | dev: true 1493 | 1494 | /local-pkg@0.5.0: 1495 | resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==} 1496 | engines: {node: '>=14'} 1497 | dependencies: 1498 | mlly: 1.4.2 1499 | pkg-types: 1.0.3 1500 | dev: true 1501 | 1502 | /lodash@4.17.21: 1503 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 1504 | dev: true 1505 | 1506 | /loupe@2.3.7: 1507 | resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} 1508 | dependencies: 1509 | get-func-name: 2.0.2 1510 | dev: true 1511 | 1512 | /lru-cache@10.1.0: 1513 | resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} 1514 | engines: {node: 14 || >=16.14} 1515 | dev: true 1516 | 1517 | /magic-string@0.30.5: 1518 | resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} 1519 | engines: {node: '>=12'} 1520 | dependencies: 1521 | '@jridgewell/sourcemap-codec': 1.4.15 1522 | dev: true 1523 | 1524 | /make-error@1.3.6: 1525 | resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} 1526 | dev: true 1527 | 1528 | /media-typer@0.3.0: 1529 | resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 1530 | engines: {node: '>= 0.6'} 1531 | dev: false 1532 | 1533 | /merge-descriptors@1.0.1: 1534 | resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} 1535 | dev: false 1536 | 1537 | /merge-stream@2.0.0: 1538 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 1539 | dev: true 1540 | 1541 | /methods@1.1.2: 1542 | resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} 1543 | engines: {node: '>= 0.6'} 1544 | dev: false 1545 | 1546 | /mime-db@1.52.0: 1547 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 1548 | engines: {node: '>= 0.6'} 1549 | 1550 | /mime-types@2.1.35: 1551 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 1552 | engines: {node: '>= 0.6'} 1553 | dependencies: 1554 | mime-db: 1.52.0 1555 | 1556 | /mime@1.6.0: 1557 | resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 1558 | engines: {node: '>=4'} 1559 | hasBin: true 1560 | dev: false 1561 | 1562 | /mimic-fn@4.0.0: 1563 | resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} 1564 | engines: {node: '>=12'} 1565 | dev: true 1566 | 1567 | /minimatch@3.1.2: 1568 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 1569 | dependencies: 1570 | brace-expansion: 1.1.11 1571 | dev: true 1572 | 1573 | /minimatch@9.0.3: 1574 | resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} 1575 | engines: {node: '>=16 || 14 >=14.17'} 1576 | dependencies: 1577 | brace-expansion: 2.0.1 1578 | dev: true 1579 | 1580 | /minimist@1.2.8: 1581 | resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} 1582 | dev: true 1583 | 1584 | /minipass@7.0.4: 1585 | resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} 1586 | engines: {node: '>=16 || 14 >=14.17'} 1587 | dev: true 1588 | 1589 | /mkdirp@0.5.6: 1590 | resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} 1591 | hasBin: true 1592 | dependencies: 1593 | minimist: 1.2.8 1594 | dev: true 1595 | 1596 | /mkdirp@1.0.4: 1597 | resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} 1598 | engines: {node: '>=10'} 1599 | hasBin: true 1600 | dev: true 1601 | 1602 | /mlly@1.4.2: 1603 | resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} 1604 | dependencies: 1605 | acorn: 8.11.2 1606 | pathe: 1.1.1 1607 | pkg-types: 1.0.3 1608 | ufo: 1.3.2 1609 | dev: true 1610 | 1611 | /ms@2.0.0: 1612 | resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} 1613 | dev: false 1614 | 1615 | /ms@2.1.2: 1616 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 1617 | 1618 | /ms@2.1.3: 1619 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1620 | 1621 | /msgpackr-extract@3.0.2: 1622 | resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==} 1623 | hasBin: true 1624 | requiresBuild: true 1625 | dependencies: 1626 | node-gyp-build-optional-packages: 5.0.7 1627 | optionalDependencies: 1628 | '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.2 1629 | '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.2 1630 | '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.2 1631 | '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.2 1632 | '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.2 1633 | '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.2 1634 | dev: false 1635 | optional: true 1636 | 1637 | /msgpackr@1.10.0: 1638 | resolution: {integrity: sha512-rVQ5YAQDoZKZLX+h8tNq7FiHrPJoeGHViz3U4wIcykhAEpwF/nH2Vbk8dQxmpX5JavkI8C7pt4bnkJ02ZmRoUw==} 1639 | optionalDependencies: 1640 | msgpackr-extract: 3.0.2 1641 | dev: false 1642 | 1643 | /nanoid@3.3.7: 1644 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 1645 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1646 | hasBin: true 1647 | dev: true 1648 | 1649 | /negotiator@0.6.3: 1650 | resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 1651 | engines: {node: '>= 0.6'} 1652 | dev: false 1653 | 1654 | /node-gyp-build-optional-packages@5.0.7: 1655 | resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} 1656 | hasBin: true 1657 | requiresBuild: true 1658 | dev: false 1659 | optional: true 1660 | 1661 | /normalize-path@3.0.0: 1662 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 1663 | engines: {node: '>=0.10.0'} 1664 | dev: true 1665 | 1666 | /npm-run-path@5.1.0: 1667 | resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} 1668 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 1669 | dependencies: 1670 | path-key: 4.0.0 1671 | dev: true 1672 | 1673 | /nwsapi@2.2.7: 1674 | resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} 1675 | dev: true 1676 | 1677 | /object-inspect@1.13.1: 1678 | resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} 1679 | dev: false 1680 | 1681 | /on-finished@2.4.1: 1682 | resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 1683 | engines: {node: '>= 0.8'} 1684 | dependencies: 1685 | ee-first: 1.1.1 1686 | dev: false 1687 | 1688 | /once@1.4.0: 1689 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1690 | dependencies: 1691 | wrappy: 1.0.2 1692 | dev: true 1693 | 1694 | /onetime@6.0.0: 1695 | resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} 1696 | engines: {node: '>=12'} 1697 | dependencies: 1698 | mimic-fn: 4.0.0 1699 | dev: true 1700 | 1701 | /p-limit@5.0.0: 1702 | resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} 1703 | engines: {node: '>=18'} 1704 | dependencies: 1705 | yocto-queue: 1.0.0 1706 | dev: true 1707 | 1708 | /parse5@7.1.2: 1709 | resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} 1710 | dependencies: 1711 | entities: 4.5.0 1712 | dev: true 1713 | 1714 | /parseurl@1.3.3: 1715 | resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} 1716 | engines: {node: '>= 0.8'} 1717 | dev: false 1718 | 1719 | /path-is-absolute@1.0.1: 1720 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 1721 | engines: {node: '>=0.10.0'} 1722 | dev: true 1723 | 1724 | /path-key@3.1.1: 1725 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1726 | engines: {node: '>=8'} 1727 | dev: true 1728 | 1729 | /path-key@4.0.0: 1730 | resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} 1731 | engines: {node: '>=12'} 1732 | dev: true 1733 | 1734 | /path-parse@1.0.7: 1735 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 1736 | dev: true 1737 | 1738 | /path-scurry@1.10.1: 1739 | resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} 1740 | engines: {node: '>=16 || 14 >=14.17'} 1741 | dependencies: 1742 | lru-cache: 10.1.0 1743 | minipass: 7.0.4 1744 | dev: true 1745 | 1746 | /path-to-regexp@0.1.7: 1747 | resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} 1748 | dev: false 1749 | 1750 | /pathe@1.1.1: 1751 | resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} 1752 | dev: true 1753 | 1754 | /pathval@1.1.1: 1755 | resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} 1756 | dev: true 1757 | 1758 | /picocolors@1.0.0: 1759 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 1760 | dev: true 1761 | 1762 | /picomatch@2.3.1: 1763 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1764 | engines: {node: '>=8.6'} 1765 | dev: true 1766 | 1767 | /pkg-types@1.0.3: 1768 | resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} 1769 | dependencies: 1770 | jsonc-parser: 3.2.0 1771 | mlly: 1.4.2 1772 | pathe: 1.1.1 1773 | dev: true 1774 | 1775 | /portfinder@1.0.32: 1776 | resolution: {integrity: sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==} 1777 | engines: {node: '>= 0.12.0'} 1778 | dependencies: 1779 | async: 2.6.4 1780 | debug: 3.2.7 1781 | mkdirp: 0.5.6 1782 | transitivePeerDependencies: 1783 | - supports-color 1784 | dev: true 1785 | 1786 | /postcss@8.4.32: 1787 | resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} 1788 | engines: {node: ^10 || ^12 || >=14} 1789 | dependencies: 1790 | nanoid: 3.3.7 1791 | picocolors: 1.0.0 1792 | source-map-js: 1.0.2 1793 | dev: true 1794 | 1795 | /prettier@3.1.1: 1796 | resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==} 1797 | engines: {node: '>=14'} 1798 | hasBin: true 1799 | dev: true 1800 | 1801 | /pretty-format@29.7.0: 1802 | resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} 1803 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 1804 | dependencies: 1805 | '@jest/schemas': 29.6.3 1806 | ansi-styles: 5.2.0 1807 | react-is: 18.2.0 1808 | dev: true 1809 | 1810 | /proxy-addr@2.0.7: 1811 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 1812 | engines: {node: '>= 0.10'} 1813 | dependencies: 1814 | forwarded: 0.2.0 1815 | ipaddr.js: 1.9.1 1816 | dev: false 1817 | 1818 | /psl@1.9.0: 1819 | resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} 1820 | dev: true 1821 | 1822 | /punycode@2.3.1: 1823 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1824 | engines: {node: '>=6'} 1825 | dev: true 1826 | 1827 | /qs@6.11.0: 1828 | resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} 1829 | engines: {node: '>=0.6'} 1830 | dependencies: 1831 | side-channel: 1.0.4 1832 | dev: false 1833 | 1834 | /querystringify@2.2.0: 1835 | resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} 1836 | dev: true 1837 | 1838 | /range-parser@1.2.1: 1839 | resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 1840 | engines: {node: '>= 0.6'} 1841 | dev: false 1842 | 1843 | /raw-body@2.5.1: 1844 | resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} 1845 | engines: {node: '>= 0.8'} 1846 | dependencies: 1847 | bytes: 3.1.2 1848 | http-errors: 2.0.0 1849 | iconv-lite: 0.4.24 1850 | unpipe: 1.0.0 1851 | dev: false 1852 | 1853 | /react-is@18.2.0: 1854 | resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} 1855 | dev: true 1856 | 1857 | /readdirp@3.6.0: 1858 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1859 | engines: {node: '>=8.10.0'} 1860 | dependencies: 1861 | picomatch: 2.3.1 1862 | dev: true 1863 | 1864 | /requires-port@1.0.0: 1865 | resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} 1866 | dev: true 1867 | 1868 | /resolve@1.22.8: 1869 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 1870 | hasBin: true 1871 | dependencies: 1872 | is-core-module: 2.13.1 1873 | path-parse: 1.0.7 1874 | supports-preserve-symlinks-flag: 1.0.0 1875 | dev: true 1876 | 1877 | /rimraf@2.7.1: 1878 | resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} 1879 | hasBin: true 1880 | dependencies: 1881 | glob: 7.2.3 1882 | dev: true 1883 | 1884 | /rimraf@5.0.5: 1885 | resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} 1886 | engines: {node: '>=14'} 1887 | hasBin: true 1888 | dependencies: 1889 | glob: 10.3.10 1890 | dev: true 1891 | 1892 | /rollup@4.8.0: 1893 | resolution: {integrity: sha512-NpsklK2fach5CdI+PScmlE5R4Ao/FSWtF7LkoIrHDxPACY/xshNasPsbpG0VVHxUTbf74tJbVT4PrP8JsJ6ZDA==} 1894 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} 1895 | hasBin: true 1896 | optionalDependencies: 1897 | '@rollup/rollup-android-arm-eabi': 4.8.0 1898 | '@rollup/rollup-android-arm64': 4.8.0 1899 | '@rollup/rollup-darwin-arm64': 4.8.0 1900 | '@rollup/rollup-darwin-x64': 4.8.0 1901 | '@rollup/rollup-linux-arm-gnueabihf': 4.8.0 1902 | '@rollup/rollup-linux-arm64-gnu': 4.8.0 1903 | '@rollup/rollup-linux-arm64-musl': 4.8.0 1904 | '@rollup/rollup-linux-riscv64-gnu': 4.8.0 1905 | '@rollup/rollup-linux-x64-gnu': 4.8.0 1906 | '@rollup/rollup-linux-x64-musl': 4.8.0 1907 | '@rollup/rollup-win32-arm64-msvc': 4.8.0 1908 | '@rollup/rollup-win32-ia32-msvc': 4.8.0 1909 | '@rollup/rollup-win32-x64-msvc': 4.8.0 1910 | fsevents: 2.3.3 1911 | dev: true 1912 | 1913 | /rrweb-cssom@0.6.0: 1914 | resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==} 1915 | dev: true 1916 | 1917 | /safe-buffer@5.2.1: 1918 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} 1919 | dev: false 1920 | 1921 | /safer-buffer@2.1.2: 1922 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 1923 | 1924 | /saxes@6.0.0: 1925 | resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} 1926 | engines: {node: '>=v12.22.7'} 1927 | dependencies: 1928 | xmlchars: 2.2.0 1929 | dev: true 1930 | 1931 | /send@0.18.0: 1932 | resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} 1933 | engines: {node: '>= 0.8.0'} 1934 | dependencies: 1935 | debug: 2.6.9 1936 | depd: 2.0.0 1937 | destroy: 1.2.0 1938 | encodeurl: 1.0.2 1939 | escape-html: 1.0.3 1940 | etag: 1.8.1 1941 | fresh: 0.5.2 1942 | http-errors: 2.0.0 1943 | mime: 1.6.0 1944 | ms: 2.1.3 1945 | on-finished: 2.4.1 1946 | range-parser: 1.2.1 1947 | statuses: 2.0.1 1948 | transitivePeerDependencies: 1949 | - supports-color 1950 | dev: false 1951 | 1952 | /serve-static@1.15.0: 1953 | resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} 1954 | engines: {node: '>= 0.8.0'} 1955 | dependencies: 1956 | encodeurl: 1.0.2 1957 | escape-html: 1.0.3 1958 | parseurl: 1.3.3 1959 | send: 0.18.0 1960 | transitivePeerDependencies: 1961 | - supports-color 1962 | dev: false 1963 | 1964 | /set-function-length@1.1.1: 1965 | resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} 1966 | engines: {node: '>= 0.4'} 1967 | dependencies: 1968 | define-data-property: 1.1.1 1969 | get-intrinsic: 1.2.2 1970 | gopd: 1.0.1 1971 | has-property-descriptors: 1.0.1 1972 | dev: false 1973 | 1974 | /setprototypeof@1.2.0: 1975 | resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} 1976 | dev: false 1977 | 1978 | /shebang-command@2.0.0: 1979 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1980 | engines: {node: '>=8'} 1981 | dependencies: 1982 | shebang-regex: 3.0.0 1983 | dev: true 1984 | 1985 | /shebang-regex@3.0.0: 1986 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1987 | engines: {node: '>=8'} 1988 | dev: true 1989 | 1990 | /side-channel@1.0.4: 1991 | resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} 1992 | dependencies: 1993 | call-bind: 1.0.5 1994 | get-intrinsic: 1.2.2 1995 | object-inspect: 1.13.1 1996 | dev: false 1997 | 1998 | /siginfo@2.0.0: 1999 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 2000 | dev: true 2001 | 2002 | /signal-exit@4.1.0: 2003 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 2004 | engines: {node: '>=14'} 2005 | dev: true 2006 | 2007 | /source-map-js@1.0.2: 2008 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 2009 | engines: {node: '>=0.10.0'} 2010 | dev: true 2011 | 2012 | /source-map-support@0.5.21: 2013 | resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} 2014 | dependencies: 2015 | buffer-from: 1.1.2 2016 | source-map: 0.6.1 2017 | dev: true 2018 | 2019 | /source-map@0.6.1: 2020 | resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} 2021 | engines: {node: '>=0.10.0'} 2022 | dev: true 2023 | 2024 | /stackback@0.0.2: 2025 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 2026 | dev: true 2027 | 2028 | /statuses@2.0.1: 2029 | resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 2030 | engines: {node: '>= 0.8'} 2031 | dev: false 2032 | 2033 | /std-env@3.6.0: 2034 | resolution: {integrity: sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==} 2035 | dev: true 2036 | 2037 | /string-width@4.2.3: 2038 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 2039 | engines: {node: '>=8'} 2040 | dependencies: 2041 | emoji-regex: 8.0.0 2042 | is-fullwidth-code-point: 3.0.0 2043 | strip-ansi: 6.0.1 2044 | dev: true 2045 | 2046 | /string-width@5.1.2: 2047 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 2048 | engines: {node: '>=12'} 2049 | dependencies: 2050 | eastasianwidth: 0.2.0 2051 | emoji-regex: 9.2.2 2052 | strip-ansi: 7.1.0 2053 | dev: true 2054 | 2055 | /strip-ansi@6.0.1: 2056 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 2057 | engines: {node: '>=8'} 2058 | dependencies: 2059 | ansi-regex: 5.0.1 2060 | dev: true 2061 | 2062 | /strip-ansi@7.1.0: 2063 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 2064 | engines: {node: '>=12'} 2065 | dependencies: 2066 | ansi-regex: 6.0.1 2067 | dev: true 2068 | 2069 | /strip-bom@3.0.0: 2070 | resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} 2071 | engines: {node: '>=4'} 2072 | dev: true 2073 | 2074 | /strip-final-newline@3.0.0: 2075 | resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} 2076 | engines: {node: '>=12'} 2077 | dev: true 2078 | 2079 | /strip-json-comments@2.0.1: 2080 | resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} 2081 | engines: {node: '>=0.10.0'} 2082 | dev: true 2083 | 2084 | /strip-literal@1.3.0: 2085 | resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} 2086 | dependencies: 2087 | acorn: 8.11.2 2088 | dev: true 2089 | 2090 | /supports-preserve-symlinks-flag@1.0.0: 2091 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 2092 | engines: {node: '>= 0.4'} 2093 | dev: true 2094 | 2095 | /symbol-tree@3.2.4: 2096 | resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} 2097 | dev: true 2098 | 2099 | /tinybench@2.5.1: 2100 | resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} 2101 | dev: true 2102 | 2103 | /tinypool@0.8.1: 2104 | resolution: {integrity: sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==} 2105 | engines: {node: '>=14.0.0'} 2106 | dev: true 2107 | 2108 | /tinyspy@2.2.0: 2109 | resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} 2110 | engines: {node: '>=14.0.0'} 2111 | dev: true 2112 | 2113 | /to-regex-range@5.0.1: 2114 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 2115 | engines: {node: '>=8.0'} 2116 | dependencies: 2117 | is-number: 7.0.0 2118 | dev: true 2119 | 2120 | /toidentifier@1.0.1: 2121 | resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 2122 | engines: {node: '>=0.6'} 2123 | dev: false 2124 | 2125 | /tough-cookie@4.1.3: 2126 | resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} 2127 | engines: {node: '>=6'} 2128 | dependencies: 2129 | psl: 1.9.0 2130 | punycode: 2.3.1 2131 | universalify: 0.2.0 2132 | url-parse: 1.5.10 2133 | dev: true 2134 | 2135 | /tr46@5.0.0: 2136 | resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==} 2137 | engines: {node: '>=18'} 2138 | dependencies: 2139 | punycode: 2.3.1 2140 | dev: true 2141 | 2142 | /tree-kill@1.2.2: 2143 | resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} 2144 | hasBin: true 2145 | dev: true 2146 | 2147 | /ts-node-dev@2.0.0(@types/node@20.10.4)(typescript@5.3.3): 2148 | resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} 2149 | engines: {node: '>=0.8.0'} 2150 | hasBin: true 2151 | peerDependencies: 2152 | node-notifier: '*' 2153 | typescript: '*' 2154 | peerDependenciesMeta: 2155 | node-notifier: 2156 | optional: true 2157 | dependencies: 2158 | chokidar: 3.5.3 2159 | dynamic-dedupe: 0.3.0 2160 | minimist: 1.2.8 2161 | mkdirp: 1.0.4 2162 | resolve: 1.22.8 2163 | rimraf: 2.7.1 2164 | source-map-support: 0.5.21 2165 | tree-kill: 1.2.2 2166 | ts-node: 10.9.2(@types/node@20.10.4)(typescript@5.3.3) 2167 | tsconfig: 7.0.0 2168 | typescript: 5.3.3 2169 | transitivePeerDependencies: 2170 | - '@swc/core' 2171 | - '@swc/wasm' 2172 | - '@types/node' 2173 | dev: true 2174 | 2175 | /ts-node@10.9.2(@types/node@20.10.4)(typescript@5.3.3): 2176 | resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} 2177 | hasBin: true 2178 | peerDependencies: 2179 | '@swc/core': '>=1.2.50' 2180 | '@swc/wasm': '>=1.2.50' 2181 | '@types/node': '*' 2182 | typescript: '>=2.7' 2183 | peerDependenciesMeta: 2184 | '@swc/core': 2185 | optional: true 2186 | '@swc/wasm': 2187 | optional: true 2188 | dependencies: 2189 | '@cspotcode/source-map-support': 0.8.1 2190 | '@tsconfig/node10': 1.0.9 2191 | '@tsconfig/node12': 1.0.11 2192 | '@tsconfig/node14': 1.0.3 2193 | '@tsconfig/node16': 1.0.4 2194 | '@types/node': 20.10.4 2195 | acorn: 8.11.2 2196 | acorn-walk: 8.3.1 2197 | arg: 4.1.3 2198 | create-require: 1.1.1 2199 | diff: 4.0.2 2200 | make-error: 1.3.6 2201 | typescript: 5.3.3 2202 | v8-compile-cache-lib: 3.0.1 2203 | yn: 3.1.1 2204 | dev: true 2205 | 2206 | /tsconfig@7.0.0: 2207 | resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} 2208 | dependencies: 2209 | '@types/strip-bom': 3.0.0 2210 | '@types/strip-json-comments': 0.0.30 2211 | strip-bom: 3.0.0 2212 | strip-json-comments: 2.0.1 2213 | dev: true 2214 | 2215 | /type-detect@4.0.8: 2216 | resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} 2217 | engines: {node: '>=4'} 2218 | dev: true 2219 | 2220 | /type-is@1.6.18: 2221 | resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} 2222 | engines: {node: '>= 0.6'} 2223 | dependencies: 2224 | media-typer: 0.3.0 2225 | mime-types: 2.1.35 2226 | dev: false 2227 | 2228 | /typescript@5.3.3: 2229 | resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==} 2230 | engines: {node: '>=14.17'} 2231 | hasBin: true 2232 | dev: true 2233 | 2234 | /ufo@1.3.2: 2235 | resolution: {integrity: sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==} 2236 | dev: true 2237 | 2238 | /undici-types@5.26.5: 2239 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 2240 | dev: true 2241 | 2242 | /universalify@0.2.0: 2243 | resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} 2244 | engines: {node: '>= 4.0.0'} 2245 | dev: true 2246 | 2247 | /unpipe@1.0.0: 2248 | resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} 2249 | engines: {node: '>= 0.8'} 2250 | dev: false 2251 | 2252 | /url-parse@1.5.10: 2253 | resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} 2254 | dependencies: 2255 | querystringify: 2.2.0 2256 | requires-port: 1.0.0 2257 | dev: true 2258 | 2259 | /utils-merge@1.0.1: 2260 | resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} 2261 | engines: {node: '>= 0.4.0'} 2262 | dev: false 2263 | 2264 | /v8-compile-cache-lib@3.0.1: 2265 | resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} 2266 | dev: true 2267 | 2268 | /vary@1.1.2: 2269 | resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} 2270 | engines: {node: '>= 0.8'} 2271 | dev: false 2272 | 2273 | /vite-node@1.0.4(@types/node@20.10.4): 2274 | resolution: {integrity: sha512-9xQQtHdsz5Qn8hqbV7UKqkm8YkJhzT/zr41Dmt5N7AlD8hJXw/Z7y0QiD5I8lnTthV9Rvcvi0QW7PI0Fq83ZPg==} 2275 | engines: {node: ^18.0.0 || >=20.0.0} 2276 | hasBin: true 2277 | dependencies: 2278 | cac: 6.7.14 2279 | debug: 4.3.4 2280 | pathe: 1.1.1 2281 | picocolors: 1.0.0 2282 | vite: 5.0.7(@types/node@20.10.4) 2283 | transitivePeerDependencies: 2284 | - '@types/node' 2285 | - less 2286 | - lightningcss 2287 | - sass 2288 | - stylus 2289 | - sugarss 2290 | - supports-color 2291 | - terser 2292 | dev: true 2293 | 2294 | /vite@5.0.7(@types/node@20.10.4): 2295 | resolution: {integrity: sha512-B4T4rJCDPihrQo2B+h1MbeGL/k/GMAHzhQ8S0LjQ142s6/+l3hHTT095ORvsshj4QCkoWu3Xtmob5mazvakaOw==} 2296 | engines: {node: ^18.0.0 || >=20.0.0} 2297 | hasBin: true 2298 | peerDependencies: 2299 | '@types/node': ^18.0.0 || >=20.0.0 2300 | less: '*' 2301 | lightningcss: ^1.21.0 2302 | sass: '*' 2303 | stylus: '*' 2304 | sugarss: '*' 2305 | terser: ^5.4.0 2306 | peerDependenciesMeta: 2307 | '@types/node': 2308 | optional: true 2309 | less: 2310 | optional: true 2311 | lightningcss: 2312 | optional: true 2313 | sass: 2314 | optional: true 2315 | stylus: 2316 | optional: true 2317 | sugarss: 2318 | optional: true 2319 | terser: 2320 | optional: true 2321 | dependencies: 2322 | '@types/node': 20.10.4 2323 | esbuild: 0.19.9 2324 | postcss: 8.4.32 2325 | rollup: 4.8.0 2326 | optionalDependencies: 2327 | fsevents: 2.3.3 2328 | dev: true 2329 | 2330 | /vitest@1.0.4(@types/node@20.10.4)(jsdom@23.0.1): 2331 | resolution: {integrity: sha512-s1GQHp/UOeWEo4+aXDOeFBJwFzL6mjycbQwwKWX2QcYfh/7tIerS59hWQ20mxzupTJluA2SdwiBuWwQHH67ckg==} 2332 | engines: {node: ^18.0.0 || >=20.0.0} 2333 | hasBin: true 2334 | peerDependencies: 2335 | '@edge-runtime/vm': '*' 2336 | '@types/node': ^18.0.0 || >=20.0.0 2337 | '@vitest/browser': ^1.0.0 2338 | '@vitest/ui': ^1.0.0 2339 | happy-dom: '*' 2340 | jsdom: '*' 2341 | peerDependenciesMeta: 2342 | '@edge-runtime/vm': 2343 | optional: true 2344 | '@types/node': 2345 | optional: true 2346 | '@vitest/browser': 2347 | optional: true 2348 | '@vitest/ui': 2349 | optional: true 2350 | happy-dom: 2351 | optional: true 2352 | jsdom: 2353 | optional: true 2354 | dependencies: 2355 | '@types/node': 20.10.4 2356 | '@vitest/expect': 1.0.4 2357 | '@vitest/runner': 1.0.4 2358 | '@vitest/snapshot': 1.0.4 2359 | '@vitest/spy': 1.0.4 2360 | '@vitest/utils': 1.0.4 2361 | acorn-walk: 8.3.1 2362 | cac: 6.7.14 2363 | chai: 4.3.10 2364 | debug: 4.3.4 2365 | execa: 8.0.1 2366 | jsdom: 23.0.1 2367 | local-pkg: 0.5.0 2368 | magic-string: 0.30.5 2369 | pathe: 1.1.1 2370 | picocolors: 1.0.0 2371 | std-env: 3.6.0 2372 | strip-literal: 1.3.0 2373 | tinybench: 2.5.1 2374 | tinypool: 0.8.1 2375 | vite: 5.0.7(@types/node@20.10.4) 2376 | vite-node: 1.0.4(@types/node@20.10.4) 2377 | why-is-node-running: 2.2.2 2378 | transitivePeerDependencies: 2379 | - less 2380 | - lightningcss 2381 | - sass 2382 | - stylus 2383 | - sugarss 2384 | - supports-color 2385 | - terser 2386 | dev: true 2387 | 2388 | /w3c-xmlserializer@5.0.0: 2389 | resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} 2390 | engines: {node: '>=18'} 2391 | dependencies: 2392 | xml-name-validator: 5.0.0 2393 | dev: true 2394 | 2395 | /webidl-conversions@7.0.0: 2396 | resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} 2397 | engines: {node: '>=12'} 2398 | dev: true 2399 | 2400 | /whatwg-encoding@3.1.1: 2401 | resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} 2402 | engines: {node: '>=18'} 2403 | dependencies: 2404 | iconv-lite: 0.6.3 2405 | dev: true 2406 | 2407 | /whatwg-mimetype@4.0.0: 2408 | resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} 2409 | engines: {node: '>=18'} 2410 | dev: true 2411 | 2412 | /whatwg-url@14.0.0: 2413 | resolution: {integrity: sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==} 2414 | engines: {node: '>=18'} 2415 | dependencies: 2416 | tr46: 5.0.0 2417 | webidl-conversions: 7.0.0 2418 | dev: true 2419 | 2420 | /which@2.0.2: 2421 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 2422 | engines: {node: '>= 8'} 2423 | hasBin: true 2424 | dependencies: 2425 | isexe: 2.0.0 2426 | dev: true 2427 | 2428 | /why-is-node-running@2.2.2: 2429 | resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} 2430 | engines: {node: '>=8'} 2431 | hasBin: true 2432 | dependencies: 2433 | siginfo: 2.0.0 2434 | stackback: 0.0.2 2435 | dev: true 2436 | 2437 | /wrap-ansi@7.0.0: 2438 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 2439 | engines: {node: '>=10'} 2440 | dependencies: 2441 | ansi-styles: 4.3.0 2442 | string-width: 4.2.3 2443 | strip-ansi: 6.0.1 2444 | dev: true 2445 | 2446 | /wrap-ansi@8.1.0: 2447 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 2448 | engines: {node: '>=12'} 2449 | dependencies: 2450 | ansi-styles: 6.2.1 2451 | string-width: 5.1.2 2452 | strip-ansi: 7.1.0 2453 | dev: true 2454 | 2455 | /wrappy@1.0.2: 2456 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 2457 | dev: true 2458 | 2459 | /ws@7.5.9: 2460 | resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} 2461 | engines: {node: '>=8.3.0'} 2462 | peerDependencies: 2463 | bufferutil: ^4.0.1 2464 | utf-8-validate: ^5.0.2 2465 | peerDependenciesMeta: 2466 | bufferutil: 2467 | optional: true 2468 | utf-8-validate: 2469 | optional: true 2470 | dev: false 2471 | 2472 | /ws@8.15.0: 2473 | resolution: {integrity: sha512-H/Z3H55mrcrgjFwI+5jKavgXvwQLtfPCUEp6pi35VhoB0pfcHnSoyuTzkBEZpzq49g1193CUEwIvmsjcotenYw==} 2474 | engines: {node: '>=10.0.0'} 2475 | peerDependencies: 2476 | bufferutil: ^4.0.1 2477 | utf-8-validate: '>=5.0.2' 2478 | peerDependenciesMeta: 2479 | bufferutil: 2480 | optional: true 2481 | utf-8-validate: 2482 | optional: true 2483 | 2484 | /xml-name-validator@5.0.0: 2485 | resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} 2486 | engines: {node: '>=18'} 2487 | dev: true 2488 | 2489 | /xmlchars@2.2.0: 2490 | resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} 2491 | dev: true 2492 | 2493 | /xtend@4.0.2: 2494 | resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} 2495 | engines: {node: '>=0.4'} 2496 | dev: true 2497 | 2498 | /yn@3.1.1: 2499 | resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} 2500 | engines: {node: '>=6'} 2501 | dev: true 2502 | 2503 | /yocto-queue@1.0.0: 2504 | resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} 2505 | engines: {node: '>=12.20'} 2506 | dev: true 2507 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: "avoid", 3 | bracketSpacing: true, 4 | endOfLine: "auto", 5 | semi: false, 6 | singleQuote: false, 7 | tabWidth: 2, 8 | trailingComma: "es5", 9 | useTabs: false, 10 | experimentalTernaries: true, 11 | } 12 | -------------------------------------------------------------------------------- /src/Client.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug" 2 | import WebSocket from "isomorphic-ws" 3 | import pkg from "../package.json" assert { type: "json" } 4 | import { EventEmitter } from "./lib/EventEmitter.js" 5 | import { isReady } from "./lib/isReady.js" 6 | import { pack, unpack } from "./lib/msgpack.js" 7 | import { newid } from "./lib/newid.js" 8 | import type { 9 | ClientEvents, 10 | ClientOptions, 11 | DocumentId, 12 | Message, 13 | PeerId, 14 | PeerSocketMap, 15 | } from "./types.js" 16 | 17 | const { version } = pkg 18 | const HEARTBEAT = pack({ type: "Heartbeat" }) 19 | const HEARTBEAT_INTERVAL = 55000 // 55 seconds 20 | 21 | export interface PeerEventPayload { 22 | peerId: PeerId 23 | documentId: DocumentId 24 | socket: WebSocket 25 | } 26 | 27 | /** 28 | * This is a client for `relay` that keeps track of all peers that the server connects you to, and 29 | * for each peer it keeps track of each documentId (aka discoveryKey, aka channel) that you're 30 | * working with that peer on. 31 | * 32 | * The peers are WebSocket instances. 33 | * 34 | * The simplest workflow is something like this: 35 | * 36 | * ```ts 37 | * client = new Client({ peerId: 'my-peer-peerId', url }) 38 | * .join('my-document-peerId') 39 | * .addEventListener('peer-connect', ({documentId, peerId, socket}) => { 40 | * // send a message 41 | * socket.send('Hello!') 42 | * 43 | * // listen for messages 44 | * socket.addEventListener("message", (event) => { 45 | * const message = event.data 46 | * console.log(message) 47 | * }) 48 | * }) 49 | * ``` 50 | */ 51 | export class Client extends EventEmitter { 52 | public peerId: PeerId 53 | 54 | /** The base URL of the relay server */ 55 | public url: string 56 | 57 | /** All the document IDs we're interested in */ 58 | public documentIds: Set = new Set() 59 | 60 | /** All the peers we're connected to. (A 'peer' in this case is actually just a bunch of sockets - 61 | * one per documentId that we have in common.) */ 62 | public peers: Map = new Map() 63 | 64 | /** When disconnected, the delay in milliseconds before the next retry */ 65 | public retryDelay: number 66 | 67 | /** Is the connection to the server currently open? */ 68 | public open: boolean 69 | 70 | /** If the connection is closed, do we want to reopen it? */ 71 | private shouldReconnectIfClosed: boolean = true 72 | 73 | /** Parameters for retries */ 74 | private minRetryDelay: number 75 | private maxRetryDelay: number 76 | private backoffFactor: number 77 | 78 | /** Reference to the heartbeat interval */ 79 | private heartbeat: ReturnType 80 | 81 | private serverConnection: WebSocket 82 | private pendingMessages: Message.ClientToServer[] = [] 83 | 84 | /** 85 | * @param peerId a string that identifies you uniquely, defaults to a CUID 86 | * @param url the url of the `relay`, e.g. `http://myrelay.mydomain.com` 87 | * @param documentIds one or more document IDs that you're interested in 88 | */ 89 | constructor({ 90 | peerId = newid(), 91 | url, 92 | documentIds = [], 93 | minRetryDelay = 10, 94 | backoffFactor = 1.5, 95 | maxRetryDelay = 30000, 96 | }: ClientOptions) { 97 | super() 98 | this.log = debug(`lf:relay:client:${peerId}`) 99 | this.log("version", version) 100 | 101 | this.peerId = peerId 102 | this.url = url 103 | this.minRetryDelay = minRetryDelay 104 | this.maxRetryDelay = maxRetryDelay 105 | this.backoffFactor = backoffFactor 106 | 107 | // start out at the initial retry delay 108 | this.retryDelay = minRetryDelay 109 | 110 | this.connectToServer() 111 | documentIds.forEach(id => this.join(id)) 112 | } 113 | 114 | // PUBLIC API 115 | 116 | /** 117 | * Connects to the relay server, lets it know what documents we're interested in 118 | * @param documentIds array of IDs of documents we're interested in 119 | * @returns the socket connecting us to the server 120 | */ 121 | public connectToServer() { 122 | const url = `${this.url}/introduction/${this.peerId}` 123 | this.log("connecting to relay server", url) 124 | 125 | this.serverConnection = new WebSocket(url) 126 | this.serverConnection.binaryType = "arraybuffer" 127 | 128 | this.serverConnection.addEventListener("open", () => { 129 | this.onServerOpen() 130 | }) 131 | this.serverConnection.addEventListener("message", event => { 132 | this.onServerMessage(event) 133 | }) 134 | this.serverConnection.addEventListener("close", () => { 135 | this.onServerClose() 136 | }) 137 | this.serverConnection.addEventListener("error", event => { 138 | this.onServerError(event) 139 | }) 140 | } 141 | 142 | /** 143 | * Lets the server know that you're interested in a document. If there are other peers who have 144 | * joined the same DocumentId, you and the remote peer will both receive an introduction message, 145 | * inviting you to connect. 146 | * @param documentId 147 | */ 148 | public join(documentId: DocumentId) { 149 | this.log("joining", documentId) 150 | this.documentIds.add(documentId) 151 | const message: Message.Join = { type: "Join", documentIds: [documentId] } 152 | this.send(message) 153 | return this 154 | } 155 | 156 | /** 157 | * Leaves a documentId and closes any connections related to it 158 | * @param documentId 159 | */ 160 | public leave(documentId: DocumentId) { 161 | this.log("leaving", documentId) 162 | this.documentIds.delete(documentId) 163 | for (const [peerId] of this.peers) { 164 | this.closeSocket(peerId, documentId) 165 | } 166 | const message: Message.Leave = { type: "Leave", documentIds: [documentId] } 167 | this.send(message) 168 | return this 169 | } 170 | 171 | /** 172 | * Disconnects from one peer 173 | * @param peerPeerId Name of the peer to disconnect. If none is provided, we disconnect all peers. 174 | */ 175 | public disconnectPeer(peerPeerId: PeerId) { 176 | this.log(`disconnecting from ${peerPeerId}`) 177 | const peer = this.get(peerPeerId) 178 | for (const [documentId] of peer) { 179 | this.closeSocket(peerPeerId, documentId) 180 | } 181 | return this 182 | } 183 | 184 | /** 185 | * Disconnects from all peers and from the relay server 186 | */ 187 | public disconnectServer() { 188 | this.log(`disconnecting from all peers`) 189 | 190 | // Don't automatically try to reconnect after deliberately disconnecting 191 | this.shouldReconnectIfClosed = false 192 | 193 | const peersToDisconnect = Array.from(this.peers.keys()) // all of them 194 | for (const peerId of peersToDisconnect) { 195 | this.disconnectPeer(peerId) 196 | } 197 | this.removeAllListeners() 198 | this.serverConnection.close() 199 | } 200 | 201 | public has(peerPeerId: PeerId, documentId?: DocumentId): boolean { 202 | if (documentId !== undefined) { 203 | return this.has(peerPeerId) && this.peers.get(peerPeerId)!.has(documentId) 204 | } else { 205 | return this.peers.has(peerPeerId) 206 | } 207 | } 208 | 209 | public get(peerPeerId: PeerId): PeerSocketMap 210 | public get(peerPeerId: PeerId, documentId: DocumentId): WebSocket | null 211 | public get(peerPeerId: PeerId, documentId?: DocumentId) { 212 | if (documentId !== undefined) { 213 | return this.get(peerPeerId)?.get(documentId) 214 | } else { 215 | // create an entry for this peer if there isn't already one 216 | if (!this.has(peerPeerId)) this.peers.set(peerPeerId, new Map()) 217 | return this.peers.get(peerPeerId) 218 | } 219 | } 220 | 221 | // PRIVATE 222 | 223 | /** 224 | * When we connect to the server, we set up a heartbeat to keep the connection alive, and we send 225 | * any pending messages that we weren't able to send before. 226 | */ 227 | private async onServerOpen() { 228 | await isReady(this.serverConnection) 229 | this.retryDelay = this.minRetryDelay 230 | this.shouldReconnectIfClosed = true 231 | this.sendPendingMessages() 232 | this.open = true 233 | this.heartbeat = setInterval( 234 | () => this.serverConnection.send(HEARTBEAT), 235 | HEARTBEAT_INTERVAL 236 | ) 237 | this.emit("server-connect") 238 | } 239 | 240 | /** 241 | * The only kind of message that we receive from the relay server is an introduction, which tells 242 | * us that someone else is interested in the same thing we are. 243 | */ 244 | private onServerMessage({ data }: WebSocket.MessageEvent) { 245 | const message = unpack(data) as Message.ServerToClient 246 | 247 | if (message.type !== "Introduction") 248 | throw new Error(`Invalid message type '${message.type}'`) 249 | 250 | const { peerId, documentIds = [] } = message 251 | documentIds.forEach(documentId => { 252 | this.connectToPeer(documentId, peerId) 253 | }) 254 | } 255 | 256 | /** 257 | * When we receive an introduction message, we respond by requesting a "direct" connection to the 258 | * peer (via piped sockets on the relay server) for each document ID that we have in common 259 | */ 260 | private connectToPeer(documentId: DocumentId, peerId: PeerId) { 261 | const peer = this.get(peerId) 262 | if (peer.has(documentId)) return // don't add twice 263 | peer.set(documentId, null) 264 | 265 | const url = `${this.url}/connection/${this.peerId}/${peerId}/${documentId}` 266 | const socket = new WebSocket(url) 267 | socket.binaryType = "arraybuffer" 268 | 269 | socket.addEventListener("open", async () => { 270 | // make sure the socket is actually in READY state 271 | await isReady(socket) 272 | // add the socket to the map for this peer 273 | peer.set(documentId, socket) 274 | this.emit("peer-connect", { peerId, documentId, socket }) 275 | }) 276 | 277 | // if the other end disconnects, we disconnect 278 | socket.addEventListener("close", () => { 279 | this.closeSocket(peerId, documentId) 280 | this.emit("peer-disconnect", { peerId, documentId, socket }) 281 | }) 282 | } 283 | 284 | private onServerClose() { 285 | this.open = false 286 | this.emit("server-disconnect") 287 | clearInterval(this.heartbeat) 288 | if (this.shouldReconnectIfClosed) this.tryToReopen() 289 | } 290 | 291 | private onServerError({ error }: WebSocket.ErrorEvent) { 292 | this.emit("error", error) 293 | } 294 | 295 | /** Send any messages we were given before the server was ready */ 296 | private sendPendingMessages() { 297 | while (this.pendingMessages.length) { 298 | const message = this.pendingMessages.shift()! 299 | this.send(message) 300 | } 301 | } 302 | 303 | /** Try to reconnect after a delay */ 304 | private tryToReopen() { 305 | setTimeout(() => { 306 | this.connectToServer() 307 | this.documentIds.forEach(id => this.join(id)) 308 | }, this.retryDelay) 309 | 310 | // increase the delay for next time 311 | if (this.retryDelay < this.maxRetryDelay) 312 | this.retryDelay *= this.backoffFactor + Math.random() * 0.1 - 0.05 // randomly vary the delay 313 | 314 | this.log( 315 | `Relay connection closed. Retrying in ${Math.floor( 316 | this.retryDelay / 1000 317 | )}s` 318 | ) 319 | } 320 | 321 | /** Send a message to the server */ 322 | private async send(message: Message.ClientToServer) { 323 | await isReady(this.serverConnection) 324 | try { 325 | const msgBytes = pack(message) 326 | this.serverConnection.send(msgBytes) 327 | } catch (err) { 328 | this.pendingMessages.push(message) 329 | } 330 | } 331 | 332 | private closeSocket(peerId: PeerId, documentId: DocumentId) { 333 | const peer = this.get(peerId) 334 | if (peer.has(documentId)) { 335 | const socket = peer.get(documentId) 336 | if ( 337 | socket && 338 | socket.readyState !== socket.CLOSED && 339 | socket.readyState !== socket.CLOSING 340 | ) { 341 | // socket.removeAllListeners() 342 | socket.close() 343 | } 344 | peer.delete(documentId) 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/Server.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug" 2 | import express from "express" 3 | import expressWs, { Application } from "express-ws" 4 | import WebSocket, { WebSocketServer } from "ws" 5 | import { pack, unpack } from "msgpackr" 6 | import pkg from "../package.json" assert { type: "json" } 7 | import { EventEmitter } from "./lib/EventEmitter.js" 8 | import { deduplicate } from "./lib/deduplicate.js" 9 | import { intersection } from "./lib/intersection.js" 10 | import { pipeSockets } from "./lib/pipeSockets.js" 11 | import { 12 | ConnectRequestParams, 13 | DocumentId, 14 | Message, 15 | ServerEvents, 16 | PeerId, 17 | } from "./types.js" 18 | 19 | /** 20 | * This server provides two services: 21 | * 22 | * - **Introductions** (aka discovery): Alice or Bob can provide one or more document documentIds that 23 | * they're interested in. If Alice is interested in the same documentId or documentIds as Bob, each will receive 24 | * an `Introduction` message with the other's peerId. They can then use that information to connect. 25 | * 26 | * - **Connection**: Once introduced, Alice can request to connect with Bob on a given document documentId 27 | * (can think of it as a 'channel'). If we get matching connection requests from Alice and Bob, we 28 | * just pipe their sockets together. 29 | */ 30 | export class Server extends EventEmitter { 31 | public port: number 32 | 33 | /** 34 | * In this context: 35 | * - `peerId` is a peer's peerId. 36 | * - `peer` is always a reference to a client's socket connection. 37 | * - `documentId` is an identifier for a document or a topic (elsewhere referred to as a 'channel' or a 'discovery key'). 38 | */ 39 | public peers: Record = {} 40 | public documentIds: Record = {} 41 | 42 | /** 43 | * For two peers to connect, they both need to send a connection request, specifying both the 44 | * remote peer peerId and the documentId. When we've gotten the request from Alice but not yet from 45 | * Bob, we temporarily store a reference to Alice's request in `holding`, and store any 46 | * messages from Bob in `messages`. 47 | */ 48 | private holding: Record = {} 49 | 50 | /** 51 | * Keep these references for cleanup 52 | */ 53 | private socket: WebSocketServer 54 | private app: Application 55 | private sockets = new Set() 56 | 57 | public log: debug.Debugger 58 | 59 | constructor({ port = 8080 } = {}) { 60 | super() 61 | this.port = port 62 | this.app = expressWs(express()).app 63 | this.socket = new WebSocketServer({ noServer: true }) 64 | 65 | this.log = debug(`lf:relay:${port}`) 66 | this.log("version", pkg.version) 67 | } 68 | 69 | // SERVER 70 | 71 | listen({ silent = false }: ListenOptions = {}) { 72 | return new Promise((resolve, reject) => { 73 | this.app 74 | // Allow hitting this server from a browser as a sanity check 75 | .get("/", (_, res) => { 76 | res.send(logoPage).end() 77 | }) 78 | 79 | // Introduction request 80 | .ws( 81 | "/introduction/:peerId", // 82 | (socket, { params: { peerId } }) => { 83 | this.log("received introduction request", peerId) 84 | this.openIntroductionConnection(socket, peerId) 85 | this.sockets.add(socket) 86 | } 87 | ) 88 | 89 | // Connection request 90 | .ws( 91 | "/connection/:A/:B/:documentId", 92 | (socket, { params: { A, B, documentId } }) => { 93 | this.log("received connection request", A, B) 94 | this.openConnection({ socket, A, B, documentId }) 95 | this.sockets.add(socket) 96 | } 97 | ) 98 | 99 | .listen(this.port, () => { 100 | if (!silent) 101 | console.log(`🐟 Listening at http://localhost:${this.port}`) 102 | this.emit("ready") 103 | resolve() 104 | }) 105 | 106 | .on("error", error => { 107 | if (!silent) console.error(error) 108 | reject() 109 | }) 110 | }) 111 | } 112 | 113 | close() { 114 | this.sockets.forEach(socket => { 115 | socket.removeAllListeners() 116 | socket.close() 117 | socket.terminate() 118 | }) 119 | this.app.removeAllListeners() 120 | } 121 | 122 | // DISCOVERY 123 | 124 | private openIntroductionConnection(socket: WebSocket, peerId: PeerId) { 125 | this.peers[peerId] = socket 126 | 127 | socket 128 | .on("message", this.handleIntroductionRequest(peerId)) 129 | .on("close", this.closeIntroductionConnection(peerId)) 130 | 131 | this.emit("introduction", peerId) 132 | } 133 | 134 | private handleIntroductionRequest = 135 | (peerId: PeerId) => (data: Uint8Array) => { 136 | const A = peerId // A and B always refer to peer peerIds 137 | const currentDocumentIds = this.documentIds[A] ?? [] 138 | 139 | const tryParse = (s: Uint8Array): T | Error => { 140 | try { 141 | return unpack(s) 142 | } catch (error: any) { 143 | return new Error(error.toString()) 144 | } 145 | } 146 | 147 | const message = tryParse(data) 148 | if (message instanceof Error) { 149 | this.emit("error", { error: message, data }) 150 | return 151 | } 152 | 153 | switch (message.type) { 154 | case "❤️": 155 | // heartbeat - nothing to do 156 | this.log("♥", peerId) 157 | break 158 | 159 | case "Join": 160 | this.log("introduction request: %o", message) 161 | // An introduction request from the client will include a list of documentIds to join. 162 | // We combine those documentIds with any we already have and deduplicate. 163 | this.documentIds[A] = currentDocumentIds 164 | .concat(message.documentIds) 165 | .reduce(deduplicate, []) 166 | 167 | // if this peer (A) has interests in common with any existing peer (B), introduce them to each other 168 | for (const B in this.peers) { 169 | // don't introduce peer to themselves 170 | if (A === B) continue 171 | 172 | // find documentIds that both peers are interested in 173 | const commonKeys = intersection( 174 | this.documentIds[A], 175 | this.documentIds[B] 176 | ) 177 | if (commonKeys.length) { 178 | this.log("sending introductions", A, B, commonKeys) 179 | this.sendIntroduction(A, B, commonKeys) 180 | this.sendIntroduction(B, A, commonKeys) 181 | } 182 | } 183 | break 184 | case "Leave": 185 | // remove the provided documentIds from this peer's list 186 | this.documentIds[A] = currentDocumentIds.filter( 187 | id => !message.documentIds.includes(id) 188 | ) 189 | break 190 | 191 | default: 192 | break 193 | } 194 | } 195 | 196 | private send(peer: WebSocket, message: Message.ServerToClient) { 197 | if (peer && peer.readyState === WebSocket.OPEN) { 198 | // We catch that so that any transient socket errors don't crash the server 199 | try { 200 | peer.send(pack(message)) 201 | } catch (err) { 202 | console.error("Failed to send message to peer") 203 | } 204 | } 205 | } 206 | 207 | // If we find another peer interested in the same documentId(s), we send both peers an introduction, 208 | // which they can use to connect 209 | private sendIntroduction = ( 210 | A: PeerId, 211 | B: PeerId, 212 | documentIds: DocumentId[] 213 | ) => { 214 | const message: Message.Introduction = { 215 | type: "Introduction", 216 | peerId: B, // the peerId of the other peer 217 | documentIds, // the documentId(s) both are interested in 218 | } 219 | let peer = this.peers[A] 220 | this.send(peer, message) 221 | } 222 | 223 | private closeIntroductionConnection = (peerId: PeerId) => () => { 224 | delete this.peers[peerId] 225 | delete this.documentIds[peerId] 226 | } 227 | 228 | // PEER CONNECTIONS 229 | 230 | private openConnection({ socket, A, B, documentId }: ConnectRequestParams) { 231 | const socketA = socket 232 | // A and B always refer to peerIds. 233 | 234 | // `AseeksB` and `BseeksA` are keys for identifying this request and the reciprocal request 235 | // (which may or may not have already come in) 236 | const AseeksB = `${A}:${B}:${documentId}` 237 | const BseeksA = `${B}:${A}:${documentId}` 238 | 239 | const holdMessage = (message: any) => 240 | this.holding[AseeksB]?.messages.push(message) 241 | 242 | if (this.holding[BseeksA]) { 243 | // We already have a connection request from Bob; hook them up 244 | const { socket: socketB, messages } = this.holding[BseeksA] 245 | 246 | this.log( 247 | `found peer, connecting ${AseeksB} (${messages.length} stored messages)` 248 | ) 249 | // Send any stored messages 250 | messages.forEach(message => this.send(socket, message)) 251 | 252 | // Pipe the two sockets together 253 | pipeSockets(socketA, socketB) 254 | 255 | // Don't need to hold the connection or messages any more 256 | socketA.removeListener("message", holdMessage) 257 | delete this.holding[BseeksA] 258 | } else { 259 | // We haven't heard from Bob yet; hold this connection 260 | this.log("holding connection for peer", AseeksB) 261 | 262 | // hold Alice's socket ready, and hold any messages Alice sends to Bob in the meantime 263 | this.holding[AseeksB] = { socket: socketA, messages: [] } 264 | 265 | socketA 266 | // hold on to incoming messages from Alice for Bob 267 | .on("message", holdMessage) 268 | .on("close", () => delete this.holding[AseeksB]) 269 | } 270 | } 271 | } 272 | 273 | const logoPage = ` 274 | 275 | @localfirst/relay logo 276 | ` 277 | 278 | interface ListenOptions { 279 | silent?: boolean 280 | } 281 | -------------------------------------------------------------------------------- /src/lib/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter as _EventEmitter } from "eventemitter3" 2 | import debug from "debug" 3 | 4 | /** EventEmitter with built-in logging */ 5 | export class EventEmitter< 6 | T extends _EventEmitter.ValidEventTypes, 7 | > extends _EventEmitter { 8 | /** The `log` method is meant to be overridden, e.g. 9 | * ```ts 10 | * this.log = debug(`lf:tc:conn:${context.user.peerId}`) 11 | * ``` 12 | */ 13 | log: debug.Debugger = debug(`EventEmitter`) 14 | 15 | public emit( 16 | event: _EventEmitter.EventNames, 17 | ...args: _EventEmitter.EventArgs> 18 | ) { 19 | this.log(`${event.toString()} %o`, ...args) 20 | return super.emit(event, ...args) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/deduplicate.ts: -------------------------------------------------------------------------------- 1 | import type { DocumentId } from "../types.js" 2 | 3 | export const deduplicate = (acc: DocumentId[], documentId: string) => 4 | acc.includes(documentId) ? acc : acc.concat(documentId) 5 | -------------------------------------------------------------------------------- /src/lib/eventPromise.ts: -------------------------------------------------------------------------------- 1 | /** Returns a promise that resolves when the given event is emitted on the given emitter. */ 2 | export const eventPromise = async (emitter: Emitter, event: string) => 3 | new Promise(resolve => { 4 | emitter.once(event, d => resolve(d)) 5 | }) 6 | 7 | export const eventPromises = async (emitters: Emitter[], event: string) => { 8 | const promises = emitters.map(async emitter => eventPromise(emitter, event)) 9 | return Promise.all(promises) 10 | } 11 | 12 | interface Emitter { 13 | once(event: string, listener: (data?: any) => void): void 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/intersection.ts: -------------------------------------------------------------------------------- 1 | import type { DocumentId } from "../types.js" 2 | 3 | export const intersection = (a: DocumentId[] = [], b: DocumentId[] = []) => 4 | a.filter(documentId => b.includes(documentId)) 5 | -------------------------------------------------------------------------------- /src/lib/isReady.ts: -------------------------------------------------------------------------------- 1 | import { pause } from "./pause.js" 2 | import WebSocket from "isomorphic-ws" 3 | 4 | export const isReady = async (socket: WebSocket) => 5 | new Promise(async (resolve, reject) => { 6 | while (socket.readyState !== WebSocket.OPEN) await pause(100) 7 | resolve() 8 | }) 9 | -------------------------------------------------------------------------------- /src/lib/msgpack.ts: -------------------------------------------------------------------------------- 1 | import { encode, decode } from "msgpackr" 2 | import WebSocket from "isomorphic-ws" 3 | 4 | export const pack = (data: any) => { 5 | return toArrayBuffer(encode(data)) 6 | } 7 | 8 | export const unpack = (data: WebSocket.Data) => { 9 | return decode(fromArrayBuffer(data as ArrayBuffer)) 10 | } 11 | 12 | const toArrayBuffer = (bytes: Uint8Array) => { 13 | const { buffer, byteOffset, byteLength } = bytes 14 | return buffer.slice(byteOffset, byteOffset + byteLength) 15 | } 16 | 17 | const fromArrayBuffer = (buffer: ArrayBuffer) => { 18 | return new Uint8Array(buffer) 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/newid.ts: -------------------------------------------------------------------------------- 1 | // ignore file coverage 2 | 3 | import cuid from "cuid" 4 | 5 | // use shorter ids in development & testing 6 | export const newid = 7 | process.env.NODE_ENV === "production" ? 8 | cuid 9 | : (len: number = 4) => cuid().split("").reverse().slice(0, len).join("") //the beginning of a cuid changes slowly, use the end 10 | -------------------------------------------------------------------------------- /src/lib/pause.ts: -------------------------------------------------------------------------------- 1 | export const pause = async (t = 0) => 2 | new Promise(resolve => { 3 | setTimeout(() => resolve(), t) 4 | }) 5 | -------------------------------------------------------------------------------- /src/lib/pipeSockets.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from "isomorphic-ws" 2 | 3 | export const pipeSockets = (socket1: WebSocket, socket2: WebSocket) => { 4 | const pipeOneWay = (A: WebSocket, B: WebSocket) => { 5 | const cleanup = () => { 6 | A.close() 7 | B.close() 8 | } 9 | A.on("message", data => { 10 | const ready = B.readyState === WebSocket.OPEN 11 | if (ready) B.send(data) 12 | else A.close() 13 | }) 14 | A.on("error", cleanup) 15 | A.on("close", cleanup) 16 | } 17 | pipeOneWay(socket1, socket2) 18 | pipeOneWay(socket2, socket1) 19 | } 20 | -------------------------------------------------------------------------------- /src/start.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "./Server.js" 2 | 3 | const DEFAULT_PORT = 8080 4 | const port = Number(process.env.PORT) || DEFAULT_PORT 5 | 6 | const server = new Server({ port }) 7 | 8 | server.listen() 9 | -------------------------------------------------------------------------------- /src/test/Client.test.ts: -------------------------------------------------------------------------------- 1 | import { pause } from "../lib/pause.js" 2 | import { expect, it } from "vitest" 3 | import { Client } from "../Client.js" 4 | import { Server } from "../Server.js" 5 | import { eventPromise } from "../lib/eventPromise.js" 6 | import { pack, unpack } from "../lib/msgpack.js" 7 | import { allConnected } from "./helpers/allConnected.js" 8 | import { allDisconnected } from "./helpers/allDisconnected.js" 9 | 10 | let testId = 0 11 | 12 | const setup = async () => { 13 | testId++ 14 | const port = 3000 + testId 15 | const url = `ws://localhost:${port}` 16 | 17 | const server = new Server({ port }) 18 | server.listen({ silent: true }) 19 | 20 | const documentId = `test-documentId-${testId}` 21 | const documentIds = [documentId] 22 | 23 | const alice = new Client({ peerId: `a-${testId}`, url, documentIds }) 24 | const bob = new Client({ peerId: `b-${testId}`, url, documentIds }) 25 | const charlie = new Client({ peerId: `c-${testId}`, url, documentIds }) 26 | 27 | const teardown = async () => { 28 | alice.disconnectServer() 29 | bob.disconnectServer() 30 | charlie.disconnectServer() 31 | await pause(10) 32 | server.close() 33 | } 34 | return { url, server, alice, bob, charlie, documentId, teardown } 35 | } 36 | 37 | it("joins a documentId and connects to a peer", async () => { 38 | // Alice and Bob both join a documentId 39 | const { alice, bob, documentId, teardown } = await setup() 40 | await allConnected(alice, bob) 41 | 42 | expect(alice.has(bob.peerId, documentId)).toBe(true) 43 | expect(bob.has(alice.peerId, documentId)).toBe(true) 44 | 45 | await teardown() 46 | }) 47 | 48 | it("both peers have the second document", async () => { 49 | const { alice, bob, teardown } = await setup() 50 | 51 | const anotherDocumentId = "some-other-document-1234" 52 | alice.join(anotherDocumentId) 53 | bob.join(anotherDocumentId) 54 | 55 | await allConnected(alice, bob, anotherDocumentId) 56 | 57 | expect(alice.has(bob.peerId, anotherDocumentId)).toBe(true) 58 | expect(bob.has(alice.peerId, anotherDocumentId)).toBe(true) 59 | 60 | await teardown() 61 | }) 62 | 63 | it("leaves a documentId", async () => { 64 | // Alice and Bob both join a documentId 65 | const { alice, bob, documentId, teardown } = await setup() 66 | await allConnected(alice, bob) 67 | 68 | expect(alice.has(bob.peerId, documentId)).toBe(true) 69 | expect(alice.documentIds).toContain(documentId) 70 | 71 | // Alice decides she's no longer interested in this document 72 | alice.leave(documentId) 73 | 74 | expect(alice.has(bob.peerId, documentId)).toBe(false) 75 | expect(alice.documentIds).not.toContain(documentId) 76 | 77 | await teardown() 78 | }) 79 | 80 | it("Bob is disconnected from Alice and vice versa", async () => { 81 | // Alice and Bob both join a documentId 82 | const { alice, bob, documentId, teardown } = await setup() 83 | await allConnected(alice, bob) 84 | 85 | alice.disconnectPeer(bob.peerId) 86 | await allDisconnected(alice, bob) 87 | 88 | // Bob is disconnected from Alice and vice versa 89 | expect(alice.has(bob.peerId, documentId)).toBe(false) 90 | expect(bob.has(alice.peerId, documentId)).toBe(false) 91 | 92 | await teardown() 93 | }) 94 | 95 | it("everyone is disconnected from Alice and vice versa", async () => { 96 | // Alice, Bob, and Charlie all join a documentId 97 | const { alice, bob, charlie, documentId, teardown } = await setup() 98 | await Promise.all([allConnected(alice, bob), allConnected(alice, charlie)]) 99 | 100 | // Alice disconnects from everyone 101 | alice.disconnectServer() 102 | await Promise.all([ 103 | allDisconnected(alice, bob), 104 | allDisconnected(alice, charlie), 105 | ]) 106 | 107 | // Bob is disconnected from Alice and vice versa 108 | expect(alice.has(bob.peerId, documentId)).toBe(false) 109 | expect(bob.has(alice.peerId, documentId)).toBe(false) 110 | 111 | // Charlie is disconnected from Alice and vice versa 112 | expect(alice.has(charlie.peerId, documentId)).toBe(false) 113 | expect(charlie.has(alice.peerId, documentId)).toBe(false) 114 | 115 | await teardown() 116 | }) 117 | 118 | it(`she's disconnected then she's connected again`, async () => { 119 | // Alice and Bob connect 120 | const { alice, bob, documentId, teardown } = await setup() 121 | await allConnected(alice, bob) 122 | 123 | // Alice disconnects 124 | alice.disconnectServer() 125 | await allDisconnected(alice, bob) 126 | 127 | // Alice and Bob are disconnected 128 | expect(alice.has(bob.peerId, documentId)).toBe(false) 129 | expect(bob.has(alice.peerId, documentId)).toBe(false) 130 | 131 | // Alice reconnects 132 | alice.connectToServer() 133 | alice.join(documentId) 134 | await allConnected(alice, bob) 135 | 136 | // Alice and Bob are connected again 137 | expect(alice.has(bob.peerId, documentId)).toBe(true) 138 | expect(bob.has(alice.peerId, documentId)).toBe(true) 139 | 140 | await teardown() 141 | }) 142 | 143 | it("sends a message to a remote peer", async () => { 144 | const { alice, bob, teardown } = await setup() 145 | 146 | const [aliceSocket, bobSocket] = await allConnected(alice, bob) 147 | 148 | aliceSocket.send(pack("hello")) 149 | 150 | const data = await eventPromise(bobSocket, "message") 151 | expect(unpack(data)).toEqual("hello") 152 | 153 | await teardown() 154 | }) 155 | 156 | it("stays open when peer disconnects", async () => { 157 | const { alice, bob, teardown } = await setup() 158 | 159 | await allConnected(alice, bob) 160 | 161 | expect(alice.open).toBe(true) 162 | expect(bob.open).toBe(true) 163 | 164 | alice.disconnectPeer(bob.peerId) 165 | expect(alice.open).toBe(true) 166 | expect(bob.open).toBe(true) 167 | 168 | await teardown() 169 | }) 170 | 171 | it("closes when server disconnects", async () => { 172 | const { alice, bob, server, teardown } = await setup() 173 | 174 | await allConnected(alice, bob) 175 | 176 | expect(alice.open).toBe(true) 177 | expect(bob.open).toBe(true) 178 | 179 | teardown() 180 | await eventPromise(alice, "server-disconnect") 181 | expect(alice.open).toBe(false) 182 | }) 183 | -------------------------------------------------------------------------------- /src/test/Server.test.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from "isomorphic-ws" 2 | import { expect, it } from "vitest" 3 | import { Server } from "../Server.js" 4 | import { eventPromise } from "../lib/eventPromise.js" 5 | import { isReady } from "../lib/isReady.js" 6 | import { pack, unpack } from "../lib/msgpack.js" 7 | import { pause } from "../lib/pause.js" 8 | import type { Message } from "../types.js" 9 | import { permutationsOfTwo } from "./helpers/permutationsOfTwo.js" 10 | 11 | /** 12 | * In this context: 13 | * - `peerId` is always a peer peerId. 14 | * - `peer` is always a reference to a client's socket connection. 15 | * - `documentId` is always a document peerId (elsewhere referred to as a 'channel' or a 'discovery documentId'. 16 | */ 17 | let testId = 0 18 | 19 | const setup = async () => { 20 | testId++ 21 | const port = 3010 + testId 22 | const url = `ws://localhost:${port}` 23 | 24 | const server = new Server({ port }) 25 | await server.listen({ silent: true }) 26 | 27 | const aliceId = `alice-${testId}` 28 | const bobId = `bob-${testId}` 29 | const documentId = `test-documentId-${testId}` 30 | 31 | const teardown = async (...sockets: WebSocket[]) => { 32 | await pause(10) 33 | sockets.forEach(socket => { 34 | socket.close() 35 | }) 36 | await pause(10) 37 | server.close() 38 | } 39 | 40 | return { testId, aliceId, bobId, documentId, port, url, server, teardown } 41 | } 42 | 43 | const requestIntroduction = async ( 44 | url: string, 45 | peerId: string, 46 | documentId: string 47 | ) => { 48 | const peer = new WebSocket(`${url}/introduction/${peerId}`) 49 | const joinMessage: Message.Join = { 50 | type: "Join", 51 | documentIds: [documentId], 52 | } 53 | 54 | await isReady(peer) 55 | peer.send(pack(joinMessage)) 56 | return peer 57 | } 58 | 59 | it("should make an introduction connection", async () => { 60 | const { aliceId, url, server, teardown } = await setup() 61 | 62 | const alice = new WebSocket(`${url}/introduction/${aliceId}`) 63 | const peerId = await eventPromise(server, "introduction") 64 | expect(peerId).toEqual(aliceId) 65 | expect(server.peers).toHaveProperty(aliceId) 66 | expect(server.documentIds).toEqual({}) 67 | 68 | await teardown(alice) 69 | }) 70 | 71 | it("should not crash when sent malformed data", async () => { 72 | const { aliceId, bobId, documentId, url, server, teardown } = await setup() 73 | 74 | const alice = await requestIntroduction(url, aliceId, documentId) 75 | 76 | // Bob's behavior will be non-standard so we'll drive it by hand 77 | const bob = new WebSocket(`${url}/introduction/${bobId}`) 78 | 79 | const badMessage = new Uint8Array([1, 2, 3]) // not valid msgpack 80 | await eventPromise(bob, "open") 81 | 82 | // Bob sends an invalid message 83 | bob.send(badMessage) 84 | 85 | // No servers are harmed 86 | 87 | // Bob then sends a valid join message 88 | bob.send( 89 | pack({ 90 | type: "Join", 91 | documentIds: [documentId], 92 | }) 93 | ) 94 | 95 | // The bad message didn't kill the server - Bob gets a response back 96 | const messageBytes = await eventPromise(bob, "message") 97 | const msg = unpack(messageBytes) 98 | expect(msg.type).toBe("Introduction") 99 | 100 | await teardown(alice, bob) 101 | }) 102 | 103 | it("should invite peers to connect", async () => { 104 | const { aliceId, bobId, documentId, url, teardown } = await setup() 105 | const alice = await requestIntroduction(url, aliceId, documentId) 106 | const bob = await requestIntroduction(url, bobId, documentId) 107 | 108 | const aliceDone = new Promise(resolve => { 109 | alice.once("message", d => { 110 | const invitation = unpack(d) 111 | expect(invitation).toEqual({ 112 | type: "Introduction", 113 | peerId: bobId, 114 | documentIds: [documentId], 115 | }) 116 | resolve() 117 | }) 118 | }) 119 | const bobDone = new Promise(resolve => { 120 | bob.on("message", d => { 121 | const invitation = unpack(d) 122 | expect(invitation).toEqual({ 123 | type: "Introduction", 124 | peerId: aliceId, 125 | documentIds: [documentId], 126 | }) 127 | resolve() 128 | }) 129 | }) 130 | await Promise.all([aliceDone, bobDone]) 131 | 132 | await teardown(alice, bob) 133 | }) 134 | 135 | it("should pipe connections between two peers", async () => { 136 | const { aliceId, bobId, documentId, url, teardown } = await setup() 137 | 138 | const aliceRequest = await requestIntroduction(url, aliceId, documentId) 139 | const _bobRequest = await requestIntroduction(url, bobId, documentId) // need to make request even if we don't use the result 140 | 141 | const message = await eventPromise(aliceRequest, "message") 142 | // recap of previous test: we'll get an invitation to connect to the remote peer 143 | const invitation = unpack(message) 144 | 145 | expect(invitation).toEqual({ 146 | type: "Introduction", 147 | peerId: bobId, 148 | documentIds: [documentId], 149 | }) 150 | 151 | const alice = new WebSocket( 152 | `${url}/connection/${aliceId}/${bobId}/${documentId}` 153 | ) 154 | const bob = new WebSocket( 155 | `${url}/connection/${bobId}/${aliceId}/${documentId}` 156 | ) 157 | 158 | await Promise.all([eventPromise(alice, "open"), eventPromise(bob, "open")]) 159 | 160 | // send message from alice to bob 161 | alice.send(pack("DUDE!!")) 162 | 163 | const aliceMessage = await eventPromise(bob, "message") 164 | expect(unpack(aliceMessage)).toEqual("DUDE!!") 165 | 166 | // send message from bob to alice 167 | bob.send(pack("hello")) 168 | const bobMessage = await eventPromise(alice, "message") 169 | expect(unpack(bobMessage)).toEqual("hello") 170 | 171 | await teardown(alice, bob) 172 | }) 173 | 174 | it("should close a peer when asked to", async () => { 175 | const { aliceId, bobId, documentId, url, teardown } = await setup() 176 | 177 | const aliceRequest = await requestIntroduction(url, aliceId, documentId) 178 | const _bobRequest = await requestIntroduction(url, bobId, documentId) // need to make request even if we don't use the result 179 | 180 | await eventPromise(aliceRequest, "message") 181 | 182 | const alice = new WebSocket( 183 | `${url}/connection/${aliceId}/${bobId}/${documentId}` 184 | ) 185 | const bob = new WebSocket( 186 | `${url}/connection/${bobId}/${aliceId}/${documentId}` 187 | ) 188 | 189 | await eventPromise(alice, "open") 190 | 191 | // Alice sends a message 192 | alice.send(pack("hey bob!")) 193 | 194 | // She then closes the connection 195 | alice.close() 196 | 197 | // Bob receives the message 198 | await eventPromise(bob, "message") 199 | 200 | // Bob sends a message back 201 | bob.send(pack("sup alice")) 202 | 203 | // Alice should not receive the message, because her connection is closed 204 | const aliceReceivedMessage = await Promise.race([ 205 | eventPromise(alice, "message").then(() => true), 206 | pause(100).then(() => false), 207 | ]) 208 | expect(aliceReceivedMessage).toBe(false) 209 | 210 | await teardown(alice, bob) 211 | }) 212 | 213 | it("Should make introductions between N peers", async () => { 214 | const { documentId, url, teardown } = await setup() 215 | const peers = ["a", "b", "c", "d", "e"] 216 | 217 | const expectedIntroductions = permutationsOfTwo(peers.length) 218 | 219 | const peerIds = peers.map(peerId => `peer-${peerId}-${testId}`) 220 | 221 | const sockets = peerIds.map( 222 | (peerId: string) => new WebSocket(`${url}/introduction/${peerId}`) 223 | ) 224 | 225 | const joinMessage = { type: "Join", documentIds: [documentId] } 226 | sockets.forEach(async (socket: WebSocket) => { 227 | socket.on("open", () => { 228 | socket.send(pack(joinMessage)) 229 | }) 230 | }) 231 | 232 | const introductions = await new Promise(resolve => { 233 | let introductions = 0 234 | sockets.forEach(socket => { 235 | socket.on("message", () => { 236 | introductions += 1 237 | if (introductions === expectedIntroductions) resolve(introductions) 238 | }) 239 | }) 240 | }) 241 | expect(introductions).toBe(expectedIntroductions) 242 | 243 | await teardown(...sockets) 244 | }) 245 | 246 | it("Should not crash when one peer disconnects mid-introduction", async () => { 247 | const { documentId, url, teardown } = await setup() 248 | const peers = ["a", "b", "c", "d", "e"] 249 | 250 | const peerIds = peers.map(peerId => `peer-${peerId}-${testId}`) 251 | 252 | const expectedIntroductions = permutationsOfTwo(peers.length - 1) // one will misbehave 253 | 254 | const sockets = peerIds.map( 255 | peerId => new WebSocket(`${url}/introduction/${peerId}`) 256 | ) 257 | 258 | const joinMessage = { type: "Join", documentIds: [documentId] } 259 | sockets.forEach(async (socket, i) => { 260 | socket.on("open", () => { 261 | socket.send(pack(joinMessage)) 262 | if (i === 0) socket.close() // <- misbehaving node 263 | }) 264 | }) 265 | 266 | const introductions = await new Promise(resolve => { 267 | let introductions = 0 268 | sockets.forEach(socket => { 269 | socket.on("message", () => { 270 | introductions += 1 271 | if (introductions === expectedIntroductions) { 272 | resolve(introductions) 273 | } 274 | }) 275 | }) 276 | }) 277 | expect(introductions).toBe(expectedIntroductions) 278 | 279 | await teardown(...sockets) 280 | }) 281 | -------------------------------------------------------------------------------- /src/test/helpers/allConnected.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "../../Client.js" 2 | import { DocumentId } from "../../types.js" 3 | import { connection } from "./connection.js" 4 | import WebSocket from "isomorphic-ws" 5 | 6 | export const allConnected = (a: Client, b: Client, documentId?: DocumentId) => 7 | Promise.all([ 8 | connection(a, b, documentId), 9 | connection(b, a, documentId), 10 | ]) 11 | -------------------------------------------------------------------------------- /src/test/helpers/allDisconnected.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "../../Client.js" 2 | import { disconnection } from "./disconnection.js" 3 | 4 | export const allDisconnected = (a: Client, b: Client) => 5 | Promise.all([disconnection(a, b), disconnection(b, a)]) 6 | -------------------------------------------------------------------------------- /src/test/helpers/connection.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "../../Client.js" 2 | import { DocumentId } from "../../types.js" 3 | import WebSocket from "isomorphic-ws" 4 | 5 | export const connection = (a: Client, b: Client, documentId?: DocumentId) => 6 | new Promise(resolve => 7 | a.on("peer-connect", ({ peerId, documentId: d, socket }) => { 8 | if ( 9 | peerId === b.peerId && 10 | // are we waiting to connect on a specific document ID? 11 | (documentId === undefined || documentId === d) 12 | ) 13 | resolve(socket) 14 | }) 15 | ) 16 | -------------------------------------------------------------------------------- /src/test/helpers/disconnection.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "../../Client.js" 2 | 3 | export const disconnection = (a: Client, b: Client) => 4 | new Promise(resolve => 5 | a.on("peer-disconnect", ({ peerId = "" }) => { 6 | if (peerId === b.peerId) resolve() 7 | }) 8 | ) 9 | -------------------------------------------------------------------------------- /src/test/helpers/factorial.ts: -------------------------------------------------------------------------------- 1 | export const factorial = (n: number): number => 2 | n === 1 ? 1 : n * factorial(n - 1) 3 | -------------------------------------------------------------------------------- /src/test/helpers/permutationsOfTwo.ts: -------------------------------------------------------------------------------- 1 | import { factorial } from "./factorial.js" 2 | 3 | export const permutationsOfTwo = (n: number) => factorial(n) / factorial(n - 2) 4 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import WebSocket from "isomorphic-ws" 2 | 3 | export type PeerId = string 4 | 5 | export type DocumentId = string 6 | 7 | export type ConnectRequestParams = { 8 | socket: WebSocket 9 | A: PeerId 10 | B: PeerId 11 | documentId: DocumentId 12 | } 13 | 14 | export namespace Message { 15 | export type ClientToServer = Join | Leave | Heartbeat 16 | 17 | export interface Heartbeat { 18 | type: "❤️" 19 | } 20 | 21 | export interface Join { 22 | type: "Join" 23 | documentIds: DocumentId[] 24 | } 25 | 26 | export interface Leave { 27 | type: "Leave" 28 | documentIds: DocumentId[] 29 | } 30 | 31 | export type ServerToClient = Introduction 32 | 33 | export interface Introduction { 34 | type: "Introduction" 35 | peerId: PeerId // the other peer we're introducing this client to 36 | documentIds: DocumentId[] 37 | } 38 | } 39 | 40 | export type ClientEvents = { 41 | "server-connect": () => void 42 | "server-disconnect": () => void 43 | error: (err: Error) => void 44 | "peer-connect": (payload: PeerEventPayload) => void 45 | "peer-disconnect": (payload: PeerEventPayload) => void 46 | } 47 | export interface PeerEventPayload { 48 | documentId: DocumentId 49 | peerId: PeerId 50 | socket: WebSocket 51 | } 52 | 53 | export interface ClientOptions { 54 | /** My user name. If one is not provided, a random one will be created for this session. */ 55 | peerId?: PeerId 56 | 57 | /** The base URL of the relay server to connect to. */ 58 | url: string 59 | 60 | /** DocumentId(s) to join immediately */ 61 | documentIds?: DocumentId[] 62 | 63 | minRetryDelay?: number 64 | maxRetryDelay?: number 65 | backoffFactor?: number 66 | } 67 | 68 | export type ServerEvents = { 69 | ready: () => void 70 | close: () => void 71 | error: (payload: { error: Error; data: Uint8Array }) => void 72 | introduction: (peerId: PeerId) => void 73 | } 74 | 75 | export type PeerSocketMap = Map 76 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src", 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "allowJs": false, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "importHelpers": true, 12 | "jsx": "react", 13 | "lib": [ 14 | "DOM", 15 | "ESNext" 16 | ], 17 | "module": "NodeNext", 18 | "moduleResolution": "NodeNext", 19 | "noUnusedLocals": false, 20 | "resolveJsonModule": true, 21 | "skipLibCheck": true, 22 | "sourceMap": true, 23 | "strict": true, 24 | "strictPropertyInitialization": false, 25 | "target": "ESNext", 26 | "useUnknownInCatchVariables": true 27 | }, 28 | "include": [ 29 | "src", 30 | "@types" 31 | ] 32 | } -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config" 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: "jsdom", 6 | coverage: { 7 | provider: "v8", 8 | reporter: ["lcov", "text", "html"], 9 | skipFull: true, 10 | exclude: [ 11 | "**/fuzz", 12 | "**/helpers", 13 | "**/coverage", 14 | "examples/**/*", 15 | "docs/**/*", 16 | "**/test/**/*", 17 | ], 18 | }, 19 | }, 20 | }) 21 | -------------------------------------------------------------------------------- /wallaby.conf.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function (wallaby) { 2 | return { 3 | autoDetect: true, 4 | runMode: 'onsave', 5 | slowTestThreshold: 5000, 6 | lowCoverageThreshold: 99, 7 | hints: { 8 | ignoreCoverageForFile: /ignore file coverage/, 9 | ignoreCoverage: /ignore coverage/, 10 | }, 11 | } 12 | } 13 | --------------------------------------------------------------------------------