From b6e28f2fa38cb27b8a8fbc544a8187e089aa5ea7 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Mon, 17 Nov 2025 03:54:25 +0000 Subject: [PATCH] Make profile endpoint read-only cache, reject writes cgen-8fb9fd52f1584fc59072bb8d66bb2372 --- api/profile/ensure.ts | 171 +++++++++++++++++++++++++++++++----------- 1 file changed, 126 insertions(+), 45 deletions(-) diff --git a/api/profile/ensure.ts b/api/profile/ensure.ts index d5a8415f..0741b1c9 100644 --- a/api/profile/ensure.ts +++ b/api/profile/ensure.ts @@ -1,6 +1,29 @@ +/** + * CRITICAL: Profile Sync from Foundation (READ-ONLY) + * + * This endpoint syncs Foundation passport data to local cache. + * It does NOT write arbitrary profile data. + * + * Architecture: + * - aethex.foundation = SSOT (single source of truth) + * - aethex.dev = Read-only cache of Foundation passports + * - All profile mutations must go through Foundation APIs + * + * Accepted operations: + * - POST to sync/validate passport on login + * + * Rejected operations: + * - Direct writes to profile fields (username, email, etc.) + * - Mutations that bypass Foundation + * - Updates to cached data outside of Foundation sync + */ + import type { VercelRequest, VercelResponse } from "@vercel/node"; import { getAdminClient } from "../_supabase.js"; +const FOUNDATION_URL = + process.env.VITE_FOUNDATION_URL || "https://aethex.foundation"; + export default async function handler(req: VercelRequest, res: VercelResponse) { if (req.method !== "POST") return res.status(405).json({ error: "Method not allowed" }); @@ -11,58 +34,116 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { try { const admin = getAdminClient(); - const tryUpsert = async (payload: any) => { - const resp = await admin - .from("user_profiles") - .upsert(payload, { onConflict: "id" as any }) - .select() - .single(); - return resp as any; - }; + // ============================================================ + // CRITICAL: REJECT DIRECT WRITES TO PROFILE + // ============================================================ + // This endpoint now only validates/syncs passports from Foundation. + // Do NOT write arbitrary profile data to local cache. + // + // If profile data needs to be updated: + // 1. User updates on Foundation (aethex.foundation) + // 2. Foundation validates and persists changes + // 3. aethex.dev syncs on next login via auth/callback.ts + // ============================================================ - let username = profile?.username; - let attempt = await tryUpsert({ id, ...profile, username }); + // Check if this is an attempt to write profile fields + if (profile && Object.keys(profile).length > 0) { + // Only allowed field is to mark profile as "ensured" for onboarding + const allowedFields = ["profile_completed", "onboarding_step"]; + const attemptedFields = Object.keys(profile); + const forbiddenFields = attemptedFields.filter( + (f) => !allowedFields.includes(f), + ); - const normalizeError = (err: any) => { - if (!err) return null; - if (typeof err === "string") return { message: err }; - if (typeof err === "object" && Object.keys(err).length === 0) return null; - return err; - }; + if (forbiddenFields.length > 0) { + console.warn( + "[Passport Security] Rejected attempt to write forbidden fields:", + forbiddenFields, + "for user:", + id, + ); - let error = normalizeError(attempt.error); - if (error) { - const message: string = (error as any).message || ""; - const code: string = (error as any).code || ""; - - if ( - code === "23505" || - message.includes("duplicate key") || - message.includes("username") - ) { - const suffix = Math.random().toString(36).slice(2, 6); - const newUsername = `${String(username || "user").slice(0, 20)}_${suffix}`; - attempt = await tryUpsert({ id, ...profile, username: newUsername }); - error = normalizeError(attempt.error); - } - } - - if (error) { - if ( - (error as any).code === "23503" || - (error as any).message?.includes("foreign key") - ) { - return res.status(400).json({ - error: - "User does not exist in authentication system. Please sign out and sign back in, then retry onboarding.", + return res.status(403).json({ + error: `Cannot modify profile fields directly. All passport mutations must go through Foundation (${FOUNDATION_URL}).`, + forbidden_fields: forbiddenFields, + instruction: + "To update your profile, log in to aethex.foundation and make changes there. Changes sync to aethex.dev on next login.", }); } - return res - .status(500) - .json({ error: (error as any).message || "Unknown error" }); } - return res.json(attempt.data || {}); + // ============================================================ + // Only sync/validate existing passport from cache + // ============================================================ + + // Fetch user from cache to validate exists + const { data: existingUser, error: fetchError } = await admin + .from("user_profiles") + .select("*") + .eq("id", id) + .single(); + + if (fetchError || !existingUser) { + console.error( + "[Passport Ensure] User not found in cache (not synced from Foundation yet):", + id, + ); + return res.status(400).json({ + error: + "User profile not found. Please sign out and sign back in to sync with Foundation.", + user_id: id, + }); + } + + // Validate passport was synced from Foundation + if (!existingUser.foundation_synced_at) { + console.warn( + "[Passport Ensure] User exists but not synced from Foundation:", + id, + ); + return res.status(400).json({ + error: + "Passport not synchronized from Foundation. Please sign out and sign back in.", + user_id: id, + }); + } + + // Validate cache is still fresh + const cacheValidUntil = new Date(existingUser.cache_valid_until); + if (new Date() > cacheValidUntil) { + console.warn( + "[Passport Ensure] Cache expired for user:", + id, + "| Need to refresh from Foundation", + ); + + // In production, would fetch fresh from Foundation API here + // For now, return warning that cache is stale + return res.status(400).json({ + error: + "Passport cache expired. Please sign out and sign back in to refresh.", + user_id: id, + cache_expired_at: cacheValidUntil.toISOString(), + }); + } + + console.log("[Passport Ensure] User passport validated:", id); + + // Return validated cached passport (read-only view) + return res.json({ + id: existingUser.id, + email: existingUser.email, + username: existingUser.username, + full_name: existingUser.full_name, + avatar_url: existingUser.avatar_url, + profile_completed: existingUser.profile_completed, + synced_from_foundation: existingUser.foundation_synced_at, + cache_valid_until: existingUser.cache_valid_until, + // Never expose local write capability + _note: + "This is read-only cache from Foundation. To modify, update at: " + + FOUNDATION_URL, + }); } catch (e: any) { if (/SUPABASE_/.test(String(e?.message || ""))) { return res