Make profile endpoint read-only cache, reject writes

cgen-8fb9fd52f1584fc59072bb8d66bb2372
This commit is contained in:
Builder.io 2025-11-17 03:54:25 +00:00
parent dc8e814746
commit b6e28f2fa3

View file

@ -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 type { VercelRequest, VercelResponse } from "@vercel/node";
import { getAdminClient } from "../_supabase.js"; 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) { export default async function handler(req: VercelRequest, res: VercelResponse) {
if (req.method !== "POST") if (req.method !== "POST")
return res.status(405).json({ error: "Method not allowed" }); return res.status(405).json({ error: "Method not allowed" });
@ -11,58 +34,116 @@ export default async function handler(req: VercelRequest, res: VercelResponse) {
try { try {
const admin = getAdminClient(); const admin = getAdminClient();
const tryUpsert = async (payload: any) => { // ============================================================
const resp = await admin // CRITICAL: REJECT DIRECT WRITES TO PROFILE
.from("user_profiles") // ============================================================
.upsert(payload, { onConflict: "id" as any }) // This endpoint now only validates/syncs passports from Foundation.
.select() // Do NOT write arbitrary profile data to local cache.
.single(); //
return resp as any; // 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; // Check if this is an attempt to write profile fields
let attempt = await tryUpsert({ id, ...profile, username }); 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 (forbiddenFields.length > 0) {
if (!err) return null; console.warn(
if (typeof err === "string") return { message: err }; "[Passport Security] Rejected attempt to write forbidden fields:",
if (typeof err === "object" && Object.keys(err).length === 0) return null; forbiddenFields,
return err; "for user:",
}; id,
);
let error = normalizeError(attempt.error); return res.status(403).json({
if (error) { error: `Cannot modify profile fields directly. All passport mutations must go through Foundation (${FOUNDATION_URL}).`,
const message: string = (error as any).message || ""; forbidden_fields: forbiddenFields,
const code: string = (error as any).code || ""; instruction:
"To update your profile, log in to aethex.foundation and make changes there. Changes sync to aethex.dev on next login.",
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(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) { } catch (e: any) {
if (/SUPABASE_/.test(String(e?.message || ""))) { if (/SUPABASE_/.test(String(e?.message || ""))) {
return res return res