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

- )}