diff --git a/.env.example b/.env.example
index 98a89aa2..305db3d7 100644
--- a/.env.example
+++ b/.env.example
@@ -10,9 +10,16 @@ E2E_ABLY_ABLY_ACCESS_TOKEN=your_control_api_access_token
# Set this to skip E2E tests even when API key is present (for CI or local dev when you don't want to run E2E tests)
# SKIP_E2E_TESTS=true
-# CI bypass secret for rate limit bypass in parallel tests
-# This should match the CI_BYPASS_SECRET on the terminal server
-CI_BYPASS_SECRET=your_ci_bypass_secret
+# Terminal server signing secret
+# MUST match the live server's SIGNING_SECRET configuration at wss://web-cli.ably.com
+# Used to:
+# 1. Sign credentials for HMAC authentication (signedConfig + signature)
+# 2. Bypass rate limiting in tests (bypassRateLimit flag in signed config)
+# Contact platform team for the actual secret
+TERMINAL_SERVER_SIGNING_SECRET=your_signing_secret
+
+# Legacy: CI_BYPASS_SECRET (still supported as fallback)
+# CI_BYPASS_SECRET=your_ci_bypass_secret
# Terminal server URL for local testing (defaults to production)
# TERMINAL_SERVER_URL=ws://localhost:8080
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
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..e3c30cee
--- /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.TERMINAL_SERVER_SIGNING_SECRET ||
+ process.env.SIGNING_SECRET ||
+ process.env.CI_BYPASS_SECRET ||
+ null
+ );
+}
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..dfd9d7ec 100644
--- a/examples/web-cli/src/components/AuthScreen.tsx
+++ b/examples/web-cli/src/components/AuthScreen.tsx
@@ -2,45 +2,75 @@ 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('');
-
- // Check if there are saved credentials to clear
- const hasSavedCredentials = localStorage.getItem('ably.web-cli.apiKey') !== null;
+ const [isLoading, setIsLoading] = useState(false);
+
+ // Check if there are saved credentials to clear (domain-scoped or old format)
+ const hasSavedCredentials = (() => {
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key?.startsWith('ably.web-cli.signedConfig.') ||
+ key?.startsWith('ably.web-cli.apiKey')) {
+ return true;
+ }
+ }
+ return false;
+ })();
- 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 = () => {
- localStorage.removeItem('ably.web-cli.apiKey');
- localStorage.removeItem('ably.web-cli.accessToken');
- localStorage.removeItem('ably.web-cli.rememberCredentials');
+ // Clear all domain-scoped signed config keys
+ const keysToRemove: string[] = [];
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key?.startsWith('ably.web-cli.signedConfig.') ||
+ key?.startsWith('ably.web-cli.signature.') ||
+ key?.startsWith('ably.web-cli.rememberCredentials.') ||
+ key?.startsWith('ably.web-cli.apiKey') ||
+ key?.startsWith('ably.web-cli.accessToken')) {
+ keysToRemove.push(key);
+ }
+ }
+
+ // Remove all identified keys
+ keysToRemove.forEach(key => localStorage.removeItem(key));
+
setError('');
// Force a refresh to show the change
window.location.reload();
@@ -81,27 +111,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 +134,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)}
-
- )}