// This file is automatically generated. Do not edit it directly. /** Storage accepted by Supabase's `auth.storage` option; `localStorage` and the editor broker both satisfy it. */ export interface PreviewAuthStorage { getItem: (key: string) => string | null | Promise; setItem: (key: string, value: string) => void | Promise; removeItem: (key: string) => void | Promise; } type BrokerRequestType = 'lovable-preview-auth:get' | 'lovable-preview-auth:set' | 'lovable-preview-auth:remove'; interface BrokerRequest { type: BrokerRequestType; requestId: string; projectId: string; key: string; value?: string; } interface BrokerReply { ok: boolean; value: string | null; } const RESULT = 'lovable-preview-auth:result'; function readBrokerReply(data: unknown, requestId: string): BrokerReply | null { if (typeof data !== 'object' || data === null) return null; if (!('type' in data) || data.type !== RESULT) return null; if (!('requestId' in data) || data.requestId !== requestId) return null; return { ok: 'ok' in data && Boolean(data.ok), value: 'value' in data && typeof data.value === 'string' ? data.value : null, }; } /** * On a Lovable preview surface, broker the auth session to the editor over * postMessage so the project's preview surfaces share one login; else localStorage. */ export function brokeredPreviewStorage(): PreviewAuthStorage | undefined { if (typeof window === 'undefined') return undefined; const host = location.hostname; const PREVIEW_ZONES = ['lovableproject.com', 'lovableproject-dev.com', 'lovable.app', 'gpt-eng.com', 'gptengineer.run']; const onPreviewZone = PREVIEW_ZONES.some((z) => host === z || host.endsWith('.' + z)); // Read the id only from non-user-controlled host positions, so a user-named // preview-- host can't smuggle another project's id. const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}'; const projectId = onPreviewZone ? (new RegExp('^(?:id-preview(?:-[a-z0-9]+)?|project)--(' + UUID + ')(?:-dev)?(?=\\.|$)', 'i').exec(host)?.[1] ?? new RegExp('^(' + UUID + ')(?=[.-])', 'i').exec(host)?.[1]) : undefined; // lib.dom types window.parent and location.ancestorOrigins as always set, but a // detached frame has no parent and not every browser implements ancestorOrigins. const parentWindow = window.parent as Window | null; const framed = parentWindow !== null && parentWindow !== window; if (!projectId || !framed) return localStorage; // Post only to the real editor ancestor, validated as a Lovable origin, so the // session token can never reach an untrusted embedder. const dev = host.endsWith('.lovableproject-dev.com') || host.endsWith('.gpt-eng.com'); const EDITOR = dev ? /^https:\/\/([a-z0-9-]+\.)*(lovable\.dev|gptengineer\.app)$|^http:\/\/localhost:3000$/ : /^https:\/\/([a-z0-9-]+\.)*(lovable\.dev|gptengineer\.app)$/; const ancestorOrigins = location.ancestorOrigins as DOMStringList | undefined; const ancestor = ancestorOrigins?.[0] ?? (document.referrer ? new URL(document.referrer).origin : ''); const editorOrigins = ancestor && EDITOR.test(ancestor) ? [ancestor] : (dev ? ['https://lovable.dev', 'http://localhost:3000'] : ['https://lovable.dev']); const TIMEOUT = 2000; const newId = () => Math.random().toString(36).slice(2) + Date.now().toString(36); const request = (type: BrokerRequestType, key: string, value?: string): Promise => new Promise((resolve) => { const requestId = newId(); let done = false; const finish = (reply: BrokerReply | null) => { if (done) return; done = true; clearTimeout(timer); window.removeEventListener('message', onMessage); resolve(reply); }; const onMessage = (e: MessageEvent) => { if (!editorOrigins.includes(e.origin)) return; const reply = readBrokerReply(e.data, requestId); if (reply) finish(reply); }; window.addEventListener('message', onMessage); const timer = setTimeout(() => { finish(null); }, TIMEOUT); const msg: BrokerRequest = { type, requestId, projectId, key }; if (value !== undefined) msg.value = value; // targetOrigin per trusted editor origin, so a session token never reaches an arbitrary embedder. for (const origin of editorOrigins) window.parent.postMessage(msg, origin); }); // The editor may not be listening yet at the first getItem, so retry once. let firstGet = true; const RETRY_DELAY = 250; // Blocked third-party storage throws here; seeding must not break broker reads. const readLocal = (key: string): string | null | undefined => { try { return localStorage.getItem(key); } catch { return undefined; } }; return { getItem: async (key) => { // Sent with GET and REMOVE so an editor that never cached this session can check or revoke it. const local = readLocal(key); let res = await request('lovable-preview-auth:get', key, local ?? undefined); if (!res && firstGet) { await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); res = await request('lovable-preview-auth:get', key, local ?? undefined); } firstGet = false; // '' is the logout tombstone: clear the local copy too so it can't resurrect if // the broker later goes silent. A null value means never-synced -> keep local. if (res?.ok && res.value !== null) { if (res.value === '') { if (local !== undefined && readLocal(key) === local) localStorage.removeItem(key); return null; } // Seed this origin so a new tab, whose editor holds no session, stays signed in. if (local === null && readLocal(key) === null) { try { localStorage.setItem(key, res.value); } catch { /* storage full: still return the editor's session */ } } return res.value; } return localStorage.getItem(key); }, setItem: (key, value) => { localStorage.setItem(key, value); return request('lovable-preview-auth:set', key, value).then((res) => { if (res?.ok && res.value !== null && localStorage.getItem(key) === value) { if (res.value === '') localStorage.removeItem(key); else localStorage.setItem(key, res.value); } }); }, removeItem: (key) => { const local = readLocal(key); localStorage.removeItem(key); return request('lovable-preview-auth:remove', key, local ?? undefined).then(() => {}); }, }; }