diff --git a/.github/workflows/pingrip.yml b/.github/workflows/pingrip.yml new file mode 100644 index 000000000..704451db6 --- /dev/null +++ b/.github/workflows/pingrip.yml @@ -0,0 +1,48 @@ +name: Pingrip + +on: + push: + branches: + - master + paths: + - 'pingrip/**' + + pull_request: + paths: + - 'pingrip/**' + - .github/workflows/pingrip.yml + + workflow_dispatch: {} + +defaults: + run: + working-directory: ./pingrip + +permissions: + id-token: write + contents: read + +jobs: + pingrip: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v3.0.0 + with: + version: 8.6.2 + - uses: actions/setup-node@v3 + with: + node-version: '22.17.0' + check-latest: true + - run: pnpm install --frozen-lockfile + - run: pnpm build + - run: pnpm check:type + - run: pnpm check:format + - run: pnpm test + - name: Publish + if: github.ref == 'refs/heads/master' || github.event_name == 'workflow_dispatch' + uses: botpress/gh-actions/publish-if-not-exists@master + with: + path: './pingrip' + diff --git a/pingrip/.prettierignore b/pingrip/.prettierignore new file mode 100644 index 000000000..86d61cd97 --- /dev/null +++ b/pingrip/.prettierignore @@ -0,0 +1,22 @@ +# based on https://gist.github.com/thinkricardo/74f37d82b686de371b0853a5d66d559c + +# ignore all files +* + +# include all folders +!**/ + +# include files to format +!*.ts +!*.tsx +!*.json +!*.yaml +!*.yml +!*.md + +# exclusions +# **/*.d.ts +pnpm-lock.yaml +gen +dist +.botpress diff --git a/pingrip/README.md b/pingrip/README.md new file mode 100644 index 000000000..49ad23211 --- /dev/null +++ b/pingrip/README.md @@ -0,0 +1,56 @@ +# PinGrip + +PinGrip is an implementation of the [Pushpin](https://pushpin.org) GRIP for WebSocket-over-HTTP tunneling. It provides +utilities for serializing and parsing WebSocket messages in the GRIP protocol format, and includes a fluent +ResponseBuilder API for constructing WebSocket responses with support for opening/closing connections, sending +text/binary messages, subscribing to channels, and configuring keep-alive behavior. + +## Usage + +```js +import * as pingrip from '@bpinternal/pingrip' + +const grip = new pingrip.outputs.GripPublisher({ + signalUrl: 'http://localhost:7999', +}) + +function onDataUpdate(channels: string[]) { + const payload = "myPayloadToSendToAClient" + grip.publish(channels, payload) +} + +/** + * A handler that receive requests and generate responses + */ +function handler(body: string) { + const channels = ['channel1', 'channel2'] // extract channels from the body for example + const { messages: _messages, error } = pingrip.messages.safeParse(Buffer.from(body)) + if (error) { + console.error(error) + return + } + for (const message of _messages) { + if (message.type === 'open') { + const response = new pingrip.outputs.ResponseBuilder() + .open() + .subscribe(channels) + .keepAlive('ping', 30) + .toResponse() + return response + } + if (message.type === 'close') { + const response = new pingrip.outputs.ResponseBuilder() + .close(message.code) + .unsubscribe(channels) + .toResponse() + return response + } + } +} +``` + +## Disclaimer ⚠️ + +This package is published under the `@bpinternal` organization. All packages of this organization are meant to be used by the [Botpress](https://github.com/botpress/botpress) team internally and are not meant for our community. Since the packages are catered to our own use-cases, they might have less stable APIs, receive breaking changes without much warning, have minimal documentation and lack community-focused support. However, these packages were still left intentionally public for an important reason : We Love Open-Source. Therefore, if you wish to install or fork this package feel absolutly free to do it. We strongly recommend that you tag your versions properly. + +The Botpress Engineering team. diff --git a/pingrip/package.json b/pingrip/package.json new file mode 100644 index 000000000..99d143300 --- /dev/null +++ b/pingrip/package.json @@ -0,0 +1,43 @@ +{ + "name": "@bpinternal/pingrip", + "version": "0.1.0", + "description": "Pushpin GRIP Websocket-over-HTTP", + "keywords": [ + "pushpin", + "grip", + "websocket" + ], + "author": "Botpress, Inc.", + "license": "MIT", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "repository": { + "url": "https://github.com/botpress/packages" + }, + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsup src/index.ts --dts --format cjs,esm --clean", + "check:format": "prettier --check .", + "fix:format": "prettier --write .", + "check:type": "tsc --noEmit", + "test": "vitest" + }, + "devDependencies": { + "@types/node": "^24.10.1", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "tsup": "8.3.5", + "vitest": "^4.0.13", + "prettier": "3.4.1" + }, + "dependencies": { + "axios": "^1.13.2" + } +} diff --git a/pingrip/pnpm-lock.yaml b/pingrip/pnpm-lock.yaml new file mode 100644 index 000000000..a9c2d2b60 --- /dev/null +++ b/pingrip/pnpm-lock.yaml @@ -0,0 +1,1825 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + axios: + specifier: ^1.13.2 + version: 1.13.2 + +devDependencies: + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 + prettier: + specifier: 3.4.1 + version: 3.4.1 + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) + tsup: + specifier: 8.3.5 + version: 8.3.5(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vitest: + specifier: ^4.0.13 + version: 4.0.14(@types/node@24.10.1) + +packages: + /@cspotcode/source-map-support@0.8.1: + resolution: + { integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== } + engines: { node: '>=12' } + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@esbuild/aix-ppc64@0.24.2: + resolution: + { integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA== } + engines: { node: '>=18' } + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/aix-ppc64@0.25.12: + resolution: + { integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA== } + engines: { node: '>=18' } + cpu: [ppc64] + os: [aix] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.24.2: + resolution: + { integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg== } + engines: { node: '>=18' } + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.25.12: + resolution: + { integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg== } + engines: { node: '>=18' } + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.24.2: + resolution: + { integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q== } + engines: { node: '>=18' } + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.25.12: + resolution: + { integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg== } + engines: { node: '>=18' } + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.24.2: + resolution: + { integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw== } + engines: { node: '>=18' } + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.25.12: + resolution: + { integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg== } + engines: { node: '>=18' } + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.24.2: + resolution: + { integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA== } + engines: { node: '>=18' } + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.25.12: + resolution: + { integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg== } + engines: { node: '>=18' } + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.24.2: + resolution: + { integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA== } + engines: { node: '>=18' } + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.25.12: + resolution: + { integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA== } + engines: { node: '>=18' } + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.24.2: + resolution: + { integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg== } + engines: { node: '>=18' } + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.25.12: + resolution: + { integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg== } + engines: { node: '>=18' } + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.24.2: + resolution: + { integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q== } + engines: { node: '>=18' } + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.25.12: + resolution: + { integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ== } + engines: { node: '>=18' } + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.24.2: + resolution: + { integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg== } + engines: { node: '>=18' } + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.25.12: + resolution: + { integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ== } + engines: { node: '>=18' } + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.24.2: + resolution: + { integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA== } + engines: { node: '>=18' } + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.25.12: + resolution: + { integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw== } + engines: { node: '>=18' } + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.24.2: + resolution: + { integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw== } + engines: { node: '>=18' } + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.25.12: + resolution: + { integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA== } + engines: { node: '>=18' } + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.24.2: + resolution: + { integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ== } + engines: { node: '>=18' } + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.25.12: + resolution: + { integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng== } + engines: { node: '>=18' } + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.24.2: + resolution: + { integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw== } + engines: { node: '>=18' } + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.25.12: + resolution: + { integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw== } + engines: { node: '>=18' } + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.24.2: + resolution: + { integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw== } + engines: { node: '>=18' } + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.25.12: + resolution: + { integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA== } + engines: { node: '>=18' } + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.24.2: + resolution: + { integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q== } + engines: { node: '>=18' } + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.25.12: + resolution: + { integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w== } + engines: { node: '>=18' } + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.24.2: + resolution: + { integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw== } + engines: { node: '>=18' } + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.25.12: + resolution: + { integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg== } + engines: { node: '>=18' } + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.24.2: + resolution: + { integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q== } + engines: { node: '>=18' } + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.25.12: + resolution: + { integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw== } + engines: { node: '>=18' } + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-arm64@0.24.2: + resolution: + { integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw== } + engines: { node: '>=18' } + cpu: [arm64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-arm64@0.25.12: + resolution: + { integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg== } + engines: { node: '>=18' } + cpu: [arm64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.24.2: + resolution: + { integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw== } + engines: { node: '>=18' } + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.25.12: + resolution: + { integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ== } + engines: { node: '>=18' } + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-arm64@0.24.2: + resolution: + { integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A== } + engines: { node: '>=18' } + cpu: [arm64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-arm64@0.25.12: + resolution: + { integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A== } + engines: { node: '>=18' } + cpu: [arm64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.24.2: + resolution: + { integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA== } + engines: { node: '>=18' } + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.25.12: + resolution: + { integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw== } + engines: { node: '>=18' } + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openharmony-arm64@0.25.12: + resolution: + { integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg== } + engines: { node: '>=18' } + cpu: [arm64] + os: [openharmony] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.24.2: + resolution: + { integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig== } + engines: { node: '>=18' } + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.25.12: + resolution: + { integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w== } + engines: { node: '>=18' } + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.24.2: + resolution: + { integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ== } + engines: { node: '>=18' } + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.25.12: + resolution: + { integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg== } + engines: { node: '>=18' } + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.24.2: + resolution: + { integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA== } + engines: { node: '>=18' } + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.25.12: + resolution: + { integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ== } + engines: { node: '>=18' } + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.24.2: + resolution: + { integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg== } + engines: { node: '>=18' } + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.25.12: + resolution: + { integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA== } + engines: { node: '>=18' } + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@jridgewell/gen-mapping@0.3.13: + resolution: + { integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== } + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + dev: true + + /@jridgewell/resolve-uri@3.1.2: + resolution: + { integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== } + engines: { node: '>=6.0.0' } + dev: true + + /@jridgewell/sourcemap-codec@1.5.5: + resolution: + { integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== } + dev: true + + /@jridgewell/trace-mapping@0.3.31: + resolution: + { integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== } + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: + { integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== } + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + dev: true + + /@rollup/rollup-android-arm-eabi@4.53.3: + resolution: + { integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w== } + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-android-arm64@4.53.3: + resolution: + { integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w== } + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-arm64@4.53.3: + resolution: + { integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA== } + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-darwin-x64@4.53.3: + resolution: + { integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ== } + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-freebsd-arm64@4.53.3: + resolution: + { integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w== } + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-freebsd-x64@4.53.3: + resolution: + { integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q== } + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.53.3: + resolution: + { integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw== } + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm-musleabihf@4.53.3: + resolution: + { integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg== } + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.53.3: + resolution: + { integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w== } + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.53.3: + resolution: + { integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A== } + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-loong64-gnu@4.53.3: + resolution: + { integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g== } + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-ppc64-gnu@4.53.3: + resolution: + { integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw== } + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.53.3: + resolution: + { integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g== } + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-riscv64-musl@4.53.3: + resolution: + { integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A== } + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-s390x-gnu@4.53.3: + resolution: + { integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg== } + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.53.3: + resolution: + { integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w== } + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.53.3: + resolution: + { integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q== } + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-openharmony-arm64@4.53.3: + resolution: + { integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw== } + cpu: [arm64] + os: [openharmony] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.53.3: + resolution: + { integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw== } + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.53.3: + resolution: + { integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA== } + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-gnu@4.53.3: + resolution: + { integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg== } + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.53.3: + resolution: + { integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ== } + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@standard-schema/spec@1.0.0: + resolution: + { integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA== } + dev: true + + /@tsconfig/node10@1.0.12: + resolution: + { integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ== } + dev: true + + /@tsconfig/node12@1.0.11: + resolution: + { integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== } + dev: true + + /@tsconfig/node14@1.0.3: + resolution: + { integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== } + dev: true + + /@tsconfig/node16@1.0.4: + resolution: + { integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== } + dev: true + + /@types/chai@5.2.3: + resolution: + { integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== } + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + dev: true + + /@types/deep-eql@4.0.2: + resolution: + { integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== } + dev: true + + /@types/estree@1.0.8: + resolution: + { integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== } + dev: true + + /@types/node@24.10.1: + resolution: + { integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ== } + dependencies: + undici-types: 7.16.0 + dev: true + + /@vitest/expect@4.0.14: + resolution: + { integrity: sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw== } + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.14 + '@vitest/utils': 4.0.14 + chai: 6.2.1 + tinyrainbow: 3.0.3 + dev: true + + /@vitest/mocker@4.0.14(vite@7.2.4): + resolution: + { integrity: sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg== } + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + dependencies: + '@vitest/spy': 4.0.14 + estree-walker: 3.0.3 + magic-string: 0.30.21 + vite: 7.2.4(@types/node@24.10.1) + dev: true + + /@vitest/pretty-format@4.0.14: + resolution: + { integrity: sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ== } + dependencies: + tinyrainbow: 3.0.3 + dev: true + + /@vitest/runner@4.0.14: + resolution: + { integrity: sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw== } + dependencies: + '@vitest/utils': 4.0.14 + pathe: 2.0.3 + dev: true + + /@vitest/snapshot@4.0.14: + resolution: + { integrity: sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag== } + dependencies: + '@vitest/pretty-format': 4.0.14 + magic-string: 0.30.21 + pathe: 2.0.3 + dev: true + + /@vitest/spy@4.0.14: + resolution: + { integrity: sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg== } + dev: true + + /@vitest/utils@4.0.14: + resolution: + { integrity: sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw== } + dependencies: + '@vitest/pretty-format': 4.0.14 + tinyrainbow: 3.0.3 + dev: true + + /acorn-walk@8.3.4: + resolution: + { integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== } + engines: { node: '>=0.4.0' } + dependencies: + acorn: 8.15.0 + dev: true + + /acorn@8.15.0: + resolution: + { integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== } + engines: { node: '>=0.4.0' } + hasBin: true + dev: true + + /any-promise@1.3.0: + resolution: + { integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== } + dev: true + + /arg@4.1.3: + resolution: + { integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== } + dev: true + + /assertion-error@2.0.1: + resolution: + { integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== } + engines: { node: '>=12' } + dev: true + + /asynckit@0.4.0: + resolution: + { integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== } + dev: false + + /axios@1.13.2: + resolution: + { integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA== } + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /bundle-require@5.1.0(esbuild@0.24.2): + resolution: + { integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA== } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + peerDependencies: + esbuild: '>=0.18' + dependencies: + esbuild: 0.24.2 + load-tsconfig: 0.2.5 + dev: true + + /cac@6.7.14: + resolution: + { integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== } + engines: { node: '>=8' } + dev: true + + /call-bind-apply-helpers@1.0.2: + resolution: + { integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== } + engines: { node: '>= 0.4' } + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + dev: false + + /chai@6.2.1: + resolution: + { integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg== } + engines: { node: '>=18' } + dev: true + + /chokidar@4.0.3: + resolution: + { integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== } + engines: { node: '>= 14.16.0' } + dependencies: + readdirp: 4.1.2 + dev: true + + /combined-stream@1.0.8: + resolution: + { integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== } + engines: { node: '>= 0.8' } + dependencies: + delayed-stream: 1.0.0 + dev: false + + /commander@4.1.1: + resolution: + { integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== } + engines: { node: '>= 6' } + dev: true + + /consola@3.4.2: + resolution: + { integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== } + engines: { node: ^14.18.0 || >=16.10.0 } + dev: true + + /create-require@1.1.1: + resolution: + { integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== } + dev: true + + /debug@4.4.3: + resolution: + { integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== } + engines: { node: '>=6.0' } + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: true + + /delayed-stream@1.0.0: + resolution: + { integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== } + engines: { node: '>=0.4.0' } + dev: false + + /diff@4.0.2: + resolution: + { integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== } + engines: { node: '>=0.3.1' } + dev: true + + /dunder-proto@1.0.1: + resolution: + { integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== } + engines: { node: '>= 0.4' } + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + dev: false + + /es-define-property@1.0.1: + resolution: + { integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== } + engines: { node: '>= 0.4' } + dev: false + + /es-errors@1.3.0: + resolution: + { integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== } + engines: { node: '>= 0.4' } + dev: false + + /es-module-lexer@1.7.0: + resolution: + { integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== } + dev: true + + /es-object-atoms@1.1.1: + resolution: + { integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== } + engines: { node: '>= 0.4' } + dependencies: + es-errors: 1.3.0 + dev: false + + /es-set-tostringtag@2.1.0: + resolution: + { integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== } + engines: { node: '>= 0.4' } + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: false + + /esbuild@0.24.2: + resolution: + { integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA== } + engines: { node: '>=18' } + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.2 + '@esbuild/android-arm': 0.24.2 + '@esbuild/android-arm64': 0.24.2 + '@esbuild/android-x64': 0.24.2 + '@esbuild/darwin-arm64': 0.24.2 + '@esbuild/darwin-x64': 0.24.2 + '@esbuild/freebsd-arm64': 0.24.2 + '@esbuild/freebsd-x64': 0.24.2 + '@esbuild/linux-arm': 0.24.2 + '@esbuild/linux-arm64': 0.24.2 + '@esbuild/linux-ia32': 0.24.2 + '@esbuild/linux-loong64': 0.24.2 + '@esbuild/linux-mips64el': 0.24.2 + '@esbuild/linux-ppc64': 0.24.2 + '@esbuild/linux-riscv64': 0.24.2 + '@esbuild/linux-s390x': 0.24.2 + '@esbuild/linux-x64': 0.24.2 + '@esbuild/netbsd-arm64': 0.24.2 + '@esbuild/netbsd-x64': 0.24.2 + '@esbuild/openbsd-arm64': 0.24.2 + '@esbuild/openbsd-x64': 0.24.2 + '@esbuild/sunos-x64': 0.24.2 + '@esbuild/win32-arm64': 0.24.2 + '@esbuild/win32-ia32': 0.24.2 + '@esbuild/win32-x64': 0.24.2 + dev: true + + /esbuild@0.25.12: + resolution: + { integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg== } + engines: { node: '>=18' } + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + dev: true + + /estree-walker@3.0.3: + resolution: + { integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== } + dependencies: + '@types/estree': 1.0.8 + dev: true + + /expect-type@1.2.2: + resolution: + { integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA== } + engines: { node: '>=12.0.0' } + dev: true + + /fdir@6.5.0(picomatch@4.0.3): + resolution: + { integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== } + engines: { node: '>=12.0.0' } + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + dependencies: + picomatch: 4.0.3 + dev: true + + /follow-redirects@1.15.11: + resolution: + { integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== } + engines: { node: '>=4.0' } + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /form-data@4.0.5: + resolution: + { integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== } + engines: { node: '>= 6' } + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + dev: false + + /fsevents@2.3.3: + resolution: + { integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.2: + resolution: + { integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== } + dev: false + + /get-intrinsic@1.3.0: + resolution: + { integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== } + engines: { node: '>= 0.4' } + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + dev: false + + /get-proto@1.0.1: + resolution: + { integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== } + engines: { node: '>= 0.4' } + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + dev: false + + /gopd@1.2.0: + resolution: + { integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== } + engines: { node: '>= 0.4' } + dev: false + + /has-symbols@1.1.0: + resolution: + { integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== } + engines: { node: '>= 0.4' } + dev: false + + /has-tostringtag@1.0.2: + resolution: + { integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== } + engines: { node: '>= 0.4' } + dependencies: + has-symbols: 1.1.0 + dev: false + + /hasown@2.0.2: + resolution: + { integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== } + engines: { node: '>= 0.4' } + dependencies: + function-bind: 1.1.2 + dev: false + + /joycon@3.1.1: + resolution: + { integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== } + engines: { node: '>=10' } + dev: true + + /lilconfig@3.1.3: + resolution: + { integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== } + engines: { node: '>=14' } + dev: true + + /lines-and-columns@1.2.4: + resolution: + { integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== } + dev: true + + /load-tsconfig@0.2.5: + resolution: + { integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg== } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + dev: true + + /lodash.sortby@4.7.0: + resolution: + { integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== } + dev: true + + /magic-string@0.30.21: + resolution: + { integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== } + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + dev: true + + /make-error@1.3.6: + resolution: + { integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== } + dev: true + + /math-intrinsics@1.1.0: + resolution: + { integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== } + engines: { node: '>= 0.4' } + dev: false + + /mime-db@1.52.0: + resolution: + { integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== } + engines: { node: '>= 0.6' } + dev: false + + /mime-types@2.1.35: + resolution: + { integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== } + engines: { node: '>= 0.6' } + dependencies: + mime-db: 1.52.0 + dev: false + + /ms@2.1.3: + resolution: + { integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== } + dev: true + + /mz@2.7.0: + resolution: + { integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== } + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + dev: true + + /nanoid@3.3.11: + resolution: + { integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + dev: true + + /object-assign@4.1.1: + resolution: + { integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== } + engines: { node: '>=0.10.0' } + dev: true + + /obug@2.1.1: + resolution: + { integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ== } + dev: true + + /pathe@2.0.3: + resolution: + { integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== } + dev: true + + /picocolors@1.1.1: + resolution: + { integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== } + dev: true + + /picomatch@4.0.3: + resolution: + { integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== } + engines: { node: '>=12' } + dev: true + + /pirates@4.0.7: + resolution: + { integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== } + engines: { node: '>= 6' } + dev: true + + /postcss-load-config@6.0.1: + resolution: + { integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g== } + engines: { node: '>= 18' } + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + dependencies: + lilconfig: 3.1.3 + dev: true + + /postcss@8.5.6: + resolution: + { integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== } + engines: { node: ^10 || ^12 || >=14 } + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: true + + /prettier@3.4.1: + resolution: + { integrity: sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg== } + engines: { node: '>=14' } + hasBin: true + dev: true + + /proxy-from-env@1.1.0: + resolution: + { integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== } + dev: false + + /punycode@2.3.1: + resolution: + { integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== } + engines: { node: '>=6' } + dev: true + + /readdirp@4.1.2: + resolution: + { integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== } + engines: { node: '>= 14.18.0' } + dev: true + + /resolve-from@5.0.0: + resolution: + { integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== } + engines: { node: '>=8' } + dev: true + + /rollup@4.53.3: + resolution: + { integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA== } + engines: { node: '>=18.0.0', npm: '>=8.0.0' } + hasBin: true + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + dev: true + + /siginfo@2.0.0: + resolution: + { integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== } + dev: true + + /source-map-js@1.2.1: + resolution: + { integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== } + engines: { node: '>=0.10.0' } + dev: true + + /source-map@0.8.0-beta.0: + resolution: + { integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== } + engines: { node: '>= 8' } + deprecated: The work that was done in this beta branch won't be included in future versions + dependencies: + whatwg-url: 7.1.0 + dev: true + + /stackback@0.0.2: + resolution: + { integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== } + dev: true + + /std-env@3.10.0: + resolution: + { integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== } + dev: true + + /sucrase@3.35.1: + resolution: + { integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw== } + engines: { node: '>=16 || 14 >=14.17' } + hasBin: true + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + dev: true + + /thenify-all@1.6.0: + resolution: + { integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== } + engines: { node: '>=0.8' } + dependencies: + thenify: 3.3.1 + dev: true + + /thenify@3.3.1: + resolution: + { integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== } + dependencies: + any-promise: 1.3.0 + dev: true + + /tinybench@2.9.0: + resolution: + { integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== } + dev: true + + /tinyexec@0.3.2: + resolution: + { integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== } + dev: true + + /tinyglobby@0.2.15: + resolution: + { integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== } + engines: { node: '>=12.0.0' } + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + dev: true + + /tinyrainbow@3.0.3: + resolution: + { integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q== } + engines: { node: '>=14.0.0' } + dev: true + + /tr46@1.0.1: + resolution: + { integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== } + dependencies: + punycode: 2.3.1 + dev: true + + /tree-kill@1.2.2: + resolution: + { integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== } + hasBin: true + dev: true + + /ts-interface-checker@0.1.13: + resolution: + { integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== } + dev: true + + /ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3): + resolution: + { integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== } + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.10.1 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /tsup@8.3.5(typescript@5.9.3): + resolution: + { integrity: sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA== } + engines: { node: '>=18' } + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + dependencies: + bundle-require: 5.1.0(esbuild@0.24.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.24.2 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1 + resolve-from: 5.0.0 + rollup: 4.53.3 + source-map: 0.8.0-beta.0 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + dev: true + + /typescript@5.9.3: + resolution: + { integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== } + engines: { node: '>=14.17' } + hasBin: true + dev: true + + /undici-types@7.16.0: + resolution: + { integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== } + dev: true + + /v8-compile-cache-lib@3.0.1: + resolution: + { integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== } + dev: true + + /vite@7.2.4(@types/node@24.10.1): + resolution: + { integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w== } + engines: { node: ^20.19.0 || >=22.12.0 } + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + dependencies: + '@types/node': 24.10.1 + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /vitest@4.0.14(@types/node@24.10.1): + resolution: + { integrity: sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw== } + engines: { node: ^20.0.0 || ^22.0.0 || >=24.0.0 } + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.14 + '@vitest/browser-preview': 4.0.14 + '@vitest/browser-webdriverio': 4.0.14 + '@vitest/ui': 4.0.14 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + dependencies: + '@types/node': 24.10.1 + '@vitest/expect': 4.0.14 + '@vitest/mocker': 4.0.14(vite@7.2.4) + '@vitest/pretty-format': 4.0.14 + '@vitest/runner': 4.0.14 + '@vitest/snapshot': 4.0.14 + '@vitest/spy': 4.0.14 + '@vitest/utils': 4.0.14 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.2.4(@types/node@24.10.1) + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + dev: true + + /webidl-conversions@4.0.2: + resolution: + { integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== } + dev: true + + /whatwg-url@7.1.0: + resolution: + { integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== } + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + dev: true + + /why-is-node-running@2.3.0: + resolution: + { integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== } + engines: { node: '>=8' } + hasBin: true + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + dev: true + + /yn@3.1.1: + resolution: + { integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== } + engines: { node: '>=6' } + dev: true diff --git a/pingrip/src/index.ts b/pingrip/src/index.ts new file mode 100644 index 000000000..217520df7 --- /dev/null +++ b/pingrip/src/index.ts @@ -0,0 +1,2 @@ +export * as outputs from './outputs' +export * as messages from './messages' diff --git a/pingrip/src/messages.test.ts b/pingrip/src/messages.test.ts new file mode 100644 index 000000000..5af493342 --- /dev/null +++ b/pingrip/src/messages.test.ts @@ -0,0 +1,233 @@ +import { describe, it, expect } from 'vitest' +import { serialize, parse, safeParse, ParseError, type Message } from './messages' + +describe('messages', () => { + describe('serialize', () => { + it('should serialize an open message', () => { + const messages: Message[] = [{ type: 'open' }] + const result = serialize(messages) + expect(result.toString()).toBe('OPEN\r\n') + }) + + it('should serialize a disconnect message', () => { + const messages: Message[] = [{ type: 'disconnect' }] + const result = serialize(messages) + expect(result.toString()).toBe('DISCONNECT\r\n') + }) + + it('should serialize a text message', () => { + const messages: Message[] = [{ type: 'text', content: 'Hello World' }] + const result = serialize(messages) + expect(result.toString()).toBe('TEXT b\r\nHello World\r\n') + }) + + it('should serialize a binary message', () => { + const content = Buffer.from([0x01, 0x02, 0x03]) + const messages: Message[] = [{ type: 'binary', content }] + const result = serialize(messages) + const expected = Buffer.concat([Buffer.from('BINARY 3\r\n'), content, Buffer.from('\r\n')]) + expect(result).toEqual(expected) + }) + + it('should serialize a close message', () => { + const messages: Message[] = [{ type: 'close', code: 1000 }] + const result = serialize(messages) + const expected = Buffer.concat([ + Buffer.from('CLOSE 2\r\n'), + Buffer.from([0x03, 0xe8]), // 1000 in big-endian + Buffer.from('\r\n') + ]) + expect(result).toEqual(expected) + }) + + it('should serialize multiple messages', () => { + const messages: Message[] = [{ type: 'open' }, { type: 'text', content: 'Hi' }, { type: 'close', code: 1000 }] + const result = serialize(messages) + expect(result.toString('utf8', 0, 6)).toBe('OPEN\r\n') + }) + + it('should handle empty text content', () => { + const messages: Message[] = [{ type: 'text', content: '' }] + const result = serialize(messages) + expect(result.toString()).toBe('TEXT\r\n') + }) + }) + + describe('parse', () => { + it('should parse an open message', () => { + const buffer = Buffer.from('OPEN\r\n') + const result = parse(buffer) + expect(result).toEqual([{ type: 'open' }]) + }) + + it('should parse a disconnect message', () => { + const buffer = Buffer.from('DISCONNECT\r\n') + const result = parse(buffer) + expect(result).toEqual([{ type: 'disconnect' }]) + }) + + it('should parse a text message', () => { + const buffer = Buffer.from('TEXT 5\r\nHello\r\n') + const result = parse(buffer) + expect(result).toEqual([{ type: 'text', content: 'Hello' }]) + }) + + it('should parse a binary message', () => { + const content = Buffer.from([0x01, 0x02, 0x03]) + const buffer = Buffer.concat([Buffer.from('BINARY 3\r\n'), content, Buffer.from('\r\n')]) + const result = parse(buffer) + expect(result).toEqual([{ type: 'binary', content }]) + }) + + it('should parse a close message', () => { + const closeCode = Buffer.alloc(2) + closeCode.writeUInt16BE(1000) + const buffer = Buffer.concat([Buffer.from('CLOSE 2\r\n'), closeCode, Buffer.from('\r\n')]) + const result = parse(buffer) + expect(result).toEqual([{ type: 'close', code: 1000 }]) + }) + + it('should parse multiple messages', () => { + const buffer = Buffer.from('OPEN\r\nTEXT 2\r\nHi\r\nDISCONNECT\r\n') + const result = parse(buffer) + expect(result).toEqual([{ type: 'open' }, { type: 'text', content: 'Hi' }, { type: 'disconnect' }]) + }) + + it('should throw ParseError on invalid message type', () => { + const buffer = Buffer.from('INVALID\r\n') + expect(() => parse(buffer)).toThrow(ParseError) + expect(() => parse(buffer)).toThrow("'INVALID' is not a valid message type.") + }) + + it('should throw ParseError on missing line ending', () => { + const buffer = Buffer.from('OPEN') + expect(() => parse(buffer)).toThrow(ParseError) + expect(() => parse(buffer)).toThrow('Could not parse body') + }) + + it('should handle empty text message', () => { + const buffer = Buffer.from('TEXT 0\r\n\r\n') + const result = parse(buffer) + expect(result).toEqual([{ type: 'text', content: '' }]) + }) + + it('should handle empty text message without content length', () => { + const buffer = Buffer.from('TEXT\r\n') + const result = parse(buffer) + expect(result).toEqual([{ type: 'text', content: '' }]) + }) + + it('should parse hexadecimal length correctly', () => { + const buffer = Buffer.from('TEXT a\r\n0123456789\r\n') + const result = parse(buffer) + expect(result).toEqual([{ type: 'text', content: '0123456789' }]) + }) + }) + + describe('round-trip serialization', () => { + it('should maintain data integrity for open message', () => { + const original: Message[] = [{ type: 'open' }] + const serialized = serialize(original) + const parsed = parse(serialized) + expect(parsed).toEqual(original) + }) + + it('should maintain data integrity for text message', () => { + const original: Message[] = [{ type: 'text', content: 'Hello World!' }] + const serialized = serialize(original) + const parsed = parse(serialized) + expect(parsed).toEqual(original) + }) + + it('should maintain data integrity for binary message', () => { + const original: Message[] = [{ type: 'binary', content: Buffer.from([0x00, 0xff, 0x42]) }] + const serialized = serialize(original) + const parsed = parse(serialized) + expect(parsed).toEqual(original) + }) + + it('should maintain data integrity for close message', () => { + const original: Message[] = [{ type: 'close', code: 1001 }] + const serialized = serialize(original) + const parsed = parse(serialized) + expect(parsed).toEqual(original) + }) + + it('should maintain data integrity for multiple messages', () => { + const original: Message[] = [ + { type: 'open' }, + { type: 'text', content: 'test' }, + { type: 'binary', content: Buffer.from([0x01]) }, + { type: 'close', code: 1000 }, + { type: 'disconnect' } + ] + const serialized = serialize(original) + const parsed = parse(serialized) + expect(parsed).toEqual(original) + }) + }) + + describe('safeParse', () => { + it('should return messages on successful parse', () => { + const buffer = Buffer.from('OPEN\r\n') + const result = safeParse(buffer) + expect(result.error).toBeUndefined() + expect(result.messages).toEqual([{ type: 'open' }]) + }) + + it('should return error on parse failure', () => { + const buffer = Buffer.from('INVALID\r\n') + const result = safeParse(buffer) + expect(result.messages).toBeUndefined() + expect(result.error).toBeInstanceOf(ParseError) + expect(result.error?.message).toContain('INVALID') + }) + + it('should return ParseError on malformed input', () => { + const buffer = Buffer.from('OPEN') + const result = safeParse(buffer) + expect(result.messages).toBeUndefined() + expect(result.error).toBeInstanceOf(ParseError) + expect(result.error?.message).toBe('Could not parse body') + }) + + it('should handle empty buffer', () => { + const buffer = Buffer.from('') + const result = safeParse(buffer) + expect(result.error).toBeUndefined() + expect(result.messages).toEqual([]) + }) + }) + + describe('edge cases', () => { + it('should handle very long text messages', () => { + const longText = 'a'.repeat(10000) + const messages: Message[] = [{ type: 'text', content: longText }] + const serialized = serialize(messages) + const parsed = parse(serialized) + expect(parsed).toEqual(messages) + }) + + it('should handle binary data with null bytes', () => { + const content = Buffer.from([0x00, 0x00, 0x00]) + const messages: Message[] = [{ type: 'binary', content }] + const serialized = serialize(messages) + const parsed = parse(serialized) + expect(parsed).toEqual(messages) + }) + + it('should handle close code 0', () => { + const messages: Message[] = [{ type: 'close', code: 0 }] + const serialized = serialize(messages) + const parsed = parse(serialized) + expect(parsed).toEqual(messages) + }) + + it('should handle max close code (65535)', () => { + const messages: Message[] = [{ type: 'close', code: 65535 }] + const serialized = serialize(messages) + const parsed = parse(serialized) + expect(parsed).toEqual(messages) + }) + }) +}) diff --git a/pingrip/src/messages.ts b/pingrip/src/messages.ts new file mode 100644 index 000000000..2f785b553 --- /dev/null +++ b/pingrip/src/messages.ts @@ -0,0 +1,111 @@ +export type Message = + | { type: 'open' } + | { type: 'disconnect' } + | { + type: 'text' + content: string + } + | { + type: 'binary' + content: Buffer + } + | { + type: 'close' + code: number + } + +const _serializeSingle = (message: Message): Buffer => { + let content = Buffer.from('') + if (message.type === 'text') { + content = Buffer.from(message.content, 'utf8') + } else if (message.type === 'binary') { + content = Buffer.from(message.content) + } else if (message.type === 'close') { + content = Buffer.alloc(2) + content.writeUInt16BE(message.code) + } + + if (content.length === 0) { + return Buffer.from(message.type.toUpperCase() + '\r\n') + } + return Buffer.concat([ + Buffer.from(message.type.toUpperCase() + ` ${content.length.toString(16)}\r\n`), + content, + Buffer.from('\r\n') + ]) +} + +export const serialize = (messages: Message[]): Buffer => { + return Buffer.concat(messages.map(_serializeSingle)) +} + +export class ParseError extends Error { + constructor(message: string) { + super(message) + } +} + +export const parse = (body: Buffer): Message[] => { + const messages: Message[] = [] + while (body.length > 0) { + const endLineIndex = body.findIndex((byte, i, array) => { + if (array.length > i) { + return byte === '\r'.charCodeAt(0) && array[i + 1] === '\n'.charCodeAt(0) + } + return false + }) + if (endLineIndex === -1) { + throw new ParseError('Could not parse body') + } + + const command = body.subarray(0, endLineIndex).toString() + const [type, _length] = command.split(/\s(.*)/s) + + if (!type || !['OPEN', 'CLOSE', 'DISCONNECT', 'TEXT', 'BINARY'].includes(type)) { + throw new ParseError(`'${type}' is not a valid message type.`) + } + + const wasLengthProvided = _length !== undefined + let length = parseInt(_length ?? '0', 16) + if (length && isNaN(length)) { + length = 0 + } + const content = body.subarray(endLineIndex + 2, endLineIndex + 2 + length) + if (type === 'OPEN') { + messages.push({ type: 'open' }) + } else if (type === 'DISCONNECT') { + messages.push({ type: 'disconnect' }) + } else if (type === 'TEXT') { + messages.push({ + type: 'text', + content: content.toString() + }) + } else if (type === 'BINARY') { + messages.push({ + type: 'binary', + content + }) + } else if (type === 'CLOSE' && length === 2) { + messages.push({ + type: 'close', + code: content.readUInt16BE(0) + }) + } + const newBufferStart = endLineIndex + 2 + (wasLengthProvided ? length + 2 : 0) + body = body.subarray(newBufferStart) + } + return messages +} + +type SafeParseResult = { messages: Message[]; error?: undefined } | { messages?: undefined; error: ParseError } + +export const safeParse = (body: Buffer): SafeParseResult => { + try { + return { messages: parse(body) } + } catch (error: unknown) { + if (error instanceof ParseError) { + return { error } + } + return { error: new ParseError(String(error)) } + } +} diff --git a/pingrip/src/outputs.ts b/pingrip/src/outputs.ts new file mode 100644 index 000000000..dba01e694 --- /dev/null +++ b/pingrip/src/outputs.ts @@ -0,0 +1,126 @@ +import * as messages from './messages' +import { type Message } from './messages' + +export { GripPublisher } from './publisher' + +export type Response = { + body: Buffer + headers: Record +} + +export class ResponseBuilder { + private _messages: Message[] = [] + + open(): OpenResponseBuilder { + this._messages.push({ + type: 'open' + }) + return new OpenResponseBuilder(this) + } + + close(code: number): CloseResponseBuilder { + this._messages.push({ + type: 'close', + code + }) + return new CloseResponseBuilder(this) + } + + text(content: string) { + this._messages.push({ + type: 'text', + content + }) + return this + } + + binary(content: Buffer) { + this._messages.push({ + type: 'binary', + content + }) + return this + } + + unsubscribe(channels: string[]) { + for (const channel of channels) { + this.text(`c:${JSON.stringify({ type: 'unsubscribe', channel })}`) + } + return this + } + + toResponse(): Response { + return { + body: messages.serialize(this._messages), + headers: {} + } + } +} + +class CloseResponseBuilder { + constructor(private _builder: ResponseBuilder) {} + + unsubscribe(channels: string[]) { + this._builder.unsubscribe(channels) + return this + } + + toResponse(): Response { + return this._builder.toResponse() + } +} + +class OpenResponseBuilder { + private _keepAliveMessage: string | null = null + private _keepAliveTimeout: number | null = null + private _channels: string[] = [] + + constructor(private _builder: ResponseBuilder) {} + + keepAlive(message: string, timeout: number) { + if (timeout < 30) { + throw new Error(`Keep Alive timeout should be at least 30 secondes. ${timeout} was given.`) + } + this._keepAliveTimeout = timeout + this._keepAliveMessage = message + return this + } + + text(content: string) { + this._builder.text(content) + return this + } + + binary(content: Buffer) { + this._builder.binary(content) + return this + } + + subscribe(channels: string[]) { + for (const channel of channels) { + this._channels.push(channel) + this.text(`c:${JSON.stringify({ type: 'subscribe', channel })}`) + } + return this + } + + toResponse(): Response { + const { body } = this._builder.toResponse() + const headers: Record = { + 'Content-Type': 'application/websocket-events', + 'Grip-Hold': 'stream', + 'Sec-WebSocket-Extensions': 'grip' + } + if (this._channels.length > 0) { + headers['Grip-Channel'] = this._channels.join(',') + } + if (this._keepAliveMessage !== null && this._keepAliveTimeout) { + const b64KeepAlive = Buffer.from(this._keepAliveMessage, 'utf-8').toString('base64') + headers['Grip-Keep-Alive'] = `${b64KeepAlive}; format=base64; timeout=${this._keepAliveTimeout};` + } + return { + body, + headers + } + } +} diff --git a/pingrip/src/publisher.ts b/pingrip/src/publisher.ts new file mode 100644 index 000000000..822e926c7 --- /dev/null +++ b/pingrip/src/publisher.ts @@ -0,0 +1,28 @@ +import axios, { AxiosInstance } from 'axios' + +type GripPublisherConfig = { + /** Url that points to Pushpin (dev) or Pubchat (staging/prod) */ + signalUrl: string +} + +export class GripPublisher { + private _client: AxiosInstance + public constructor(config: GripPublisherConfig) { + this._client = axios.create({ baseURL: config.signalUrl }) + } + + publish(channels: string[], payload: string) { + const id = Math.random().toString(16).slice(2) + for (const channel of channels) { + this._client.post('/publish', { + channel: channel, + id: id, + formats: { + 'ws-message': { + content: payload + } + } + }) + } + } +} diff --git a/pingrip/tsconfig.json b/pingrip/tsconfig.json new file mode 100644 index 000000000..50572daa3 --- /dev/null +++ b/pingrip/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}