diff --git a/README.md b/README.md index d6439da63..e3d5e6a6b 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ cloud/ │ ├── eliza/ # elizaOS integration │ │ ├── agent-runtime.ts # AgentRuntime wrapper │ │ ├── agent.ts # Agent management -│ │ └── plugin-assistant/ # Custom elizaOS plugin +│ │ └── plugin-cloud-bootstrap/ # Cloud bootstrap plugin (@eliza-cloud/plugin-assistant id) │ ├── config/ # Configuration │ │ ├── env-validator.ts # Environment validation │ │ ├── env-consolidation.ts # Config helpers diff --git a/app/actions/apps.ts b/app/actions/apps.ts deleted file mode 100644 index 52841e50f..000000000 --- a/app/actions/apps.ts +++ /dev/null @@ -1,153 +0,0 @@ -"use server"; - -/** - * Server actions for app-related operations. - * Includes promotional asset upload and deletion. - */ - -import { appsRepository } from "@/db/repositories/apps"; -import { requireAuthWithOrg } from "@/lib/auth"; -import { deleteBlob, isValidBlobUrl, uploadToBlob } from "@/lib/blob"; -import { appsService } from "@/lib/services/apps"; -import { logger } from "@/lib/utils/logger"; - -interface PromotionalAsset { - type: "social_card" | "banner" | "custom"; - url: string; - size: { width: number; height: number }; - generatedAt: string; -} - -/** - * Uploads a promotional asset image for an app. - * - * @param appId - The app ID to add the asset to. - * @param formData - Form data containing the image file. - * @returns Success status with the uploaded asset info, or error details. - */ -export async function uploadPromotionalAsset(appId: string, formData: FormData) { - try { - const user = await requireAuthWithOrg(); - const file = formData.get("file") as File; - - if (!file) { - return { success: false, error: "No file provided" }; - } - - // Validate file type - if (!file.type.startsWith("image/")) { - return { - success: false, - error: "Invalid file type. Please upload an image.", - }; - } - - // Validate file size (max 10MB) - if (file.size > 10 * 1024 * 1024) { - return { success: false, error: "File too large. Maximum size is 10MB." }; - } - - // Verify app ownership - const app = await appsService.getById(appId); - if (!app || app.organization_id !== user.organization_id) { - return { success: false, error: "App not found" }; - } - - // Upload to blob storage - const arrayBuffer = await file.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - - const { url } = await uploadToBlob(buffer, { - filename: file.name, - contentType: file.type, - folder: "promotional-assets", - userId: user.id, - }); - - // Get image dimensions from form data if provided by client, otherwise use defaults - // Client can extract dimensions using browser's Image API before upload - const widthStr = formData.get("width") as string | null; - const heightStr = formData.get("height") as string | null; - const width = widthStr ? parseInt(widthStr, 10) : 1200; - const height = heightStr ? parseInt(heightStr, 10) : 630; - - const newAsset: PromotionalAsset = { - type: "custom", - url, - size: { width, height }, - generatedAt: new Date().toISOString(), - }; - - // Atomically append to existing assets (avoids race conditions) - await appsRepository.appendPromotionalAsset(appId, newAsset); - - logger.info("[Apps Action] Uploaded promotional asset", { - appId, - url, - userId: user.id, - }); - - return { success: true, asset: newAsset }; - } catch (error) { - logger.error("[Apps Action] Error uploading promotional asset:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Failed to upload asset", - }; - } -} - -/** - * Deletes a promotional asset from an app. - * - * @param appId - The app ID to remove the asset from. - * @param assetUrl - The URL of the asset to delete. - * @returns Success status or error details. - */ -export async function deletePromotionalAsset(appId: string, assetUrl: string) { - try { - const user = await requireAuthWithOrg(); - - // Verify app ownership - const app = await appsService.getById(appId); - if (!app || app.organization_id !== user.organization_id) { - return { success: false, error: "App not found" }; - } - - // Atomically remove the asset (avoids race conditions) - const { removedAsset } = await appsRepository.removePromotionalAsset(appId, assetUrl); - - if (!removedAsset) { - return { success: false, error: "Asset not found" }; - } - - // Try to delete from blob storage if it's our blob URL - const assetWithUrl = removedAsset as { url: string }; - if (isValidBlobUrl(assetWithUrl.url)) { - try { - await deleteBlob(assetWithUrl.url); - logger.info("[Apps Action] Deleted blob for promotional asset", { - appId, - url: assetWithUrl.url, - }); - } catch (blobError) { - // Log but don't fail - the database is already updated - logger.warn("[Apps Action] Failed to delete blob, continuing:", blobError); - } - } - - logger.info("[Apps Action] Deleted promotional asset", { - appId, - assetUrl, - userId: user.id, - }); - - return { success: true }; - } catch (error) { - logger.error("[Apps Action] Error deleting promotional asset:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Failed to delete asset", - }; - } -} diff --git a/app/actions/conversations.ts b/app/actions/conversations.ts deleted file mode 100644 index 7b41e106f..000000000 --- a/app/actions/conversations.ts +++ /dev/null @@ -1,94 +0,0 @@ -"use server"; - -import { revalidatePath } from "next/cache"; -import { requireAuth } from "@/lib/auth"; -import { conversationsService } from "@/lib/services/conversations"; - -/** - * Creates a new conversation for the authenticated user. - * - * @param data - Conversation data containing title and model. - * @returns Success status with the created conversation. - */ -export async function createConversationAction(data: { title: string; model: string }) { - const user = await requireAuth(); - - const conversation = await conversationsService.create({ - title: data.title, - model: data.model, - organization_id: user.organization_id, - user_id: user.id, - status: "active", - }); - - revalidatePath("/dashboard/chat"); - return { success: true, conversation }; -} - -/** - * Updates the title of an existing conversation. - * - * @param conversationId - The ID of the conversation to update. - * @param title - The new title for the conversation. - * @returns Success status with the updated conversation, or error if not found. - */ -export async function updateConversationTitleAction(conversationId: string, title: string) { - await requireAuth(); - - const conversation = await conversationsService.update(conversationId, { - title, - }); - - if (!conversation) { - return { success: false, error: "Conversation not found" }; - } - - revalidatePath("/dashboard/chat"); - return { success: true, conversation }; -} - -/** - * Deletes a conversation. - * - * @param conversationId - The ID of the conversation to delete. - * @returns Success status. - */ -export async function deleteConversationAction(conversationId: string) { - await requireAuth(); - - await conversationsService.delete(conversationId); - - revalidatePath("/dashboard/chat"); - return { success: true }; -} - -/** - * Lists all conversations for the authenticated user. - * - * @returns Success status with array of conversations (limited to 50). - */ -export async function listUserConversationsAction() { - const user = await requireAuth(); - - const conversations = await conversationsService.listByUser(user.id, 50); - - return { success: true, conversations }; -} - -/** - * Gets a conversation with its messages. - * - * @param conversationId - The ID of the conversation to retrieve. - * @returns Success status with the conversation and messages, or error if not found. - */ -export async function getConversationAction(conversationId: string) { - await requireAuth(); - - const conversation = await conversationsService.getWithMessages(conversationId); - - if (!conversation) { - return { success: false, error: "Conversation not found" }; - } - - return { success: true, conversation }; -} diff --git a/app/auth/error/page-old.tsx b/app/auth/error/page-old.tsx deleted file mode 100644 index bd470a348..000000000 --- a/app/auth/error/page-old.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client"; - -import { - Button, - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@elizaos/cloud-ui"; -import { useLogin, usePrivy } from "@privy-io/react-auth"; -import { AlertCircle, Loader2 } from "lucide-react"; -import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -import { Suspense, useState } from "react"; - -function AuthErrorContent() { - const { login } = useLogin(); - const { ready } = usePrivy(); - const searchParams = useSearchParams(); - const reason = searchParams.get("reason") || "unknown"; - const [isLoggingIn, setIsLoggingIn] = useState(false); - - const errorMessages: Record = { - auth_failed: { - title: "Authentication Failed", - description: "We could not authenticate your account. Please try signing in again.", - }, - sync_failed: { - title: "Authentication Sync Failed", - description: "We could not sync your account information. Please try signing in again.", - }, - unknown: { - title: "Authentication Error", - description: "An unexpected error occurred during authentication. Please try again.", - }, - }; - - const error = errorMessages[reason] || errorMessages.unknown; - - const handleLogin = async () => { - setIsLoggingIn(true); - await login(); - setTimeout(() => setIsLoggingIn(false), 1000); - }; - - const isLoading = !ready || isLoggingIn; - - return ( -
- - -
- -
- {error.title} - {error.description} -
- -
- - -
-
- If this problem persists, please contact support. -
-
-
-
- ); -} - -/** - * Authentication error page with dynamic error messages based on reason query parameter. - * Supports retry functionality and displays appropriate error details. - */ -export default function AuthErrorPage() { - return ( - - - -
- -
- Authentication Error - Loading error details... -
-
- - } - > - -
- ); -} diff --git a/app/globals.css b/app/globals.css index 3fa50c8c1..513ed2199 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1475,12 +1475,6 @@ button:not(:disabled), animation-delay: 0.8s; } -/* OnChainTrust button hover */ -.onchain-trust-button:hover { - background-color: transparent !important; - color: #e1e1e1 !important; -} - /* ======================================== Landing Page Chat Components ======================================== */ diff --git a/app/login/page-old.tsx b/app/login/page-old.tsx deleted file mode 100644 index 1ee07ddc0..000000000 --- a/app/login/page-old.tsx +++ /dev/null @@ -1,602 +0,0 @@ -"use client"; - -import { BrandButton, BrandCard, CornerBrackets, Input } from "@elizaos/cloud-ui"; -import { useLoginWithEmail, useLoginWithOAuth, usePrivy } from "@privy-io/react-auth"; -import { ArrowLeft, Chrome, Github, Loader2, Mail, Wallet } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Suspense, useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; -import LandingHeader from "@/packages/ui/src/components/layout/landing-header"; - -// Discord SVG Icon Component -const DiscordIcon = ({ className }: { className?: string }) => ( - - - -); - -function LoginPageContent() { - const { ready, authenticated, login } = usePrivy(); - const { sendCode, loginWithCode, state: emailState } = useLoginWithEmail(); - const { initOAuth } = useLoginWithOAuth(); - const router = useRouter(); - const searchParams = useSearchParams(); - - const [email, setEmail] = useState(""); - const [code, setCode] = useState(""); - const [showCodeInput, setShowCodeInput] = useState(false); - const [loadingButton, setLoadingButton] = useState(null); - const [isSyncing, setIsSyncing] = useState(false); - const [isProcessingOAuth, setIsProcessingOAuth] = useState(() => { - // Initialize OAuth processing state on client-side only to prevent SSR hydration mismatch - if (typeof window === "undefined") return false; - const urlParams = new URLSearchParams(window.location.search); - const hasOAuthParams = - urlParams.has("privy_oauth_code") || - urlParams.has("privy_oauth_state") || - urlParams.has("code") || - urlParams.has("state"); - const sessionFlag = sessionStorage.getItem("oauth_login_pending"); - return hasOAuthParams || sessionFlag === "true"; - }); - - // Check if this is a signup intent (from "Get Started" button) - const isSignupIntent = searchParams.get("intent") === "signup"; - - // Guard against multiple simultaneous login() calls (critical for macOS/Brave) - const loginInProgressRef = useRef(false); - const lastLoginAttemptRef = useRef(0); - - // Redirect after authentication - respects returnTo parameter - useEffect(() => { - if (ready && authenticated) { - // Clear OAuth session flag and guards - sessionStorage.removeItem("oauth_login_pending"); - loginInProgressRef.current = false; - // Use setTimeout to avoid synchronous setState in effect - setTimeout(() => { - setLoadingButton(null); - setIsProcessingOAuth(false); - // Show syncing state before redirect - setIsSyncing(true); - }, 0); - - // Get the return URL from search params, default to dashboard - const returnTo = searchParams.get("returnTo"); - // Validate returnTo is a relative path (security: prevent open redirects) - const isValidReturnTo = returnTo && returnTo.startsWith("/") && !returnTo.startsWith("//"); - const redirectUrl = isValidReturnTo ? returnTo : "/dashboard"; - - // Small delay to ensure the sync message is visible - // Use router.replace to avoid polluting browser history with /login - const timer = setTimeout(() => { - router.replace(redirectUrl); - }, 100); - - return () => clearTimeout(timer); - } else if (ready && !authenticated) { - // If we're ready but not authenticated, ensure guard is cleared - // (handles case where user closes modal without connecting) - if (loginInProgressRef.current && !loadingButton) { - loginInProgressRef.current = false; - } - // If OAuth processing timed out (ready but not authenticated after callback) - // clear the flag after a small delay to allow Privy to finish - if (isProcessingOAuth) { - const timeout = setTimeout(() => { - setIsProcessingOAuth(false); - sessionStorage.removeItem("oauth_login_pending"); - }, 3000); // Give Privy 3 seconds to complete auth - return () => clearTimeout(timeout); - } - } - }, [ready, authenticated, router, loadingButton, isProcessingOAuth, searchParams]); - - // Monitor email state to show code input - useEffect(() => { - if (emailState.status === "awaiting-code-input") { - // Use setTimeout to avoid synchronous setState in effect - setTimeout(() => { - setShowCodeInput(true); - }, 0); - } - }, [emailState.status]); - - const handleSendCode = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!email || !email.includes("@")) { - toast.error("Please enter a valid email address"); - return; - } - - setLoadingButton("email"); - await sendCode({ email }); - toast.success("Verification code sent to your email"); - setShowCodeInput(true); - setLoadingButton(null); - }; - - const handleVerifyCode = async (e: React.FormEvent) => { - e.preventDefault(); - - if (!code || code.length !== 6) { - toast.error("Please enter a valid 6-digit code"); - return; - } - - setLoadingButton("verify"); - await loginWithCode({ code }); - toast.success("Email verified! Setting up your account..."); - // Privy will auto-redirect to dashboard via our useEffect - setLoadingButton(null); - }; - - const handleOAuthLogin = async (provider: "google" | "discord" | "github") => { - setLoadingButton(provider); - // Set session flag to detect OAuth callback when returning - sessionStorage.setItem("oauth_login_pending", "true"); - toast.loading(`Redirecting to ${provider}...`); - await initOAuth({ provider }); - // This will redirect to OAuth provider - }; - - const handleWalletConnect = async () => { - // Guard: Prevent multiple simultaneous login attempts (macOS/Brave issue) - if (loginInProgressRef.current) { - return; - } - - // Debounce: Prevent rapid successive calls (500ms cooldown) - const now = Date.now(); - if (now - lastLoginAttemptRef.current < 500) { - return; - } - - // Guard: Don't open login if already authenticated - if (authenticated) { - return; - } - - // Set guards - loginInProgressRef.current = true; - lastLoginAttemptRef.current = now; - setLoadingButton("wallet"); - - // Use login() instead of connectWallet() for authentication - // This opens the Privy modal (non-blocking, returns immediately) - // Authentication state changes are handled via the authenticated state in useEffect - login(); - - // Reset the guard after a short delay to allow modal to open - // If authentication succeeds, the useEffect will handle redirect - // If user closes modal, this timeout resets the guard for retry - setTimeout(() => { - // Only reset if still in progress (not authenticated yet) - if (loginInProgressRef.current) { - loginInProgressRef.current = false; - setLoadingButton(null); - } - }, 2000); // 2 second timeout - }; - - const handleBackToEmail = () => { - setShowCodeInput(false); - setCode(""); - }; - - // Show loading state while checking authentication or processing OAuth callback - if (!ready || isProcessingOAuth) { - return ( -
- {/* Header */} - - - {/* Fullscreen background video */} -