├── .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 |
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 | [](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 | [](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 | [](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 | [](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 |
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 |
--------------------------------------------------------------------------------