From 4943bf4208fd7039de834da3d3728dd1082fd5f6 Mon Sep 17 00:00:00 2001 From: Matt Hammond Date: Fri, 9 Jan 2026 17:14:18 +0000 Subject: [PATCH 01/12] feat: implement signedConfig and signature support for HMAC authentication - Add support for signedConfig and signature in AblyCliTerminal for HMAC authentication. - Update createAuthPayload to prioritize signedConfig over direct credentials. - Enhance tests to verify correct payload structure when using signedConfig with and without signature. --- packages/react-web-cli/README.md | 129 +++++++++++++++-- .../src/AblyCliTerminal.resize.test.tsx | 14 +- .../src/AblyCliTerminal.test.tsx | 136 +++++++++++++----- .../react-web-cli/src/AblyCliTerminal.tsx | 105 ++++++-------- packages/react-web-cli/src/terminal-shared.ts | 52 ++++++- 5 files changed, 322 insertions(+), 114 deletions(-) diff --git a/packages/react-web-cli/README.md b/packages/react-web-cli/README.md index eb5846eb..c907c151 100644 --- a/packages/react-web-cli/README.md +++ b/packages/react-web-cli/README.md @@ -10,7 +10,7 @@ A React component for embedding an interactive Ably CLI terminal in web applicat ## Features * Embed a **fully-featured Ably CLI** session (xterm.js) inside any React app. -* Secure WebSocket connection to the Ably terminal-server using your **API Key** (required) and an optional **Access Token** for Control-API commands. +* Secure WebSocket connection to the Ably terminal-server using HMAC-signed authentication with **signedConfig** and **signature**. * First-class terminal UX: * Terminal-native status messages with ANSI colors * Animated spinner while (re)connecting @@ -42,25 +42,48 @@ pnpm add @ably/react-web-cli - React 17.0.0 or higher - A running instance of the Ably CLI terminal server (see [@ably/cli-terminal-server](https://github.com/ably/cli-terminal-server)) -- Valid Ably API Key (required) and – optionally – an Access Token for Control-API commands +- A backend signing endpoint to generate HMAC-signed credentials (see implementation guide below) ## Usage ```tsx -import { useState } from "react"; +import { useState, useEffect } from "react"; import { AblyCliTerminal } from "@ably/react-web-cli"; export default function MyTerminal() { const [status, setStatus] = useState("disconnected"); + const [signedConfig, setSignedConfig] = useState(""); + const [signature, setSignature] = useState(""); + + // Sign credentials via your backend endpoint + // See examples/web-cli/api/sign.ts for implementation reference + async function signCredentials(apiKey: string) { + const response = await fetch("/api/sign", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ apiKey }), + }); + const { signedConfig, signature } = await response.json(); + setSignedConfig(signedConfig); + setSignature(signature); + } + + // Authenticate on mount (or when user provides credentials) + useEffect(() => { + const apiKey = "YOUR_ABLY_API_KEY"; // Get from user input or config + signCredentials(apiKey); + }, []); + + if (!signedConfig || !signature) { + return
Loading...
; + } return (
console.log("session ended", reason)} @@ -80,8 +103,8 @@ export default function MyTerminal() { | Prop | Type | Required | Default | Description | |------|------|----------|---------|-------------| | `websocketUrl` | string | ✅ | - | URL of the WebSocket terminal server | -| `ablyApiKey` | string | ✅ | - | Ably API key (data-plane) **required** | -| `ablyAccessToken` | string | No | - | Optional Control-API access token | +| `signedConfig` | string | ✅ | - | JSON-encoded signed config containing credentials and metadata (HMAC authenticated) | +| `signature` | string | ✅ | - | HMAC-SHA256 signature of the signedConfig for secure authentication | | `initialCommand` | string | No | - | Command to run on startup | | `onConnectionStatusChange` | function | No | - | Callback when connection status changes | | `onSessionId` | function | No | - | Callback when session ID is received | @@ -89,10 +112,92 @@ export default function MyTerminal() { | `maxReconnectAttempts` | number | No | 15 | Maximum reconnection attempts before giving up | | `resumeOnReload` | boolean | No | false | Whether to attempt to resume an existing session after page reload | | `enableSplitScreen` | boolean | No | false | Enable split-screen mode with a second independent terminal | -| `ablyEndpoint` | string | No | - | Optional SDK endpoint client option | -| `ablyControlHost` | string | No | - | Optional Control API hostname | -*\* `ablyApiKey` is mandatory. `ablyAccessToken` is optional and only needed for Control-API commands (e.g. accounts, apps, keys). +**Note:** The `signedConfig` and `signature` must be generated by **your backend** using HMAC-SHA256 signing. Never sign credentials in the browser - this would expose your signing secret. + +**Reference implementations:** +- **Vercel:** See `examples/web-cli/api/sign.ts` for a serverless function example +- **Node.js:** See `examples/web-cli/server/sign-handler.ts` for the shared signing logic +- **Vite Dev:** See `examples/web-cli/vite.config.ts` for development middleware + +Your signing secret must match the terminal server's `SIGNING_SECRET` configuration. Contact your platform team for the secret or refer to the [terminal server documentation](https://github.com/ably/cli-terminal-server). + +## Implementing a Signing Endpoint + +You **must** implement a backend endpoint to sign credentials. Here's a minimal example: + +```typescript +// Example: /api/sign endpoint (Node.js/Express/Vercel) +import crypto from "crypto"; + +export default function handler(req, res) { + const { apiKey } = req.body; + const secret = process.env.SIGNING_SECRET; // From environment variable + + // Build config object + const config = { + apiKey, + timestamp: Date.now(), + }; + + // Sign it with HMAC-SHA256 + const configString = JSON.stringify(config); + const hmac = crypto.createHmac("sha256", secret); + hmac.update(configString); + const signature = hmac.digest("hex"); + + res.json({ signedConfig: configString, signature }); +} +``` + +**Security Requirements:** +- ⚠️ **Never** embed your signing secret in client-side code +- ⚠️ **Always** sign on your secure backend server +- ✅ Signing secret must match the terminal server's configuration +- ✅ Use HTTPS in production to protect API keys in transit + +**Full implementation examples:** +- See `examples/web-cli/` directory for complete working examples +- Includes Vercel serverless function, Vite dev middleware, and React integration + +## Breaking Changes & Migration + +### HMAC-Signed Authentication (Breaking Change) + +**This is a breaking change.** The authentication mechanism has been updated to use HMAC-signed credentials for improved security. + +**Old API (deprecated):** +```tsx + +``` + +**New API (required):** +```tsx + +``` + +**Migration Steps:** + +1. **Implement backend signing:** Create a `/api/sign` endpoint on your backend (see "Implementing a Signing Endpoint" section above) +2. **Get signing secret:** Obtain the `SIGNING_SECRET` from your platform team (must match terminal server configuration) +3. **Update your React code:** + - Remove old props: `ablyApiKey`, `ablyAccessToken`, `ablyEndpoint`, `ablyControlHost` + - Call your `/api/sign` endpoint to get `signedConfig` and `signature` + - Pass the signed credentials to `AblyCliTerminal` + +**Why this change?** + +The new HMAC-signed authentication prevents credential tampering and ensures that configuration settings (like endpoint and control host) are authenticated by the server. This provides better security for embedded terminal sessions. ## Connection States diff --git a/packages/react-web-cli/src/AblyCliTerminal.resize.test.tsx b/packages/react-web-cli/src/AblyCliTerminal.resize.test.tsx index e0c64ad3..d69f2f2d 100644 --- a/packages/react-web-cli/src/AblyCliTerminal.resize.test.tsx +++ b/packages/react-web-cli/src/AblyCliTerminal.resize.test.tsx @@ -54,6 +54,14 @@ vi.mock("lucide-react", () => ({ X: () => null, })); +// Helper to create test signed config +const createTestSignedConfig = (apiKey: string = "test-key") => { + return JSON.stringify({ + apiKey, + timestamp: Date.now(), + }); +}; + describe("AblyCliTerminal – debounced fit", () => { beforeEach(() => { fitSpy.mockClear(); @@ -62,7 +70,11 @@ describe("AblyCliTerminal – debounced fit", () => { test("fit() is called initially and at most once during rapid resize events", async () => { const { unmount } = render( - , + , ); // Initial fit is called once diff --git a/packages/react-web-cli/src/AblyCliTerminal.test.tsx b/packages/react-web-cli/src/AblyCliTerminal.test.tsx index 90141195..721266d3 100644 --- a/packages/react-web-cli/src/AblyCliTerminal.test.tsx +++ b/packages/react-web-cli/src/AblyCliTerminal.test.tsx @@ -143,6 +143,25 @@ vi.mock("./utils/crypto", () => ({ }), })); +// Helper to create test signed configs +const createTestSignedConfig = ( + apiKey: string = "test-key", + accessToken?: string, +) => { + const config: Record = { + apiKey, + timestamp: Date.now(), + }; + if (accessToken) { + config.accessToken = accessToken; + } + return JSON.stringify(config); +}; + +// Default test signed config and signature +const DEFAULT_SIGNED_CONFIG = createTestSignedConfig("test-key", "test-token"); +const DEFAULT_SIGNATURE = "test-signature-mock"; + // Simple minimal test component to verify hooks work in the test environment const MinimalHookComponent = () => { const [state] = React.useState("test"); @@ -374,8 +393,8 @@ describe("AblyCliTerminal - Connection Status and Animation", () => { return render( { ); }); - test("includes both apiKey and accessToken in auth payload when both provided", async () => { - renderTerminal({ ablyApiKey: "key123", ablyAccessToken: "tokenXYZ" }); + test("includes signedConfig, signature, and extracted credentials in auth payload", async () => { + const mockConfig = JSON.stringify({ + apiKey: "appId.keyId:keySecret", + timestamp: Date.now(), + accessToken: "dashboard-token", + }); + const mockSignature = "abc123mockhmacsha256signature"; + + renderTerminal({ + signedConfig: mockConfig, + signature: mockSignature, + }); + + // Wait until the WebSocket mock fires the automatic 'open' event and the component sends auth payload + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); + }); + await waitFor(() => expect(mockSend).toHaveBeenCalled()); + + const sentPayload = JSON.parse(mockSend.mock.calls[0][0]); + // Signed config auth should be present + expect(sentPayload.config).toBe(mockConfig); + expect(sentPayload.signature).toBe(mockSignature); + // Credentials should be extracted from signed config for server convenience + expect(sentPayload.apiKey).toBe("appId.keyId:keySecret"); + expect(sentPayload.accessToken).toBe("dashboard-token"); + }); + + test("works without accessToken in signed config (anonymous mode)", async () => { + const mockConfig = JSON.stringify({ + apiKey: "appId.keyId:keySecret", + timestamp: Date.now(), + // No accessToken - anonymous mode + }); + const mockSignature = "abc123mockhmacsha256signature"; + + renderTerminal({ + signedConfig: mockConfig, + signature: mockSignature, + }); // Wait until the WebSocket mock fires the automatic 'open' event and the component sends auth payload await act(async () => { @@ -1073,8 +1130,12 @@ describe("AblyCliTerminal - Connection Status and Animation", () => { await waitFor(() => expect(mockSend).toHaveBeenCalled()); const sentPayload = JSON.parse(mockSend.mock.calls[0][0]); - expect(sentPayload.apiKey).toBe("key123"); - expect(sentPayload.accessToken).toBe("tokenXYZ"); + // Signed config should be present + expect(sentPayload.config).toBe(mockConfig); + expect(sentPayload.signature).toBe(mockSignature); + // Only apiKey extracted, no accessToken + expect(sentPayload.apiKey).toBe("appId.keyId:keySecret"); + expect(sentPayload.accessToken).toBeUndefined(); }); test("increments only once when both error and close events fire for the same failure", async () => { @@ -1411,8 +1472,8 @@ describe("AblyCliTerminal - Connection Status and Animation", () => { , @@ -1743,8 +1804,8 @@ describe("AblyCliTerminal - Credential Validation", () => { return render( { ); // Render with different credentials (which will generate a different hash) - // The mock will generate 'hash-new-key:new-token' which won't match 'old-hash-value' - renderTerminal({ ablyApiKey: "new-key", ablyAccessToken: "new-token" }); + const newConfig = createTestSignedConfig("new-key", "new-token"); + renderTerminal({ signedConfig: newConfig }); // Wait a bit for credential validation to complete await act(async () => { @@ -1819,8 +1880,8 @@ describe("AblyCliTerminal - Credential Validation", () => { // Spy on console.log before rendering const consoleLogSpy = vi.spyOn(console, "log"); - // Render with matching credentials - renderTerminal({ ablyApiKey: "test-key", ablyAccessToken: "test-token" }); + // Render with matching credentials (default config has test-key:test-token) + renderTerminal(); // Wait for the session to be restored await waitFor(() => { @@ -1869,9 +1930,12 @@ describe("AblyCliTerminal - Credential Validation", () => { }, 10_000); test("stores credential hash when new session is created", async () => { + const customConfig = createTestSignedConfig( + "test-key-123", + "test-token-456", + ); renderTerminal({ - ablyApiKey: "test-key-123", - ablyAccessToken: "test-token-456", + signedConfig: customConfig, }); // Wait for initialization @@ -1914,7 +1978,7 @@ describe("AblyCliTerminal - Credential Validation", () => { "hash-to-purge", ); - renderTerminal({ ablyApiKey: "test-key", ablyAccessToken: "test-token" }); + renderTerminal(); await act(async () => { await Promise.resolve(); @@ -1948,8 +2012,9 @@ describe("AblyCliTerminal - Credential Validation", () => { ).toBeNull(); }, 10_000); - test("handles missing credentials (undefined apiKey)", async () => { - renderTerminal({ ablyApiKey: undefined, ablyAccessToken: "test-token" }); + test("handles config without accessToken (anonymous mode)", async () => { + const anonymousConfig = createTestSignedConfig("test-key"); // No accessToken + renderTerminal({ signedConfig: anonymousConfig }); // Wait for initialization await act(async () => { @@ -1974,7 +2039,7 @@ describe("AblyCliTerminal - Credential Validation", () => { await new Promise((resolve) => setTimeout(resolve, 20)); }); - // Should store session and hash even with undefined apiKey (domain-scoped) + // Should store session and hash even without accessToken (anonymous mode) expect( globalThis.sessionStorage.getItem("ably.cli.sessionId.web-cli.ably.com"), ).toBe("session-no-key"); @@ -1982,7 +2047,7 @@ describe("AblyCliTerminal - Credential Validation", () => { globalThis.sessionStorage.getItem( "ably.cli.credentialHash.web-cli.ably.com", ), - ).toBe("hash-:test-token"); + ).toBe("hash-test-key:"); // apiKey present, no accessToken }, 10_000); test("does not store session when resumeOnReload is false", async () => { @@ -2054,8 +2119,8 @@ describe("AblyCliTerminal - Cross-Domain Security", () => { return render( { test("credentials are not shared between different serverUrls", async () => { // First, render with the default server and store credentials + const secureConfig = createTestSignedConfig( + "secure-key-123", + "secure-token-456", + ); const { unmount } = renderTerminal({ - ablyApiKey: "secure-key-123", - ablyAccessToken: "secure-token-456", + signedConfig: secureConfig, }); // Wait for initialization @@ -2103,11 +2171,10 @@ describe("AblyCliTerminal - Cross-Domain Security", () => { unmount(); mockSend.mockClear(); - // Now render with a different server URL + // Now render with a different server URL (reusing secureConfig from above) renderTerminal({ websocketUrl: "wss://attacker.example.com", - ablyApiKey: "secure-key-123", - ablyAccessToken: "secure-token-456", + signedConfig: secureConfig, }); // Wait for WebSocket connection and the open event to be triggered @@ -2158,8 +2225,6 @@ describe("AblyCliTerminal - Cross-Domain Security", () => { // Render terminal connecting to web-cli.ably.com renderTerminal({ websocketUrl: "wss://web-cli.ably.com", - ablyApiKey: "test-key", - ablyAccessToken: "test-token", resumeOnReload: true, }); @@ -2211,10 +2276,13 @@ describe("AblyCliTerminal - Cross-Domain Security", () => { ); // Render terminal with a different, potentially malicious domain + const differentConfig = createTestSignedConfig( + "different-key", + "different-token", + ); renderTerminal({ websocketUrl: "wss://evil-attacker.com", - ablyApiKey: "different-key", - ablyAccessToken: "different-token", + signedConfig: differentConfig, }); // Wait for WebSocket connection and the open event to be triggered @@ -2294,8 +2362,8 @@ describe("AblyCliTerminal - Initial Command Execution", () => { return render( void; onSessionEnd?: (reason: string) => void; @@ -110,10 +119,6 @@ export interface AblyCliTerminalProperties { */ resumeOnReload?: boolean; maxReconnectAttempts?: number; - /** - * CI authentication token for bypassing rate limits in tests - */ - ciAuthToken?: string; /** * When true, enables split-screen mode with a second independent terminal. * A split icon will be displayed in the top-right corner when in single-pane mode. @@ -124,29 +129,6 @@ export interface AblyCliTerminalProperties { * Set to false when controlling split externally to hide the internal UI affordance. */ showSplitControl?: boolean; - /** - * An optional value to set as the ABLY_ENDPOINT environment variable when - * starting the CLI, which controls which endpoint the SDK client uses. - * - * For example, to start the CLI configured to use the sandbox cluster, set - * ablyEndpoint to 'nonprod:sandbox'. - * - * When unset, the CLI will use the main production endpoint. - * - * See https://sdk.ably.com/builds/ably/specification/main/features/#endpoint-configuration - */ - ablyEndpoint?: string; - /** - * An optional value to set as the ABLY_CONTROL_HOST environment variable - * when starting the CLI, which controls which hostname the CLI uses to - * connect to the Control API. - * - * For example, to start the CLI configured to use the staging Control API, - * set ablyControlHost to 'staging-control.ably-dev.net'. - * - * When unset, the CLI uses the production Control API at control.ably.net. - */ - ablyControlHost?: string; } export interface AblyCliTerminalHandle { @@ -185,8 +167,8 @@ import { isHijackMetaChunk } from "./terminal-shared"; const AblyCliTerminalInner = ( { websocketUrl, - ablyAccessToken, - ablyApiKey, + signedConfig, + signature, initialCommand, onConnectionStatusChange, onSessionEnd, @@ -195,8 +177,6 @@ const AblyCliTerminalInner = ( maxReconnectAttempts, enableSplitScreen = false, showSplitControl = true, - ablyEndpoint, - ablyControlHost, }: AblyCliTerminalProperties, reference: React.Ref, ) => { @@ -1464,18 +1444,6 @@ const AblyCliTerminalInner = ( const socketReference = useRef(null); // Ref to hold the current socket for cleanup - const additionalEnvironmentVariables: Record = {}; - if (ablyEndpoint) { - debugLog(`⚠️ DIAGNOSTIC: Setting ABLY_ENDPOINT to "${ablyEndpoint}"`); - additionalEnvironmentVariables["ABLY_ENDPOINT"] = ablyEndpoint; - } - if (ablyControlHost) { - debugLog( - `⚠️ DIAGNOSTIC: Setting ABLY_CONTROL_HOST to "${ablyControlHost}"`, - ); - additionalEnvironmentVariables["ABLY_CONTROL_HOST"] = ablyControlHost; - } - const handleWebSocketOpen = useCallback(() => { // console.log('[AblyCLITerminal] WebSocket opened'); // Clear connection timeout since we successfully connected @@ -1514,12 +1482,12 @@ const AblyCliTerminalInner = ( // Don't send the initial command yet - wait for prompt detection } - // Send auth payload - but no additional data + // Send auth payload with signed config const payload = createAuthPayload( - ablyApiKey, - ablyAccessToken, sessionId, - additionalEnvironmentVariables, + undefined, // no additional env vars needed - they're in the signed config + signedConfig, + signature, ); debugLog( @@ -1556,8 +1524,6 @@ const AblyCliTerminalInner = ( debugLog("WebSocket OPEN handler completed. sessionId:", sessionId); }, [ clearAnimationMessages, - ablyAccessToken, - ablyApiKey, initialCommand, updateConnectionStatusAndExpose, clearPtyBuffer, @@ -1565,6 +1531,8 @@ const AblyCliTerminalInner = ( resumeOnReload, clearConnectionTimeout, credentialHash, + signedConfig, + signature, ]); const handleWebSocketMessage = useCallback( @@ -2669,8 +2637,25 @@ const AblyCliTerminalInner = ( // Initialize session and validate credentials useEffect(() => { const initializeSession = async () => { - // Calculate current credential hash - const currentHash = await hashCredentials(ablyApiKey, ablyAccessToken); + // Extract apiKey and accessToken from signed config for credential hashing + // This allows session resumption when the same credentials are used + let apiKeyForHash: string | undefined; + let accessTokenForHash: string | undefined; + try { + const parsedConfig = JSON.parse(signedConfig); + apiKeyForHash = parsedConfig.apiKey; + accessTokenForHash = parsedConfig.accessToken; + } catch { + console.warn( + "[AblyCLITerminal] Failed to parse signedConfig for credential hash", + ); + } + + // Calculate current credential hash from credentials in signed config + const currentHash = await hashCredentials( + apiKeyForHash, + accessTokenForHash, + ); setCredentialHash(currentHash); if (!resumeOnReload || globalThis.window === undefined) { @@ -2731,7 +2716,7 @@ const AblyCliTerminalInner = ( }; initializeSession(); - }, [ablyApiKey, ablyAccessToken, resumeOnReload, websocketUrl]); + }, [signedConfig, resumeOnReload, websocketUrl]); // Store credential hash when it becomes available if we already have a sessionId useEffect(() => { @@ -2947,12 +2932,12 @@ const AblyCliTerminalInner = ( secondaryTerm.current.focus(); } - // Send auth payload - only include necessary data + // Send auth payload with signed config const payload = createAuthPayload( - ablyApiKey, - ablyAccessToken, secondarySessionId, - additionalEnvironmentVariables, + undefined, // no additional env vars needed - they're in the signed config + signedConfig, + signature, ); if (newSocket.readyState === WebSocket.OPEN) { @@ -3264,8 +3249,8 @@ const AblyCliTerminalInner = ( return newSocket; }, [ websocketUrl, - ablyAccessToken, - ablyApiKey, + signedConfig, + signature, resumeOnReload, secondarySessionId, ]); diff --git a/packages/react-web-cli/src/terminal-shared.ts b/packages/react-web-cli/src/terminal-shared.ts index 3144cfce..c66faa69 100644 --- a/packages/react-web-cli/src/terminal-shared.ts +++ b/packages/react-web-cli/src/terminal-shared.ts @@ -164,16 +164,27 @@ export interface AuthPayload { sessionId?: string | null; environmentVariables?: Record; ciAuthToken?: string; + /** + * JSON-encoded signed config string for HMAC authentication. + * When provided with signature, the server extracts credentials from this. + */ + config?: string; + /** + * HMAC-SHA256 signature of the config string. + * Required when using signed config authentication. + */ + signature?: string; } /** - * Creates authentication payload for WebSocket connection + * Creates authentication payload for WebSocket connection. + * Requires signed config authentication (HMAC-signed credentials from the dashboard). */ export function createAuthPayload( - apiKey?: string, - accessToken?: string, sessionId?: string | null, additionalEnvVars?: Record, + signedConfig?: string, + signature?: string, ): AuthPayload { const payload: AuthPayload = { environmentVariables: { @@ -183,19 +194,46 @@ export function createAuthPayload( }, }; - if (apiKey) payload.apiKey = apiKey; - if (accessToken) payload.accessToken = accessToken; + // Signed config authentication is required + if (!signedConfig || !signature) { + console.error( + "[createAuthPayload] Missing signedConfig or signature - authentication will fail", + ); + } else { + payload.config = signedConfig; + payload.signature = signature; + + // Extract fields from signed config for server convenience + // Server uses these to set environment variables like ABLY_ANONYMOUS_USER_MODE + try { + const parsedConfig = JSON.parse(signedConfig); + if (parsedConfig.apiKey) { + payload.apiKey = parsedConfig.apiKey; + } + if (parsedConfig.accessToken) { + payload.accessToken = parsedConfig.accessToken; + } + console.log("[createAuthPayload] Using signed config auth", { + hasApiKey: !!payload.apiKey, + hasAccessToken: !!payload.accessToken, + }); + } catch (error) { + console.warn("[createAuthPayload] Failed to parse signed config:", error); + } + } + if (sessionId) payload.sessionId = sessionId; // Check for CI auth token in window object // This will be injected during test execution const win = globalThis as any; if (win.__ABLY_CLI_CI_AUTH_TOKEN__) { - payload.ciAuthToken = win.__ABLY_CLI_CI_AUTH_TOKEN__; + const ciToken: string = win.__ABLY_CLI_CI_AUTH_TOKEN__; + payload.ciAuthToken = ciToken; // Debug logging in CI if (win.__ABLY_CLI_CI_MODE__ === "true") { console.log("[CI Auth] Including CI auth token in payload", { - tokenLength: payload.ciAuthToken.length, + tokenLength: ciToken.length, testGroup: win.__ABLY_CLI_TEST_GROUP__ || "unknown", runId: win.__ABLY_CLI_RUN_ID__ || "unknown", }); From 66e92aecab527f4800da0960e3dd4aba3e78e25a Mon Sep 17 00:00:00 2001 From: Kenneth Kalmer Date: Tue, 13 Jan 2026 12:16:02 +0000 Subject: [PATCH 02/12] chore: additional comments to clarify skipped tests --- .../src/AblyCliTerminal.test.tsx | 19 +++++++++++++------ test/unit/commands/queues/delete.test.ts | 2 ++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/react-web-cli/src/AblyCliTerminal.test.tsx b/packages/react-web-cli/src/AblyCliTerminal.test.tsx index 721266d3..9e462b0b 100644 --- a/packages/react-web-cli/src/AblyCliTerminal.test.tsx +++ b/packages/react-web-cli/src/AblyCliTerminal.test.tsx @@ -774,6 +774,8 @@ describe("AblyCliTerminal - Connection Status and Animation", () => { }); test.skip("shows installation tip after 6 seconds during connection attempts", async () => { + // SKIPPED: This test has timing issues with fake timers in CI environments + // The 6-second delay doesn't advance consistently with vi.advanceTimersByTime vi.useFakeTimers(); try { @@ -828,6 +830,8 @@ describe("AblyCliTerminal - Connection Status and Animation", () => { }); test.skip("shows installation tip during reconnection after 6 seconds", async () => { + // SKIPPED: This test has timing issues with fake timers in CI environments + // The 6-second delay doesn't advance consistently with vi.advanceTimersByTime vi.useFakeTimers(); try { @@ -909,6 +913,8 @@ describe("AblyCliTerminal - Connection Status and Animation", () => { }); test.skip("manual reconnect resets attempt counter after max attempts reached - skipped due to CI timing issues", async () => { + // SKIPPED: This test has timing issues in CI environments + // Manual reconnect state transitions don't complete reliably with mocked timers // Set up max attempts reached state vi.mocked(GlobalReconnect.isMaxAttemptsReached).mockReturnValue(true); vi.mocked(GlobalReconnect.getMaxAttempts).mockReturnValue(5); @@ -1205,7 +1211,8 @@ describe("AblyCliTerminal - Connection Status and Animation", () => { }); test.skip("connection timeout triggers error after 30 seconds", async () => { - // Skip this test for now due to timing issues with fake timers + // SKIPPED: This test has timing issues with fake timers + // The 30-second timeout doesn't trigger consistently with vi.advanceTimersByTime vi.useFakeTimers(); renderTerminal(); @@ -1607,8 +1614,8 @@ describe("AblyCliTerminal - Connection Status and Animation", () => { }); test.skip("prompt detection correctly handles ANSI color codes", async () => { - // Skip this test due to React fiber internal structure changes that are not stable - + // SKIPPED: This test depends on React fiber internal structure + // React's internal structure is not stable across versions and breaks this test // Create a mock component and socket const mockSocket = { readyState: WebSocket.OPEN, @@ -1645,9 +1652,9 @@ describe("AblyCliTerminal - Connection Status and Animation", () => { }); test.skip("onConnectionStatusChange only reports status for the primary terminal in split-screen mode", async () => { - // Skip this test if the environment is not stable enough - // This test verifies implementation details that are subject to change - // The core functionality is tested through proper unit and integration tests + // SKIPPED: This test verifies implementation details that are subject to change + // Core functionality is covered by other unit and integration tests + // The environment is not stable enough for this internal implementation test vi.spyOn(console, "log").mockImplementation(() => {}); // Suppress console.log during test // Mark this test as skipped since it requires internal details that are not stable diff --git a/test/unit/commands/queues/delete.test.ts b/test/unit/commands/queues/delete.test.ts index 8e0418e9..7bcda39e 100644 --- a/test/unit/commands/queues/delete.test.ts +++ b/test/unit/commands/queues/delete.test.ts @@ -375,6 +375,7 @@ describe("queues:delete command", () => { describe("confirmation prompt handling", () => { it.skip("should cancel deletion when user responds no to confirmation", async () => { // SKIPPED: stdin handling in tests is problematic with runCommand + // The runCommand test helper doesn't properly pipe stdin to the spawned process const appId = getMockConfigManager().getCurrentAppId()!; const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; @@ -401,6 +402,7 @@ describe("queues:delete command", () => { it.skip("should proceed with deletion when user confirms", async () => { // SKIPPED: stdin handling in tests is problematic with runCommand + // The runCommand test helper doesn't properly pipe stdin to the spawned process const appId = getMockConfigManager().getCurrentAppId()!; const mockQueueId = `${appId}:us-east-1-a:${mockQueueName}`; From d0d51d59b1219c2a1d2897874d656f05175b83f3 Mon Sep 17 00:00:00 2001 From: Kenneth Kalmer Date: Tue, 13 Jan 2026 12:16:34 +0000 Subject: [PATCH 03/12] fix: handle hanging child process in `did-you-mean.test.ts` --- test/unit/commands/did-you-mean.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/unit/commands/did-you-mean.test.ts b/test/unit/commands/did-you-mean.test.ts index fc0a2f31..f6dd8adc 100644 --- a/test/unit/commands/did-you-mean.test.ts +++ b/test/unit/commands/did-you-mean.test.ts @@ -21,7 +21,7 @@ describe("Did You Mean Functionality", () => { it( "should show Y/N prompt for misspelled commands", async () => - new Promise((resolve) => { + new Promise((resolve, reject) => { const child = spawn("node", [binPath, "interactive"], { stdio: ["pipe", "pipe", "pipe"], env: { @@ -34,6 +34,16 @@ describe("Did You Mean Functionality", () => { let output = ""; let foundPrompt = false; + // Safety timeout to force kill if process hangs + const killTimeout = setTimeout(() => { + child.kill("SIGTERM"); + reject( + new Error( + "Test timed out - process did not exit. Output: " + output, + ), + ); + }, 10000); + child.stdout.on("data", (data) => { output += data.toString(); if ( @@ -56,6 +66,7 @@ describe("Did You Mean Functionality", () => { }, 2000); child.on("exit", () => { + clearTimeout(killTimeout); expect(foundPrompt).toBe(true); expect(output).toContain( "account current is not an ably command", From d47b43403493b8e0e904178b80000e57ed1efcd4 Mon Sep 17 00:00:00 2001 From: Kenneth Kalmer Date: Tue, 13 Jan 2026 12:17:52 +0000 Subject: [PATCH 04/12] fix: add missing pnpm version to `.tool-versions` --- .tool-versions | 1 + 1 file changed, 1 insertion(+) diff --git a/.tool-versions b/.tool-versions index 604be079..41f6b3bb 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1,2 @@ nodejs 22.14.0 +pnpm 10.28.0 From 50727cc8ed91cff9ed29d94157a4f6389bfd58a2 Mon Sep 17 00:00:00 2001 From: Kenneth Kalmer Date: Tue, 13 Jan 2026 19:43:20 +0000 Subject: [PATCH 05/12] feat(web-cli): add /api/sign endpoint for credential signing Add server-side signing endpoint that works across all environments: - Development: Vite plugin (configureServer) - Preview/Tests: Vite plugin (configurePreviewServer) - Production: Vercel serverless function All three share signing logic from server/sign-handler.ts. Supports SIGNING_SECRET, TERMINAL_SERVER_SIGNING_SECRET, and CI_BYPASS_SECRET environment variables for compatibility. Co-Authored-By: Claude Sonnet 4.5 (1M context) --- examples/web-cli/api/sign.ts | 48 ++ examples/web-cli/package.json | 1 + examples/web-cli/server/sign-handler.ts | 62 ++ examples/web-cli/vite.config.ts | 76 ++- pnpm-lock.yaml | 822 +++++++++++++++++++++++- 5 files changed, 988 insertions(+), 21 deletions(-) create mode 100644 examples/web-cli/api/sign.ts create mode 100644 examples/web-cli/server/sign-handler.ts diff --git a/examples/web-cli/api/sign.ts b/examples/web-cli/api/sign.ts new file mode 100644 index 00000000..eb3293a2 --- /dev/null +++ b/examples/web-cli/api/sign.ts @@ -0,0 +1,48 @@ +import type { VercelRequest, VercelResponse } from "@vercel/node"; +import { signCredentials, getSigningSecret } from "../server/sign-handler.js"; + +/** + * Vercel Serverless Function: Sign credentials for terminal authentication + * + * This endpoint signs API keys with HMAC-SHA256 to create signed configs + * that can be validated by the terminal server. + * + * Environment Variables Required: + * - SIGNING_SECRET or TERMINAL_SERVER_SIGNING_SECRET + * + * Request Body: + * - apiKey: string (required) - Ably API key in format "appId.keyId:secret" + * - bypassRateLimit: boolean (optional) - Set to true for CI/testing + * + * Response: + * - signedConfig: string - JSON-encoded config that was signed + * - signature: string - HMAC-SHA256 hex signature + */ +export default async function handler( + req: VercelRequest, + res: VercelResponse, +) { + // Only accept POST requests + if (req.method !== "POST") { + return res.status(405).json({ error: "Method not allowed" }); + } + + // Get signing secret from environment + const secret = getSigningSecret(); + + if (!secret) { + console.error("[/api/sign] Signing secret not configured"); + return res.status(500).json({ error: "Signing secret not configured" }); + } + + const { apiKey, bypassRateLimit } = req.body; + + if (!apiKey) { + return res.status(400).json({ error: "apiKey is required" }); + } + + // Use shared signing logic + const result = signCredentials({ apiKey, bypassRateLimit }, secret); + + res.status(200).json(result); +} diff --git a/examples/web-cli/package.json b/examples/web-cli/package.json index 4a6b0f0f..636b2619 100644 --- a/examples/web-cli/package.json +++ b/examples/web-cli/package.json @@ -23,6 +23,7 @@ "@tailwindcss/vite": "^4.1.5", "@types/react": "^18.3.20", "@types/react-dom": "^18.3.5", + "@vercel/node": "^5.5.17", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.21", "eslint": "^9.21.0", diff --git a/examples/web-cli/server/sign-handler.ts b/examples/web-cli/server/sign-handler.ts new file mode 100644 index 00000000..e2a31f5c --- /dev/null +++ b/examples/web-cli/server/sign-handler.ts @@ -0,0 +1,62 @@ +import crypto from "crypto"; + +/** + * Shared signing logic for credential authentication + * Used by: Vercel function, Vite middleware, and preview server + */ + +export interface SignRequest { + apiKey: string; + bypassRateLimit?: boolean; +} + +export interface SignResponse { + signedConfig: string; + signature: string; +} + +/** + * Sign credentials using HMAC-SHA256 + * @param request - Request containing apiKey and optional flags + * @param secret - Signing secret from environment + * @returns Signed config and signature + */ +export function signCredentials( + request: SignRequest, + secret: string, +): SignResponse { + const { apiKey, bypassRateLimit } = request; + + // Build config object (matches terminal server expectations) + const config = { + apiKey, + timestamp: Date.now(), + bypassRateLimit: bypassRateLimit || false, + }; + + // Serialize to JSON - this exact string is what gets signed + const configString = JSON.stringify(config); + + // Generate HMAC-SHA256 signature + const hmac = crypto.createHmac("sha256", secret); + hmac.update(configString); + const signature = hmac.digest("hex"); + + return { + signedConfig: configString, + signature, + }; +} + +/** + * Get signing secret from environment variables + * Checks multiple variable names for compatibility + */ +export function getSigningSecret(): string | null { + return ( + process.env.SIGNING_SECRET || + process.env.TERMINAL_SERVER_SIGNING_SECRET || + process.env.CI_BYPASS_SECRET || + null + ); +} diff --git a/examples/web-cli/vite.config.ts b/examples/web-cli/vite.config.ts index 71a2b1f5..e9aa281b 100644 --- a/examples/web-cli/vite.config.ts +++ b/examples/web-cli/vite.config.ts @@ -1,15 +1,79 @@ import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; +import { defineConfig, Plugin } from "vite"; import tsconfigPaths from "vite-tsconfig-paths"; import tailwindcss from "@tailwindcss/vite"; +import { signCredentials, getSigningSecret } from "./server/sign-handler.js"; + +// Custom plugin to add /api/sign endpoint for both dev and preview +function apiSignPlugin(): Plugin { + // Shared middleware handler for both dev and preview servers + const apiSignMiddleware = (req: any, res: any, next: any) => { + // Only handle /api/sign requests + if (!req.url?.startsWith("/api/sign")) { + return next(); + } + + if (req.method !== "POST") { + res.statusCode = 405; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "Method not allowed" })); + return; + } + + const secret = getSigningSecret(); + if (!secret) { + res.statusCode = 500; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "Signing secret not configured" })); + return; + } + + // Parse request body + let body = ""; + req.on("data", (chunk: any) => { + body += chunk; + }); + + req.on("end", () => { + try { + const { apiKey, bypassRateLimit } = JSON.parse(body); + + if (!apiKey) { + res.statusCode = 400; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "apiKey is required" })); + return; + } + + // Use shared signing logic + const result = signCredentials({ apiKey, bypassRateLimit }, secret); + + res.statusCode = 200; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify(result)); + } catch (error) { + console.error("[/api/sign] Error:", error); + res.statusCode = 400; + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ error: "Invalid request body" })); + } + }); + }; + + return { + name: "api-sign", + configureServer(server) { + server.middlewares.use(apiSignMiddleware); + }, + configurePreviewServer(server) { + server.middlewares.use(apiSignMiddleware); + }, + }; +} // https://vitejs.dev/config/ export default defineConfig({ - plugins: [ - react(), - tsconfigPaths(), - tailwindcss(), - ], + plugins: [react(), tsconfigPaths(), tailwindcss(), apiSignPlugin()], server: { host: true, https: undefined, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1d5e128..f1757323 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -211,7 +211,7 @@ importers: version: 11.1.0 vitest: specifier: ^4.0.0 - version: 4.0.14(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) + version: 4.0.14(@edge-runtime/vm@3.2.0)(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) examples/web-cli: dependencies: @@ -242,16 +242,19 @@ importers: version: 4.1.5 '@tailwindcss/vite': specifier: ^4.1.5 - version: 4.1.5(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) + version: 4.1.5(vite@6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) '@types/react': specifier: ^18.3.20 version: 18.3.20 '@types/react-dom': specifier: ^18.3.5 version: 18.3.6(@types/react@18.3.20) + '@vercel/node': + specifier: ^5.5.17 + version: 5.5.17(rollup@4.40.2) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.4.1(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) + version: 4.4.1(vite@6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) autoprefixer: specifier: ^10.4.21 version: 10.4.21(postcss@8.5.3) @@ -281,10 +284,10 @@ importers: version: 8.30.1(eslint@9.34.0(jiti@2.4.2))(typescript@5.7.3) vite: specifier: ^6.2.7 - version: 6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + version: 6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) + version: 5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) packages/react-web-cli: dependencies: @@ -348,7 +351,7 @@ importers: version: 6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) vitest: specifier: ^4.0.0 - version: 4.0.14(@types/node@22.14.1)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) + version: 4.0.14(@edge-runtime/vm@3.2.0)(@types/node@22.14.1)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) packages: @@ -697,6 +700,26 @@ packages: resolution: {integrity: sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==} engines: {node: '>=18'} + '@edge-runtime/format@2.2.1': + resolution: {integrity: sha512-JQTRVuiusQLNNLe2W9tnzBlV/GvSVcozLl4XZHk5swnRZ/v6jp8TqR8P7sqmJsQqblDZ3EztcWmLDbhRje/+8g==} + engines: {node: '>=16'} + + '@edge-runtime/node-utils@2.3.0': + resolution: {integrity: sha512-uUtx8BFoO1hNxtHjp3eqVPC/mWImGb2exOfGjMLUoipuWgjej+f4o/VP4bUI8U40gu7Teogd5VTeZUkGvJSPOQ==} + engines: {node: '>=16'} + + '@edge-runtime/ponyfill@2.4.2': + resolution: {integrity: sha512-oN17GjFr69chu6sDLvXxdhg0Qe8EZviGSuqzR9qOiKh4MhFYGdBBcqRNzdmYeAdeRzOW2mM9yil4RftUQ7sUOA==} + engines: {node: '>=16'} + + '@edge-runtime/primitives@4.1.0': + resolution: {integrity: sha512-Vw0lbJ2lvRUqc7/soqygUX216Xb8T3WBZ987oywz6aJqRxcwSVWwr9e+Nqo2m9bxobA9mdbWNNoRY6S9eko1EQ==} + engines: {node: '>=16'} + + '@edge-runtime/vm@3.2.0': + resolution: {integrity: sha512-0dEVyRLM/lG4gp1R/Ik5bfPl/1wX00xFwd5KcNH602tzBa09oF7pbTKETEhR1GjZ75K6OJnYFu8II2dyMhONMw==} + engines: {node: '>=16'} + '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} @@ -1227,6 +1250,10 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1424,10 +1451,22 @@ packages: '@types/node': optional: true + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1455,6 +1494,11 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} + engines: {node: '>=18'} + hasBin: true + '@modelcontextprotocol/sdk@1.10.1': resolution: {integrity: sha512-xNYdFdkJqEfIaTVP1gPKoEvluACHZsHZegIoICX8DM1o6Qf3G5u2BQJHmgd0n4YgRPqqK/u1ujQvrgAxxSJT9w==} engines: {node: '>=18'} @@ -1544,6 +1588,15 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.40.0': resolution: {integrity: sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==} cpu: [arm] @@ -2113,6 +2166,15 @@ packages: '@types/react-dom': optional: true + '@ts-morph/common@0.11.1': + resolution: {integrity: sha512-7hWZS0NRpEsNV8vWJzg7FEz6V8MaLNeJOmwmghqUXTpzk16V1LLZhdo+4QvE/+zv4cVci0OviuJFnqhEfoV3+g==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + '@tsconfig/node14@1.0.3': resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} @@ -2191,6 +2253,9 @@ packages: '@types/node-fetch@2.6.13': resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + '@types/node@16.18.11': + resolution: {integrity: sha512-3oJbGBUWuS6ahSnEq1eN2XrCyf4YsWI8OyCvo7c64zQJNplk3mO84t53o8lfTk+2ji59g5ycfc6qQ3fdHliHuA==} + '@types/node@20.17.30': resolution: {integrity: sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==} @@ -2449,6 +2514,23 @@ packages: cpu: [x64] os: [win32] + '@vercel/build-utils@13.2.5': + resolution: {integrity: sha512-Hrg+bEj/p78l9th4t4bHP3B0KwxBgLo4uXatPj8i7nFubhknYfDnL4hKYmw2H4Isyrr5VPOIeMM55Ci+idmm6Q==} + + '@vercel/error-utils@2.0.3': + resolution: {integrity: sha512-CqC01WZxbLUxoiVdh9B/poPbNpY9U+tO1N9oWHwTl5YAZxcqXmmWJ8KNMFItJCUUWdY3J3xv8LvAuQv2KZ5YdQ==} + + '@vercel/nft@1.1.1': + resolution: {integrity: sha512-mKMGa7CEUcXU75474kOeqHbtvK1kAcu4wiahhmlUenB5JbTQB8wVlDI8CyHR3rpGo0qlzoRWqcDzI41FUoBJCA==} + engines: {node: '>=20'} + hasBin: true + + '@vercel/node@5.5.17': + resolution: {integrity: sha512-TITMUvRrTsqVbxw+GXHrpg4wNARnpOec31i954F52Stlfj3VYKz1xOENMvE547VjHG4eMBoZTgQycJHOMV/fJA==} + + '@vercel/static-config@3.1.2': + resolution: {integrity: sha512-2d+TXr6K30w86a+WbMbGm2W91O0UzO5VeemZYBBUJbCjk/5FLLGIi8aV6RS2+WmaRvtcqNTn2pUA7nCOK3bGcQ==} + '@vitejs/plugin-react@4.4.1': resolution: {integrity: sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2542,6 +2624,10 @@ packages: '@zeit/schemas@2.36.0': resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + ably@2.14.0: resolution: {integrity: sha512-GWNza+URnh/W5IuoJX7nXJpQCs2Dxby6t5A20vL3PBqGIJceA94/1xje4HOZbqFtMEPkRVsYHBIEuQRWL+CuvQ==} engines: {node: '>=16'} @@ -2562,6 +2648,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2591,6 +2682,9 @@ packages: ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + ajv@8.6.3: + resolution: {integrity: sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==} + ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -2695,12 +2789,23 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async-listen@3.0.0: + resolution: {integrity: sha512-V+SsTpDqkrWTimiotsyl33ePSjA5/KrithwupuvJ6ztsqPvGv6ge4OredFhPffVXiLN/QUWvE0XcqJaYgt6fOg==} + engines: {node: '>= 14'} + + async-listen@3.0.1: + resolution: {integrity: sha512-cWMaNwUJnf37C/S5TfCkk/15MwbPRwVYALA2jtjkbHjCmAPiDXyNJy2q3p1KAZzDLHAWyarUWSujUoHR4pEgrA==} + engines: {node: '>= 14'} + async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -2732,6 +2837,9 @@ packages: resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} engines: {node: '>= 0.8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -2882,6 +2990,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -2890,6 +3002,9 @@ packages: resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==} engines: {node: '>=8'} + cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -2937,6 +3052,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + code-block-writer@10.1.1: + resolution: {integrity: sha512-67ueh2IRGst/51p0n6FvPrnRjAGHY5F8xdjkgrYE7DDzpJe6qA07RYQ9VcoUeo5ATOjSOiWpSL3SWBRRbempMw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -2996,6 +3114,10 @@ packages: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} engines: {node: '>= 0.6'} + convert-hrtime@3.0.0: + resolution: {integrity: sha512-7V+KqSvMiHp8yWDuwfww06XleMWVVB9b9tURBx+G7UTADuo5hYPuowKloz4OzOqbPezxgo+fdQ1522WzPG4OeA==} + engines: {node: '>=8'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -3018,6 +3140,9 @@ packages: resolution: {integrity: sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==} engines: {node: '>= 0.4.0'} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@6.0.6: resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} engines: {node: '>=4.8'} @@ -3199,6 +3324,11 @@ packages: ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + edge-runtime@2.5.9: + resolution: {integrity: sha512-pk+k0oK0PVXdlT4oRp4lwh+unuKB7Ng4iZ2HB+EZ7QCEQizX360Rp/F4aRpgpRgdP2ufB35N+1KppHmYjqIGSg==} + engines: {node: '>=16'} + hasBin: true + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3253,6 +3383,9 @@ packages: resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} + es-module-lexer@1.4.1: + resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} + es-module-lexer@1.6.0: resolution: {integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==} @@ -3275,6 +3408,131 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild-android-64@0.14.47: + resolution: {integrity: sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + esbuild-android-arm64@0.14.47: + resolution: {integrity: sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + esbuild-darwin-64@0.14.47: + resolution: {integrity: sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + esbuild-darwin-arm64@0.14.47: + resolution: {integrity: sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + esbuild-freebsd-64@0.14.47: + resolution: {integrity: sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + esbuild-freebsd-arm64@0.14.47: + resolution: {integrity: sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + esbuild-linux-32@0.14.47: + resolution: {integrity: sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + esbuild-linux-64@0.14.47: + resolution: {integrity: sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + esbuild-linux-arm64@0.14.47: + resolution: {integrity: sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + esbuild-linux-arm@0.14.47: + resolution: {integrity: sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + esbuild-linux-mips64le@0.14.47: + resolution: {integrity: sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + esbuild-linux-ppc64le@0.14.47: + resolution: {integrity: sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + esbuild-linux-riscv64@0.14.47: + resolution: {integrity: sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + esbuild-linux-s390x@0.14.47: + resolution: {integrity: sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + esbuild-netbsd-64@0.14.47: + resolution: {integrity: sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + esbuild-openbsd-64@0.14.47: + resolution: {integrity: sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + esbuild-sunos-64@0.14.47: + resolution: {integrity: sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + esbuild-windows-32@0.14.47: + resolution: {integrity: sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + esbuild-windows-64@0.14.47: + resolution: {integrity: sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + esbuild-windows-arm64@0.14.47: + resolution: {integrity: sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + esbuild@0.14.47: + resolution: {integrity: sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.25.2: resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} engines: {node: '>=18'} @@ -3545,6 +3803,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -3662,6 +3923,9 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -3835,6 +4099,10 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@13.0.0: + resolution: {integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==} + engines: {node: 20 || >=22} + globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -4322,6 +4590,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-to-ts@1.6.4: + resolution: {integrity: sha512-pR4yQ9DHz6itqswtHCm26mw45FSNfQ9rEQjosaZErhn5J3J2sIViQiz8rDaezjKAhFGpmsoczYVBgGHzFw/stA==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -4512,6 +4783,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -4613,6 +4888,10 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4631,6 +4910,19 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -4690,16 +4982,34 @@ packages: engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead + node-fetch@2.6.9: + resolution: {integrity: sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-fetch@3.3.2: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-pty@1.0.0: resolution: {integrity: sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==} node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -4882,6 +5192,10 @@ packages: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} + parse-ms@2.1.0: + resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==} + engines: {node: '>=6'} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -4896,6 +5210,9 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-case@3.0.4: resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} @@ -4925,9 +5242,19 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} + engines: {node: 20 || >=22} + path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@6.1.0: + resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} @@ -4939,6 +5266,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -5026,6 +5356,10 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + pretty-ms@7.0.1: + resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} + engines: {node: '>=10'} + pretty-ms@9.2.0: resolution: {integrity: sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==} engines: {node: '>=18'} @@ -5382,6 +5716,10 @@ packages: signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.0.2: + resolution: {integrity: sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==} + engines: {node: '>=14'} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -5570,6 +5908,10 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + tar@7.5.2: + resolution: {integrity: sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==} + engines: {node: '>=18'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -5577,6 +5919,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + time-span@4.0.0: + resolution: {integrity: sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==} + engines: {node: '>=10'} + tiny-jsonc@1.0.2: resolution: {integrity: sha512-f5QDAfLq6zIVSyCZQZhhyl0QS6MvAyTxgz4X4x3+EoCktNWEYJ6PeoEA97fyb98njpBNNi88ybpD7m+BDFXaCw==} @@ -5632,6 +5978,9 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -5652,6 +6001,23 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-morph@12.0.0: + resolution: {integrity: sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA==} + + ts-node@10.9.1: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + ts-node@11.0.0-beta.1: resolution: {integrity: sha512-WMSROP+1pU22Q/Tm40mjfRg130yD8i0g6ROST04ZpocfH8sl1zD75ON4XQMcBEVViXMVemJBH0alflE7xePdRA==} hasBin: true @@ -5666,6 +6032,9 @@ packages: '@swc/wasm': optional: true + ts-toolbelt@6.15.5: + resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==} + tsconfck@3.1.5: resolution: {integrity: sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==} engines: {node: ^18 || >=20} @@ -5764,6 +6133,11 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + typescript@5.7.3: resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} @@ -5774,6 +6148,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + ulid@2.4.0: resolution: {integrity: sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==} hasBin: true @@ -5788,6 +6167,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + unicorn-magic@0.1.0: resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} engines: {node: '>=18'} @@ -5954,6 +6337,9 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -5977,6 +6363,9 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -6062,6 +6451,14 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -6792,6 +7189,18 @@ snapshots: '@csstools/css-tokenizer@3.0.3': {} + '@edge-runtime/format@2.2.1': {} + + '@edge-runtime/node-utils@2.3.0': {} + + '@edge-runtime/ponyfill@2.4.2': {} + + '@edge-runtime/primitives@4.1.0': {} + + '@edge-runtime/vm@3.2.0': + dependencies: + '@edge-runtime/primitives': 4.1.0 + '@emnapi/core@1.4.3': dependencies: '@emnapi/wasi-threads': 1.0.2 @@ -7104,6 +7513,8 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@fastify/busboy@2.1.1': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -7331,6 +7742,12 @@ snapshots: optionalDependencies: '@types/node': 20.17.30 + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -7340,6 +7757,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -7369,6 +7790,19 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@mapbox/node-pre-gyp@2.0.3': + dependencies: + consola: 3.4.2 + detect-libc: 2.0.4 + https-proxy-agent: 7.0.6 + node-fetch: 2.6.9 + nopt: 8.1.0 + semver: 7.7.2 + tar: 7.5.2 + transitivePeerDependencies: + - encoding + - supports-color + '@modelcontextprotocol/sdk@1.10.1': dependencies: content-type: 1.0.5 @@ -7512,6 +7946,14 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@rollup/pluginutils@5.3.0(rollup@4.40.2)': + dependencies: + '@types/estree': 1.0.7 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.40.2 + '@rollup/rollup-android-arm-eabi@4.40.0': optional: true @@ -8073,12 +8515,12 @@ snapshots: postcss: 8.5.3 tailwindcss: 4.1.5 - '@tailwindcss/vite@4.1.5(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4))': + '@tailwindcss/vite@4.1.5(vite@6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4))': dependencies: '@tailwindcss/node': 4.1.5 '@tailwindcss/oxide': 4.1.5 tailwindcss: 4.1.5 - vite: 6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + vite: 6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) '@testing-library/dom@10.4.0': dependencies: @@ -8111,6 +8553,17 @@ snapshots: '@types/react': 18.3.20 '@types/react-dom': 18.3.6(@types/react@18.3.20) + '@ts-morph/common@0.11.1': + dependencies: + fast-glob: 3.3.3 + minimatch: 3.1.2 + mkdirp: 1.0.4 + path-browserify: 1.0.1 + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + '@tsconfig/node14@1.0.3': {} '@tsconfig/node16@1.0.4': {} @@ -8205,6 +8658,8 @@ snapshots: '@types/node': 20.17.30 form-data: 4.0.4 + '@types/node@16.18.11': {} + '@types/node@20.17.30': dependencies: undici-types: 6.19.8 @@ -8578,6 +9033,78 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.5.0': optional: true + '@vercel/build-utils@13.2.5': {} + + '@vercel/error-utils@2.0.3': {} + + '@vercel/nft@1.1.1(rollup@4.40.2)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.3 + '@rollup/pluginutils': 5.3.0(rollup@4.40.2) + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 13.0.0 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.3 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vercel/node@5.5.17(rollup@4.40.2)': + dependencies: + '@edge-runtime/node-utils': 2.3.0 + '@edge-runtime/primitives': 4.1.0 + '@edge-runtime/vm': 3.2.0 + '@types/node': 16.18.11 + '@vercel/build-utils': 13.2.5 + '@vercel/error-utils': 2.0.3 + '@vercel/nft': 1.1.1(rollup@4.40.2) + '@vercel/static-config': 3.1.2 + async-listen: 3.0.0 + cjs-module-lexer: 1.2.3 + edge-runtime: 2.5.9 + es-module-lexer: 1.4.1 + esbuild: 0.14.47 + etag: 1.8.1 + mime-types: 2.1.35 + node-fetch: 2.6.9 + path-to-regexp: 6.1.0 + path-to-regexp-updated: path-to-regexp@6.3.0 + ts-morph: 12.0.0 + ts-node: 10.9.1(@types/node@16.18.11)(typescript@4.9.5) + typescript: 4.9.5 + typescript5: typescript@5.9.3 + undici: 5.28.4 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - encoding + - rollup + - supports-color + + '@vercel/static-config@3.1.2': + dependencies: + ajv: 8.6.3 + json-schema-to-ts: 1.6.4 + ts-morph: 12.0.0 + + '@vitejs/plugin-react@4.4.1(vite@6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4))': + dependencies: + '@babel/core': 7.26.10 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-react@4.4.1(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4))': dependencies: '@babel/core': 7.26.10 @@ -8602,7 +9129,7 @@ snapshots: obug: 2.1.1 std-env: 3.10.0 tinyrainbow: 3.0.3 - vitest: 4.0.14(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) + vitest: 4.0.14(@edge-runtime/vm@3.2.0)(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) transitivePeerDependencies: - supports-color @@ -8613,7 +9140,7 @@ snapshots: eslint: 9.34.0(jiti@2.4.2) optionalDependencies: typescript: 5.8.3 - vitest: 4.0.14(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) + vitest: 4.0.14(@edge-runtime/vm@3.2.0)(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) transitivePeerDependencies: - supports-color @@ -8668,7 +9195,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.14(@types/node@22.14.1)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) + vitest: 4.0.14(@edge-runtime/vm@3.2.0)(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4) '@vitest/utils@4.0.14': dependencies: @@ -8699,6 +9226,8 @@ snapshots: '@zeit/schemas@2.36.0': {} + abbrev@3.0.1: {} + ably@2.14.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@ably/msgpack-js': 0.4.0 @@ -8724,6 +9253,10 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -8756,6 +9289,13 @@ snapshots: require-from-string: 2.0.2 uri-js: 4.4.1 + ajv@8.6.3: + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -8873,6 +9413,10 @@ snapshots: async-function@1.0.0: {} + async-listen@3.0.0: {} + + async-listen@3.0.1: {} + async-mutex@0.5.0: dependencies: tslib: 2.8.1 @@ -8881,6 +9425,8 @@ snapshots: dependencies: retry: 0.13.1 + async-sema@3.1.1: {} + async@3.2.6: {} asynckit@0.4.0: {} @@ -8909,6 +9455,10 @@ snapshots: dependencies: safe-buffer: 5.1.2 + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -9096,10 +9646,14 @@ snapshots: dependencies: readdirp: 4.1.2 + chownr@3.0.0: {} + ci-info@3.9.0: {} ci-info@4.2.0: {} + cjs-module-lexer@1.2.3: {} + clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -9142,6 +9696,8 @@ snapshots: clsx@2.1.1: {} + code-block-writer@10.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -9199,6 +9755,8 @@ snapshots: content-type@1.0.5: {} + convert-hrtime@3.0.0: {} + convert-source-map@2.0.0: {} cookie-signature@1.2.2: {} @@ -9216,6 +9774,8 @@ snapshots: corser@2.0.1: {} + create-require@1.1.1: {} + cross-spawn@6.0.6: dependencies: nice-try: 1.0.5 @@ -9368,6 +9928,18 @@ snapshots: dependencies: safe-buffer: 5.2.1 + edge-runtime@2.5.9: + dependencies: + '@edge-runtime/format': 2.2.1 + '@edge-runtime/ponyfill': 2.4.2 + '@edge-runtime/vm': 3.2.0 + async-listen: 3.0.1 + mri: 1.2.0 + picocolors: 1.0.0 + pretty-ms: 7.0.1 + signal-exit: 4.0.2 + time-span: 4.0.0 + ee-first@1.1.1: {} ejs@3.1.10: @@ -9476,6 +10048,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@1.4.1: {} + es-module-lexer@1.6.0: {} es-module-lexer@1.7.0: {} @@ -9501,6 +10075,89 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild-android-64@0.14.47: + optional: true + + esbuild-android-arm64@0.14.47: + optional: true + + esbuild-darwin-64@0.14.47: + optional: true + + esbuild-darwin-arm64@0.14.47: + optional: true + + esbuild-freebsd-64@0.14.47: + optional: true + + esbuild-freebsd-arm64@0.14.47: + optional: true + + esbuild-linux-32@0.14.47: + optional: true + + esbuild-linux-64@0.14.47: + optional: true + + esbuild-linux-arm64@0.14.47: + optional: true + + esbuild-linux-arm@0.14.47: + optional: true + + esbuild-linux-mips64le@0.14.47: + optional: true + + esbuild-linux-ppc64le@0.14.47: + optional: true + + esbuild-linux-riscv64@0.14.47: + optional: true + + esbuild-linux-s390x@0.14.47: + optional: true + + esbuild-netbsd-64@0.14.47: + optional: true + + esbuild-openbsd-64@0.14.47: + optional: true + + esbuild-sunos-64@0.14.47: + optional: true + + esbuild-windows-32@0.14.47: + optional: true + + esbuild-windows-64@0.14.47: + optional: true + + esbuild-windows-arm64@0.14.47: + optional: true + + esbuild@0.14.47: + optionalDependencies: + esbuild-android-64: 0.14.47 + esbuild-android-arm64: 0.14.47 + esbuild-darwin-64: 0.14.47 + esbuild-darwin-arm64: 0.14.47 + esbuild-freebsd-64: 0.14.47 + esbuild-freebsd-arm64: 0.14.47 + esbuild-linux-32: 0.14.47 + esbuild-linux-64: 0.14.47 + esbuild-linux-arm: 0.14.47 + esbuild-linux-arm64: 0.14.47 + esbuild-linux-mips64le: 0.14.47 + esbuild-linux-ppc64le: 0.14.47 + esbuild-linux-riscv64: 0.14.47 + esbuild-linux-s390x: 0.14.47 + esbuild-netbsd-64: 0.14.47 + esbuild-openbsd-64: 0.14.47 + esbuild-sunos-64: 0.14.47 + esbuild-windows-32: 0.14.47 + esbuild-windows-64: 0.14.47 + esbuild-windows-arm64: 0.14.47 + esbuild@0.25.2: optionalDependencies: '@esbuild/aix-ppc64': 0.25.2 @@ -9980,6 +10637,8 @@ snapshots: estraverse@5.3.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.7 @@ -10132,6 +10791,8 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-uri-to-path@1.0.0: {} + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -10315,6 +10976,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@13.0.0: + dependencies: + minimatch: 10.1.1 + minipass: 7.1.2 + path-scurry: 2.0.1 + globals@11.12.0: {} globals@13.24.0: @@ -10821,6 +11488,11 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-to-ts@1.6.4: + dependencies: + '@types/json-schema': 7.0.15 + ts-toolbelt: 6.15.5 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -10990,6 +11662,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.4: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -11063,6 +11737,10 @@ snapshots: min-indent@1.0.1: {} + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -11079,6 +11757,14 @@ snapshots: minipass@7.1.2: {} + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + mkdirp@1.0.4: {} + + mri@1.2.0: {} + mrmime@2.0.1: {} ms@2.0.0: {} @@ -11127,18 +11813,28 @@ snapshots: node-domexception@1.0.0: {} + node-fetch@2.6.9: + dependencies: + whatwg-url: 5.0.0 + node-fetch@3.3.2: dependencies: data-uri-to-buffer: 4.0.1 fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-gyp-build@4.8.4: {} + node-pty@1.0.0: dependencies: nan: 2.22.2 node-releases@2.0.19: {} + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -11376,6 +12072,8 @@ snapshots: index-to-position: 1.1.0 type-fest: 4.40.0 + parse-ms@2.1.0: {} + parse-ms@4.0.0: {} parse5@7.2.1: @@ -11389,6 +12087,8 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + path-browserify@1.0.1: {} + path-case@3.0.4: dependencies: dot-case: 3.0.4 @@ -11411,14 +12111,25 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.4 + minipass: 7.1.2 + path-to-regexp@3.3.0: {} + path-to-regexp@6.1.0: {} + + path-to-regexp@6.3.0: {} + path-to-regexp@8.2.0: {} path-type@4.0.0: {} pathe@2.0.3: {} + picocolors@1.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -11480,6 +12191,10 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + pretty-ms@7.0.1: + dependencies: + parse-ms: 2.1.0 + pretty-ms@9.2.0: dependencies: parse-ms: 4.0.0 @@ -11941,6 +12656,8 @@ snapshots: signal-exit@3.0.7: {} + signal-exit@4.0.2: {} + signal-exit@4.1.0: {} sirv@3.0.2: @@ -12144,6 +12861,14 @@ snapshots: tapable@2.2.1: {} + tar@7.5.2: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -12152,6 +12877,10 @@ snapshots: dependencies: any-promise: 1.3.0 + time-span@4.0.0: + dependencies: + convert-hrtime: 3.0.0 + tiny-jsonc@1.0.2: {} tinybench@2.9.0: {} @@ -12199,6 +12928,8 @@ snapshots: dependencies: tldts: 6.1.86 + tr46@0.0.3: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -12219,6 +12950,29 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-morph@12.0.0: + dependencies: + '@ts-morph/common': 0.11.1 + code-block-writer: 10.1.1 + + ts-node@10.9.1(@types/node@16.18.11)(typescript@4.9.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 16.18.11 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + ts-node@11.0.0-beta.1(@types/node@20.17.30)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -12235,6 +12989,8 @@ snapshots: typescript: 5.8.3 v8-compile-cache-lib: 3.0.1 + ts-toolbelt@6.15.5: {} + tsconfck@3.1.5(typescript@5.7.3): optionalDependencies: typescript: 5.7.3 @@ -12362,10 +13118,14 @@ snapshots: transitivePeerDependencies: - supports-color + typescript@4.9.5: {} + typescript@5.7.3: {} typescript@5.8.3: {} + typescript@5.9.3: {} + ulid@2.4.0: {} unbox-primitive@1.1.0: @@ -12379,6 +13139,10 @@ snapshots: undici-types@6.21.0: {} + undici@5.28.4: + dependencies: + '@fastify/busboy': 2.1.1 + unicorn-magic@0.1.0: {} unicorn-magic@0.3.0: {} @@ -12454,17 +13218,32 @@ snapshots: vary@1.1.2: {} - vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)): + vite-tsconfig-paths@5.1.4(typescript@5.7.3)(vite@6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)): dependencies: debug: 4.4.0(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.7.3) optionalDependencies: - vite: 6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) + vite: 6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) transitivePeerDependencies: - supports-color - typescript + vite@6.3.4(@types/node@16.18.11)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4): + dependencies: + esbuild: 0.25.4 + fdir: 6.4.4(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.3 + rollup: 4.40.2 + tinyglobby: 0.2.13 + optionalDependencies: + '@types/node': 16.18.11 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.29.2 + tsx: 4.19.4 + vite@6.3.4(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4): dependencies: esbuild: 0.25.4 @@ -12495,7 +13274,7 @@ snapshots: lightningcss: 1.29.2 tsx: 4.19.4 - vitest@4.0.14(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4): + vitest@4.0.14(@edge-runtime/vm@3.2.0)(@types/node@20.17.30)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4): dependencies: '@vitest/expect': 4.0.14 '@vitest/mocker': 4.0.14(vite@6.3.4(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) @@ -12518,6 +13297,7 @@ snapshots: vite: 6.3.4(@types/node@20.17.30)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) why-is-node-running: 2.3.0 optionalDependencies: + '@edge-runtime/vm': 3.2.0 '@types/node': 20.17.30 '@vitest/ui': 4.0.14(vitest@4.0.14) jsdom: 26.1.0 @@ -12534,7 +13314,7 @@ snapshots: - tsx - yaml - vitest@4.0.14(@types/node@22.14.1)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4): + vitest@4.0.14(@edge-runtime/vm@3.2.0)(@types/node@22.14.1)(@vitest/ui@4.0.14)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(tsx@4.19.4): dependencies: '@vitest/expect': 4.0.14 '@vitest/mocker': 4.0.14(vite@6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4)) @@ -12557,6 +13337,7 @@ snapshots: vite: 6.3.4(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(tsx@4.19.4) why-is-node-running: 2.3.0 optionalDependencies: + '@edge-runtime/vm': 3.2.0 '@types/node': 22.14.1 '@vitest/ui': 4.0.14(vitest@4.0.14) jsdom: 26.1.0 @@ -12583,6 +13364,8 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} webidl-conversions@7.0.0: {} @@ -12602,6 +13385,11 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 @@ -12702,6 +13490,10 @@ snapshots: yallist@3.1.1: {} + yallist@5.0.0: {} + + yn@3.1.1: {} + yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.2: {} From ff5105fddb61e1bfc789bcb9a85e4d7d9b6a8024 Mon Sep 17 00:00:00 2001 From: Kenneth Kalmer Date: Tue, 13 Jan 2026 19:43:45 +0000 Subject: [PATCH 06/12] feat(web-cli): update example app for signed config authentication - App.tsx: Call /api/sign endpoint to get signed credentials - Store signedConfig/signature in localStorage/sessionStorage (domain-scoped) - Pass signed credentials to AblyCliTerminal component - Remove accessToken fields from forms (not confirmed to work with signing yet) - Handle migration from old credential storage format (clear old keys) - Form authentication now works via server-side signing Co-Authored-By: Claude Sonnet 4.5 (1M context) --- examples/web-cli/src/App.tsx | 238 ++++++++++-------- .../web-cli/src/components/AuthScreen.tsx | 72 +++--- .../web-cli/src/components/AuthSettings.tsx | 132 +++++----- 3 files changed, 229 insertions(+), 213 deletions(-) diff --git a/examples/web-cli/src/App.tsx b/examples/web-cli/src/App.tsx index c8093a40..c5dd584c 100644 --- a/examples/web-cli/src/App.tsx +++ b/examples/web-cli/src/App.tsx @@ -32,20 +32,25 @@ const getCIAuthToken = (): string | undefined => { return (window as CliWindow).__ABLY_CLI_CI_AUTH_TOKEN__; }; -// Get credentials from various sources +// Get signed credentials from various sources const getInitialCredentials = () => { const urlParams = new URLSearchParams(window.location.search); - + // Get the domain from the WebSocket URL for scoping const wsUrl = getWebSocketUrl(); const wsDomain = new URL(wsUrl).host; - + // Check if we should clear credentials (for testing) if (urlParams.get('clearCredentials') === 'true') { + // Clear new signed format + localStorage.removeItem(`ably.web-cli.signedConfig.${wsDomain}`); + localStorage.removeItem(`ably.web-cli.signature.${wsDomain}`); + localStorage.removeItem(`ably.web-cli.rememberCredentials.${wsDomain}`); + sessionStorage.removeItem(`ably.web-cli.signedConfig.${wsDomain}`); + sessionStorage.removeItem(`ably.web-cli.signature.${wsDomain}`); + // Also clear old format (migration) localStorage.removeItem(`ably.web-cli.apiKey.${wsDomain}`); localStorage.removeItem(`ably.web-cli.accessToken.${wsDomain}`); - localStorage.removeItem(`ably.web-cli.rememberCredentials.${wsDomain}`); - // Also clear from sessionStorage sessionStorage.removeItem(`ably.web-cli.apiKey.${wsDomain}`); sessionStorage.removeItem(`ably.web-cli.accessToken.${wsDomain}`); // Remove the clearCredentials param from URL @@ -53,59 +58,77 @@ const getInitialCredentials = () => { cleanUrl.searchParams.delete('clearCredentials'); window.history.replaceState(null, '', cleanUrl.toString()); } - - // Check localStorage for persisted credentials (if user chose to remember) + + // Check query parameters (only in development/test environments) + const qsSignedConfig = urlParams.get('signedConfig'); + const qsSignature = urlParams.get('signature'); + + if (qsSignedConfig && qsSignature) { + // Security check: only allow query param auth in development/test environments + const isProduction = import.meta.env.PROD && + !window.location.hostname.includes('localhost') && + !window.location.hostname.includes('127.0.0.1'); + + if (isProduction) { + console.error('[App] Security Warning: Signed credentials in query parameters are not allowed in production.'); + console.error('[App] Credentials contain API keys that can leak through browser history, server logs, and shared URLs.'); + // Clear the sensitive query parameters from the URL + const cleanUrl = new URL(window.location.href); + cleanUrl.searchParams.delete('signedConfig'); + cleanUrl.searchParams.delete('signature'); + cleanUrl.searchParams.delete('clearCredentials'); + window.history.replaceState(null, '', cleanUrl.toString()); + // Don't use these credentials - fall through to storage check + } else { + console.log('[App] Using signed config from query parameters (dev/test mode)'); + return { + signedConfig: qsSignedConfig, + signature: qsSignature, + source: 'query' as const + }; + } + } + + // Check localStorage for persisted signed credentials (if user chose to remember) const rememberCredentials = localStorage.getItem(`ably.web-cli.rememberCredentials.${wsDomain}`) === 'true'; if (rememberCredentials) { - const storedApiKey = localStorage.getItem(`ably.web-cli.apiKey.${wsDomain}`); - const storedAccessToken = localStorage.getItem(`ably.web-cli.accessToken.${wsDomain}`); - if (storedApiKey) { - return { - apiKey: storedApiKey, - accessToken: storedAccessToken || undefined, + const storedSignedConfig = localStorage.getItem(`ably.web-cli.signedConfig.${wsDomain}`); + const storedSignature = localStorage.getItem(`ably.web-cli.signature.${wsDomain}`); + if (storedSignedConfig && storedSignature) { + console.log('[App] Using signed config from localStorage'); + return { + signedConfig: storedSignedConfig, + signature: storedSignature, source: 'localStorage' as const }; } } - - // Check sessionStorage for session-only credentials - const sessionApiKey = sessionStorage.getItem(`ably.web-cli.apiKey.${wsDomain}`); - const sessionAccessToken = sessionStorage.getItem(`ably.web-cli.accessToken.${wsDomain}`); - if (sessionApiKey) { - return { - apiKey: sessionApiKey, - accessToken: sessionAccessToken || undefined, + + // Check sessionStorage for session-only signed credentials + const sessionSignedConfig = sessionStorage.getItem(`ably.web-cli.signedConfig.${wsDomain}`); + const sessionSignature = sessionStorage.getItem(`ably.web-cli.signature.${wsDomain}`); + if (sessionSignedConfig && sessionSignature) { + console.log('[App] Using signed config from sessionStorage'); + return { + signedConfig: sessionSignedConfig, + signature: sessionSignature, source: 'session' as const }; } - // Then check query parameters (only in non-production environments) - const qsApiKey = urlParams.get('apikey') || urlParams.get('apiKey'); - const qsAccessToken = urlParams.get('accessToken') || urlParams.get('accesstoken'); - - // Security check: only allow query param auth in development/test environments - const isProduction = import.meta.env.PROD && !window.location.hostname.includes('localhost') && !window.location.hostname.includes('127.0.0.1'); - - if (qsApiKey) { - if (isProduction) { - console.error('Security Warning: API keys in query parameters are not allowed in production environments.'); - // Clear the sensitive query parameters from the URL - const cleanUrl = new URL(window.location.href); - cleanUrl.searchParams.delete('apikey'); - cleanUrl.searchParams.delete('apiKey'); - cleanUrl.searchParams.delete('accessToken'); - cleanUrl.searchParams.delete('accesstoken'); - window.history.replaceState(null, '', cleanUrl.toString()); - } else { - return { - apiKey: qsApiKey, - accessToken: qsAccessToken || undefined, - source: 'query' as const - }; - } + // Check for old format credentials (migration) + const oldApiKey = localStorage.getItem(`ably.web-cli.apiKey.${wsDomain}`) || + sessionStorage.getItem(`ably.web-cli.apiKey.${wsDomain}`); + if (oldApiKey) { + console.warn('[App] Found old credential format. Please re-authenticate with signed credentials.'); + // Clear old format + localStorage.removeItem(`ably.web-cli.apiKey.${wsDomain}`); + localStorage.removeItem(`ably.web-cli.accessToken.${wsDomain}`); + sessionStorage.removeItem(`ably.web-cli.apiKey.${wsDomain}`); + sessionStorage.removeItem(`ably.web-cli.accessToken.${wsDomain}`); } - return { apiKey: undefined, accessToken: undefined, source: 'none' as const }; + return { signedConfig: undefined, signature: undefined, source: 'none' as const }; }; function App() { @@ -117,11 +140,11 @@ function App() { const [displayMode, setDisplayMode] = useState<"fullscreen" | "drawer">(initialMode); const [showAuthSettings, setShowAuthSettings] = useState(false); - // Initialize credentials + // Initialize signed credentials const initialCreds = getInitialCredentials(); - const [apiKey, setApiKey] = useState(initialCreds.apiKey); - const [accessToken, setAccessToken] = useState(initialCreds.accessToken); - const [isAuthenticated, setIsAuthenticated] = useState(Boolean(initialCreds.apiKey && initialCreds.apiKey.trim())); + const [signedConfig, setSignedConfig] = useState(initialCreds.signedConfig); + const [signature, setSignature] = useState(initialCreds.signature); + const [isAuthenticated, setIsAuthenticated] = useState(Boolean(initialCreds.signedConfig && initialCreds.signature)); const [authSource, setAuthSource] = useState(initialCreds.source); // Get the URL and domain early for use in state initialization const currentWebsocketUrl = getWebSocketUrl(); @@ -144,64 +167,79 @@ function App() { }, []); // Handle authentication - const handleAuthenticate = useCallback((newApiKey: string, newAccessToken: string, remember?: boolean) => { - // Clear any existing session data when credentials change (domain-scoped) - sessionStorage.removeItem(`ably.cli.sessionId.${wsDomain}`); - sessionStorage.removeItem(`ably.cli.secondarySessionId.${wsDomain}`); - sessionStorage.removeItem(`ably.cli.isSplit.${wsDomain}`); - - setApiKey(newApiKey); - setAccessToken(newAccessToken); - setIsAuthenticated(true); - setShowAuthSettings(false); - - // Determine if we should remember based on parameter or current state - const shouldRemember = remember !== undefined ? remember : rememberCredentials; - - if (shouldRemember) { - // Store in localStorage for persistence (domain-scoped) - localStorage.setItem(`ably.web-cli.apiKey.${wsDomain}`, newApiKey); - localStorage.setItem(`ably.web-cli.rememberCredentials.${wsDomain}`, 'true'); - if (newAccessToken) { - localStorage.setItem(`ably.web-cli.accessToken.${wsDomain}`, newAccessToken); - } else { - localStorage.removeItem(`ably.web-cli.accessToken.${wsDomain}`); + const handleAuthenticate = useCallback(async (newApiKey: string, remember?: boolean) => { + try { + // Call /api/sign endpoint to get signed config + const response = await fetch('/api/sign', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + apiKey: newApiKey, + bypassRateLimit: false + }) + }); + + if (!response.ok) { + const error = await response.json(); + console.error('[App] Failed to sign credentials:', error); + throw new Error(error.error || 'Failed to sign credentials'); } - setAuthSource('localStorage'); - } else { - // Store only in sessionStorage (domain-scoped) - sessionStorage.setItem(`ably.web-cli.apiKey.${wsDomain}`, newApiKey); - if (newAccessToken) { - sessionStorage.setItem(`ably.web-cli.accessToken.${wsDomain}`, newAccessToken); + + const { signedConfig: newSignedConfig, signature: newSignature } = await response.json(); + + // Clear any existing session data when credentials change (domain-scoped) + sessionStorage.removeItem(`ably.cli.sessionId.${wsDomain}`); + sessionStorage.removeItem(`ably.cli.secondarySessionId.${wsDomain}`); + sessionStorage.removeItem(`ably.cli.isSplit.${wsDomain}`); + + setSignedConfig(newSignedConfig); + setSignature(newSignature); + setIsAuthenticated(true); + setShowAuthSettings(false); + + // Determine if we should remember based on parameter or current state + const shouldRemember = remember !== undefined ? remember : rememberCredentials; + + if (shouldRemember) { + // Store in localStorage for persistence (domain-scoped) + localStorage.setItem(`ably.web-cli.signedConfig.${wsDomain}`, newSignedConfig); + localStorage.setItem(`ably.web-cli.signature.${wsDomain}`, newSignature); + localStorage.setItem(`ably.web-cli.rememberCredentials.${wsDomain}`, 'true'); + setAuthSource('localStorage'); } else { - sessionStorage.removeItem(`ably.web-cli.accessToken.${wsDomain}`); + // Store only in sessionStorage (domain-scoped) + sessionStorage.setItem(`ably.web-cli.signedConfig.${wsDomain}`, newSignedConfig); + sessionStorage.setItem(`ably.web-cli.signature.${wsDomain}`, newSignature); + // Clear from localStorage if it was there (domain-scoped) + localStorage.removeItem(`ably.web-cli.signedConfig.${wsDomain}`); + localStorage.removeItem(`ably.web-cli.signature.${wsDomain}`); + localStorage.removeItem(`ably.web-cli.rememberCredentials.${wsDomain}`); + setAuthSource('session'); } - // Clear from localStorage if it was there (domain-scoped) - localStorage.removeItem(`ably.web-cli.apiKey.${wsDomain}`); - localStorage.removeItem(`ably.web-cli.accessToken.${wsDomain}`); - localStorage.removeItem(`ably.web-cli.rememberCredentials.${wsDomain}`); - setAuthSource('session'); + + setRememberCredentials(shouldRemember); + } catch (error) { + console.error('[App] Authentication error:', error); + throw error; } - - setRememberCredentials(shouldRemember); }, [rememberCredentials, wsDomain]); // Handle auth settings save - const handleAuthSettingsSave = useCallback((newApiKey: string, newAccessToken: string, remember: boolean) => { + const handleAuthSettingsSave = useCallback(async (newApiKey: string, remember: boolean) => { if (newApiKey) { - handleAuthenticate(newApiKey, newAccessToken, remember); + await handleAuthenticate(newApiKey, remember); } else { // Clear all credentials - go back to auth screen (domain-scoped) sessionStorage.removeItem(`ably.cli.sessionId.${wsDomain}`); sessionStorage.removeItem(`ably.cli.secondarySessionId.${wsDomain}`); sessionStorage.removeItem(`ably.cli.isSplit.${wsDomain}`); - sessionStorage.removeItem(`ably.web-cli.apiKey.${wsDomain}`); - sessionStorage.removeItem(`ably.web-cli.accessToken.${wsDomain}`); - localStorage.removeItem(`ably.web-cli.apiKey.${wsDomain}`); - localStorage.removeItem(`ably.web-cli.accessToken.${wsDomain}`); + sessionStorage.removeItem(`ably.web-cli.signedConfig.${wsDomain}`); + sessionStorage.removeItem(`ably.web-cli.signature.${wsDomain}`); + localStorage.removeItem(`ably.web-cli.signedConfig.${wsDomain}`); + localStorage.removeItem(`ably.web-cli.signature.${wsDomain}`); localStorage.removeItem(`ably.web-cli.rememberCredentials.${wsDomain}`); - setApiKey(undefined); - setAccessToken(undefined); + setSignedConfig(undefined); + setSignature(undefined); setIsAuthenticated(false); setShowAuthSettings(false); setRememberCredentials(false); @@ -220,11 +258,11 @@ function App() { // Prepare the terminal component instance to pass it down const termRef = useRef(null); const TerminalInstance = useCallback(() => ( - isAuthenticated && apiKey && apiKey.trim() ? ( + isAuthenticated && signedConfig && signature ? ( ) : null - ), [isAuthenticated, apiKey, accessToken, handleConnectionChange, handleSessionEnd, handleSessionId, currentWebsocketUrl]); + ), [isAuthenticated, signedConfig, signature, handleConnectionChange, handleSessionEnd, handleSessionId, currentWebsocketUrl]); // Show auth screen if not authenticated if (!isAuthenticated) { @@ -323,8 +360,7 @@ function App() { isOpen={showAuthSettings} onClose={() => setShowAuthSettings(false)} onSave={handleAuthSettingsSave} - currentApiKey={apiKey} - currentAccessToken={accessToken} + currentSignedConfig={signedConfig} rememberCredentials={rememberCredentials} />
diff --git a/examples/web-cli/src/components/AuthScreen.tsx b/examples/web-cli/src/components/AuthScreen.tsx index f8e1d180..4639a966 100644 --- a/examples/web-cli/src/components/AuthScreen.tsx +++ b/examples/web-cli/src/components/AuthScreen.tsx @@ -2,45 +2,57 @@ import React, { useState } from 'react'; import { Key, Lock, Terminal, AlertCircle, ArrowRight, Save, RefreshCw } from 'lucide-react'; interface AuthScreenProps { - onAuthenticate: (apiKey: string, accessToken: string, remember?: boolean) => void; + onAuthenticate: (apiKey: string, remember?: boolean) => void; rememberCredentials: boolean; onRememberChange: (remember: boolean) => void; } -export const AuthScreen: React.FC = ({ - onAuthenticate, +export const AuthScreen: React.FC = ({ + onAuthenticate, rememberCredentials, - onRememberChange + onRememberChange }) => { const [apiKey, setApiKey] = useState(''); - const [accessToken, setAccessToken] = useState(''); const [error, setError] = useState(''); + const [isLoading, setIsLoading] = useState(false); // Check if there are saved credentials to clear const hasSavedCredentials = localStorage.getItem('ably.web-cli.apiKey') !== null; - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); + setIsLoading(true); - if (!apiKey.trim()) { - setError('API Key is required to connect to Ably'); - return; - } + try { + if (!apiKey.trim()) { + setError('API Key is required to connect to Ably'); + return; + } - // Basic validation for API key format - if (!apiKey.includes(':')) { - setError('API Key should be in the format: app_name.key_name:key_secret'); - return; - } + // Basic validation for API key format + if (!apiKey.includes(':')) { + setError('API Key should be in the format: app_name.key_name:key_secret'); + return; + } - onAuthenticate(apiKey.trim(), accessToken.trim(), rememberCredentials); + await onAuthenticate(apiKey.trim(), rememberCredentials); + } catch (error) { + console.error('[AuthScreen] Authentication failed:', error); + setError(error instanceof Error ? error.message : 'Authentication failed'); + } finally { + setIsLoading(false); + } }; const handleClearSavedCredentials = () => { + // Clear new signed format + localStorage.removeItem('ably.web-cli.signedConfig'); + localStorage.removeItem('ably.web-cli.signature'); + localStorage.removeItem('ably.web-cli.rememberCredentials'); + // Also clear old format (migration) localStorage.removeItem('ably.web-cli.apiKey'); localStorage.removeItem('ably.web-cli.accessToken'); - localStorage.removeItem('ably.web-cli.rememberCredentials'); setError(''); // Force a refresh to show the change window.location.reload(); @@ -81,27 +93,6 @@ export const AuthScreen: React.FC = ({

-
- - { - setAccessToken(e.target.value); - setError(''); // Clear error when user types - }} - placeholder="Your JWT access token" - className="w-full px-4 py-3 bg-gray-800 border border-gray-700 rounded-md text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" - /> -

- Only required if you're using token authentication instead of an API key -

-
- {error && (
@@ -125,9 +116,10 @@ export const AuthScreen: React.FC = ({ diff --git a/examples/web-cli/src/components/AuthSettings.tsx b/examples/web-cli/src/components/AuthSettings.tsx index 97c9cf95..1fd9e48e 100644 --- a/examples/web-cli/src/components/AuthSettings.tsx +++ b/examples/web-cli/src/components/AuthSettings.tsx @@ -4,51 +4,59 @@ import { X, Key, Lock, AlertCircle, CheckCircle, Save } from 'lucide-react'; interface AuthSettingsProps { isOpen: boolean; onClose: () => void; - onSave: (apiKey: string, accessToken: string, remember: boolean) => void; - currentApiKey?: string; - currentAccessToken?: string; + onSave: (apiKey: string, remember: boolean) => void; + currentSignedConfig?: string; rememberCredentials: boolean; } // Helper function to redact sensitive credentials const redactCredential = (credential: string | undefined): string => { if (!credential) return ''; - + // For API keys in format "appId.keyId:secret" if (credential.includes(':')) { - const [keyName, secret] = credential.split(':'); + const [keyName] = credential.split(':'); // Show full app ID and key ID, but redact the secret return `${keyName}:****`; } - + // For tokens, show first few and last few characters if (credential.length > 20) { return `${credential.substring(0, 6)}...${credential.substring(credential.length - 4)}`; } - + return credential.substring(0, 4) + '...'; }; +// Helper to extract API key from signed config +const extractApiKey = (signedConfig: string | undefined): string => { + if (!signedConfig) return ''; + try { + const config = JSON.parse(signedConfig); + return config.apiKey || ''; + } catch { + return ''; + } +}; + export const AuthSettings: React.FC = ({ isOpen, onClose, onSave, - currentApiKey = '', - currentAccessToken = '', + currentSignedConfig = '', rememberCredentials }) => { + const currentApiKey = extractApiKey(currentSignedConfig); const [apiKey, setApiKey] = useState(currentApiKey); - const [accessToken, setAccessToken] = useState(currentAccessToken); const [remember, setRemember] = useState(rememberCredentials); const [error, setError] = useState(''); useEffect(() => { - setApiKey(currentApiKey); - setAccessToken(currentAccessToken); + setApiKey(extractApiKey(currentSignedConfig)); setRemember(rememberCredentials); - }, [currentApiKey, currentAccessToken, rememberCredentials, isOpen]); + }, [currentSignedConfig, rememberCredentials, isOpen]); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); @@ -63,7 +71,12 @@ export const AuthSettings: React.FC = ({ return; } - onSave(apiKey.trim(), accessToken.trim(), remember); + try { + await onSave(apiKey.trim(), remember); + } catch (error) { + console.error('[AuthSettings] Save failed:', error); + setError(error instanceof Error ? error.message : 'Failed to save credentials'); + } }; if (!isOpen) return null; @@ -90,15 +103,10 @@ export const AuthSettings: React.FC = ({

API Key: {redactCredential(currentApiKey)}

- {currentAccessToken && ( -

- Access Token: {redactCredential(currentAccessToken)} -

- )}