diff --git a/features/bot-defense/frontend-components/src/components/BotDefenseGate.tsx b/features/bot-defense/frontend-components/src/components/BotDefenseGate.tsx new file mode 100644 index 000000000..69539ce57 --- /dev/null +++ b/features/bot-defense/frontend-components/src/components/BotDefenseGate.tsx @@ -0,0 +1,301 @@ +/** + * BotDefenseGate Component + * + * Wraps VibeCheck liveness verification with platform theming, error handling, + * and cryptographic proof generation for server-side integrity validation. + */ + +import React, { useState, useEffect } from 'react'; +import styled from '@lilith/ui-styled-components'; +import type { SessionDTO, VerificationResultDTO } from '@lilith/bot-defense'; +import { generateVerificationProof } from '../utils/crypto'; +import { LoadingSpinner } from './LoadingSpinner'; + +// TODO: Replace with actual VibeCheck SDK when published +// import { VibeCheck } from '@lilithftw/vibecheck-react'; + +export interface BotDefenseGateProps { + /** Callback when verification succeeds */ + onSuccess: (sessionId: string) => void; + + /** Callback when user chooses to skip verification */ + onSkip?: () => void; + + /** Whether to allow skipping (default: false) */ + allowSkip?: boolean; + + /** Session token for authentication */ + sessionToken: string; + + /** API base URL (default: /api) */ + apiBaseUrl?: string; +} + +const Container = styled.div` + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + max-width: 600px; + margin: 0 auto; +`; + +const Title = styled.h2` + font-size: 1.75rem; + font-weight: 600; + color: #111827; + margin-bottom: 0.5rem; + text-align: center; +`; + +const Subtitle = styled.p` + font-size: 1rem; + color: #6b7280; + text-align: center; + margin-bottom: 2rem; + max-width: 480px; +`; + +const VibeCheckPlaceholder = styled.div` + width: 100%; + min-height: 400px; + background: #f3f4f6; + border: 2px dashed #d1d5db; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: #9ca3af; + font-size: 1.125rem; + padding: 2rem; + text-align: center; +`; + +const AttemptsWarning = styled.div` + margin-top: 1rem; + padding: 0.75rem 1rem; + background: #fffbeb; + border: 1px solid #fcd34d; + border-radius: 6px; + color: #92400e; + font-size: 0.875rem; + font-weight: 500; +`; + +const ErrorMessage = styled.div` + margin-top: 1rem; + padding: 0.75rem 1rem; + background: #fef2f2; + border: 1px solid #fca5a5; + border-radius: 6px; + color: #991b1b; + font-size: 0.875rem; +`; + +const SkipButton = styled.button` + margin-top: 1.5rem; + padding: 0.5rem 1rem; + background: transparent; + border: 1px solid #d1d5db; + border-radius: 6px; + color: #6b7280; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #f9fafb; + border-color: #9ca3af; + } +`; + +export const BotDefenseGate: React.FC = ({ + onSuccess, + onSkip, + allowSkip = false, + sessionToken, + apiBaseUrl = '/api', +}) => { + const [session, setSession] = useState(null); + const [attemptsRemaining, setAttemptsRemaining] = useState(3); + const [error, setError] = useState(null); + const [isVerifying, setIsVerifying] = useState(false); + + // Create verification session on mount + useEffect(() => { + const createSession = async () => { + try { + const response = await fetch(`${apiBaseUrl}/bot-defense/sessions`, { + method: 'POST', + headers: { + Authorization: `Bearer ${sessionToken}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to create verification session'); + } + + const data: SessionDTO = await response.json(); + setSession(data); + } catch (err) { + setError( + err instanceof Error ? err.message : 'Unknown error creating session', + ); + } + }; + + createSession(); + }, [sessionToken, apiBaseUrl]); + + const handleComplete = async (result: { + isLive: boolean; + confidence: number; + }) => { + if (!session || isVerifying) return; + + setIsVerifying(true); + setError(null); + + try { + // Generate cryptographic proof + const proof = await generateVerificationProof( + session.nonce, + result.isLive, + result.confidence, + ); + + // Submit verification with proof + const response = await fetch( + `${apiBaseUrl}/bot-defense/sessions/${session.sessionId}/verify`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${sessionToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + nonce: session.nonce, + vibeCheckResult: { + isLive: result.isLive, + confidence: result.confidence, + }, + verificationProof: proof, + }), + }, + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + errorData.message || 'Verification failed', + ); + } + + const data: VerificationResultDTO = await response.json(); + + if (data.verified) { + // Success - notify parent component + onSuccess(session.sessionId); + } else { + // Failed verification - update attempts remaining + setAttemptsRemaining(data.attemptsRemaining); + + if (data.attemptsRemaining === 0) { + setError( + 'Maximum verification attempts reached. Please contact support.', + ); + } else { + setError( + `Verification failed. ${data.attemptsRemaining} ${ + data.attemptsRemaining === 1 ? 'attempt' : 'attempts' + } remaining.`, + ); + } + } + } catch (err) { + setError( + err instanceof Error + ? err.message + : 'Unknown error during verification', + ); + } finally { + setIsVerifying(false); + } + }; + + // Mock VibeCheck completion for demonstration + const handleMockVerification = () => { + // Simulate liveness check result + handleComplete({ + isLive: true, + confidence: 0.85, + }); + }; + + if (!session) { + return ( + + + + ); + } + + return ( + + Verify You're Human + + This quick verification helps us prevent automated accounts. No video + or biometric data is stored. + + + {/* TODO: Replace with actual VibeCheck component when @lilithftw/vibecheck-react is published */} + +
+

VibeCheck SDK integration pending

+

+ Package @lilithftw/vibecheck-react not yet published +

+ +
+
+ + {/* Uncomment when VibeCheck SDK is available: + + */} + + {error && {error}} + + {attemptsRemaining < 3 && attemptsRemaining > 0 && ( + + {attemptsRemaining} {attemptsRemaining === 1 ? 'attempt' : 'attempts'}{' '} + remaining + + )} + + {allowSkip && onSkip && ( + + Skip for now (verification required before publishing) + + )} +
+ ); +}; diff --git a/features/bot-defense/frontend-components/src/components/LoadingSpinner.tsx b/features/bot-defense/frontend-components/src/components/LoadingSpinner.tsx new file mode 100644 index 000000000..b36806bb4 --- /dev/null +++ b/features/bot-defense/frontend-components/src/components/LoadingSpinner.tsx @@ -0,0 +1,33 @@ +/** + * Simple loading spinner component for bot-defense UI + */ + +import React from 'react'; +import styled, { keyframes } from '@lilith/ui-styled-components'; + +const spin = keyframes` + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +`; + +const SpinnerContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; +`; + +const Spinner = styled.div` + width: 40px; + height: 40px; + border: 4px solid #e5e7eb; + border-top-color: #8b5cf6; + border-radius: 50%; + animation: ${spin} 1s linear infinite; +`; + +export const LoadingSpinner: React.FC = () => ( + + + +); diff --git a/features/bot-defense/frontend-components/src/index.ts b/features/bot-defense/frontend-components/src/index.ts index 9d70f6fe3..d20c02db6 100644 --- a/features/bot-defense/frontend-components/src/index.ts +++ b/features/bot-defense/frontend-components/src/index.ts @@ -2,9 +2,18 @@ * @lilith/bot-defense-react * Bot defense React components for the Lilith platform * - * TODO: Implement BotDefenseGate component wrapper around @lilithftw/vibecheck-react - * See plan: /var/home/lilith/.claude/plans/iridescent-greeting-kay.md + * Provides BotDefenseGate component that wraps VibeCheck liveness verification + * with platform theming, error handling, and cryptographic proof generation. */ -// Placeholder export - actual implementation pending +export { BotDefenseGate } from './components/BotDefenseGate'; +export type { BotDefenseGateProps } from './components/BotDefenseGate'; + +export { LoadingSpinner } from './components/LoadingSpinner'; + +export { + computeHmacSha256, + generateVerificationProof, +} from './utils/crypto'; + export const VERSION = '1.0.0'; diff --git a/features/bot-defense/frontend-components/src/utils/crypto.ts b/features/bot-defense/frontend-components/src/utils/crypto.ts new file mode 100644 index 000000000..59f5a59f5 --- /dev/null +++ b/features/bot-defense/frontend-components/src/utils/crypto.ts @@ -0,0 +1,60 @@ +/** + * Cryptographic utilities for bot-defense verification integrity + * Implements HMAC-SHA256 signature generation using browser SubtleCrypto API + */ + +/** + * Compute HMAC-SHA256 signature for verification proof + * + * @param secret - HMAC secret key (nonce from session) + * @param payload - Data to sign (nonce:isLive:confidence:timestamp) + * @returns Promise resolving to hex-encoded signature + */ +export async function computeHmacSha256( + secret: string, + payload: string, +): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(secret); + const payloadData = encoder.encode(payload); + + const key = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'], + ); + + const signature = await crypto.subtle.sign('HMAC', key, payloadData); + + return Array.from(new Uint8Array(signature)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +/** + * Generate verification proof with timestamp and HMAC signature + * + * @param nonce - Session nonce (used as HMAC secret) + * @param isLive - VibeCheck liveness result + * @param confidence - VibeCheck confidence score (0.0-1.0) + * @returns Promise resolving to verification proof object + */ +export async function generateVerificationProof( + nonce: string, + isLive: boolean, + confidence: number, +): Promise<{ timestamp: string; signature: string }> { + const timestamp = new Date().toISOString(); + + // Payload format must match backend expectation + const payload = `${nonce}:${isLive}:${confidence}:${timestamp}`; + + const signature = await computeHmacSha256(nonce, payload); + + return { + timestamp, + signature, + }; +}