diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90e7a16..7499705 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,8 +89,46 @@ jobs: - name: Test run: pnpm test + test-ws-signaling-proxy: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./packages/ws-signaling-proxy + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4.1.0 + with: + version: 10.7.1 + + - name: Set node LTS + uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: pnpm + + - name: Install + run: pnpm install + + - name: Build + run: pnpm build + + - name: Lint + run: pnpm lint + + - name: Format + run: pnpm format:check + + - name: Typecheck + run: pnpm typecheck + release: - needs: [test-sdk, test-proxy] + needs: [test-sdk, test-proxy, test-ws-signaling-proxy] runs-on: ubuntu-latest defaults: run: diff --git a/packages/ws-signaling-proxy/.env.example b/packages/ws-signaling-proxy/.env.example new file mode 100644 index 0000000..3b03c08 --- /dev/null +++ b/packages/ws-signaling-proxy/.env.example @@ -0,0 +1,8 @@ +# Required: Your Decart API key +DECART_API_KEY=sk-your-api-key-here + +# Optional: Decart WebSocket base URL (default: wss://api3.decart.ai) +DECART_BASE_URL=wss://api3.decart.ai + +# Optional: Port for the proxy server (default: 8080) +PORT=8080 diff --git a/packages/ws-signaling-proxy/README.md b/packages/ws-signaling-proxy/README.md new file mode 100644 index 0000000..e786586 --- /dev/null +++ b/packages/ws-signaling-proxy/README.md @@ -0,0 +1,55 @@ +# ws-signaling-proxy + +Reference implementation of a WebSocket signaling proxy for Decart's realtime models. Sits between end-user clients and Decart's API, forwarding all signaling messages (SDP offers/answers, ICE candidates, prompts, etc.) while keeping your API key server-side. + +WebRTC media flows directly between the client and Decart — the proxy only handles the control plane. + +``` + signaling signaling + Client <----WebSocket----> Proxy <----WebSocket----> Decart + | + Client <----------------WebRTC (direct)--------------> Decart + audio/video +``` + +## Quick start + +```bash +cp .env.example .env # add your DECART_API_KEY +pnpm install +pnpm dev # starts proxy on ws://localhost:8080 +``` + +Clients connect to: + +``` +ws://localhost:8080/v1/stream?model=lucy_2_rt +``` + +## Environment variables + +| Variable | Required | Default | Description | +|---|---|---|---| +| `DECART_API_KEY` | Yes | — | Your Decart API key | +| `DECART_BASE_URL` | No | `wss://api3.decart.ai` | Decart WebSocket endpoint | +| `PORT` | No | `8080` | Proxy listen port | + +## Scripts + +| Command | Description | +|---|---| +| `pnpm dev` | Start with hot reload | +| `pnpm build` | Compile TypeScript to `dist/` | +| `pnpm start` | Run compiled output | +| `pnpm test:e2e` | Run e2e test (requires `DECART_API_KEY`) | + +## How it works + +Each client WebSocket connection creates a `ProxySession` that: + +1. Opens an upstream connection to Decart with the server's API key +2. Forwards all messages bidirectionally +3. Buffers client messages until the upstream connection is ready +4. Propagates close events in both directions + +The proxy does not inspect or modify message contents — it's a transparent pipe with structured logging. diff --git a/packages/ws-signaling-proxy/package.json b/packages/ws-signaling-proxy/package.json new file mode 100644 index 0000000..bb2a5ea --- /dev/null +++ b/packages/ws-signaling-proxy/package.json @@ -0,0 +1,27 @@ +{ + "name": "ws-signaling-proxy", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx --watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit", + "lint": "biome check --error-on-warnings", + "format": "biome check --write --unsafe", + "format:check": "biome check", + "test:e2e": "node --test --import tsx test/e2e.ts" + }, + "dependencies": { + "ws": "^8.18.0" + }, + "devDependencies": { + "@biomejs/biome": "2.3.8", + "@types/node": "^22", + "@types/ws": "^8", + "tsx": "^4", + "typescript": "^5" + }, + "packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a" +} diff --git a/packages/ws-signaling-proxy/src/index.ts b/packages/ws-signaling-proxy/src/index.ts new file mode 100644 index 0000000..e125312 --- /dev/null +++ b/packages/ws-signaling-proxy/src/index.ts @@ -0,0 +1,52 @@ +import { createServer } from "node:http"; +import { type WebSocket, WebSocketServer } from "ws"; +import { ProxySession } from "./proxy-session.js"; + +const DECART_API_KEY = process.env.DECART_API_KEY; +const DECART_BASE_URL = process.env.DECART_BASE_URL ?? "wss://api3.decart.ai"; +const PORT = Number(process.env.PORT ?? 8080); + +if (!DECART_API_KEY) { + console.error("DECART_API_KEY is required"); + process.exit(1); +} + +const server = createServer((_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("ws-signaling-proxy"); +}); + +const wss = new WebSocketServer({ server }); + +wss.on("connection", (clientWs: WebSocket, req) => { + // Accept Decart-style URLs: /v1/stream?api_key=...&model=lucy_2_rt + // The proxy ignores api_key from the client and uses its own. + const url = new URL(req.url ?? "/", `http://${req.headers.host}`); + const model = url.searchParams.get("model") ?? "lucy_2_rt"; + + console.log(`[proxy] client connected from ${req.url} (model=${model})`); + + const session = new ProxySession(clientWs, { + decartApiKey: DECART_API_KEY, + model, + decartBaseUrl: DECART_BASE_URL, + }); + + session.start(); +}); + +server.listen(PORT, () => { + console.log(`[proxy] listening on ws://localhost:${PORT}`); + console.log(`[proxy] connect with: ws://localhost:${PORT}/?model=lucy_2_rt`); +}); + +const shutdown = () => { + console.log("\n[proxy] shutting down..."); + for (const client of wss.clients) { + client.close(1001, "server shutting down"); + } + server.close(() => process.exit(0)); +}; + +process.on("SIGINT", shutdown); +process.on("SIGTERM", shutdown); diff --git a/packages/ws-signaling-proxy/src/proxy-session.ts b/packages/ws-signaling-proxy/src/proxy-session.ts new file mode 100644 index 0000000..c88de83 --- /dev/null +++ b/packages/ws-signaling-proxy/src/proxy-session.ts @@ -0,0 +1,158 @@ +import WebSocket from "ws"; +import type { IncomingMessage, OutgoingMessage } from "./types.js"; + +export class ProxySession { + private upstream: WebSocket | null = null; + private _sessionId: string | null = null; + private closed = false; + private upstreamReady = false; + private pendingMessages: { data: WebSocket.RawData; isBinary: boolean }[] = []; + + constructor( + private clientWs: WebSocket, + private config: { + decartApiKey: string; + model: string; + decartBaseUrl: string; + }, + ) {} + + get sessionId() { + return this._sessionId; + } + + start() { + const url = `${this.config.decartBaseUrl}/v1/stream?api_key=${this.config.decartApiKey}&model=${this.config.model}`; + this.upstream = new WebSocket(url); + + this.upstream.on("open", () => { + console.log(`[proxy] upstream connected (model=${this.config.model})`); + this.upstreamReady = true; + for (const { data, isBinary } of this.pendingMessages) { + this.upstream?.send(data, { binary: isBinary }); + this.logIncomingMessage(data); + } + this.pendingMessages = []; + }); + + this.upstream.on("error", (err) => { + console.error(`[proxy] upstream error: ${err.message}`); + this.close(1011, "upstream connection error"); + }); + + // Client → Decart (buffer until upstream is open, preserve text/binary frame type) + this.clientWs.on("message", (data, isBinary) => { + if (this.upstreamReady && this.upstream?.readyState === WebSocket.OPEN) { + this.upstream.send(data, { binary: isBinary }); + this.logIncomingMessage(data); + } else { + this.pendingMessages.push({ data, isBinary }); + } + }); + + // Decart → Client (preserve text/binary frame type) + this.upstream.on("message", (data, isBinary) => { + if (this.clientWs.readyState === WebSocket.OPEN) { + this.clientWs.send(data, { binary: isBinary }); + this.logOutgoingMessage(data); + } + }); + + // Close propagation + this.clientWs.on("close", (code, reason) => { + console.log(`[${this._sessionId ?? "?"}] client disconnected (code=${code})`); + this.close(code, reason.toString()); + }); + + this.upstream.on("close", (code, reason) => { + const reasonStr = reason.toString(); + console.log( + `[${this._sessionId ?? "?"}] upstream disconnected (code=${code}${reasonStr ? `, reason=${reasonStr}` : ""})`, + ); + this.close(code, reasonStr); + }); + } + + close(code?: number, reason?: string) { + if (this.closed) return; + this.closed = true; + + const safeCode = this.sanitizeCloseCode(code); + if (this.upstream && this.upstream.readyState !== WebSocket.CLOSED) { + this.upstream.close(safeCode, reason); + } + if (this.clientWs.readyState !== WebSocket.CLOSED) { + this.clientWs.close(safeCode, reason); + } + } + + private sanitizeCloseCode(code?: number): number { + if (code !== undefined && (code === 1000 || code >= 3000)) { + return code; + } + return 1000; + } + + private logIncomingMessage(data: WebSocket.RawData) { + try { + const msg = JSON.parse(data.toString()) as IncomingMessage; + const id = this._sessionId ?? "?"; + switch (msg.type) { + case "prompt": + console.log(`[${id}] → prompt: ${msg.prompt.slice(0, 80)}`); + break; + case "set_image": + console.log(`[${id}] → set_image (has_prompt=${Boolean(msg.prompt)})`); + break; + case "offer": + console.log(`[${id}] → offer`); + break; + case "ice-candidate": + break; // too noisy + } + } catch { + // non-JSON — forwarded as-is + } + } + + private logOutgoingMessage(data: WebSocket.RawData) { + try { + const msg = JSON.parse(data.toString()) as OutgoingMessage; + if (msg.type === "session_id") { + this._sessionId = msg.session_id; + } + const id = this._sessionId ?? "?"; + switch (msg.type) { + case "session_id": + console.log(`[${id}] session started (server=${msg.server_ip}:${msg.server_port})`); + break; + case "prompt_ack": + console.log(`[${id}] ← prompt_ack (success=${msg.success})`); + break; + case "set_image_ack": + console.log(`[${id}] ← set_image_ack (success=${msg.success})`); + break; + case "generation_started": + console.log(`[${id}] ← generation started`); + break; + case "generation_ended": + console.log(`[${id}] ← ended: ${msg.reason} (${msg.seconds}s)`); + break; + case "error": + console.error(`[${id}] ← error: ${msg.error}`); + break; + case "ice-restart": + console.log(`[${id}] ← ice-restart`); + break; + case "answer": + console.log(`[${id}] ← answer`); + break; + case "generation_tick": + case "ice-candidate": + break; // too noisy + } + } catch { + // non-JSON — forwarded as-is + } + } +} diff --git a/packages/ws-signaling-proxy/src/types.ts b/packages/ws-signaling-proxy/src/types.ts new file mode 100644 index 0000000..841115f --- /dev/null +++ b/packages/ws-signaling-proxy/src/types.ts @@ -0,0 +1,102 @@ +export interface OfferMessage { + type: "offer"; + sdp: string; +} + +export interface IceCandidateMessage { + type: "ice-candidate"; + candidate: { + candidate: string; + sdpMLineIndex: number; + sdpMid: string; + usernameFragment?: string; + } | null; +} + +export interface PromptMessage { + type: "prompt"; + prompt: string; + enhance_prompt?: boolean; +} + +export interface SetImageMessage { + type: "set_image"; + image_data: string | null; + prompt?: string; + enhance_prompt?: boolean; +} + +export type IncomingMessage = OfferMessage | IceCandidateMessage | PromptMessage | SetImageMessage; + +export interface AnswerMessage { + type: "answer"; + sdp: string; +} + +export interface SessionIdMessage { + type: "session_id"; + session_id: string; + server_ip: string; + server_port: number; +} + +export interface PromptAckMessage { + type: "prompt_ack"; + prompt: string; + success: boolean; + error: string | null; +} + +export interface SetImageAckMessage { + type: "set_image_ack"; + success: boolean; + error: string | null; +} + +export interface GenerationStartedMessage { + type: "generation_started"; +} + +export interface GenerationTickMessage { + type: "generation_tick"; + seconds: number; +} + +export type GenerationEndedReason = + | "disconnect" + | "timeout" + | "moderation_violation" + | "error" + | "insufficient_credits"; + +export interface GenerationEndedMessage { + type: "generation_ended"; + seconds: number; + reason: GenerationEndedReason; +} + +export interface ErrorMessage { + type: "error"; + error: string; +} + +export interface IceRestartMessage { + type: "ice-restart"; + turn_config: { + username: string; + credential: string; + server_url: string; + }; +} + +export type OutgoingMessage = + | AnswerMessage + | IceCandidateMessage + | SessionIdMessage + | PromptAckMessage + | SetImageAckMessage + | GenerationStartedMessage + | GenerationTickMessage + | GenerationEndedMessage + | ErrorMessage + | IceRestartMessage; diff --git a/packages/ws-signaling-proxy/test/e2e.ts b/packages/ws-signaling-proxy/test/e2e.ts new file mode 100644 index 0000000..610e6fc --- /dev/null +++ b/packages/ws-signaling-proxy/test/e2e.ts @@ -0,0 +1,94 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { test } from "node:test"; +import WebSocket from "ws"; + +const DECART_API_KEY = process.env.DECART_API_KEY; + +// 512x512 black PNG +const TEST_IMAGE = + "iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAIAAAB7GkOtAAADEUlEQVR4nO3BgQAAAADDoPlTX+EAVQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMBvArQAAVkUTe8AAAAASUVORK5CYII="; + +test("e2e: signaling flow through proxy", { timeout: 30_000 }, async (t) => { + if (!DECART_API_KEY) { + throw new Error("DECART_API_KEY is required"); + } + + const port = 10000 + Math.floor(Math.random() * 50000); + + // Start proxy as child process + const server = spawn("node", ["--import", "tsx", "src/index.ts"], { + env: { + ...process.env, + PORT: String(port), + DECART_API_KEY: DECART_API_KEY as string, + }, + stdio: ["ignore", "pipe", "pipe"], + }); + server.stderr?.on("data", (d: Buffer) => process.stderr.write(d)); + t.after(() => server.kill()); + + await new Promise((resolve, reject) => { + server.stdout?.on("data", (d: Buffer) => { + process.stdout.write(d); + if (d.toString().includes("listening on")) resolve(); + }); + server.on("error", reject); + }); + + // Connect client + const ws = new WebSocket(`ws://localhost:${port}/v1/stream?model=lucy_2_rt`); + await new Promise((r, e) => { + ws.on("open", r); + ws.on("error", e); + }); + t.after(() => ws.close()); + + // Collect messages, resolve waiters by type + // biome-ignore lint/suspicious/noExplicitAny: test code + const received: any[] = []; + // biome-ignore lint/suspicious/noExplicitAny: test code + const waiters = new Map void>(); + ws.on("message", (raw) => { + const msg = JSON.parse(raw.toString()); + received.push(msg); + waiters.get(msg.type)?.(msg); + waiters.delete(msg.type); + }); + + // biome-ignore lint/suspicious/noExplicitAny: test code + function waitFor(type: string, ms = 15_000): Promise { + const found = received.find((m) => m.type === type); + if (found) return Promise.resolve(found); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`Timeout waiting for "${type}"`)), ms); + waiters.set(type, (msg) => { + clearTimeout(timer); + resolve(msg); + }); + }); + } + + // set_image → set_image_ack + ws.send( + JSON.stringify({ + type: "set_image", + image_data: TEST_IMAGE, + prompt: "Test prompt", + enhance_prompt: false, + }), + ); + assert.equal((await waitFor("set_image_ack")).success, true); + console.log(" ✓ set_image_ack"); + + // prompt → prompt_ack + ws.send( + JSON.stringify({ + type: "prompt", + prompt: "Cyberpunk neon city", + enhance_prompt: true, + }), + ); + assert.equal((await waitFor("prompt_ack")).success, true); + console.log(" ✓ prompt_ack"); +}); diff --git a/packages/ws-signaling-proxy/tsconfig.json b/packages/ws-signaling-proxy/tsconfig.json new file mode 100644 index 0000000..0cfaf4e --- /dev/null +++ b/packages/ws-signaling-proxy/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "sourceMap": true, + "isolatedModules": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdc5764..2758195 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -342,6 +342,28 @@ importers: specifier: ^4.0.18 version: 4.0.18(@types/node@22.17.1)(@vitest/browser-playwright@4.0.18)(jiti@2.5.1)(msw@2.11.3(@types/node@22.17.1)(typescript@5.9.2))(tsx@4.21.0)(yaml@2.8.1) + packages/ws-signaling-proxy: + dependencies: + ws: + specifier: ^8.18.0 + version: 8.19.0 + devDependencies: + '@biomejs/biome': + specifier: 2.3.8 + version: 2.3.8 + '@types/node': + specifier: ^22 + version: 22.17.1 + '@types/ws': + specifier: ^8 + version: 8.18.1 + tsx: + specifier: ^4 + version: 4.21.0 + typescript: + specifier: ^5 + version: 5.9.3 + packages: '@babel/code-frame@7.27.1': @@ -1513,8 +1535,8 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} - '@oxc-project/types@0.113.0': - resolution: {integrity: sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==} + '@oxc-project/types@0.114.0': + resolution: {integrity: sha512-//nBfbzHQHvJs8oFIjv6coZ6uxQ4alLfiPe6D5vit6c4pmxATHHlVwgB1k+Hv4yoAMyncdxgRBF5K4BYWUCzvA==} '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -1522,79 +1544,79 @@ packages: '@quansync/fs@0.1.4': resolution: {integrity: sha512-vy/41FCdnIalPTQCb2Wl0ic1caMdzGus4ktDp+gpZesQNydXcx8nhh8qB3qMPbGkictOTaXgXEUUfQEm8DQYoA==} - '@rolldown/binding-android-arm64@1.0.0-rc.4': - resolution: {integrity: sha512-vRq9f4NzvbdZavhQbjkJBx7rRebDKYR9zHfO/Wg486+I7bSecdUapzCm5cyXoK+LHokTxgSq7A5baAXUZkIz0w==} + '@rolldown/binding-android-arm64@1.0.0-rc.5': + resolution: {integrity: sha512-zCEmUrt1bggwgBgeKLxNj217J1OrChrp3jJt24VK9jAharSTeVaHODNL+LpcQVhRz+FktYWfT9cjo5oZ99ZLpg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.4': - resolution: {integrity: sha512-kFgEvkWLqt3YCgKB5re9RlIrx9bRsvyVUnaTakEpOPuLGzLpLapYxE9BufJNvPg8GjT6mB1alN4yN1NjzoeM8Q==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.5': + resolution: {integrity: sha512-ZP9xb9lPAex36pvkNWCjSEJW/Gfdm9I3ssiqOFLmpZ/vosPXgpoGxCmh+dX1Qs+/bWQE6toNFXWWL8vYoKoK9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.4': - resolution: {integrity: sha512-JXmaOJGsL/+rsmMfutcDjxWM2fTaVgCHGoXS7nE8Z3c9NAYjGqHvXrAhMUZvMpHS/k7Mg+X7n/MVKb7NYWKKww==} + '@rolldown/binding-darwin-x64@1.0.0-rc.5': + resolution: {integrity: sha512-7IdrPunf6dp9mywMgTOKMMGDnMHQ6+h5gRl6LW8rhD8WK2kXX0IwzcM5Zc0B5J7xQs8QWOlKjv8BJsU/1CD3pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.4': - resolution: {integrity: sha512-ep3Catd6sPnHTM0P4hNEvIv5arnDvk01PfyJIJ+J3wVCG1eEaPo09tvFqdtcaTrkwQy0VWR24uz+cb4IsK53Qw==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.5': + resolution: {integrity: sha512-o/JCk+dL0IN68EBhZ4DqfsfvxPfMeoM6cJtxORC1YYoxGHZyth2Kb2maXDb4oddw2wu8iIbnYXYPEzBtAF5CAg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.4': - resolution: {integrity: sha512-LwA5ayKIpnsgXJEwWc3h8wPiS33NMIHd9BhsV92T8VetVAbGe2qXlJwNVDGHN5cOQ22R9uYvbrQir2AB+ntT2w==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.5': + resolution: {integrity: sha512-IIBwTtA6VwxQLcEgq2mfrUgam7VvPZjhd/jxmeS1npM+edWsrrpRLHUdze+sk4rhb8/xpP3flemgcZXXUW6ukw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.4': - resolution: {integrity: sha512-AC1WsGdlV1MtGay/OQ4J9T7GRadVnpYRzTcygV1hKnypbYN20Yh4t6O1Sa2qRBMqv1etulUknqXjc3CTIsBu6A==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.5': + resolution: {integrity: sha512-KSol1De1spMZL+Xg7K5IBWXIvRWv7+pveaxFWXpezezAG7CS6ojzRjtCGCiLxQricutTAi/LkNWKMsd2wNhMKQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.4': - resolution: {integrity: sha512-lU+6rgXXViO61B4EudxtVMXSOfiZONR29Sys5VGSetUY7X8mg9FCKIIjcPPj8xNDeYzKl+H8F/qSKOBVFJChCQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.5': + resolution: {integrity: sha512-WFljyDkxtXRlWxMjxeegf7xMYXxUr8u7JdXlOEWKYgDqEgxUnSEsVDxBiNWQ1D5kQKwf8Wo4sVKEYPRhCdsjwA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.4': - resolution: {integrity: sha512-DZaN1f0PGp/bSvKhtw50pPsnln4T13ycDq1FrDWRiHmWt1JeW+UtYg9touPFf8yt993p8tS2QjybpzKNTxYEwg==} + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.5': + resolution: {integrity: sha512-CUlplTujmbDWp2gamvrqVKi2Or8lmngXT1WxsizJfts7JrvfGhZObciaY/+CbdbS9qNnskvwMZNEhTPrn7b+WA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.4': - resolution: {integrity: sha512-RnGxwZLN7fhMMAItnD6dZ7lvy+TI7ba+2V54UF4dhaWa/p8I/ys1E73KO6HmPmgz92ZkfD8TXS1IMV8+uhbR9g==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.5': + resolution: {integrity: sha512-wdf7g9NbVZCeAo2iGhsjJb7I8ZFfs6X8bumfrWg82VK+8P6AlLXwk48a1ASiJQDTS7Svq2xVzZg3sGO2aXpHRA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.4': - resolution: {integrity: sha512-6lcI79+X8klGiGd8yHuTgQRjuuJYNggmEml+RsyN596P23l/zf9FVmJ7K0KVKkFAeYEdg0iMUKyIxiV5vebDNQ==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.5': + resolution: {integrity: sha512-0CWY7ubu12nhzz+tkpHjoG3IRSTlWYe0wrfJRf4qqjqQSGtAYgoL9kwzdvlhaFdZ5ffVeyYw9qLsChcjUMEloQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.4': - resolution: {integrity: sha512-wz7ohsKCAIWy91blZ/1FlpPdqrsm1xpcEOQVveWoL6+aSPKL4VUcoYmmzuLTssyZxRpEwzuIxL/GDsvpjaBtOw==} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.5': + resolution: {integrity: sha512-LztXnGzv6t2u830mnZrFLRVqT/DPJ9DL4ZTz/y93rqUVkeHjMMYIYaFj+BUthiYxbVH9dH0SZYufETspKY/NhA==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.4': - resolution: {integrity: sha512-cfiMrfuWCIgsFmcVG0IPuO6qTRHvF7NuG3wngX1RZzc6dU8FuBFb+J3MIR5WrdTNozlumfgL4cvz+R4ozBCvsQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.5': + resolution: {integrity: sha512-jUct1XVeGtyjqJXEAfvdFa8xoigYZ2rge7nYEm70ppQxpfH9ze2fbIrpHmP2tNM2vL/F6Dd0CpXhpjPbC6bSxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.4': - resolution: {integrity: sha512-p6UeR9y7ht82AH57qwGuFYn69S6CZ7LLKdCKy/8T3zS9VTrJei2/CGsTUV45Da4Z9Rbhc7G4gyWQ/Ioamqn09g==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.5': + resolution: {integrity: sha512-VQ8F9ld5gw29epjnVGdrx8ugiLTe8BMqmhDYy7nGbdeDo4HAt4bgdZvLbViEhg7DZyHLpiEUlO5/jPSUrIuxRQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1605,8 +1627,8 @@ packages: '@rolldown/pluginutils@1.0.0-beta.40': resolution: {integrity: sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w==} - '@rolldown/pluginutils@1.0.0-rc.4': - resolution: {integrity: sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==} + '@rolldown/pluginutils@1.0.0-rc.5': + resolution: {integrity: sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw==} '@rollup/rollup-android-arm-eabi@4.46.2': resolution: {integrity: sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==} @@ -1914,6 +1936,9 @@ packages: '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -3043,8 +3068,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-rc.4: - resolution: {integrity: sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==} + rolldown@1.0.0-rc.5: + resolution: {integrity: sha512-0AdalTs6hNTioaCYIkAa7+xsmHBfU5hCNclZnM/lp7lGGDuUOb6N4BVNtwiomybbencDjq/waKjTImqiGCs5sw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -4604,7 +4629,7 @@ snapshots: '@open-draft/until@2.1.0': {} - '@oxc-project/types@0.113.0': {} + '@oxc-project/types@0.114.0': {} '@polka/url@1.0.0-next.29': {} @@ -4612,52 +4637,52 @@ snapshots: dependencies: quansync: 0.2.10 - '@rolldown/binding-android-arm64@1.0.0-rc.4': + '@rolldown/binding-android-arm64@1.0.0-rc.5': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.4': + '@rolldown/binding-darwin-arm64@1.0.0-rc.5': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.4': + '@rolldown/binding-darwin-x64@1.0.0-rc.5': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.4': + '@rolldown/binding-freebsd-x64@1.0.0-rc.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.4': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.4': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.4': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.4': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.5': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.4': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.5': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.4': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.5': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.4': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.5': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.4': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.4': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.5': optional: true '@rolldown/pluginutils@1.0.0-beta.27': {} '@rolldown/pluginutils@1.0.0-beta.40': {} - '@rolldown/pluginutils@1.0.0-rc.4': {} + '@rolldown/pluginutils@1.0.0-rc.5': {} '@rollup/rollup-android-arm-eabi@4.46.2': optional: true @@ -5036,6 +5061,10 @@ snapshots: '@types/statuses@2.0.6': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.17.1 + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@25.0.3)(jiti@2.5.1)(tsx@4.21.0)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 @@ -6276,7 +6305,7 @@ snapshots: rettime@0.7.0: {} - rolldown-plugin-dts@0.15.6(rolldown@1.0.0-rc.4)(typescript@5.9.2): + rolldown-plugin-dts@0.15.6(rolldown@1.0.0-rc.5)(typescript@5.9.2): dependencies: '@babel/generator': 7.28.5 '@babel/parser': 7.28.5 @@ -6286,31 +6315,31 @@ snapshots: debug: 4.4.1 dts-resolver: 2.1.1 get-tsconfig: 4.10.1 - rolldown: 1.0.0-rc.4 + rolldown: 1.0.0-rc.5 optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.0-rc.4: + rolldown@1.0.0-rc.5: dependencies: - '@oxc-project/types': 0.113.0 - '@rolldown/pluginutils': 1.0.0-rc.4 + '@oxc-project/types': 0.114.0 + '@rolldown/pluginutils': 1.0.0-rc.5 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.4 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.4 - '@rolldown/binding-darwin-x64': 1.0.0-rc.4 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.4 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.4 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.4 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.4 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.4 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.4 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.4 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.4 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.4 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.4 + '@rolldown/binding-android-arm64': 1.0.0-rc.5 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.5 + '@rolldown/binding-darwin-x64': 1.0.0-rc.5 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.5 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.5 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.5 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.5 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.5 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.5 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.5 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.5 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.5 rollup-plugin-inject@3.0.2: dependencies: @@ -6645,8 +6674,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-rc.4 - rolldown-plugin-dts: 0.15.6(rolldown@1.0.0-rc.4)(typescript@5.9.2) + rolldown: 1.0.0-rc.5 + rolldown-plugin-dts: 0.15.6(rolldown@1.0.0-rc.5)(typescript@5.9.2) semver: 7.7.3 tinyexec: 1.0.1 tinyglobby: 0.2.14