From e84066cae47ff4a33616d6a0a7176ff2e82af295 Mon Sep 17 00:00:00 2001 From: Jay Date: Sun, 2 Nov 2025 16:16:33 -0500 Subject: [PATCH] Converted go-roots-ws to typescript. --- .gitignore | 1 + LICENSE | 21 + README.md | 344 +++++++ c2p | 1 + package-lock.json | 1711 +++++++++++++++++++++++++++++++++ package.json | 50 + src/envelope/enclose.test.ts | 262 +++++ src/envelope/enclose.ts | 95 ++ src/envelope/envelope.test.ts | 101 ++ src/envelope/envelope.ts | 52 + src/envelope/find.test.ts | 509 ++++++++++ src/envelope/find.ts | 332 +++++++ src/envelope/index.ts | 3 + src/errors/index.ts | 70 ++ src/index.ts | 1 + src/types.ts | 29 + tsconfig.json | 17 + vitest.config.ts | 7 + 18 files changed, 3606 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 c2p create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/envelope/enclose.test.ts create mode 100644 src/envelope/enclose.ts create mode 100644 src/envelope/envelope.test.ts create mode 100644 src/envelope/envelope.ts create mode 100644 src/envelope/find.test.ts create mode 100644 src/envelope/find.ts create mode 100644 src/envelope/index.ts create mode 100644 src/errors/index.ts create mode 100644 src/index.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ce8b424 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 nostr:npub10mtatsat7ph6rsq0w8u8npt8d86x4jfr2nqjnvld2439q6f8ugqq0x27hf + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8bf0d57 --- /dev/null +++ b/README.md @@ -0,0 +1,344 @@ +# TS-Roots-WS - Nostr WebSocket Transport for TypeScript + +Source: https://git.wisehodl.dev/jay/ts-roots-ws + +Mirror: https://github.com/wisehodl/ts-roots-ws + +## What this library does + +`ts-roots-ws` is a consensus-layer Nostr protocol websocket transport library for TypeScript. It only provides primitives for working with Nostr protocol websocket connection states and messages: + +- WebSocket Connection States +- Envelope Structure +- Message Validation +- Protocol Message Creation +- Protocol Message Parsing +- Standard Label Handling + +## What this library does not do + +`ts-roots-ws` serves as a foundation for other libraries and applications to implement higher level transport abstractions on top of it, including: + +- Connection Management +- Event Loops +- Subscription Handling +- State Management +- Reconnection Logic + +## Installation + +1. Add `ts-roots-ws` to your project: + +```bash +npm install @wisehodl/roots-ws +``` + +2. Import the packages: + +```typescript +import { + encloseEvent, + encloseSubscriptionEvent, + findEvent, + findSubscriptionEvent, + getLabel, + isStandardLabel +} from "@wisehodl/roots-ws/envelope"; + +import { ConnectionStatus } from "@wisehodl/roots-ws"; +import { InvalidJSONError, WrongEnvelopeLabelError } from "@wisehodl/roots-ws/errors"; +``` + +3. Access functions with appropriate namespaces. + +## Usage Examples + +### Envelope Creation + +#### Create EVENT envelope + +```typescript +// Create an event using ts-roots +import { Event } from "@wisehodl/roots/events"; + +const event: Event = { + id: "abc123", + pubkey: "def456", + kind: 1, + content: "Hello Nostr!", + created_at: Math.floor(Date.now() / 1000), + tags: [], + sig: "" +}; + +// Convert to JSON +const eventJSON = JSON.stringify(event); + +// Create envelope +const env = encloseEvent(eventJSON); +// Result: ["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!","created_at":1636394097}] +``` + +#### Create subscription EVENT envelope + +```typescript +// Create an event using ts-roots +import { Event } from "@wisehodl/roots/events"; + +const event: Event = { + id: "abc123", + pubkey: "def456", + kind: 1, + content: "Hello Nostr!", + created_at: Math.floor(Date.now() / 1000), + tags: [], + sig: "" +}; + +// Convert to JSON +const eventJSON = JSON.stringify(event); + +// Create envelope with subscription ID +const subID = "sub1"; +const env = encloseSubscriptionEvent(subID, eventJSON); +// Result: ["EVENT","sub1",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!","created_at":1636394097}] +``` + +#### Create REQ envelope + +```typescript +// Create filters using ts-roots +import { Filter } from "@wisehodl/roots/filters"; + +const since = Math.floor(Date.now() / 1000) - (24 * 60 * 60); +const limit = 50; + +const filter1: Filter = { + kinds: [1], + limit: limit, + since: since +}; + +const filter2: Filter = { + authors: ["def456"] +}; + +// Convert to JSON +const filter1JSON = JSON.stringify(filter1); +const filter2JSON = JSON.stringify(filter2); + +// Create envelope +const subID = "sub1"; +const filtersJSON = [filter1JSON, filter2JSON]; +const env = encloseReq(subID, filtersJSON); +// Result: ["REQ","sub1",{"kinds":[1],"limit":50,"since":1636307697},{"authors":["def456"]}] +``` + +#### Create other envelope types + +```typescript +// Create CLOSE envelope +const env1 = encloseClose("sub1"); +// Result: ["CLOSE","sub1"] + +// Create EOSE envelope +const env2 = encloseEOSE("sub1"); +// Result: ["EOSE","sub1"] + +// Create NOTICE envelope +const env3 = encloseNotice("This is a notice"); +// Result: ["NOTICE","This is a notice"] + +// Create OK envelope +const env4 = encloseOK("abc123", true, "Event accepted"); +// Result: ["OK","abc123",true,"Event accepted"] + +// Create AUTH challenge +const env5 = encloseAuthChallenge("random-challenge-string"); +// Result: ["AUTH","random-challenge-string"] + +// Create AUTH response +import { Event } from "@wisehodl/roots/events"; + +const authEvent: Event = { + id: "abc123", + pubkey: "def456", + kind: 22242, + content: "", + created_at: Math.floor(Date.now() / 1000), + tags: [], + sig: "" +}; + +// Convert to JSON +const authEventJSON = JSON.stringify(authEvent); + +// Create envelope +const env6 = encloseAuthResponse(authEventJSON); +// Result: ["AUTH",{"id":"abc123","pubkey":"def456","kind":22242,"content":"","created_at":1636394097}] +``` + +--- + +### Envelope Parsing + +#### Extract label from envelope + +```typescript +const env = `["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`; +try { + const label = getLabel(env); + // label: "EVENT" + + // Check if label is standard + const isStandard = isStandardLabel(label); + // isStandard: true +} catch (err) { + console.error(err); +} +``` + +#### Extract event from EVENT envelope + +```typescript +const env = `["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`; +try { + const eventObj = findEvent(env); + + // Parse into ts-roots Event if needed + import { Event, validate } from "@wisehodl/roots/events"; + + // Validate the event + try { + validate(eventObj as Event); + } catch (err) { + console.error(`Invalid event: ${err.message}`); + } + + // Now you can access event properties + console.log(eventObj.id, eventObj.kind, eventObj.content); +} catch (err) { + console.error(err); +} +``` + +#### Extract subscription event + +```typescript +const env = `["EVENT","sub1",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`; +try { + const [subID, eventObj] = findSubscriptionEvent(env); + + // Parse into ts-roots Event if needed + import { Event } from "@wisehodl/roots/events"; + + console.log(`Subscription: ${subID}, Event ID: ${eventObj.id}`); +} catch (err) { + console.error(err); +} +``` + +#### Extract subscription request + +```typescript +const env = `["REQ","sub1",{"kinds":[1],"limit":50},{"authors":["def456"]}]`; +try { + const [subID, filtersObj] = findReq(env); + + // Parse each filter + import { Filter } from "@wisehodl/roots/filters"; + + // Now you can use the filter objects + filtersObj.forEach((filter, i) => { + console.log(`Filter ${i}: `, filter); + }); +} catch (err) { + console.error(err); +} +``` + +#### Extract other envelope types + +```typescript +// Extract OK response +const env1 = `["OK","abc123",true,"Event accepted"]`; +try { + const [eventID, status, message] = findOK(env1); + // eventID: "abc123" + // status: true + // message: "Event accepted" +} catch (err) { + console.error(err); +} + +// Extract EOSE message +const env2 = `["EOSE","sub1"]`; +try { + const subID = findEOSE(env2); + // subID: "sub1" +} catch (err) { + console.error(err); +} + +// Extract CLOSE message +const env3 = `["CLOSE","sub1"]`; +try { + const subID = findClose(env3); + // subID: "sub1" +} catch (err) { + console.error(err); +} + +// Extract CLOSED message +const env4 = `["CLOSED","sub1","Subscription complete"]`; +try { + const [subID, message] = findClosed(env4); + // subID: "sub1" + // message: "Subscription complete" +} catch (err) { + console.error(err); +} + +// Extract NOTICE message +const env5 = `["NOTICE","This is a notice"]`; +try { + const message = findNotice(env5); + // message: "This is a notice" +} catch (err) { + console.error(err); +} + +// Extract AUTH challenge +const env6 = `["AUTH","random-challenge-string"]`; +try { + const challenge = findAuthChallenge(env6); + // challenge: "random-challenge-string" +} catch (err) { + console.error(err); +} + +// Extract AUTH response +const env7 = `["AUTH",{"id":"abc123","pubkey":"def456","kind":22242,"content":""}]`; +try { + const authEvent = findAuthResponse(env7); + + // Parse into ts-roots Event if needed + import { Event } from "@wisehodl/roots/events"; +} catch (err) { + console.error(err); +} +``` + +## Testing + +This library contains a comprehensive suite of unit tests. Run them with: + +```bash +npm test +``` + +Or for a single run: + +```bash +npm run test:run +``` diff --git a/c2p b/c2p new file mode 100755 index 0000000..4069269 --- /dev/null +++ b/c2p @@ -0,0 +1 @@ +code2prompt -e "c2p" -e "package-lock.json" . diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d83e84d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1711 @@ +{ + "name": "@wisehodl/roots-ws", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@wisehodl/roots-ws", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/node": "^24.9.1", + "prettier": "^3.5.3", + "typescript": "^5.9.3", + "vite": "^7.1.12", + "vitest": "^4.0.2" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", + "integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "javascript-natural-sort": "^0.7.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">18.12" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz", + "integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.6.tgz", + "integrity": "sha512-5j8UUlBVhOjhj4lR2Nt9sEV8b4WtbcYh8vnfhTNA2Kn5+smtevzjNq+xlBuVhnFGXiyPPNzGrOVvmyHWkS5QGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.6", + "@vitest/utils": "4.0.6", + "chai": "^6.0.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.6.tgz", + "integrity": "sha512-3COEIew5HqdzBFEYN9+u0dT3i/NCwppLnO1HkjGfAP1Vs3vti1Hxm/MvcbC4DAn3Szo1M7M3otiAaT83jvqIjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.19" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.6.tgz", + "integrity": "sha512-4vptgNkLIA1W1Nn5X4x8rLJBzPiJwnPc+awKtfBE5hNMVsoAl/JCCPPzNrbf+L4NKgklsis5Yp2gYa+XAS442g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.6.tgz", + "integrity": "sha512-trPk5qpd7Jj+AiLZbV/e+KiiaGXZ8ECsRxtnPnCrJr9OW2mLB72Cb824IXgxVz/mVU3Aj4VebY+tDTPn++j1Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.6", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.6.tgz", + "integrity": "sha512-PaYLt7n2YzuvxhulDDu6c9EosiRuIE+FI2ECKs6yvHyhoga+2TBWI8dwBjs+IeuQaMtZTfioa9tj3uZb7nev1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.6", + "magic-string": "^0.30.19", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.6.tgz", + "integrity": "sha512-g9jTUYPV1LtRPRCQfhbMintW7BTQz1n6WXYQYRQ25qkyffA4bjVXjkROokZnv7t07OqfaFKw1lPzqKGk1hmNuQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.6.tgz", + "integrity": "sha512-bG43VS3iYKrMIZXBo+y8Pti0O7uNju3KvNn6DrQWhQQKcLavMB+0NZfO1/QBAEbq0MaQ3QjNsnnXlGQvsh0Z6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.6", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.0.tgz", + "integrity": "sha512-aUTnJc/JipRzJrNADXVvpVqi6CO0dn3nx4EVPxijri+fj3LUUDyZQOgVeW54Ob3Y1Xh9Iz8f+CgaCl8v0mn9bA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "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 + } + } + }, + "node_modules/vitest": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.6.tgz", + "integrity": "sha512-gR7INfiVRwnEOkCk47faros/9McCZMp5LM+OMNWGLaDBSvJxIzwjgNFufkuePBNaesGRnLmNfW+ddbUJRZn0nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.6", + "@vitest/mocker": "4.0.6", + "@vitest/pretty-format": "4.0.6", + "@vitest/runner": "4.0.6", + "@vitest/snapshot": "4.0.6", + "@vitest/spy": "4.0.6", + "@vitest/utils": "4.0.6", + "debug": "^4.4.3", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.19", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.6", + "@vitest/browser-preview": "4.0.6", + "@vitest/browser-webdriverio": "4.0.6", + "@vitest/ui": "4.0.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "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 + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1174a1c --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "@wisehodl/roots-ws", + "version": "0.1.0", + "description": "Nostr WebSocket transport primitives for TypeScript", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./envelope": { + "import": "./dist/envelope/index.js", + "types": "./dist/envelope/index.d.ts" + }, + "./errors": { + "import": "./dist/errors/index.js", + "types": "./dist/errors/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "lint": "tsc --noEmit", + "format": "prettier -w src", + "test": "vitest", + "test:run": "vitest run" + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/node": "^24.9.1", + "prettier": "^3.5.3", + "typescript": "^5.9.3", + "vite": "^7.1.12", + "vitest": "^4.0.2" + }, + "prettier": { + "importOrder": [ + "^@(/)(.*)", + "^[./]" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "plugins": [ + "@trivago/prettier-plugin-sort-imports" + ] + } +} diff --git a/src/envelope/enclose.test.ts b/src/envelope/enclose.test.ts new file mode 100644 index 0000000..6106278 --- /dev/null +++ b/src/envelope/enclose.test.ts @@ -0,0 +1,262 @@ +import { describe, expect, it } from "vitest"; + +import { + encloseAuthChallenge, + encloseAuthResponse, + encloseClose, + encloseClosed, + encloseEOSE, + encloseEvent, + encloseNotice, + encloseOK, + encloseReq, + encloseSubscriptionEvent, +} from "./enclose"; + +describe("encloseEvent", () => { + const cases = [ + { + name: "empty event", + event: "{}", + want: '["EVENT",{}]', + }, + { + name: "invalid json", + event: "in[valid,]", + want: '["EVENT",in[valid,]]', + }, + { + name: "populated event", + event: '{"id":"abc123","kind":1,"sig":"abc123"}', + want: '["EVENT",{"id":"abc123","kind":1,"sig":"abc123"}]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseEvent(tc.event); + expect(got).toEqual(tc.want); + }); + }); +}); + +describe("encloseOK", () => { + const cases = [ + { + name: "successful event", + eventID: "abc123", + status: true, + message: "Event accepted", + want: '["OK","abc123",true,"Event accepted"]', + }, + { + name: "rejected event", + eventID: "xyz789", + status: false, + message: "Invalid signature", + want: '["OK","xyz789",false,"Invalid signature"]', + }, + { + name: "empty message", + eventID: "def456", + status: true, + message: "", + want: '["OK","def456",true,""]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseOK(tc.eventID, tc.status, tc.message); + expect(got).toEqual(tc.want); + }); + }); +}); + +describe("encloseReq", () => { + const cases = [ + { + name: "single filter", + subID: "sub1", + filters: ['{"kinds":[1],"limit":10}'], + want: '["REQ","sub1",{"kinds":[1],"limit":10}]', + }, + { + name: "multiple filters", + subID: "sub2", + filters: ['{"kinds":[1]}', '{"authors":["abc"]}'], + want: '["REQ","sub2",{"kinds":[1]},{"authors":["abc"]}]', + }, + { + name: "no filters", + subID: "sub3", + filters: [], + want: '["REQ","sub3"]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseReq(tc.subID, tc.filters); + expect(got).toEqual(tc.want); + }); + }); +}); + +describe("encloseSubscriptionEvent", () => { + const cases = [ + { + name: "basic event", + subID: "sub1", + event: '{"id":"abc123","kind":1}', + want: '["EVENT","sub1",{"id":"abc123","kind":1}]', + }, + { + name: "empty event", + subID: "sub2", + event: "{}", + want: '["EVENT","sub2",{}]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseSubscriptionEvent(tc.subID, tc.event); + expect(got).toEqual(tc.want); + }); + }); +}); + +describe("encloseEOSE", () => { + const cases = [ + { + name: "valid subscription ID", + subID: "sub1", + want: '["EOSE","sub1"]', + }, + { + name: "empty subscription ID", + subID: "", + want: '["EOSE",""]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseEOSE(tc.subID); + expect(got).toEqual(tc.want); + }); + }); +}); + +describe("encloseClose", () => { + const cases = [ + { + name: "valid subscription ID", + subID: "sub1", + want: '["CLOSE","sub1"]', + }, + { + name: "empty subscription ID", + subID: "", + want: '["CLOSE",""]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseClose(tc.subID); + expect(got).toEqual(tc.want); + }); + }); +}); + +describe("encloseClosed", () => { + const cases = [ + { + name: "with message", + subID: "sub1", + message: "Subscription complete", + want: '["CLOSED","sub1","Subscription complete"]', + }, + { + name: "empty message", + subID: "sub2", + message: "", + want: '["CLOSED","sub2",""]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseClosed(tc.subID, tc.message); + expect(got).toEqual(tc.want); + }); + }); +}); + +describe("encloseNotice", () => { + const cases = [ + { + name: "valid message", + message: "This is a notice", + want: '["NOTICE","This is a notice"]', + }, + { + name: "empty message", + message: "", + want: '["NOTICE",""]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseNotice(tc.message); + expect(got).toEqual(tc.want); + }); + }); +}); + +describe("encloseAuthChallenge", () => { + const cases = [ + { + name: "valid challenge", + challenge: "random-challenge-string", + want: '["AUTH","random-challenge-string"]', + }, + { + name: "empty challenge", + challenge: "", + want: '["AUTH",""]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseAuthChallenge(tc.challenge); + expect(got).toEqual(tc.want); + }); + }); +}); + +describe("encloseAuthResponse", () => { + const cases = [ + { + name: "valid event", + event: '{"id":"abc123","kind":22242}', + want: '["AUTH",{"id":"abc123","kind":22242}]', + }, + { + name: "empty event", + event: "{}", + want: '["AUTH",{}]', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + const got = encloseAuthResponse(tc.event); + expect(got).toEqual(tc.want); + }); + }); +}); diff --git a/src/envelope/enclose.ts b/src/envelope/enclose.ts new file mode 100644 index 0000000..f573bc7 --- /dev/null +++ b/src/envelope/enclose.ts @@ -0,0 +1,95 @@ +import { Envelope } from "../types"; + +/** + * Creates an EVENT envelope for publishing events. + * It wraps the provided event JSON in the format ["EVENT", event]. + */ +export function encloseEvent(event: string): Envelope { + return `["EVENT",${event}]`; +} + +/** + * Creates an OK envelope acknowledging receipt of an event. + * Format: ["OK", eventID, status, message] + */ +export function encloseOK( + eventID: string, + status: boolean, + message: string, +): Envelope { + return `["OK","${eventID}",${status},"${message}"]`; +} + +/** + * Creates a REQ envelope for subscription requests. + * Format: ["REQ", subID, filter1, filter2, ...] + */ +export function encloseReq(subID: string, filters: string[]): Envelope { + let envelope = `["REQ","${subID}"`; + + for (const filter of filters) { + envelope += `,${filter}`; + } + + envelope += "]"; + return envelope; +} + +/** + * Creates an EVENT envelope for delivering subscription events. + * Format: ["EVENT", subID, event] + */ +export function encloseSubscriptionEvent( + subID: string, + event: string, +): Envelope { + return `["EVENT","${subID}",${event}]`; +} + +/** + * Creates an EOSE (End of Stored Events) envelope. + * Format: ["EOSE", subID] + */ +export function encloseEOSE(subID: string): Envelope { + return `["EOSE","${subID}"]`; +} + +/** + * Creates a CLOSE envelope for ending a subscription. + * Format: ["CLOSE", subID] + */ +export function encloseClose(subID: string): Envelope { + return `["CLOSE","${subID}"]`; +} + +/** + * Creates a CLOSED envelope for indicating a terminated subscription. + * Format: ["CLOSED", subID, message] + */ +export function encloseClosed(subID: string, message: string): Envelope { + return `["CLOSED","${subID}","${message}"]`; +} + +/** + * Creates a NOTICE envelope for responder messages. + * Format: ["NOTICE", message] + */ +export function encloseNotice(message: string): Envelope { + return `["NOTICE","${message}"]`; +} + +/** + * Creates an AUTH challenge envelope. + * Format: ["AUTH", challenge] + */ +export function encloseAuthChallenge(challenge: string): Envelope { + return `["AUTH","${challenge}"]`; +} + +/** + * Creates an AUTH response envelope. + * Format: ["AUTH", event] + */ +export function encloseAuthResponse(event: string): Envelope { + return `["AUTH",${event}]`; +} diff --git a/src/envelope/envelope.test.ts b/src/envelope/envelope.test.ts new file mode 100644 index 0000000..99b653d --- /dev/null +++ b/src/envelope/envelope.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; + +import { getLabel, getStandardLabels, isStandardLabel } from "./envelope"; + +describe("getLabel", () => { + const cases = [ + { + name: "valid envelope with EVENT label", + env: '["EVENT",{"id":"abc123"}]', + wantLabel: "EVENT", + }, + { + name: "valid envelope with custom label", + env: '["TEST",{"data":"value"}]', + wantLabel: "TEST", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "empty array", + env: "[]", + wantErrText: "empty envelope", + }, + { + name: "label not a string", + env: '[123,{"id":"abc123"}]', + wantErrText: "label is not a string", + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + getLabel(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const got = getLabel(tc.env); + expect(got).toEqual(tc.wantLabel); + } + }); + }); +}); + +describe("getStandardLabels", () => { + it("returns the correct standard labels", () => { + const expected = new Set([ + "EVENT", + "REQ", + "CLOSE", + "CLOSED", + "EOSE", + "NOTICE", + "OK", + "AUTH", + ]); + + const labels = getStandardLabels(); + + // Check that we have the exact same number of labels + expect(labels.size).toBe(expected.size); + + // Check that all expected labels are present + expected.forEach((label) => { + expect(labels.has(label)).toBe(true); + }); + }); +}); + +describe("isStandardLabel", () => { + const standardCases = [ + "EVENT", + "REQ", + "CLOSE", + "CLOSED", + "EOSE", + "NOTICE", + "OK", + "AUTH", + ]; + + const nonStandardCases = ["TEST", "CUSTOM", "event", "REQ1", ""]; + + standardCases.forEach((label) => { + it(`${label} should be standard`, () => { + expect(isStandardLabel(label)).toBe(true); + }); + }); + + nonStandardCases.forEach((label) => { + it(`${label} should not be standard`, () => { + expect(isStandardLabel(label)).toBe(false); + }); + }); +}); diff --git a/src/envelope/envelope.ts b/src/envelope/envelope.ts new file mode 100644 index 0000000..4420225 --- /dev/null +++ b/src/envelope/envelope.ts @@ -0,0 +1,52 @@ +import { + InvalidEnvelopeError, + InvalidJSONError, + WrongFieldTypeError, +} from "../errors"; +import { Envelope } from "../types"; + +/** + * Gets the label from an envelope. + */ +export function getLabel(env: Envelope): string { + try { + const arr = JSON.parse(env); + if (!Array.isArray(arr) || arr.length < 1) { + throw new InvalidEnvelopeError("empty envelope"); + } + + if (typeof arr[0] !== "string") { + throw new WrongFieldTypeError("label is not a string"); + } + + return arr[0]; + } catch (err) { + if (err instanceof Error && !(err instanceof InvalidJSONError)) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Returns a set of standard Nostr WebSocket message labels. + */ +export function getStandardLabels(): Set { + return new Set([ + "EVENT", + "REQ", + "CLOSE", + "CLOSED", + "EOSE", + "NOTICE", + "OK", + "AUTH", + ]); +} + +/** + * Checks if the given label is a standard Nostr WebSocket message label. + */ +export function isStandardLabel(label: string): boolean { + return getStandardLabels().has(label); +} diff --git a/src/envelope/find.test.ts b/src/envelope/find.test.ts new file mode 100644 index 0000000..dc7f07d --- /dev/null +++ b/src/envelope/find.test.ts @@ -0,0 +1,509 @@ +import { describe, expect, it } from "vitest"; + +import { + findAuthChallenge, + findAuthResponse, + findClose, + findClosed, + findEOSE, + findEvent, + findNotice, + findOK, + findReq, + findSubscriptionEvent, +} from "./find"; + +describe("findEvent", () => { + const cases = [ + { + name: "valid event", + env: '["EVENT",{"id":"abc123","kind":1}]', + wantEvent: '{"id":"abc123","kind":1}', + }, + { + name: "wrong label", + env: '["REQ",{"id":"abc123","kind":1}]', + wantErrText: "expected EVENT, got REQ", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["EVENT"]', + wantErrText: "expected 2 elements, got 1", + }, + { + name: "extraneous elements", + env: '["EVENT",{"id":"abc123"},"extra"]', + wantEvent: '{"id":"abc123"}', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findEvent(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const got = findEvent(tc.env); + expect(JSON.stringify(got)).toEqual(tc.wantEvent); + } + }); + }); +}); + +describe("findSubscriptionEvent", () => { + const cases = [ + { + name: "valid event", + env: '["EVENT","sub1",{"id":"abc123","kind":1}]', + wantSubID: "sub1", + wantEvent: '{"id":"abc123","kind":1}', + }, + { + name: "wrong label", + env: '["REQ","sub1",{"id":"abc123","kind":1}]', + wantErrText: "expected EVENT, got REQ", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["EVENT","sub1"]', + wantErrText: "expected 3 elements, got 2", + }, + { + name: "extraneous elements", + env: '["EVENT","sub1",{"id":"abc123"},"extra"]', + wantSubID: "sub1", + wantEvent: '{"id":"abc123"}', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findSubscriptionEvent(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const [gotSubID, gotEvent] = findSubscriptionEvent(tc.env); + expect(gotSubID).toEqual(tc.wantSubID); + expect(JSON.stringify(gotEvent)).toEqual(tc.wantEvent); + } + }); + }); +}); + +describe("findOK", () => { + const cases = [ + { + name: "accepted event", + env: '["OK","abc123",true,"Event accepted"]', + wantEventID: "abc123", + wantStatus: true, + wantMessage: "Event accepted", + }, + { + name: "rejected event", + env: '["OK","xyz789",false,"Invalid signature"]', + wantEventID: "xyz789", + wantStatus: false, + wantMessage: "Invalid signature", + }, + { + name: "wrong status type", + env: '["OK","abc123","ok","Event accepted"]', + wantErrText: "status is not the expected type", + }, + { + name: "wrong label", + env: '["EVENT","abc123",true,"Event accepted"]', + wantErrText: "expected OK, got EVENT", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["OK","abc123",true]', + wantErrText: "expected 4 elements, got 3", + }, + { + name: "extraneous elements", + env: '["OK","abc123",true,"Event accepted","extra"]', + wantEventID: "abc123", + wantStatus: true, + wantMessage: "Event accepted", + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findOK(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const [gotEventID, gotStatus, gotMessage] = findOK(tc.env); + expect(gotEventID).toEqual(tc.wantEventID); + expect(gotStatus).toEqual(tc.wantStatus); + expect(gotMessage).toEqual(tc.wantMessage); + } + }); + }); +}); + +describe("findReq", () => { + const cases = [ + { + name: "single filter", + env: '["REQ","sub1",{"kinds":[1],"limit":10}]', + wantSubID: "sub1", + wantFilters: ['{"kinds":[1],"limit":10}'], + }, + { + name: "multiple filters", + env: '["REQ","sub2",{"kinds":[1]},{"authors":["abc"]}]', + wantSubID: "sub2", + wantFilters: ['{"kinds":[1]}', '{"authors":["abc"]}'], + }, + { + name: "no filters", + env: '["REQ","sub3"]', + wantSubID: "sub3", + wantFilters: [], + }, + { + name: "wrong label", + env: '["EVENT","sub1",{"kinds":[1],"limit":10}]', + wantErrText: "expected REQ, got EVENT", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["REQ"]', + wantErrText: "expected 2 elements, got 1", + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findReq(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const [gotSubID, gotFilters] = findReq(tc.env); + expect(gotSubID).toEqual(tc.wantSubID); + expect(gotFilters.map((f) => JSON.stringify(f))).toEqual( + tc.wantFilters, + ); + } + }); + }); +}); + +describe("findEOSE", () => { + const cases = [ + { + name: "valid EOSE", + env: '["EOSE","sub1"]', + wantSubID: "sub1", + }, + { + name: "wrong label", + env: '["EVENT","sub1"]', + wantErrText: "expected EOSE, got EVENT", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["EOSE"]', + wantErrText: "expected 2 elements, got 1", + }, + { + name: "extraneous elements", + env: '["EOSE","sub1","extra"]', + wantSubID: "sub1", + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findEOSE(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const gotSubID = findEOSE(tc.env); + expect(gotSubID).toEqual(tc.wantSubID); + } + }); + }); +}); + +describe("findClose", () => { + const cases = [ + { + name: "valid CLOSE", + env: '["CLOSE","sub1"]', + wantSubID: "sub1", + }, + { + name: "wrong label", + env: '["EVENT","sub1"]', + wantErrText: "expected CLOSE, got EVENT", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["CLOSE"]', + wantErrText: "expected 2 elements, got 1", + }, + { + name: "extraneous elements", + env: '["CLOSE","sub1","extra"]', + wantSubID: "sub1", + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findClose(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const gotSubID = findClose(tc.env); + expect(gotSubID).toEqual(tc.wantSubID); + } + }); + }); +}); + +describe("findClosed", () => { + const cases = [ + { + name: "valid CLOSED", + env: '["CLOSED","sub1","Subscription complete"]', + wantSubID: "sub1", + wantMessage: "Subscription complete", + }, + { + name: "wrong label", + env: '["EVENT","sub1","Subscription complete"]', + wantErrText: "expected CLOSED, got EVENT", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["CLOSED","sub1"]', + wantErrText: "expected 3 elements, got 2", + }, + { + name: "extraneous elements", + env: '["CLOSED","sub1","Subscription complete","extra"]', + wantSubID: "sub1", + wantMessage: "Subscription complete", + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findClosed(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const [gotSubID, gotMessage] = findClosed(tc.env); + expect(gotSubID).toEqual(tc.wantSubID); + expect(gotMessage).toEqual(tc.wantMessage); + } + }); + }); +}); + +describe("findNotice", () => { + const cases = [ + { + name: "valid NOTICE", + env: '["NOTICE","This is a notice"]', + wantMessage: "This is a notice", + }, + { + name: "wrong label", + env: '["EVENT","This is a notice"]', + wantErrText: "expected NOTICE, got EVENT", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["NOTICE"]', + wantErrText: "expected 2 elements, got 1", + }, + { + name: "extraneous elements", + env: '["NOTICE","This is a notice","extra"]', + wantMessage: "This is a notice", + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findNotice(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const gotMessage = findNotice(tc.env); + expect(gotMessage).toEqual(tc.wantMessage); + } + }); + }); +}); + +describe("findAuthChallenge", () => { + const cases = [ + { + name: "valid AUTH challenge", + env: '["AUTH","random-challenge-string"]', + wantChallenge: "random-challenge-string", + }, + { + name: "wrong label", + env: '["EVENT","random-challenge-string"]', + wantErrText: "expected AUTH, got EVENT", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["AUTH"]', + wantErrText: "expected 2 elements, got 1", + }, + { + name: "extraneous elements", + env: '["AUTH","random-challenge-string","extra"]', + wantChallenge: "random-challenge-string", + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findAuthChallenge(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const gotChallenge = findAuthChallenge(tc.env); + expect(gotChallenge).toEqual(tc.wantChallenge); + } + }); + }); +}); + +describe("findAuthResponse", () => { + const cases = [ + { + name: "valid AUTH response", + env: '["AUTH",{"id":"abc123","kind":22242}]', + wantEvent: '{"id":"abc123","kind":22242}', + }, + { + name: "wrong label", + env: '["EVENT",{"id":"abc123","kind":22242}]', + wantErrText: "expected AUTH, got EVENT", + }, + { + name: "invalid json", + env: "invalid", + wantErrText: "Unexpected token", + }, + { + name: "missing elements", + env: '["AUTH"]', + wantErrText: "expected 2 elements, got 1", + }, + { + name: "extraneous elements", + env: '["AUTH",{"id":"abc123","kind":22242},"extra"]', + wantEvent: '{"id":"abc123","kind":22242}', + }, + ]; + + cases.forEach((tc) => { + it(tc.name, () => { + if (tc.wantErrText) { + try { + findAuthResponse(tc.env); + expect.fail("Expected function to throw an error"); + } catch (err) { + expect((err as Error).message).toContain(tc.wantErrText); + } + } else { + const gotEvent = findAuthResponse(tc.env); + expect(JSON.stringify(gotEvent)).toEqual(tc.wantEvent); + } + }); + }); +}); diff --git a/src/envelope/find.ts b/src/envelope/find.ts new file mode 100644 index 0000000..4d6a6a2 --- /dev/null +++ b/src/envelope/find.ts @@ -0,0 +1,332 @@ +import { + InvalidEnvelopeError, + InvalidJSONError, + WrongEnvelopeLabelError, + WrongFieldTypeError, +} from "../errors"; +import { Envelope } from "../types"; + +/** + * Helper function that ensures the JSON array has at least the minimum length required. + */ +export function checkArrayLength(arr: any[], minLen: number): void { + if (arr.length < minLen) { + throw new InvalidEnvelopeError( + `expected ${minLen} elements, got ${arr.length}`, + ); + } +} + +/** + * Helper function that verifies that the envelope label matches the expected one. + */ +export function checkLabel(got: string, want: string): void { + if (got !== want) { + throw new WrongEnvelopeLabelError(`expected ${want}, got ${got}`); + } +} + +/** + * Extracts an event from an EVENT envelope with no subscription ID. + * Expected Format: ["EVENT", event] + */ +export function findEvent(env: Envelope): any { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 2); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "EVENT"); + + return arr[1]; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Extracts an event and subscription ID from an EVENT envelope. + * Expected Format: ["EVENT", subID, event] + */ +export function findSubscriptionEvent( + env: Envelope, +): [subID: string, event: any] { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 3); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "EVENT"); + + const subID = arr[1]; + if (typeof subID !== "string") { + throw new WrongFieldTypeError("subscription ID is not a string"); + } + + return [subID, arr[2]]; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Extracts eventID, status, and message from an OK envelope. + * Expected Format: ["OK", eventID, status, message] + */ +export function findOK( + env: Envelope, +): [eventID: string, status: boolean, message: string] { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 4); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "OK"); + + const eventID = arr[1]; + if (typeof eventID !== "string") { + throw new WrongFieldTypeError("event ID is not a string"); + } + + if (typeof arr[2] !== "boolean") { + throw new WrongFieldTypeError("status is not the expected type"); + } + const status = arr[2]; + + const message = arr[3]; + if (typeof message !== "string") { + throw new WrongFieldTypeError("message is not a string"); + } + + return [eventID, status, message]; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Extracts subscription ID and filters from a REQ envelope. + * Expected Format: ["REQ", subID, filter1, filter2, ...] + */ +export function findReq(env: Envelope): [subID: string, filters: any[]] { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 2); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "REQ"); + + const subID = arr[1]; + if (typeof subID !== "string") { + throw new WrongFieldTypeError("subscription ID is not a string"); + } + + const filters: any[] = []; + for (let i = 2; i < arr.length; i++) { + filters.push(arr[i]); + } + + return [subID, filters]; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Extracts subscription ID from an EOSE envelope. + * Expected Format: ["EOSE", subID] + */ +export function findEOSE(env: Envelope): string { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 2); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "EOSE"); + + const subID = arr[1]; + if (typeof subID !== "string") { + throw new WrongFieldTypeError("subscription ID is not a string"); + } + + return subID; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Extracts subscription ID from a CLOSE envelope. + * Expected Format: ["CLOSE", subID] + */ +export function findClose(env: Envelope): string { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 2); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "CLOSE"); + + const subID = arr[1]; + if (typeof subID !== "string") { + throw new WrongFieldTypeError("subscription ID is not a string"); + } + + return subID; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Extracts subscription ID and message from a CLOSED envelope. + * Expected Format: ["CLOSED", subID, message] + */ +export function findClosed(env: Envelope): [subID: string, message: string] { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 3); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "CLOSED"); + + const subID = arr[1]; + if (typeof subID !== "string") { + throw new WrongFieldTypeError("subscription ID is not a string"); + } + + const message = arr[2]; + if (typeof message !== "string") { + throw new WrongFieldTypeError("message is not a string"); + } + + return [subID, message]; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Extracts message from a NOTICE envelope. + * Expected Format: ["NOTICE", message] + */ +export function findNotice(env: Envelope): string { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 2); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "NOTICE"); + + const message = arr[1]; + if (typeof message !== "string") { + throw new WrongFieldTypeError("message is not a string"); + } + + return message; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Extracts challenge from an AUTH challenge envelope. + * Expected Format: ["AUTH", challenge] + */ +export function findAuthChallenge(env: Envelope): string { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 2); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "AUTH"); + + const challenge = arr[1]; + if (typeof challenge !== "string") { + throw new WrongFieldTypeError("challenge is not a string"); + } + + return challenge; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} + +/** + * Extracts event from an AUTH response envelope. + * Expected Format: ["AUTH", event] + */ +export function findAuthResponse(env: Envelope): any { + try { + const arr = JSON.parse(env); + checkArrayLength(arr, 2); + + const label = arr[0]; + if (typeof label !== "string") { + throw new WrongFieldTypeError("envelope label is not a string"); + } + checkLabel(label, "AUTH"); + + // The second element should be an object (the event) + return arr[1]; + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new InvalidJSONError(`${err}`); + } +} diff --git a/src/envelope/index.ts b/src/envelope/index.ts new file mode 100644 index 0000000..747b6d1 --- /dev/null +++ b/src/envelope/index.ts @@ -0,0 +1,3 @@ +export * from "./envelope"; +export * from "./enclose"; +export * from "./find"; diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..d632200 --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,70 @@ +/** + * Base error class for all roots-ws errors. + */ +export class RootsWSError extends Error { + constructor(message: string) { + super(message); + this.name = this.constructor.name; + // This sets the prototype correctly for instanceof checks + Object.setPrototypeOf(this, new.target.prototype); + } +} + +/** + * Data Structure Errors + */ + +/** + * Indicates that a byte sequence could not be parsed as valid JSON. + * This is typically returned when unmarshaling fails during envelope processing. + */ +export class InvalidJSONError extends RootsWSError { + constructor(message: string = "invalid JSON") { + super(message); + } +} + +/** + * Indicates that a required field is absent from a data structure. + * This is returned when validating that all mandatory components are present. + */ +export class MissingFieldError extends RootsWSError { + constructor(message: string = "missing required field") { + super(message); + } +} + +/** + * Indicates that a field's type does not match the expected type. + * This is returned when unmarshaling a specific value fails due to type mismatch. + */ +export class WrongFieldTypeError extends RootsWSError { + constructor(message: string = "wrong field type") { + super(message); + } +} + +/** + * Envelope Errors + */ + +/** + * Indicates that a message does not conform to the Nostr envelope structure. + * This typically occurs when an array has incorrect number of elements for its message type. + */ +export class InvalidEnvelopeError extends RootsWSError { + constructor(message: string = "invalid envelope format") { + super(message); + } +} + +/** + * Indicates that an envelope's label does not match the expected type. + * This is returned when attempting to parse an envelope using a Find function that + * expects a different label than what was provided. + */ +export class WrongEnvelopeLabelError extends RootsWSError { + constructor(message: string = "wrong envelope label") { + super(message); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..eea524d --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..3b3ce02 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,29 @@ +/** + * Represents a Nostr websocket message. + */ +export type Envelope = string; + +/** + * Represents the current state of a WebSocket connection. + */ +export enum ConnectionStatus { + /** + * Indicates the connection is not active and no connection attempt is in progress. + */ + Disconnected = 0, + + /** + * Indicates a connection attempt is currently in progress but not yet established. + */ + Connecting = 1, + + /** + * Indicates the connection is active and ready for message exchange. + */ + Connected = 2, + + /** + * Indicates the connection is in the process of shutting down gracefully. + */ + Closing = 3, +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..90bda97 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "bundler" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..e2ec332 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + }, +});