From 54355323d91d6a0b948cbd585b5150c25e0145f5 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Sun, 7 Dec 2025 19:37:43 +0100 Subject: [PATCH] Add ServiceHost, ServiceClient, and RondevuService for high-level service management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add RondevuService: High-level API for username claiming and service publishing with Ed25519 signatures - Add ServiceHost: Manages offer pool for hosting services with auto-replacement - Add ServiceClient: Connects to hosted services with automatic reconnection - Add NoOpSignaler: Placeholder signaler for connection setup - Integrate Ed25519 signature functionality from @noble/ed25519 - Add ESLint and Prettier configuration with 4-space indentation - Add demo with local signaling test - Version bump to 0.10.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .prettierrc.json | 9 + demo/README.md | 141 ++ demo/demo.js | 304 ++++ demo/index.html | 280 ++++ eslint.config.js | 52 + package-lock.json | 2947 ++++++++++++++++++++++++++++++++++++- package.json | 21 +- src/api.ts | 295 ++-- src/bin.ts | 49 +- src/connection-manager.ts | 9 - src/connection.ts | 255 +++- src/event-bus.ts | 110 +- src/index.ts | 49 +- src/noop-signaler.ts | 35 + src/rondevu-service.ts | 168 +++ src/service-client.ts | 244 +++ src/service-host.ts | 236 +++ src/signaler.ts | 80 +- src/types.ts | 44 +- src/webrtc-context.ts | 35 + vite.config.js | 10 + 21 files changed, 5066 insertions(+), 307 deletions(-) create mode 100644 .prettierrc.json create mode 100644 demo/README.md create mode 100644 demo/demo.js create mode 100644 demo/index.html create mode 100644 eslint.config.js delete mode 100644 src/connection-manager.ts create mode 100644 src/noop-signaler.ts create mode 100644 src/rondevu-service.ts create mode 100644 src/service-client.ts create mode 100644 src/service-host.ts create mode 100644 src/webrtc-context.ts create mode 100644 vite.config.js diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..f899ee1 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,9 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 4, + "useTabs": false, + "trailingComma": "es5", + "printWidth": 100, + "arrowParens": "avoid" +} diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..82d485e --- /dev/null +++ b/demo/README.md @@ -0,0 +1,141 @@ +# Rondevu WebRTC Local Test + +Simple side-by-side demo for testing `WebRTCRondevuConnection` with **local signaling** (no server required). + +## Quick Start + +```bash +npm run dev +``` + +Opens browser at `http://localhost:3000` + +## How It Works + +This demo uses **local in-memory signaling** to test WebRTC connections between two peers on the same page. The `LocalSignaler` class simulates a signaling server by directly exchanging ICE candidates and SDP between peers. + +### Architecture + +- **LocalSignaler**: Implements the `Signaler` interface with local peer-to-peer communication +- **Host (Peer A)**: Creates the offer (offerer role) +- **Client (Peer B)**: Receives the offer and creates answer (answerer role) +- **ICE Exchange**: Candidates are automatically exchanged between peers through the linked signalers + +## Usage Steps + +1. **Create Host** (Peer A) + - Click "1️⃣ Create Host Connection" on the left side + - The host will create an offer and display it + - Status changes to "Connecting" + +2. **Create Client** (Peer B) + - Click "2️⃣ Create Client Connection" on the right side + - The client receives the host's offer automatically + - Creates an answer and sends it back to the host + - Both peers exchange ICE candidates + +3. **Connection Established** + - Watch the status indicators turn green ("Connected") + - Activity logs show the connection progress + +4. **Send Messages** + - Type a message in either peer's input field + - Click "📤 Send" or press Enter + - Messages appear in the other peer's activity log + +## Features + +- ✅ **No signaling server required** - Everything runs locally +- ✅ **Automatic ICE candidate exchange** - Signalers handle candidate exchange +- ✅ **Real-time activity logs** - See exactly what's happening +- ✅ **Connection state indicators** - Visual feedback for connection status +- ✅ **Bidirectional messaging** - Send messages in both directions + +## Code Structure + +### demo.js + +```javascript +// LocalSignaler - Implements local signaling +class LocalSignaler { + addIceCandidate(candidate) // Called when local peer has a candidate + addListener(callback) // Listen for remote candidates + linkTo(remoteSignaler) // Connect two signalers together +} + +// Create and link signalers +const hostSignaler = new LocalSignaler('HOST', 'CLIENT') +const clientSignaler = new LocalSignaler('CLIENT', 'HOST') +hostSignaler.linkTo(clientSignaler) +clientSignaler.linkTo(hostSignaler) + +// Create connections +const hostConnection = new WebRTCRondevuConnection({ + id: 'test-connection', + host: 'client-peer', + service: 'test.demo@1.0.0', + offer: null, // No offer = offerer role + context: new WebRTCContext(hostSignaler) +}) + +const clientConnection = new WebRTCRondevuConnection({ + id: 'test-connection', + host: 'host-peer', + service: 'test.demo@1.0.0', + offer: hostConnection.connection.localDescription, // With offer = answerer role + context: new WebRTCContext(clientSignaler) +}) +``` + +### index.html + +- Side-by-side layout for Host and Client +- Status indicators (disconnected/connecting/connected) +- SDP display areas (offer/answer) +- Message input and send buttons +- Activity logs for each peer + +## Debugging + +Open the browser console to see detailed logs: + +- `[HOST]` - Logs from the host connection +- `[CLIENT]` - Logs from the client connection +- ICE candidate exchange +- Connection state changes +- Message send/receive events + +## Comparison: Local vs Remote Signaling + +### Local Signaling (This Demo) +```javascript +const signaler = new LocalSignaler('HOST', 'CLIENT') +signaler.linkTo(remoteSignaler) // Direct link +``` + +**Pros**: No server, instant testing, no network latency +**Cons**: Only works for same-page testing + +### Remote Signaling (Production) +```javascript +const api = new RondevuAPI('https://api.ronde.vu', credentials) +const signaler = new RondevuSignaler(api, offerId) +``` + +**Pros**: Real peer discovery, works across networks +**Cons**: Requires signaling server, network latency + +## Next Steps + +After testing locally, you can: + +1. Switch to `RondevuSignaler` for real signaling server testing +2. Test across different browsers/devices +3. Test with STUN/TURN servers for NAT traversal +4. Implement production signaling with Rondevu API + +## Files + +- `index.html` - UI layout and styling +- `demo.js` - Local signaling implementation and WebRTC logic +- `README.md` - This file diff --git a/demo/demo.js b/demo/demo.js new file mode 100644 index 0000000..20afbd5 --- /dev/null +++ b/demo/demo.js @@ -0,0 +1,304 @@ +import { WebRTCRondevuConnection } from '../src/index.js' +import { WebRTCContext } from '../src/webrtc-context.js' + +// Local signaling implementation for testing +class LocalSignaler { + constructor(name, remoteName) { + this.name = name + this.remoteName = remoteName + this.iceCandidates = [] + this.iceListeners = [] + this.remote = null + this.remoteIceCandidates = [] + this.offerCallbacks = [] + this.answerCallbacks = [] + } + + // Link two signalers together + linkTo(remoteSignaler) { + this.remote = remoteSignaler + this.remoteIceCandidates = remoteSignaler.iceCandidates + } + + // Set local offer (called when offer is created) + setOffer(offer) { + console.log(`[${this.name}] Setting offer`) + // Notify remote peer about the offer + if (this.remote) { + this.remote.offerCallbacks.forEach(callback => callback(offer)) + } + } + + // Set local answer (called when answer is created) + setAnswer(answer) { + console.log(`[${this.name}] Setting answer`) + // Notify remote peer about the answer + if (this.remote) { + this.remote.answerCallbacks.forEach(callback => callback(answer)) + } + } + + // Listen for offers from remote peer + addOfferListener(callback) { + this.offerCallbacks.push(callback) + return () => { + const index = this.offerCallbacks.indexOf(callback) + if (index > -1) { + this.offerCallbacks.splice(index, 1) + } + } + } + + // Listen for answers from remote peer + addAnswerListener(callback) { + this.answerCallbacks.push(callback) + return () => { + const index = this.answerCallbacks.indexOf(callback) + if (index > -1) { + this.answerCallbacks.splice(index, 1) + } + } + } + + // Add local ICE candidate (called by local connection) + addIceCandidate(candidate) { + console.log(`[${this.name}] Adding ICE candidate:`, candidate.candidate) + this.iceCandidates.push(candidate) + + // Immediately send to remote peer if linked + if (this.remote) { + setTimeout(() => { + this.remote.iceListeners.forEach(listener => { + console.log(`[${this.name}] Sending ICE to ${this.remoteName}`) + listener(candidate) + }) + }, 10) + } + } + + // Listen for remote ICE candidates + addListener(callback) { + console.log(`[${this.name}] Adding ICE listener`) + this.iceListeners.push(callback) + + // Send any existing remote candidates + this.remoteIceCandidates.forEach(candidate => { + setTimeout(() => callback(candidate), 10) + }) + + return () => { + const index = this.iceListeners.indexOf(callback) + if (index > -1) { + this.iceListeners.splice(index, 1) + } + } + } +} + +// Create signalers for host and client +const hostSignaler = new LocalSignaler('HOST', 'CLIENT') +const clientSignaler = new LocalSignaler('CLIENT', 'HOST') + +// Link them together for bidirectional communication +hostSignaler.linkTo(clientSignaler) +clientSignaler.linkTo(hostSignaler) + +// Store connections +let hostConnection = null +let clientConnection = null + +// UI Update functions +function updateStatus(peer, state) { + const statusEl = document.getElementById(`status-${peer}`) + if (statusEl) { + statusEl.className = `status ${state}` + statusEl.textContent = state.charAt(0).toUpperCase() + state.slice(1) + } +} + +function addLog(peer, message) { + const logEl = document.getElementById(`log-${peer}`) + if (logEl) { + const time = new Date().toLocaleTimeString() + logEl.innerHTML += `
[${time}] ${message}
` + logEl.scrollTop = logEl.scrollHeight + } +} + +// Create Host (Offerer) +async function createHost() { + try { + addLog('a', 'Creating host connection (offerer)...') + + const hostContext = new WebRTCContext(hostSignaler) + + hostConnection = new WebRTCRondevuConnection({ + id: 'test-connection', + host: 'client-peer', + service: 'test.demo@1.0.0', + offer: null, + context: hostContext, + }) + + // Listen for state changes + hostConnection.events.on('state-change', state => { + console.log('[HOST] State changed:', state) + updateStatus('a', state) + addLog('a', `State changed to: ${state}`) + }) + + // Listen for messages + hostConnection.events.on('message', message => { + console.log('[HOST] Received message:', message) + addLog('a', `📨 Received: ${message}`) + }) + + addLog('a', '✅ Host connection created') + updateStatus('a', 'connecting') + + // Wait for host to be ready (offer created and set) + await hostConnection.ready + addLog('a', '✅ Host offer created') + + // Get the offer + const offer = hostConnection.connection.localDescription + document.getElementById('offer-a').value = JSON.stringify(offer, null, 2) + + addLog('a', 'Offer ready to send to client') + } catch (error) { + console.error('[HOST] Error:', error) + addLog('a', `❌ Error: ${error.message}`) + updateStatus('a', 'disconnected') + } +} + +// Create Client (Answerer) +async function createClient() { + try { + addLog('b', 'Creating client connection (answerer)...') + + // Get offer from host + if (!hostConnection) { + alert('Please create host first!') + return + } + + const offer = hostConnection.connection.localDescription + if (!offer) { + alert('Host offer not ready yet!') + return + } + + addLog('b', 'Got offer from host') + + const clientContext = new WebRTCContext(clientSignaler) + + clientConnection = new WebRTCRondevuConnection({ + id: 'test-connection', + host: 'host-peer', + service: 'test.demo@1.0.0', + offer: offer, + context: clientContext, + }) + + // Listen for state changes + clientConnection.events.on('state-change', state => { + console.log('[CLIENT] State changed:', state) + updateStatus('b', state) + addLog('b', `State changed to: ${state}`) + }) + + // Listen for messages + clientConnection.events.on('message', message => { + console.log('[CLIENT] Received message:', message) + addLog('b', `📨 Received: ${message}`) + }) + + addLog('b', '✅ Client connection created') + updateStatus('b', 'connecting') + + // Wait for client to be ready + await clientConnection.ready + addLog('b', '✅ Client answer created') + + // Get the answer + const answer = clientConnection.connection.localDescription + document.getElementById('answer-b').value = JSON.stringify(answer, null, 2) + + // Set answer on host + addLog('b', 'Setting answer on host...') + await hostConnection.connection.setRemoteDescription(answer) + addLog('b', '✅ Answer set on host') + addLog('a', '✅ Answer received from client') + } catch (error) { + console.error('[CLIENT] Error:', error) + addLog('b', `❌ Error: ${error.message}`) + updateStatus('b', 'disconnected') + } +} + +// Send test message from host to client +function sendFromHost() { + if (!hostConnection) { + alert('Please create host first!') + return + } + + const message = document.getElementById('message-a').value || 'Hello from Host!' + addLog('a', `📤 Sending: ${message}`) + hostConnection + .sendMessage(message) + .then(success => { + if (success) { + addLog('a', '✅ Message sent successfully') + } else { + addLog('a', '⚠️ Message queued (not connected)') + } + }) + .catch(error => { + addLog('a', `❌ Error sending: ${error.message}`) + }) +} + +// Send test message from client to host +function sendFromClient() { + if (!clientConnection) { + alert('Please create client first!') + return + } + + const message = document.getElementById('message-b').value || 'Hello from Client!' + addLog('b', `📤 Sending: ${message}`) + clientConnection + .sendMessage(message) + .then(success => { + if (success) { + addLog('b', '✅ Message sent successfully') + } else { + addLog('b', '⚠️ Message queued (not connected)') + } + }) + .catch(error => { + addLog('b', `❌ Error sending: ${error.message}`) + }) +} + +// Attach event listeners when DOM is ready +document.addEventListener('DOMContentLoaded', () => { + // Clear all textareas on load + document.getElementById('offer-a').value = '' + document.getElementById('answer-b').value = '' + + // Make functions globally available (for console testing) + window.createHost = createHost + window.createClient = createClient + window.sendFromHost = sendFromHost + window.sendFromClient = sendFromClient + + console.log('🚀 Local signaling test loaded') + console.log('Steps:') + console.log('1. Click "Create Host" (Peer A)') + console.log('2. Click "Create Client" (Peer B)') + console.log('3. Wait for connection to establish') + console.log('4. Send messages between peers') +}) diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..9bfdc52 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,280 @@ + + + + + + Rondevu WebRTC Local Test + + + +
+

🔗 Rondevu WebRTC Local Test

+ +
+ +
+

Peer A (Host/Offerer)

+ +
Disconnected
+ +
+ +
+ +
+

Local Offer (SDP)

+ +
+ +
+

Send Message

+
+ + +
+
+ +
+

Activity Log

+
+
+
+ + +
+

Peer B (Client/Answerer)

+ +
Disconnected
+ +
+ +
+ +
+

Local Answer (SDP)

+ +
+ +
+

Send Message

+
+ + +
+
+ +
+

Activity Log

+
+
+
+
+
+ + + + diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..018b907 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,52 @@ +import js from '@eslint/js' +import tsPlugin from '@typescript-eslint/eslint-plugin' +import tsParser from '@typescript-eslint/parser' +import prettierConfig from 'eslint-config-prettier' +import prettierPlugin from 'eslint-plugin-prettier' +import unicorn from 'eslint-plugin-unicorn' +import globals from 'globals' + +export default [ + js.configs.recommended, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + globals: { + ...globals.browser, + ...globals.node, + RTCPeerConnection: 'readonly', + RTCIceCandidate: 'readonly', + RTCSessionDescriptionInit: 'readonly', + RTCIceCandidateInit: 'readonly', + BufferSource: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tsPlugin, + prettier: prettierPlugin, + unicorn: unicorn, + }, + rules: { + ...tsPlugin.configs.recommended.rules, + ...prettierConfig.rules, + 'prettier/prettier': 'error', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + 'unicorn/filename-case': [ + 'error', + { + case: 'kebabCase', + ignore: ['^README\\.md$'], + }, + ], + }, + }, + { + ignores: ['dist/**', 'node_modules/**', '*.config.js'], + }, +] diff --git a/package-lock.json b/package-lock.json index 84240b0..532e61f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,723 @@ "@xtr-dev/rondevu-client": "^0.9.2" }, "devDependencies": { - "typescript": "^5.9.3" + "@eslint/js": "^9.39.1", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^62.0.0", + "globals": "^16.5.0", + "prettier": "^3.7.4", + "typescript": "^5.9.3", + "vite": "^7.2.6" + } + }, + "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/@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/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@noble/ed25519": { @@ -25,6 +741,575 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/type-utils": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.48.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@xtr-dev/rondevu-client": { "version": "0.9.2", "resolved": "https://registry.npmjs.org/@xtr-dev/rondevu-client/-/rondevu-client-0.9.2.tgz", @@ -34,6 +1319,1511 @@ "@noble/ed25519": "^3.0.0" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz", + "integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/builtin-modules": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001759", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", + "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz", + "integrity": "sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.266", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", + "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", + "dev": true, + "license": "ISC" + }, + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "62.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-62.0.0.tgz", + "integrity": "sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "@eslint-community/eslint-utils": "^4.9.0", + "@eslint/plugin-kit": "^0.4.0", + "change-case": "^5.4.4", + "ci-info": "^4.3.1", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.46.0", + "esquery": "^1.6.0", + "find-up-simple": "^1.0.1", + "globals": "^16.4.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^5.0.0", + "jsesc": "^3.1.0", + "pluralize": "^8.0.0", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.13.0", + "semver": "^7.7.3", + "strip-indent": "^4.1.1" + }, + "engines": { + "node": "^20.10.0 || >=21.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=9.38.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "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/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-builtin-module": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^5.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "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.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "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/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -47,6 +2837,161 @@ "engines": { "node": ">=14.17" } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 9036d68..f29759c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/rondevu-client", - "version": "0.9.2", + "version": "0.10.0", "description": "TypeScript client for Rondevu with durable WebRTC connections, automatic reconnection, and message queuing", "type": "module", "main": "dist/index.js", @@ -8,6 +8,10 @@ "scripts": { "build": "tsc", "typecheck": "tsc --noEmit", + "dev": "vite", + "lint": "eslint src demo --ext .ts,.tsx,.js", + "lint:fix": "eslint src demo --ext .ts,.tsx,.js --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,js}\" \"demo/**/*.{ts,tsx,js,html}\"", "prepublishOnly": "npm run build" }, "keywords": [ @@ -20,14 +24,23 @@ "author": "", "license": "MIT", "devDependencies": { - "typescript": "^5.9.3" + "@eslint/js": "^9.39.1", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^62.0.0", + "globals": "^16.5.0", + "prettier": "^3.7.4", + "typescript": "^5.9.3", + "vite": "^7.2.6" }, "files": [ "dist", "README.md" ], "dependencies": { - "@noble/ed25519": "^3.0.0", - "@xtr-dev/rondevu-client": "^0.9.2" + "@noble/ed25519": "^3.0.0" } } diff --git a/src/api.ts b/src/api.ts index 061fcda..09d2069 100644 --- a/src/api.ts +++ b/src/api.ts @@ -2,55 +2,83 @@ * Rondevu API Client - Single class for all API endpoints */ +import * as ed25519 from '@noble/ed25519' + +// Set SHA-512 hash function for ed25519 (required in @noble/ed25519 v3+) +ed25519.hashes.sha512Async = async (message: Uint8Array) => { + return new Uint8Array(await crypto.subtle.digest('SHA-512', message as BufferSource)) +} + export interface Credentials { - peerId: string; - secret: string; + peerId: string + secret: string +} + +export interface Keypair { + publicKey: string + privateKey: string } export interface OfferRequest { - sdp: string; - topics?: string[]; - ttl?: number; - secret?: string; + sdp: string + topics?: string[] + ttl?: number + secret?: string } export interface Offer { - id: string; - peerId: string; - sdp: string; - topics: string[]; - ttl: number; - createdAt: number; - expiresAt: number; - answererPeerId?: string; + id: string + peerId: string + sdp: string + topics: string[] + ttl: number + createdAt: number + expiresAt: number + answererPeerId?: string } export interface ServiceRequest { - username: string; - serviceFqn: string; - sdp: string; - ttl?: number; - isPublic?: boolean; - metadata?: Record; - signature: string; - message: string; + username: string + serviceFqn: string + sdp: string + ttl?: number + isPublic?: boolean + metadata?: Record + signature: string + message: string } export interface Service { - serviceId: string; - uuid: string; - offerId: string; - username: string; - serviceFqn: string; - isPublic: boolean; - metadata?: Record; - createdAt: number; - expiresAt: number; + serviceId: string + uuid: string + offerId: string + username: string + serviceFqn: string + isPublic: boolean + metadata?: Record + createdAt: number + expiresAt: number } export interface IceCandidate { - candidate: RTCIceCandidateInit; - createdAt: number; + candidate: RTCIceCandidateInit + createdAt: number +} + +/** + * Helper: Convert Uint8Array to base64 string + */ +function bytesToBase64(bytes: Uint8Array): string { + const binString = Array.from(bytes, byte => String.fromCodePoint(byte)).join('') + return btoa(binString) +} + +/** + * Helper: Convert base64 string to Uint8Array + */ +function base64ToBytes(base64: string): Uint8Array { + const binString = atob(base64) + return Uint8Array.from(binString, char => char.codePointAt(0)!) } /** @@ -67,11 +95,56 @@ export class RondevuAPI { */ private getAuthHeader(): Record { if (!this.credentials) { - return {}; + return {} } return { - 'Authorization': `Bearer ${this.credentials.peerId}:${this.credentials.secret}` - }; + Authorization: `Bearer ${this.credentials.peerId}:${this.credentials.secret}`, + } + } + + // ============================================ + // Ed25519 Cryptography Helpers + // ============================================ + + /** + * Generate an Ed25519 keypair for username claiming and service publishing + */ + static async generateKeypair(): Promise { + const privateKey = ed25519.utils.randomSecretKey() + const publicKey = await ed25519.getPublicKeyAsync(privateKey) + + return { + publicKey: bytesToBase64(publicKey), + privateKey: bytesToBase64(privateKey), + } + } + + /** + * Sign a message with an Ed25519 private key + */ + static async signMessage(message: string, privateKeyBase64: string): Promise { + const privateKey = base64ToBytes(privateKeyBase64) + const encoder = new TextEncoder() + const messageBytes = encoder.encode(message) + + const signature = await ed25519.signAsync(messageBytes, privateKey) + return bytesToBase64(signature) + } + + /** + * Verify a signature + */ + static async verifySignature( + message: string, + signatureBase64: string, + publicKeyBase64: string + ): Promise { + const publicKey = base64ToBytes(publicKeyBase64) + const signature = base64ToBytes(signatureBase64) + const encoder = new TextEncoder() + const messageBytes = encoder.encode(message) + + return await ed25519.verifyAsync(signature, messageBytes, publicKey) } // ============================================ @@ -84,15 +157,15 @@ export class RondevuAPI { async register(): Promise { const response = await fetch(`${this.baseUrl}/register`, { method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); + headers: { 'Content-Type': 'application/json' }, + }) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Registration failed: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Registration failed: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } // ============================================ @@ -107,17 +180,17 @@ export class RondevuAPI { method: 'POST', headers: { 'Content-Type': 'application/json', - ...this.getAuthHeader() + ...this.getAuthHeader(), }, - body: JSON.stringify({ offers }) - }); + body: JSON.stringify({ offers }), + }) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to create offers: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to create offers: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } /** @@ -125,15 +198,15 @@ export class RondevuAPI { */ async getOffer(offerId: string): Promise { const response = await fetch(`${this.baseUrl}/offers/${offerId}`, { - headers: this.getAuthHeader() - }); + headers: this.getAuthHeader(), + }) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to get offer: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to get offer: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } /** @@ -144,14 +217,14 @@ export class RondevuAPI { method: 'POST', headers: { 'Content-Type': 'application/json', - ...this.getAuthHeader() + ...this.getAuthHeader(), }, - body: JSON.stringify({ sdp, secret }) - }); + body: JSON.stringify({ sdp, secret }), + }) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to answer offer: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to answer offer: ${error.error || response.statusText}`) } } @@ -160,19 +233,19 @@ export class RondevuAPI { */ async getAnswer(offerId: string): Promise<{ sdp: string } | null> { const response = await fetch(`${this.baseUrl}/offers/${offerId}/answer`, { - headers: this.getAuthHeader() - }); + headers: this.getAuthHeader(), + }) if (response.status === 404) { - return null; // No answer yet + return null // No answer yet } if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to get answer: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to get answer: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } /** @@ -180,15 +253,15 @@ export class RondevuAPI { */ async searchOffers(topic: string): Promise { const response = await fetch(`${this.baseUrl}/offers?topic=${encodeURIComponent(topic)}`, { - headers: this.getAuthHeader() - }); + headers: this.getAuthHeader(), + }) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to search offers: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to search offers: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } // ============================================ @@ -203,14 +276,14 @@ export class RondevuAPI { method: 'POST', headers: { 'Content-Type': 'application/json', - ...this.getAuthHeader() + ...this.getAuthHeader(), }, - body: JSON.stringify({ candidates }) - }); + body: JSON.stringify({ candidates }), + }) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to add ICE candidates: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to add ICE candidates: ${error.error || response.statusText}`) } } @@ -221,14 +294,14 @@ export class RondevuAPI { const response = await fetch( `${this.baseUrl}/offers/${offerId}/ice-candidates?since=${since}`, { headers: this.getAuthHeader() } - ); + ) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to get ICE candidates: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to get ICE candidates: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } // ============================================ @@ -243,17 +316,17 @@ export class RondevuAPI { method: 'POST', headers: { 'Content-Type': 'application/json', - ...this.getAuthHeader() + ...this.getAuthHeader(), }, - body: JSON.stringify(service) - }); + body: JSON.stringify(service), + }) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to publish service: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to publish service: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } /** @@ -261,15 +334,15 @@ export class RondevuAPI { */ async getService(uuid: string): Promise { const response = await fetch(`${this.baseUrl}/services/${uuid}`, { - headers: this.getAuthHeader() - }); + headers: this.getAuthHeader(), + }) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to get service: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to get service: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } /** @@ -279,14 +352,14 @@ export class RondevuAPI { const response = await fetch( `${this.baseUrl}/services?username=${encodeURIComponent(username)}`, { headers: this.getAuthHeader() } - ); + ) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to search services: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to search services: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } /** @@ -296,14 +369,14 @@ export class RondevuAPI { const response = await fetch( `${this.baseUrl}/services?serviceFqn=${encodeURIComponent(serviceFqn)}`, { headers: this.getAuthHeader() } - ); + ) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to search services: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to search services: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } /** @@ -313,14 +386,14 @@ export class RondevuAPI { const response = await fetch( `${this.baseUrl}/services?username=${encodeURIComponent(username)}&serviceFqn=${encodeURIComponent(serviceFqn)}`, { headers: this.getAuthHeader() } - ); + ) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to search services: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to search services: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } // ============================================ @@ -333,14 +406,14 @@ export class RondevuAPI { async checkUsername(username: string): Promise<{ available: boolean; owner?: string }> { const response = await fetch( `${this.baseUrl}/usernames/${encodeURIComponent(username)}/check` - ); + ) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to check username: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to check username: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } /** @@ -356,20 +429,20 @@ export class RondevuAPI { method: 'POST', headers: { 'Content-Type': 'application/json', - ...this.getAuthHeader() + ...this.getAuthHeader(), }, body: JSON.stringify({ publicKey, signature, - message - }) - }); + message, + }), + }) if (!response.ok) { - const error = await response.json().catch(() => ({ error: 'Unknown error' })); - throw new Error(`Failed to claim username: ${error.error || response.statusText}`); + const error = await response.json().catch(() => ({ error: 'Unknown error' })) + throw new Error(`Failed to claim username: ${error.error || response.statusText}`) } - return await response.json(); + return await response.json() } } diff --git a/src/bin.ts b/src/bin.ts index d29ef24..9d02d65 100644 --- a/src/bin.ts +++ b/src/bin.ts @@ -1,15 +1,42 @@ - +/** + * Binnable - A cleanup function that can be synchronous or asynchronous + * + * Used to unsubscribe from events, close connections, or perform other cleanup operations. + */ export type Binnable = () => void | Promise +/** + * Create a cleanup function collector (garbage bin) + * + * Collects cleanup functions and provides a single `clean()` method to execute all of them. + * Useful for managing multiple cleanup operations in a single place. + * + * @returns A function that accepts cleanup functions and has a `clean()` method + * + * @example + * ```typescript + * const bin = createBin(); + * + * // Add cleanup functions + * bin( + * () => console.log('Cleanup 1'), + * () => connection.close(), + * () => clearInterval(timer) + * ); + * + * // Later, clean everything + * bin.clean(); // Executes all cleanup functions + * ``` + */ export const createBin = () => { const bin: Binnable[] = [] - return Object.assign( - (...rubbish: Binnable[]) => bin.push(...rubbish), - { - clean: (): void => { - bin.forEach(binnable => binnable()) - bin.length = 0 - } - } - ) -} \ No newline at end of file + return Object.assign((...rubbish: Binnable[]) => bin.push(...rubbish), { + /** + * Execute all cleanup functions and clear the bin + */ + clean: (): void => { + bin.forEach(binnable => binnable()) + bin.length = 0 + }, + }) +} diff --git a/src/connection-manager.ts b/src/connection-manager.ts deleted file mode 100644 index 5faa882..0000000 --- a/src/connection-manager.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * ConnectionManager - Manages WebRTC peer connections - */ - -export class ConnectionManager { - constructor() { - // TODO: Initialize connection manager - } -} diff --git a/src/connection.ts b/src/connection.ts index 666733e..5f69dcd 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -1,76 +1,229 @@ -import {ConnectionEvents, ConnectionInterface, Message, QueueMessageOptions, Signaler} from "./types"; -import {EventBus} from "./event-bus"; -import {createBin} from "./bin"; +import { + ConnectionEvents, + ConnectionInterface, + ConnectionStates, + isConnectionState, + Message, + QueueMessageOptions, + Signaler, +} from './types.js' +import { EventBus } from './event-bus.js' +import { createBin } from './bin.js' +import { WebRTCContext } from './webrtc-context' +export type WebRTCRondevuConnectionOptions = { + id: string + service: string + offer: RTCSessionDescriptionInit | null + context: WebRTCContext +} + +/** + * WebRTCRondevuConnection - WebRTC peer connection wrapper with Rondevu signaling + * + * Manages a WebRTC peer connection lifecycle including: + * - Automatic offer/answer creation based on role + * - ICE candidate exchange via Rondevu signaling server + * - Connection state management with type-safe events + * - Data channel creation and message handling + * + * The connection automatically determines its role (offerer or answerer) based on whether + * an offer is provided in the constructor. The offerer creates the data channel, while + * the answerer receives it via the 'datachannel' event. + * + * @example + * ```typescript + * // Offerer side (creates offer) + * const connection = new WebRTCRondevuConnection( + * 'conn-123', + * 'peer-username', + * 'chat.service@1.0.0' + * ); + * + * await connection.ready; // Wait for local offer + * const sdp = connection.connection.localDescription!.sdp!; + * // Send sdp to signaling server... + * + * // Answerer side (receives offer) + * const connection = new WebRTCRondevuConnection( + * 'conn-123', + * 'peer-username', + * 'chat.service@1.0.0', + * { type: 'offer', sdp: remoteOfferSdp } + * ); + * + * await connection.ready; // Wait for local answer + * const answerSdp = connection.connection.localDescription!.sdp!; + * // Send answer to signaling server... + * + * // Both sides: Set up signaler and listen for state changes + * connection.setSignaler(signaler); + * connection.events.on('state-change', (state) => { + * console.log('Connection state:', state); + * }); + * ``` + */ export class WebRTCRondevuConnection implements ConnectionInterface { - private readonly connection: RTCPeerConnection; - private readonly side: 'offer' | 'answer'; - public readonly expiresAt: number = 0; - public readonly lastActive: number = 0; - public readonly events: EventBus = new EventBus(); - private signaler!: Signaler; // Will be set by setSignaler() - private readonly _ready: Promise; - private _state: ConnectionInterface['state'] = 'disconnected'; + private readonly side: 'offer' | 'answer' + public readonly expiresAt: number = 0 + public readonly lastActive: number = 0 + public readonly events: EventBus = new EventBus() + public readonly ready: Promise private iceBin = createBin() + private ctx: WebRTCContext + public id: string + public service: string + private _conn: RTCPeerConnection | null = null + private _state: ConnectionInterface['state'] = 'disconnected' - constructor( - public readonly id: string, - public readonly host: string, - public readonly service: string, - offer?: RTCSessionDescriptionInit) { - this.connection = new RTCPeerConnection(); - this.side = offer ? 'answer' : 'offer'; - const ready = offer - ? this.connection.setRemoteDescription(offer) - .then(() => this.connection.createAnswer()) - .then(answer => this.connection.setLocalDescription(answer)) - : this.connection.createOffer() - .then(offer => this.connection.setLocalDescription(offer)); - this._ready = ready.then(() => this.setState('connecting')) - .then(() => this.startIceListeners()) - } + constructor({ context: ctx, offer, id, service }: WebRTCRondevuConnectionOptions) { + this.ctx = ctx + this.id = id + this.service = service + this._conn = ctx.createPeerConnection() + this.side = offer ? 'answer' : 'offer' - private setState(state: ConnectionInterface['state']) { - this._state = state; - this.events.emit('state-change', state); - } - - private startIceListeners() { - const listener = ({candidate}: {candidate: RTCIceCandidate | null}) => { - if (candidate) this.signaler.addIceCandidate(candidate) + // setup data channel + if (offer) { + this._conn.addEventListener('datachannel', e => { + const channel = e.channel + channel.addEventListener('message', e => { + console.log('Message from peer:', e) + }) + channel.addEventListener('open', () => { + channel.send('I am ' + this.side) + }) + }) + } else { + const channel = this._conn.createDataChannel('vu.ronde.protocol') + channel.addEventListener('message', e => { + console.log('Message from peer:', e) + }) + channel.addEventListener('open', () => { + channel.send('I am ' + this.side) + }) } - this.connection.addEventListener('icecandidate', listener) + + // setup description exchange + this.ready = offer + ? this._conn + .setRemoteDescription(offer) + .then(() => this._conn?.createAnswer()) + .then(async answer => { + if (!answer || !this._conn) throw new Error('Connection disappeared') + await this._conn.setLocalDescription(answer) + return await ctx.signaler.setAnswer(answer) + }) + : this._conn.createOffer().then(async offer => { + if (!this._conn) throw new Error('Connection disappeared') + await this._conn.setLocalDescription(offer) + return await ctx.signaler.setOffer(offer) + }) + + // propagate connection state changes + this._conn.addEventListener('connectionstatechange', () => { + console.log(this.side, 'connection state changed: ', this._conn!.connectionState) + const state = isConnectionState(this._conn!.connectionState) + ? this._conn!.connectionState + : 'disconnected' + this.setState(state) + }) + + this._conn.addEventListener('iceconnectionstatechange', () => { + console.log(this.side, 'ice connection state changed: ', this._conn!.iceConnectionState) + }) + + // start ICE candidate exchange when gathering begins + this._conn.addEventListener('icegatheringstatechange', () => { + if (this._conn!.iceGatheringState === 'gathering') { + this.startIce() + } else if (this._conn!.iceGatheringState === 'complete') { + this.stopIce() + } + }) + } + + /** + * Getter method for retrieving the current connection. + * + * @return {RTCPeerConnection|null} The current connection instance. + */ + public get connection(): RTCPeerConnection | null { + return this._conn + } + + /** + * Update connection state and emit state-change event + */ + private setState(state: ConnectionInterface['state']) { + this._state = state + this.events.emit('state-change', state) + } + + /** + * Start ICE candidate exchange when gathering begins + */ + private startIce() { + const listener = ({ candidate }: { candidate: RTCIceCandidate | null }) => { + if (candidate) this.ctx.signaler.addIceCandidate(candidate) + } + if (!this._conn) throw new Error('Connection disappeared') + this._conn.addEventListener('icecandidate', listener) this.iceBin( - this.signaler.addListener((candidate: RTCIceCandidate) => this.connection.addIceCandidate(candidate)), - () => this.connection.removeEventListener('icecandidate', listener) + this.ctx.signaler.addListener((candidate: RTCIceCandidate) => + this._conn?.addIceCandidate(candidate) + ), + () => this._conn?.removeEventListener('icecandidate', listener) ) } - private stopIceListeners() { + /** + * Stop ICE candidate exchange when gathering completes + */ + private stopIce() { this.iceBin.clean() } /** - * Set the signaler for ICE candidate exchange - * Must be called before connection is ready + * Disconnects the current connection and cleans up resources. + * Closes the active connection if it exists, resets the connection instance to null, + * stops the ICE process, and updates the state to 'disconnected'. + * + * @return {void} No return value. */ - setSignaler(signaler: Signaler): void { - this.signaler = signaler; + disconnect(): void { + this._conn?.close() + this._conn = null + this.stopIce() + this.setState('disconnected') } + /** + * Current connection state + */ get state() { - return this._state; - } - - get ready(): Promise { - return this._ready; + return this._state } + /** + * Queue a message for sending when connection is established + * + * @param message - Message to queue (string or ArrayBuffer) + * @param options - Queue options (e.g., expiration time) + */ queueMessage(message: Message, options: QueueMessageOptions = {}): Promise { - return Promise.resolve(undefined); + // TODO: Implement message queuing + return Promise.resolve(undefined) } + /** + * Send a message immediately + * + * @param message - Message to send (string or ArrayBuffer) + * @returns Promise resolving to true if sent successfully + */ sendMessage(message: Message): Promise { - return Promise.resolve(false); + // TODO: Implement message sending via data channel + return Promise.resolve(false) } -} \ No newline at end of file +} diff --git a/src/event-bus.ts b/src/event-bus.ts index 893756a..410374d 100644 --- a/src/event-bus.ts +++ b/src/event-bus.ts @@ -2,7 +2,7 @@ * Type-safe EventBus with event name to payload type mapping */ -type EventHandler = (data: T) => void; +type EventHandler = (data: T) => void /** * EventBus - Type-safe event emitter with inferred event data types @@ -27,64 +27,68 @@ type EventHandler = (data: T) => void; * }); */ export class EventBus> { - private handlers: Map>; + private handlers: Map> - constructor() { - this.handlers = new Map(); - } - - /** - * Subscribe to an event - */ - on(event: K, handler: EventHandler): void { - if (!this.handlers.has(event)) { - this.handlers.set(event, new Set()); + constructor() { + this.handlers = new Map() } - this.handlers.get(event)!.add(handler); - } - /** - * Subscribe to an event once (auto-unsubscribe after first call) - */ - once(event: K, handler: EventHandler): void { - const wrappedHandler = (data: TEvents[K]) => { - handler(data); - this.off(event, wrappedHandler); - }; - this.on(event, wrappedHandler); - } + /** + * Subscribe to an event + * Returns a cleanup function to unsubscribe + */ + on(event: K, handler: EventHandler): () => void { + if (!this.handlers.has(event)) { + this.handlers.set(event, new Set()) + } + this.handlers.get(event)!.add(handler) - /** - * Unsubscribe from an event - */ - off(event: K, handler: EventHandler): void { - const eventHandlers = this.handlers.get(event); - if (eventHandlers) { - eventHandlers.delete(handler); - if (eventHandlers.size === 0) { - this.handlers.delete(event); - } + // Return cleanup function + return () => this.off(event, handler) } - } - /** - * Emit an event with data - */ - emit(event: K, data: TEvents[K]): void { - const eventHandlers = this.handlers.get(event); - if (eventHandlers) { - eventHandlers.forEach(handler => handler(data)); + /** + * Subscribe to an event once (auto-unsubscribe after first call) + */ + once(event: K, handler: EventHandler): void { + const wrappedHandler = (data: TEvents[K]) => { + handler(data) + this.off(event, wrappedHandler) + } + this.on(event, wrappedHandler) } - } - /** - * Remove all handlers for a specific event, or all handlers if no event specified - */ - clear(event?: K): void { - if (event !== undefined) { - this.handlers.delete(event); - } else { - this.handlers.clear(); + /** + * Unsubscribe from an event + */ + off(event: K, handler: EventHandler): void { + const eventHandlers = this.handlers.get(event) + if (eventHandlers) { + eventHandlers.delete(handler) + if (eventHandlers.size === 0) { + this.handlers.delete(event) + } + } } - } -} \ No newline at end of file + + /** + * Emit an event with data + */ + emit(event: K, data: TEvents[K]): void { + const eventHandlers = this.handlers.get(event) + if (eventHandlers) { + eventHandlers.forEach(handler => handler(data)) + } + } + + /** + * Remove all handlers for a specific event, or all handlers if no event specified + */ + clear(event?: K): void { + if (event !== undefined) { + this.handlers.delete(event) + } else { + this.handlers.clear() + } + } +} diff --git a/src/index.ts b/src/index.ts index 3431a8a..8d84bc5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,29 +3,38 @@ * WebRTC peer signaling client */ -export { ConnectionManager } from './connection-manager.js'; -export { EventBus } from './event-bus.js'; -export { RondevuAPI } from './api.js'; -export { RondevuSignaler } from './signaler.js'; -export { WebRTCRondevuConnection } from './connection.js'; -export { createBin } from './bin.js'; +export { EventBus } from './event-bus.js' +export { RondevuAPI } from './api.js' +export { RondevuService } from './rondevu-service.js' +export { RondevuSignaler } from './signaler.js' +export { ServiceHost } from './service-host.js' +export { ServiceClient } from './service-client.js' +export { WebRTCRondevuConnection } from './connection.js' +export { createBin } from './bin.js' // Export types export type { - ConnectionInterface, - QueueMessageOptions, - Message, - ConnectionEvents, - Signaler -} from './types.js'; + ConnectionInterface, + QueueMessageOptions, + Message, + ConnectionEvents, + Signaler, +} from './types.js' export type { - Credentials, - OfferRequest, - Offer, - ServiceRequest, - Service, - IceCandidate -} from './api.js'; + Credentials, + Keypair, + OfferRequest, + Offer, + ServiceRequest, + Service, + IceCandidate, +} from './api.js' -export type { Binnable } from './bin.js'; +export type { Binnable } from './bin.js' + +export type { RondevuServiceOptions, PublishServiceOptions } from './rondevu-service.js' + +export type { ServiceHostOptions, ServiceHostEvents } from './service-host.js' + +export type { ServiceClientOptions, ServiceClientEvents } from './service-client.js' diff --git a/src/noop-signaler.ts b/src/noop-signaler.ts new file mode 100644 index 0000000..b6441fd --- /dev/null +++ b/src/noop-signaler.ts @@ -0,0 +1,35 @@ +import { Signaler } from './types.js' +import { Binnable } from './bin.js' + +/** + * NoOpSignaler - A signaler that does nothing + * Used as a placeholder during connection setup before the real signaler is available + */ +export class NoOpSignaler implements Signaler { + addIceCandidate(_candidate: RTCIceCandidate): void { + // No-op + } + + addListener(_callback: (candidate: RTCIceCandidate) => void): Binnable { + // Return no-op cleanup function + return () => {} + } + + addOfferListener(_callback: (offer: RTCSessionDescriptionInit) => void): Binnable { + // Return no-op cleanup function + return () => {} + } + + addAnswerListener(_callback: (answer: RTCSessionDescriptionInit) => void): Binnable { + // Return no-op cleanup function + return () => {} + } + + async setOffer(_offer: RTCSessionDescriptionInit): Promise { + // No-op + } + + async setAnswer(_answer: RTCSessionDescriptionInit): Promise { + // No-op + } +} diff --git a/src/rondevu-service.ts b/src/rondevu-service.ts new file mode 100644 index 0000000..7ebd055 --- /dev/null +++ b/src/rondevu-service.ts @@ -0,0 +1,168 @@ +import { RondevuAPI, Credentials, Keypair, Service, ServiceRequest } from './api.js' + +export interface RondevuServiceOptions { + apiUrl: string + username: string + keypair?: Keypair + credentials?: Credentials +} + +export interface PublishServiceOptions { + serviceFqn: string + sdp: string + ttl?: number + isPublic?: boolean + metadata?: Record +} + +/** + * RondevuService - High-level service management with automatic signature handling + * + * Provides a simplified API for: + * - Username claiming with Ed25519 signatures + * - Service publishing with automatic signature generation + * - Keypair management + * + * @example + * ```typescript + * // Initialize service (generates keypair automatically) + * const service = new RondevuService({ + * apiUrl: 'https://signal.example.com', + * username: 'myusername', + * }) + * + * await service.initialize() + * + * // Claim username (one time) + * await service.claimUsername() + * + * // Publish a service + * const publishedService = await service.publishService({ + * serviceFqn: 'chat.app@1.0.0', + * sdp: offerSdp, + * ttl: 300000, + * isPublic: true, + * }) + * ``` + */ +export class RondevuService { + private readonly api: RondevuAPI + private readonly username: string + private keypair: Keypair | null = null + private usernameClaimed = false + + constructor(options: RondevuServiceOptions) { + this.username = options.username + this.keypair = options.keypair || null + this.api = new RondevuAPI(options.apiUrl, options.credentials) + } + + /** + * Initialize the service - generates keypair if not provided + * Call this before using other methods + */ + async initialize(): Promise { + if (!this.keypair) { + this.keypair = await RondevuAPI.generateKeypair() + } + + // Register with API if no credentials provided + if (!this.api['credentials']) { + const credentials = await this.api.register() + ;(this.api as any).credentials = credentials + } + } + + /** + * Claim the username with Ed25519 signature + * Should be called once before publishing services + */ + async claimUsername(): Promise { + if (!this.keypair) { + throw new Error('Service not initialized. Call initialize() first.') + } + + // Check if username is already claimed + const check = await this.api.checkUsername(this.username) + if (!check.available) { + // Verify it's claimed by us + if (check.owner === this.keypair.publicKey) { + this.usernameClaimed = true + return + } + throw new Error(`Username "${this.username}" is already claimed by another user`) + } + + // Generate signature for username claim + const message = `claim-username-${this.username}-${Date.now()}` + const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey) + + // Claim the username + await this.api.claimUsername(this.username, this.keypair.publicKey, signature, message) + this.usernameClaimed = true + } + + /** + * Publish a service with automatic signature generation + */ + async publishService(options: PublishServiceOptions): Promise { + if (!this.keypair) { + throw new Error('Service not initialized. Call initialize() first.') + } + + if (!this.usernameClaimed) { + throw new Error( + 'Username not claimed. Call claimUsername() first or the server will reject the service.' + ) + } + + const { serviceFqn, sdp, ttl, isPublic, metadata } = options + + // Generate signature for service publication + const message = `publish-${this.username}-${serviceFqn}-${Date.now()}` + const signature = await RondevuAPI.signMessage(message, this.keypair.privateKey) + + // Create service request + const serviceRequest: ServiceRequest = { + username: this.username, + serviceFqn, + sdp, + signature, + message, + ttl, + isPublic, + metadata, + } + + // Publish to server + return await this.api.publishService(serviceRequest) + } + + /** + * Get the current keypair (for backup/storage) + */ + getKeypair(): Keypair | null { + return this.keypair + } + + /** + * Get the public key + */ + getPublicKey(): string | null { + return this.keypair?.publicKey || null + } + + /** + * Check if username has been claimed + */ + isUsernameClaimed(): boolean { + return this.usernameClaimed + } + + /** + * Access to underlying API for advanced operations + */ + getAPI(): RondevuAPI { + return this.api + } +} diff --git a/src/service-client.ts b/src/service-client.ts new file mode 100644 index 0000000..8eabf96 --- /dev/null +++ b/src/service-client.ts @@ -0,0 +1,244 @@ +import { WebRTCRondevuConnection } from './connection.js' +import { WebRTCContext } from './webrtc-context.js' +import { RondevuService } from './rondevu-service.js' +import { RondevuSignaler } from './signaler.js' +import { EventBus } from './event-bus.js' +import { createBin } from './bin.js' +import { ConnectionInterface } from './types.js' + +export interface ServiceClientOptions { + username: string + serviceFqn: string + rondevuService: RondevuService + autoReconnect?: boolean + reconnectDelay?: number + maxReconnectAttempts?: number +} + +export interface ServiceClientEvents { + connected: ConnectionInterface + disconnected: { reason: string } + reconnecting: { attempt: number; maxAttempts: number } + error: Error +} + +/** + * ServiceClient - Connects to a hosted service + * + * Searches for available service offers and establishes a WebRTC connection. + * Optionally supports automatic reconnection on failure. + * + * @example + * ```typescript + * const rondevuService = new RondevuService({ + * apiUrl: 'https://signal.example.com', + * username: 'client-user', + * }) + * + * await rondevuService.initialize() + * + * const client = new ServiceClient({ + * username: 'host-user', + * serviceFqn: 'chat.app@1.0.0', + * rondevuService, + * autoReconnect: true, + * }) + * + * await client.connect() + * + * client.events.on('connected', (conn) => { + * console.log('Connected to service') + * conn.sendMessage('Hello!') + * }) + * ``` + */ +export class ServiceClient { + private readonly username: string + private readonly serviceFqn: string + private readonly rondevuService: RondevuService + private readonly autoReconnect: boolean + private readonly reconnectDelay: number + private readonly maxReconnectAttempts: number + private connection: WebRTCRondevuConnection | null = null + private reconnectAttempts = 0 + private reconnectTimeout: ReturnType | null = null + private readonly bin = createBin() + private isConnecting = false + + public readonly events = new EventBus() + + constructor(options: ServiceClientOptions) { + this.username = options.username + this.serviceFqn = options.serviceFqn + this.rondevuService = options.rondevuService + this.autoReconnect = options.autoReconnect !== false + this.reconnectDelay = options.reconnectDelay || 2000 + this.maxReconnectAttempts = options.maxReconnectAttempts || 5 + } + + /** + * Connect to the service + */ + async connect(): Promise { + if (this.isConnecting) { + throw new Error('Already connecting') + } + + if (this.connection && this.connection.state === 'connected') { + return this.connection + } + + this.isConnecting = true + + try { + // Search for available services + const services = await this.rondevuService + .getAPI() + .searchServices(this.username, this.serviceFqn) + + if (services.length === 0) { + throw new Error(`No services found for ${this.username}/${this.serviceFqn}`) + } + + // Get the first available service + const service = services[0] + + // Get service details including SDP + const serviceDetails = await this.rondevuService.getAPI().getService(service.uuid) + + // Create WebRTC context with signaler for this offer + const signaler = new RondevuSignaler( + this.rondevuService.getAPI(), + serviceDetails.offerId + ) + const context = new WebRTCContext(signaler) + + // Create connection (answerer role) + const conn = new WebRTCRondevuConnection({ + id: `client-${this.serviceFqn}-${Date.now()}`, + service: this.serviceFqn, + offer: { + type: 'offer', + sdp: serviceDetails.sdp, + }, + context, + }) + + // Wait for answer to be created + await conn.ready + + // Get answer SDP + if (!conn.connection?.localDescription?.sdp) { + throw new Error('Failed to create answer SDP') + } + + const answerSdp = conn.connection.localDescription.sdp + + // Send answer to server + await this.rondevuService.getAPI().answerOffer(serviceDetails.offerId, answerSdp) + + // Track connection + this.connection = conn + this.reconnectAttempts = 0 + + // Listen for state changes + const cleanup = conn.events.on('state-change', state => { + this.handleConnectionStateChange(state) + }) + this.bin(cleanup) + + this.isConnecting = false + + // Emit connected event when actually connected + if (conn.state === 'connected') { + this.events.emit('connected', conn) + } + + return conn + } catch (error) { + this.isConnecting = false + this.events.emit('error', error as Error) + throw error + } + } + + /** + * Disconnect from the service + */ + disconnect(): void { + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout) + this.reconnectTimeout = null + } + + if (this.connection) { + this.connection.disconnect() + this.connection = null + } + + this.bin.clean() + this.reconnectAttempts = 0 + } + + /** + * Get the current connection + */ + getConnection(): WebRTCRondevuConnection | null { + return this.connection + } + + /** + * Check if currently connected + */ + isConnected(): boolean { + return this.connection?.state === 'connected' + } + + /** + * Handle connection state changes + */ + private handleConnectionStateChange(state: ConnectionInterface['state']): void { + if (state === 'connected') { + this.events.emit('connected', this.connection!) + this.reconnectAttempts = 0 + } else if (state === 'disconnected') { + this.events.emit('disconnected', { reason: 'Connection closed' }) + + // Attempt reconnection if enabled + if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) { + this.scheduleReconnect() + } + } + } + + /** + * Schedule a reconnection attempt + */ + private scheduleReconnect(): void { + if (this.reconnectTimeout) { + return + } + + this.reconnectAttempts++ + + this.events.emit('reconnecting', { + attempt: this.reconnectAttempts, + maxAttempts: this.maxReconnectAttempts, + }) + + // Exponential backoff + const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1) + + this.reconnectTimeout = setTimeout(() => { + this.reconnectTimeout = null + this.connect().catch(error => { + this.events.emit('error', error as Error) + + // Schedule next attempt if we haven't exceeded max attempts + if (this.reconnectAttempts < this.maxReconnectAttempts) { + this.scheduleReconnect() + } + }) + }, delay) + } +} diff --git a/src/service-host.ts b/src/service-host.ts new file mode 100644 index 0000000..fb2449f --- /dev/null +++ b/src/service-host.ts @@ -0,0 +1,236 @@ +import { WebRTCRondevuConnection } from './connection.js' +import { WebRTCContext } from './webrtc-context.js' +import { RondevuService } from './rondevu-service.js' +import { RondevuSignaler } from './signaler.js' +import { NoOpSignaler } from './noop-signaler.js' +import { EventBus } from './event-bus.js' +import { createBin } from './bin.js' +import { ConnectionInterface } from './types.js' + +export interface ServiceHostOptions { + service: string + rondevuService: RondevuService + maxPeers?: number + ttl?: number + isPublic?: boolean + metadata?: Record +} + +export interface ServiceHostEvents { + connection: ConnectionInterface + 'connection-closed': { connectionId: string; reason: string } + error: Error +} + +/** + * ServiceHost - Manages a pool of WebRTC offers for a service + * + * Maintains up to maxPeers concurrent offers, automatically replacing + * them when connections are established or expire. + * + * @example + * ```typescript + * const rondevuService = new RondevuService({ + * apiUrl: 'https://signal.example.com', + * username: 'myusername', + * }) + * + * await rondevuService.initialize() + * await rondevuService.claimUsername() + * + * const host = new ServiceHost({ + * service: 'chat.app@1.0.0', + * rondevuService, + * maxPeers: 5, + * }) + * + * await host.start() + * + * host.events.on('connection', (conn) => { + * console.log('New connection:', conn.id) + * conn.events.on('message', (msg) => { + * console.log('Message:', msg) + * }) + * }) + * ``` + */ +export class ServiceHost { + private connections = new Map() + private readonly service: string + private readonly rondevuService: RondevuService + private readonly maxPeers: number + private readonly ttl: number + private readonly isPublic: boolean + private readonly metadata?: Record + private readonly bin = createBin() + private isStarted = false + + public readonly events = new EventBus() + + constructor(options: ServiceHostOptions) { + this.service = options.service + this.rondevuService = options.rondevuService + this.maxPeers = options.maxPeers || 20 + this.ttl = options.ttl || 300000 + this.isPublic = options.isPublic !== false + this.metadata = options.metadata + } + + /** + * Start hosting the service - creates initial pool of offers + */ + async start(): Promise { + if (this.isStarted) { + throw new Error('ServiceHost already started') + } + + this.isStarted = true + await this.fillOfferPool() + } + + /** + * Stop hosting - closes all connections and cleans up + */ + stop(): void { + this.isStarted = false + this.connections.forEach(conn => conn.disconnect()) + this.connections.clear() + this.bin.clean() + } + + /** + * Get current number of active connections + */ + getConnectionCount(): number { + return Array.from(this.connections.values()).filter(conn => conn.state === 'connected') + .length + } + + /** + * Get current number of pending offers + */ + getPendingOfferCount(): number { + return Array.from(this.connections.values()).filter(conn => conn.state === 'connecting') + .length + } + + /** + * Fill the offer pool up to maxPeers + */ + private async fillOfferPool(): Promise { + const currentOffers = this.connections.size + const needed = this.maxPeers - currentOffers + + if (needed <= 0) { + return + } + + // Create multiple offers in parallel + const offerPromises: Promise[] = [] + for (let i = 0; i < needed; i++) { + offerPromises.push(this.createOffer()) + } + + await Promise.allSettled(offerPromises) + } + + /** + * Create a single offer and publish it + */ + private async createOffer(): Promise { + try { + // Create temporary context with NoOp signaler + const tempContext = new WebRTCContext(new NoOpSignaler()) + + // Create connection (offerer role) + const conn = new WebRTCRondevuConnection({ + id: `${this.service}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`, + service: this.service, + offer: null, + context: tempContext, + }) + + // Wait for offer to be created + await conn.ready + + // Get offer SDP + if (!conn.connection?.localDescription?.sdp) { + throw new Error('Failed to create offer SDP') + } + + const sdp = conn.connection.localDescription.sdp + + // Publish service offer + const service = await this.rondevuService.publishService({ + serviceFqn: this.service, + sdp, + ttl: this.ttl, + isPublic: this.isPublic, + metadata: this.metadata, + }) + + // Replace with real signaler now that we have offerId + const realSignaler = new RondevuSignaler(this.rondevuService.getAPI(), service.offerId) + ;(tempContext as any).signaler = realSignaler + + // Track connection + this.connections.set(conn.id, conn) + + // Listen for state changes + const cleanup = conn.events.on('state-change', state => { + this.handleConnectionStateChange(conn, state) + }) + + this.bin(cleanup) + } catch (error) { + this.events.emit('error', error as Error) + } + } + + /** + * Handle connection state changes + */ + private handleConnectionStateChange( + conn: WebRTCRondevuConnection, + state: ConnectionInterface['state'] + ): void { + if (state === 'connected') { + // Connection established - emit event + this.events.emit('connection', conn) + + // Create new offer to replace this one + if (this.isStarted) { + this.fillOfferPool().catch(error => { + this.events.emit('error', error as Error) + }) + } + } else if (state === 'disconnected') { + // Connection closed - remove and create new offer + this.connections.delete(conn.id) + this.events.emit('connection-closed', { + connectionId: conn.id, + reason: state, + }) + + if (this.isStarted) { + this.fillOfferPool().catch(error => { + this.events.emit('error', error as Error) + }) + } + } + } + + /** + * Get all active connections + */ + getConnections(): WebRTCRondevuConnection[] { + return Array.from(this.connections.values()) + } + + /** + * Get a specific connection by ID + */ + getConnection(connectionId: string): WebRTCRondevuConnection | undefined { + return this.connections.get(connectionId) + } +} diff --git a/src/signaler.ts b/src/signaler.ts index 997ab62..0559673 100644 --- a/src/signaler.ts +++ b/src/signaler.ts @@ -1,6 +1,6 @@ -import {Signaler} from "./types"; -import {Binnable} from "./bin"; -import {RondevuAPI} from "./api"; +import { Signaler } from './types.js' +import { Binnable } from './bin.js' +import { RondevuAPI } from './api.js' /** * RondevuSignaler - Handles ICE candidate exchange via Rondevu API @@ -12,18 +12,31 @@ export class RondevuSignaler implements Signaler { private offerId: string ) {} + addOfferListener(callback: (offer: RTCSessionDescriptionInit) => void): Binnable { + throw new Error('Method not implemented.') + } + addAnswerListener(callback: (answer: RTCSessionDescriptionInit) => void): Binnable { + throw new Error('Method not implemented.') + } + setOffer(offer: RTCSessionDescriptionInit): Promise { + throw new Error('Method not implemented.') + } + setAnswer(answer: RTCSessionDescriptionInit): Promise { + throw new Error('Method not implemented.') + } + /** - * Send local ICE candidate to signaling server + * Send a local ICE candidate to signaling server */ async addIceCandidate(candidate: RTCIceCandidate): Promise { - const candidateData = candidate.toJSON(); + const candidateData = candidate.toJSON() // Skip empty candidates if (!candidateData.candidate || candidateData.candidate === '') { - return; + return } - await this.api.addIceCandidates(this.offerId, [candidateData]); + await this.api.addIceCandidates(this.offerId, [candidateData]) } /** @@ -31,52 +44,61 @@ export class RondevuSignaler implements Signaler { * Returns cleanup function to stop polling */ addListener(callback: (candidate: RTCIceCandidate) => void): Binnable { - let lastTimestamp = 0; - let polling = true; + let lastTimestamp = 0 + let polling = true const poll = async () => { while (polling) { try { - const candidates = await this.api.getIceCandidates(this.offerId, lastTimestamp); + const candidates = await this.api.getIceCandidates(this.offerId, lastTimestamp) // Process each candidate for (const item of candidates) { - if (item.candidate && item.candidate.candidate && item.candidate.candidate !== '') { + if ( + item.candidate && + item.candidate.candidate && + item.candidate.candidate !== '' + ) { try { - const rtcCandidate = new RTCIceCandidate(item.candidate); - callback(rtcCandidate); - lastTimestamp = item.createdAt; + const rtcCandidate = new RTCIceCandidate(item.candidate) + callback(rtcCandidate) + lastTimestamp = item.createdAt } catch (err) { - console.warn('Failed to process ICE candidate:', err); - lastTimestamp = item.createdAt; + console.warn('Failed to process ICE candidate:', err) + lastTimestamp = item.createdAt } } else { - lastTimestamp = item.createdAt; + lastTimestamp = item.createdAt } } } catch (err) { // If offer not found or expired, stop polling - if (err instanceof Error && (err.message.includes('404') || err.message.includes('410'))) { - console.warn('Offer not found or expired, stopping ICE polling'); - polling = false; - break; + if ( + err instanceof Error && + (err.message.includes('404') || err.message.includes('410')) + ) { + console.warn('Offer not found or expired, stopping ICE polling') + polling = false + break } - console.error('Error polling for ICE candidates:', err); + console.error('Error polling for ICE candidates:', err) } // Poll every second if (polling) { - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise(resolve => setTimeout(resolve, 1000)) } } - }; + } - // Start polling in background - poll(); + // Start polling in the background + poll().then(() => { + console.log('ICE polling started') + }) // Return cleanup function return () => { - polling = false; - }; + polling = false + } } -} \ No newline at end of file +} diff --git a/src/types.ts b/src/types.ts index 66d2511..2691992 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,34 +1,42 @@ /** * Core connection types */ -import {EventBus} from "./event-bus"; -import {Binnable} from "./bin"; +import { EventBus } from './event-bus.js' +import { Binnable } from './bin.js' -export type Message = string | ArrayBuffer; +export type Message = string | ArrayBuffer export interface QueueMessageOptions { - expiresAt?: number; + expiresAt?: number } export interface ConnectionEvents { 'state-change': ConnectionInterface['state'] - 'message': Message; + message: Message } -export interface ConnectionInterface { - id: string; - host: string; - service: string; - state: 'connected' | 'disconnected' | 'connecting'; - lastActive: number; - expiresAt?: number; - events: EventBus; +export const ConnectionStates = ['connected', 'disconnected', 'connecting'] as const - queueMessage(message: Message, options?: QueueMessageOptions): Promise; - sendMessage(message: Message): Promise; +export const isConnectionState = (state: string): state is (typeof ConnectionStates)[number] => + ConnectionStates.includes(state as any) + +export interface ConnectionInterface { + id: string + service: string + state: (typeof ConnectionStates)[number] + lastActive: number + expiresAt?: number + events: EventBus + + queueMessage(message: Message, options?: QueueMessageOptions): Promise + sendMessage(message: Message): Promise } export interface Signaler { - addIceCandidate(candidate: RTCIceCandidate): Promise | void; - addListener(callback: (candidate: RTCIceCandidate) => void): Binnable; -} \ No newline at end of file + addIceCandidate(candidate: RTCIceCandidate): Promise | void + addListener(callback: (candidate: RTCIceCandidate) => void): Binnable + addOfferListener(callback: (offer: RTCSessionDescriptionInit) => void): Binnable + addAnswerListener(callback: (answer: RTCSessionDescriptionInit) => void): Binnable + setOffer(offer: RTCSessionDescriptionInit): Promise + setAnswer(answer: RTCSessionDescriptionInit): Promise +} diff --git a/src/webrtc-context.ts b/src/webrtc-context.ts new file mode 100644 index 0000000..d16687f --- /dev/null +++ b/src/webrtc-context.ts @@ -0,0 +1,35 @@ +import { Signaler } from './types' + +export class WebRTCContext { + constructor(public readonly signaler: Signaler) {} + + createPeerConnection(): RTCPeerConnection { + return new RTCPeerConnection({ + iceServers: [ + { + urls: 'stun:stun.relay.metered.ca:80', + }, + { + urls: 'turn:standard.relay.metered.ca:80', + username: 'c53a9c971da5e6f3bc959d8d', + credential: 'QaccPqtPPaxyokXp', + }, + { + urls: 'turn:standard.relay.metered.ca:80?transport=tcp', + username: 'c53a9c971da5e6f3bc959d8d', + credential: 'QaccPqtPPaxyokXp', + }, + { + urls: 'turn:standard.relay.metered.ca:443', + username: 'c53a9c971da5e6f3bc959d8d', + credential: 'QaccPqtPPaxyokXp', + }, + { + urls: 'turns:standard.relay.metered.ca:443?transport=tcp', + username: 'c53a9c971da5e6f3bc959d8d', + credential: 'QaccPqtPPaxyokXp', + }, + ], + }) + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..62c9f36 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + root: 'demo', + server: { + port: 3000, + open: true, + allowedHosts: ['241284034b20.ngrok-free.app'] + } +});