diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 89badecd..f4af6ee6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -47,7 +47,9 @@ "Bash(vercel:*)", "WebFetch(domain:console.groq.com)", "Bash(ls:*)", - "Bash(sed:*)" + "Bash(sed:*)", + "Bash(echo $CLERK_JWT_ISSUER_DOMAIN)", + "Bash(curl:*)" ], "deny": [] } diff --git a/convex/rateLimit.ts b/convex/rateLimit.ts index 7b56b6e1..aa43704d 100644 --- a/convex/rateLimit.ts +++ b/convex/rateLimit.ts @@ -23,7 +23,8 @@ interface RateLimitEntry { resetTime: number; } -// Simple in-memory rate limiting (for production, use Redis or similar) +// Simple in-memory rate limiting with bounded size (for production, use Redis or similar) +const MAX_RATE_LIMIT_ENTRIES = 10000; // Prevent unbounded growth const rateLimitStore = new Map(); /** @@ -38,6 +39,11 @@ export async function checkRateLimit( throw new Error("Authentication required for rate limiting"); } + // More aggressive cleanup to prevent memory leaks + if (Math.random() < 0.05 || rateLimitStore.size > MAX_RATE_LIMIT_ENTRIES) { + cleanupExpiredRateLimits(); + } + const config = RATE_LIMITS[operation]; const key = `${identity.subject}:${operation}`; const now = Date.now(); @@ -95,20 +101,35 @@ export async function enforceRateLimit( } /** - * Clean up expired rate limit entries (should be called periodically) + * Clean up expired rate limit entries (called during rate limit checks) */ -export function cleanupExpiredRateLimits(): void { +function cleanupExpiredRateLimits(): void { const now = Date.now(); + const keysToDelete: string[] = []; + for (const [key, entry] of rateLimitStore.entries()) { if (now > entry.resetTime) { - rateLimitStore.delete(key); + keysToDelete.push(key); + } + } + + // Delete expired entries + for (const key of keysToDelete) { + rateLimitStore.delete(key); + } + + // If still too many entries, delete oldest ones (LRU-style) + if (rateLimitStore.size > MAX_RATE_LIMIT_ENTRIES) { + const entries = Array.from(rateLimitStore.entries()); + entries.sort((a, b) => a[1].resetTime - b[1].resetTime); + + const numToDelete = rateLimitStore.size - MAX_RATE_LIMIT_ENTRIES; + for (let i = 0; i < numToDelete; i++) { + rateLimitStore.delete(entries[i][0]); } } } -// Clean up expired entries every 5 minutes -setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000); - /** * Get current rate limit status for a user and operation */ diff --git a/src/App.tsx b/src/App.tsx index cdcff644..6d8b9885 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -16,14 +16,18 @@ import PrivacyPolicy from "./pages/PrivacyPolicy"; import Chat from "./pages/Chat"; import AuthGuard from "./components/AuthGuard"; import UserSync from "./components/UserSync"; +import { AuthWrapper } from "./components/AuthWrapper"; +import { AuthErrorBoundary } from "./components/AuthErrorBoundary"; import E2BDemo from "./pages/E2BDemo"; const queryClient = new QueryClient(); const App = () => ( - - + + + +
@@ -56,8 +60,10 @@ const App = () => (
-
-
+
+ +
+
); diff --git a/src/components/AuthErrorBoundary.tsx b/src/components/AuthErrorBoundary.tsx new file mode 100644 index 00000000..5a2a675a --- /dev/null +++ b/src/components/AuthErrorBoundary.tsx @@ -0,0 +1,73 @@ +import React, { Component, ErrorInfo, ReactNode } from 'react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; +} + +interface State { + hasError: boolean; + error?: Error; +} + +export class AuthErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + // Log error without exposing sensitive auth details + console.error('Authentication error caught by boundary:', { + message: error.message, + stack: error.stack, + componentStack: errorInfo.componentStack + }); + } + + render() { + if (this.state.hasError) { + return this.props.fallback || ( +
+
+
+
+ + + +
+
+

+ Authentication Error +

+

+ There was a problem with authentication. Please refresh the page to try again. +

+ +
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/src/components/AuthProvider.tsx b/src/components/AuthProvider.tsx new file mode 100644 index 00000000..b7e3e66e --- /dev/null +++ b/src/components/AuthProvider.tsx @@ -0,0 +1,84 @@ +import React, { createContext, useContext, useEffect, useState, useCallback } from 'react'; +import { useAuth as useClerkAuth, useUser } from '@clerk/clerk-react'; +import type { UserResource } from '@clerk/types'; +import { useAuthToken } from '@/lib/auth-token'; + +interface AuthContextType { + isAuthenticated: boolean; + isLoading: boolean; + user: UserResource | null | undefined; + token: string | null; + refreshAuth: () => Promise; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { isSignedIn, isLoaded } = useClerkAuth(); + const { user } = useUser(); + const { getValidToken, clearStoredToken } = useAuthToken(); + const [token, setToken] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const refreshAuth = useCallback(async () => { + try { + if (isSignedIn && isLoaded) { + const newToken = await getValidToken(); + setToken(newToken); + } else { + setToken(null); + clearStoredToken(); + } + } catch (error) { + console.error('Failed to refresh auth token'); + setToken(null); + clearStoredToken(); + } + }, [isSignedIn, isLoaded, getValidToken, clearStoredToken]); + + // Main auth effect - handles initial load and auth state changes + useEffect(() => { + const handleAuth = async () => { + if (!isLoaded) { + // Still loading Clerk, keep loading state + return; + } + + setIsLoading(true); + await refreshAuth(); + setIsLoading(false); + }; + + handleAuth(); + }, [isSignedIn, isLoaded, user?.id, refreshAuth]); + + // Periodic token refresh to prevent expiration + useEffect(() => { + if (isSignedIn && isLoaded) { + const interval = setInterval(refreshAuth, 4 * 60 * 1000); // Every 4 minutes + return () => clearInterval(interval); + } + }, [isSignedIn, isLoaded, refreshAuth]); + + const value: AuthContextType = { + isAuthenticated: isLoaded && isSignedIn && !!token, + isLoading: isLoading || !isLoaded, + user, + token, + refreshAuth, + }; + + return ( + + {children} + + ); +}; + +export const useAuthContext = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuthContext must be used within an AuthProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/components/AuthWrapper.tsx b/src/components/AuthWrapper.tsx new file mode 100644 index 00000000..7d308e80 --- /dev/null +++ b/src/components/AuthWrapper.tsx @@ -0,0 +1,47 @@ +import React, { useEffect } from 'react'; +import { useConvexAuth } from 'convex/react'; +import { useAuth as useClerkAuth } from '@clerk/clerk-react'; +import { useAuthToken } from '@/lib/auth-token'; + +interface AuthWrapperProps { + children: React.ReactNode; +} + +export const AuthWrapper: React.FC = ({ children }) => { + const convexAuth = useConvexAuth(); + const clerkAuth = useClerkAuth(); + const { getValidToken, clearStoredToken } = useAuthToken(); + + useEffect(() => { + // Handle authentication recovery on page load/refresh + const handleAuthRecovery = async () => { + // If Convex shows not authenticated but Clerk is signed in + if (!convexAuth.isAuthenticated && !convexAuth.isLoading && clerkAuth.isSignedIn && clerkAuth.isLoaded) { + try { + // Get a fresh token to sync with Convex + const freshToken = await getValidToken(); + if (freshToken) { + // Let Convex naturally re-authenticate without forcing reload + console.log('Auth token refreshed for Convex sync'); + } + } catch (error) { + console.error('Auth recovery failed'); + clearStoredToken(); + } + } + }; + + // Run recovery after initial auth check + const timeout = setTimeout(handleAuthRecovery, 1000); + return () => clearTimeout(timeout); + }, [convexAuth.isAuthenticated, convexAuth.isLoading, clerkAuth.isSignedIn, clerkAuth.isLoaded, getValidToken, clearStoredToken]); + + // Handle sign out cleanup + useEffect(() => { + if (!clerkAuth.isSignedIn && clerkAuth.isLoaded) { + clearStoredToken(); + } + }, [clerkAuth.isSignedIn, clerkAuth.isLoaded, clearStoredToken]); + + return <>{children}; +}; \ No newline at end of file diff --git a/src/components/UserSync.tsx b/src/components/UserSync.tsx index 704d57f0..6b6f0c74 100644 --- a/src/components/UserSync.tsx +++ b/src/components/UserSync.tsx @@ -1,12 +1,13 @@ import { useEffect } from 'react'; import { useConvexAuth } from 'convex/react'; -import { useUser } from '@clerk/clerk-react'; +import { useUser, useAuth as useClerkAuth } from '@clerk/clerk-react'; import { useMutation } from 'convex/react'; import { api } from '../../convex/_generated/api'; export default function UserSync({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useConvexAuth(); const { user: clerkUser } = useUser(); + const { isSignedIn } = useClerkAuth(); const upsertUser = useMutation(api.users.upsertUser); useEffect(() => { diff --git a/src/lib/auth-provider.tsx b/src/lib/auth-provider.tsx new file mode 100644 index 00000000..6eb90719 --- /dev/null +++ b/src/lib/auth-provider.tsx @@ -0,0 +1,42 @@ +import { useAuth as useClerkAuth } from '@clerk/clerk-react'; +import { useConvexAuth } from 'convex/react'; +import { useAuthToken } from './auth-token'; + +// Enhanced auth hook that combines Clerk and Convex auth with secure token management +export const useAuthWithTokens = () => { + const clerkAuth = useClerkAuth(); + const convexAuth = useConvexAuth(); + const { getValidToken, clearStoredToken } = useAuthToken(); + + // Enhanced getToken that uses secure memory-based storage + const getTokenSecure = async () => { + try { + // Get token using the secure token manager + if (clerkAuth.isSignedIn && clerkAuth.isLoaded) { + return await getValidToken(); + } + + return null; + } catch (error) { + console.error('Error getting auth token'); + clearStoredToken(); + return null; + } + }; + + // Clear tokens on sign out + const signOut = async () => { + clearStoredToken(); + if (clerkAuth.signOut) { + await clerkAuth.signOut(); + } + }; + + return { + ...clerkAuth, + ...convexAuth, + getToken: getTokenSecure, + signOut, + isFullyAuthenticated: convexAuth.isAuthenticated && clerkAuth.isSignedIn, + }; +}; \ No newline at end of file diff --git a/src/lib/auth-token.ts b/src/lib/auth-token.ts new file mode 100644 index 00000000..f5329fad --- /dev/null +++ b/src/lib/auth-token.ts @@ -0,0 +1,75 @@ +import { useAuth as useClerkAuth } from '@clerk/clerk-react'; +import { useCallback } from 'react'; + +// Memory-based auth token management (secure alternative to cookies) +class AuthTokenManager { + private token: string | null = null; + private tokenFetchTime: number | null = null; + private readonly TOKEN_REFRESH_THRESHOLD = 4 * 60 * 1000; // 4 minutes + + setToken(token: string | null): void { + this.token = token; + this.tokenFetchTime = token ? Date.now() : null; + } + + getToken(): string | null { + return this.token; + } + + shouldRefreshToken(): boolean { + if (!this.token || !this.tokenFetchTime) return false; + return Date.now() - this.tokenFetchTime > this.TOKEN_REFRESH_THRESHOLD; + } + + clearToken(): void { + this.token = null; + this.tokenFetchTime = null; + } + + hasToken(): boolean { + return !!this.token; + } +} + +export const authTokenManager = new AuthTokenManager(); + +// Simplified hook that relies on Clerk's built-in session management +export const useAuthToken = () => { + const { getToken, isSignedIn, isLoaded } = useClerkAuth(); + + const getValidToken = useCallback(async (): Promise => { + if (!isSignedIn || !isLoaded) { + authTokenManager.clearToken(); + return null; + } + + const cachedToken = authTokenManager.getToken(); + + // Return cached token if it's recent enough + if (cachedToken && !authTokenManager.shouldRefreshToken()) { + return cachedToken; + } + + try { + // Get fresh token from Clerk (Clerk handles validation internally) + const freshToken = await getToken(); + authTokenManager.setToken(freshToken); + return freshToken; + } catch (error) { + // Log error without exposing token details + console.error('Failed to refresh auth token'); + authTokenManager.clearToken(); + return null; + } + }, [getToken, isSignedIn, isLoaded]); + + const clearStoredToken = useCallback(() => { + authTokenManager.clearToken(); + }, []); + + return { + getValidToken, + clearStoredToken, + hasStoredToken: () => authTokenManager.hasToken() + }; +}; \ No newline at end of file diff --git a/src/lib/security.ts b/src/lib/security.ts index 595a0aa7..8b54c712 100644 --- a/src/lib/security.ts +++ b/src/lib/security.ts @@ -507,7 +507,7 @@ export const csrf = { export const createTrustedTypesPolicy = () => { if (typeof window !== 'undefined' && 'trustedTypes' in window) { try { - return (window as any).trustedTypes.createPolicy('default', { + return (window as typeof window & { trustedTypes: { createPolicy: (name: string, policy: unknown) => unknown } }).trustedTypes.createPolicy('default', { createHTML: (input: string) => sanitizeHTML(input), createScriptURL: (input: string) => { const url = sanitizeUrl(input); diff --git a/src/main.tsx b/src/main.tsx index 01738510..89ddcea4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -58,14 +58,17 @@ root.render( {import.meta.env.VITE_PUBLIC_POSTHOG_KEY ? ( diff --git a/src/pages/PrivacyPolicy.tsx b/src/pages/PrivacyPolicy.tsx index 0bef484a..32d3544a 100644 --- a/src/pages/PrivacyPolicy.tsx +++ b/src/pages/PrivacyPolicy.tsx @@ -33,28 +33,31 @@ const PrivacyPolicy = () => {

2. INFORMATION WE COLLECT

-

2.1. Information You Provide Directly:

+

2.1. Messages and Communications:

    +
  • Chat messages and conversations you have with our AI platform
  • +
  • All messages are encrypted and stored securely
  • Account registration information (name, email address)
  • -
  • Profile information you choose to provide
  • -
  • Code and content you create or upload
  • Communications with our support team
-

2.2. Information Collected Automatically:

+

2.2. Analytics Data:

    -
  • Usage data and analytics
  • -
  • Device and browser information
  • -
  • IP addresses and location data
  • -
  • Cookies and similar tracking technologies
  • +
  • Usage patterns and platform interactions (anonymized)
  • +
  • Performance metrics to improve service quality
  • +
  • Basic device and browser information
  • +
  • Session duration and feature usage statistics
+

+ Important: We collect analytics to improve our service, but we do not sell your data to third parties. +

-

2.3. Information from Third Parties:

+

2.3. Authentication Information:

    -
  • OAuth provider information (GitHub, Google) when you choose to authenticate
  • -
  • Analytics and performance monitoring data
  • +
  • OAuth provider information when you choose to authenticate
  • +
  • Session tokens and authentication credentials (encrypted)
@@ -98,27 +101,33 @@ const PrivacyPolicy = () => {

5. INFORMATION SHARING AND DISCLOSURE

-

We do not sell your personal information. We may share your information only in these limited circumstances:

+
+

Our Data Commitment

+

+ We do not sell your personal information or data to third parties. Your privacy is fundamental to our service, and we are committed to protecting your encrypted messages and personal information. +

+
+

We may share your information only in these strictly limited circumstances:

-

5.1. With Your Consent:

-

When you explicitly authorize us to share specific information.

+

5.1. With Your Explicit Consent:

+

Only when you explicitly authorize us to share specific information for a particular purpose.

-

5.2. Service Providers:

-

With trusted third-party providers who assist in operating our platform (hosting, analytics, payment processing) under strict confidentiality agreements.

+

5.2. Essential Service Providers:

+

With trusted third-party providers who assist in operating our platform (secure hosting, payment processing) under strict confidentiality agreements. These providers cannot access your encrypted messages.

-

5.3. AI Service Providers:

-

Code generation requests may be sent to third-party AI providers (OpenAI, Anthropic, etc.) with privacy protections in place.

+

5.3. Analytics Partners:

+

Anonymized usage analytics may be shared with analytics services to help improve our platform. No personal messages or identifiable information is included.

5.4. Legal Requirements:

-

When required by law, court order, or to protect our rights and safety.

+

Only when required by valid legal process, court order, or to protect our users' safety and security.

5.5. Business Transfers:

-

In connection with mergers, acquisitions, or asset sales, with appropriate privacy protections.

+

In the unlikely event of a merger or acquisition, with appropriate privacy protections and user notification.

@@ -153,26 +162,40 @@ const PrivacyPolicy = () => {

8. SECURITY MEASURES

+
+

Message Encryption

+

+ All messages and conversations are encrypted both in transit and at rest. This means your conversations are protected and unreadable even if unauthorized access occurs. +

+

We implement comprehensive security measures including:

    -
  • Encryption in transit and at rest
  • -
  • Regular security audits and monitoring
  • -
  • Access controls and authentication requirements
  • -
  • Incident response procedures
  • -
  • Regular security training for our team
  • +
  • End-to-end encryption for all messages and sensitive data
  • +
  • Encryption in transit (TLS/SSL) and at rest (AES-256)
  • +
  • Regular security audits and penetration testing
  • +
  • Multi-factor authentication and access controls
  • +
  • 24/7 monitoring and incident response procedures
  • +
  • Regular security training for our development team
  • +
  • Secure data centers with physical access controls
-

9. COOKIES AND TRACKING TECHNOLOGIES

-

We use cookies and similar technologies to:

+

9. ANALYTICS AND TRACKING

+

We use cookies and analytics technologies to:

    -
  • Maintain your login session
  • -
  • Remember your preferences
  • -
  • Analyze usage patterns
  • -
  • Improve our services
  • +
  • Maintain your secure login session
  • +
  • Remember your preferences and settings
  • +
  • Analyze usage patterns to improve our AI platform
  • +
  • Monitor performance and identify technical issues
  • +
  • Understand which features are most valuable to users
-

You can control cookie settings through your browser preferences.

+
+

+ Analytics Transparency: We collect analytics to make ZapDev better for everyone, but we never sell this data or use it for advertising. All analytics data is anonymized and aggregated. +

+
+

You can control cookie settings through your browser preferences, though some features may require certain cookies to function properly.