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)}
-
- )}