chore(components): 🔧 Update TypeScript component files (4 tsx components)
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
4b09ccbf7b
commit
91d5dc8562
4 changed files with 406 additions and 3 deletions
|
|
@ -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<BotDefenseGateProps> = ({
|
||||
onSuccess,
|
||||
onSkip,
|
||||
allowSkip = false,
|
||||
sessionToken,
|
||||
apiBaseUrl = '/api',
|
||||
}) => {
|
||||
const [session, setSession] = useState<SessionDTO | null>(null);
|
||||
const [attemptsRemaining, setAttemptsRemaining] = useState(3);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<Container>
|
||||
<LoadingSpinner />
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Title>Verify You're Human</Title>
|
||||
<Subtitle>
|
||||
This quick verification helps us prevent automated accounts. No video
|
||||
or biometric data is stored.
|
||||
</Subtitle>
|
||||
|
||||
{/* TODO: Replace with actual VibeCheck component when @lilithftw/vibecheck-react is published */}
|
||||
<VibeCheckPlaceholder>
|
||||
<div>
|
||||
<p>VibeCheck SDK integration pending</p>
|
||||
<p style={{ marginTop: '1rem', fontSize: '0.875rem' }}>
|
||||
Package @lilithftw/vibecheck-react not yet published
|
||||
</p>
|
||||
<button
|
||||
onClick={handleMockVerification}
|
||||
disabled={isVerifying}
|
||||
style={{
|
||||
marginTop: '1rem',
|
||||
padding: '0.5rem 1rem',
|
||||
cursor: isVerifying ? 'not-allowed' : 'pointer',
|
||||
}}
|
||||
>
|
||||
{isVerifying ? 'Verifying...' : 'Simulate Verification (Dev Only)'}
|
||||
</button>
|
||||
</div>
|
||||
</VibeCheckPlaceholder>
|
||||
|
||||
{/* Uncomment when VibeCheck SDK is available:
|
||||
<VibeCheck
|
||||
onComplete={handleComplete}
|
||||
config={{
|
||||
blinkThreshold: 0.2,
|
||||
headMovementThreshold: 0.15,
|
||||
depthThreshold: 0.1,
|
||||
}}
|
||||
/>
|
||||
*/}
|
||||
|
||||
{error && <ErrorMessage>{error}</ErrorMessage>}
|
||||
|
||||
{attemptsRemaining < 3 && attemptsRemaining > 0 && (
|
||||
<AttemptsWarning>
|
||||
{attemptsRemaining} {attemptsRemaining === 1 ? 'attempt' : 'attempts'}{' '}
|
||||
remaining
|
||||
</AttemptsWarning>
|
||||
)}
|
||||
|
||||
{allowSkip && onSkip && (
|
||||
<SkipButton onClick={onSkip}>
|
||||
Skip for now (verification required before publishing)
|
||||
</SkipButton>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 = () => (
|
||||
<SpinnerContainer>
|
||||
<Spinner />
|
||||
</SpinnerContainer>
|
||||
);
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
60
features/bot-defense/frontend-components/src/utils/crypto.ts
Normal file
60
features/bot-defense/frontend-components/src/utils/crypto.ts
Normal file
|
|
@ -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<string> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue