/* ════════════════════════════════════════════════════════════════════════ * TuringDNA engine — static auth bootstrap * ──────────────────────────────────────────────────────────────────────── * * Runs FIRST on page load (before app.js). Two jobs: * * 1. Pick up the Supabase access token from the URL fragment. The * /app iframe wrapper at turingdna.com/app forwards the JWT via * location.hash because we can't share cookies cross-origin * (turingdna.com → winter4000-syntheogenesis.hf.space). We read it * once, stash it in sessionStorage, then scrub the hash so it * doesn't end up in the browser history. * * 2. Wrap window.fetch so every /api/* call automatically carries the * token in an Authorization: Bearer header. app.js can stay * unmodified — it calls fetch('/api/...') as before, and the wrapper * injects the auth. * * 402 handling: if any /api/* call returns 402 with kind="auth_required", * we redirect the iframe parent (window.top) to /signup. Anonymous users * who've used their free run see a clean handoff to the sign-up page * rather than a JSON error in the chat panel. * * No external deps. Works without Supabase being configured (the engine * gracefully falls back to anonymous mode and the server-side auth * module also degrades open). Lives at /static/auth.js, loaded before * app.js in index.html. * ════════════════════════════════════════════════════════════════════════ */ (function () { 'use strict'; const TOKEN_KEY = 'td_access_token'; const REFRESH_KEY = 'td_refresh_token'; const USER_ID_KEY = 'td_user_id'; const USER_EMAIL_KEY = 'td_user_email'; // Origins we accept a live token-refresh push FROM (the /app wrapper). // The message handler WRITES auth tokens to sessionStorage, so this // allowlist is a security boundary — never widen it to '*'. Apex + www // cover both ways the wrapper may be served. const TRUSTED_PARENT_ORIGINS = [ 'https://turingdna.com', 'https://www.turingdna.com', ]; // Timestamp (ms) of the last fresh token the parent pushed in. Used to // tell a silent refresh ("the token was renewed under us") apart from a // dead session ("refresh failed → must nag the user"). let _lastTokenRefreshAt = 0; // ─── 1. Read tokens from URL fragment, if present ──────────────── // The fragment is set by /app's iframe wrapper when the user is // signed in. Format: // #access_token=...&refresh_token=...&user_id=...&user_email=... // Single writer for the token quartet — used by both the URL-fragment // path (initial load) and the live postMessage path (token rotation // while the iframe stays mounted). Only the access token is required; // the rest are best-effort. function _stashTokens(at, rt, uid, uem) { if (!at) return false; try { sessionStorage.setItem(TOKEN_KEY, at); if (rt) sessionStorage.setItem(REFRESH_KEY, rt); if (uid) sessionStorage.setItem(USER_ID_KEY, uid); if (uem) sessionStorage.setItem(USER_EMAIL_KEY, uem); } catch (_) { // Some browsers disable sessionStorage in private mode — // we lose persistence but the wrapper re-pushes the token // on every refresh / refocus anyway. return false; } return true; } function _readFragmentAndStash() { const hash = window.location.hash || ''; if (!hash.startsWith('#') || !hash.includes('access_token=')) return; const params = new URLSearchParams(hash.slice(1)); const at = params.get('access_token'); const rt = params.get('refresh_token'); const uid = params.get('user_id'); const uem = params.get('user_email'); if (!at) return; _stashTokens(at, rt, uid, uem); // Scrub the SENSITIVE keys from the hash so the token doesn't end up // in browser history, but keep a non-secret `view=` deep-link (e.g. // from turingdna.com/app/#turing → …#access_token=…&view=turing) alive // so app.js's currentRoute() — which runs later, after this synchronous // script — can still route to it. Use replaceState so back still works. try { const view = params.get('view'); const cleanUrl = window.location.pathname + window.location.search + (view ? '#view=' + encodeURIComponent(view) : ''); window.history.replaceState(null, '', cleanUrl); } catch (_) { /* ignore */ } // 2026-05-28 — Fix for "signed-in users still hit the 5-min wall": // A returning user who hit the wall as anonymous still has the // localStorage trial-timer state from that session. Even though // every entry-point into _showTrialModal() guards on _accessToken(), // the guard is fragile — clear the state outright the moment we // know they're authenticated. Cookies on the engine origin are // independent of this and still represent the trial, but the // server-side gate also checks the JWT FIRST and short-circuits // before reading the cookie, so the cookie can sit harmless. try { localStorage.removeItem('td_trial_started_at'); } catch (_) {} } _readFragmentAndStash(); // ─── 2. Token accessors ───────────────────────────────────────── function _accessToken() { try { return sessionStorage.getItem(TOKEN_KEY) || null; } catch (_) { return null; } } function _userEmail() { try { return sessionStorage.getItem(USER_EMAIL_KEY) || null; } catch (_) { return null; } } // Wipe the cached session so a dead token can't keep masking a fresh // sign-in (the "session out of sync that won't clear" loop). function _clearTokens() { try { sessionStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(REFRESH_KEY); sessionStorage.removeItem(USER_ID_KEY); sessionStorage.removeItem(USER_EMAIL_KEY); } catch (_) { /* private mode — nothing to clear */ } } // If the user was already signed in (sessionStorage carried over from a // prior load in this tab), wipe the legacy trial state too. Same // rationale as the wipe inside _readFragmentAndStash, but covers the // case where the fragment-handoff doesn't run on this load (e.g. the // wrapper re-used a cached iframe URL without the hash). if (_accessToken()) { try { localStorage.removeItem('td_trial_started_at'); } catch (_) {} } // ─── 3. Wrap fetch to attach Authorization on same-origin /api/* ── // We only attach the token for same-origin requests to /api/*. We // don't want to leak tokens to BLAST / AlphaFold / vendor URLs. const _origFetch = window.fetch.bind(window); window.fetch = function patchedFetch(input, init) { try { const url = (typeof input === 'string') ? input : (input && input.url) || ''; const isApi = url.startsWith('/api/') || url.startsWith(window.location.origin + '/api/'); if (isApi) { const token = _accessToken(); if (token) { init = init || {}; const headers = new Headers(init.headers || {}); if (!headers.has('Authorization')) { headers.set('Authorization', 'Bearer ' + token); } init.headers = headers; } } } catch (_) { /* if wrapper errors, fall through to original fetch */ } const p = _origFetch(input, init); // Auth-failure handler — fires on EVERY fetch so any protected // endpoint can trigger the gate: // // 402 + kind=auth_required → trial expired (anon ran out // of the 5-min window). Send // them to /signin. // 403 + kind=signin_required → endpoint requires an account // AND the user appears anon to // the backend (no JWT, or // expired JWT). For a user who // THINKS they're signed in but // whose token has rolled over, // reloading the top window is // the cheapest reset — the // /app wrapper calls // refreshSession() and injects // the fresh JWT into the iframe // URL fragment. For a genuinely // anonymous user, the wrapper's // no-session branch fires and // /signin shows up via the // normal flow. return p.then((res) => { if (!res) return res; if (res.status !== 402 && res.status !== 403) return res; // Clone so the caller can still read the body if it wants. res.clone().json().then((data) => { if (!data || !data.kind) return; // 2026-05-28 — Critical branch: if the user HAS a JWT in // sessionStorage and we STILL got a quota/sign-in error, // the JWT was sent and rejected by the backend. This is // NOT a "user is anonymous" case — they're signed in, // their token just isn't being accepted right now // (expired, clock skew, race with autoRefreshToken in // the wrapper, etc.). Bouncing them to /signin would // round-trip them straight back to /app with the same // dead session (the wrapper's getSession() returns the // cached value first). This is the "thrown back to the // start" symptom users reported on Instagram after // signing up. // // Correct response: surface a "session expired" modal // that, on click, refreshes the top window so the // wrapper's refreshSession() path can mint a fresh JWT // and inject it into the iframe. No automatic reload — // we learned in commit 50d9d32 that auto-reloads cause // loops when the refresh-token is also dead. const signedIn = !!_accessToken(); if (signedIn) { // Try a silent, no-reload token refresh via the wrapper // first; only nag if that fails (root-cause fix for the // "reload won't clear it" session loop). _onSignedInAuthFailure(data && data.signup_url); return; } if (data.kind === 'auth_required') { _redirectToSignup(data.signup_url); } else if (data.kind === 'signin_required') { // Genuinely-anonymous case: dispatch the event so // view-specific UI (e.g. the CRISPR controller's // sign-in modal) can react. Same idiom as 50d9d32. try { window.dispatchEvent(new CustomEvent( 'td:signin-required', { detail: { signup_url: data.signup_url } }, )); } catch (_) { /* old browsers — fine */ } } }).catch(() => { /* not JSON, ignore */ }); return res; }); }; function _redirectToSignup(url) { // Default to /signin (most users have an account); the page itself // links to /signup for new users. Backend sends an absolute URL // in its 402 payload — we prefer that whenever present. const target = url || 'https://turingdna.com/signin/?from=app'; // If we're inside the iframe (the production setup), redirect // the TOP window so /signup loads on turingdna.com, not inside // the iframe. If we're standalone, redirect this window. try { if (window.top && window.top !== window.self) { window.top.location.href = target; return; } } catch (_) { /* cross-origin parent — fall through */ } window.location.href = target; } // ─── 4. Trial-timer modal ─────────────────────────────────────── // 5-minute anonymous trial. The backend stamps two response headers // on every /api/run for anon users: // // X-TD-Trial-Started-At unix seconds when their trial began // X-TD-Trial-Remaining-S seconds left in the window // // We read those headers from the fetch response and arm a setTimeout // for the remaining time. When it fires, we inject a modal that // blocks the engine UI with a "Sign in to keep going" CTA. The // modal is built and styled in JS so we don't need to touch the // engine's main HTML/CSS — it's a clean drop-in. // // If the user reloads, the timer state survives via localStorage // (key = TRIAL_START_KEY) — they don't get a fresh 5 min by hitting // refresh. Server cookie is the authoritative source; localStorage // is just used to know when to render the modal without waiting for // a fresh /api/run response. const TRIAL_START_KEY = 'td_trial_started_at'; const TRIAL_SECONDS = 300; // mirrors auth.py default; the server // is authoritative — the modal only // controls the visual countdown timing let _trialTimerId = null; let _modalShown = false; function _ensureModalEl() { let el = document.getElementById('td-trial-modal'); if (el) return el; el = document.createElement('div'); el.id = 'td-trial-modal'; el.setAttribute('role', 'dialog'); el.setAttribute('aria-modal', 'true'); // Docked right-hand sheet (not a centred overlay) — light scrim so the // workspace stays visible behind it instead of being fully covered. el.style.cssText = [ 'position:fixed', 'inset:0', 'z-index:99999', 'display:flex', 'align-items:stretch', 'justify-content:flex-end', 'background:rgba(10,10,10,0.16)', 'opacity:0', 'transition:opacity 200ms ease', 'font-family:"Source Serif 4",Georgia,serif', 'color:#0A0A0A', ].join(';'); const card = document.createElement('div'); card.style.cssText = [ 'width:400px', 'max-width:100%', 'height:100%', 'background:#F7F5F0', 'padding:40px 32px 32px', 'border-left:1px solid #D6D3D1', 'box-shadow:-18px 0 50px rgba(20,19,16,0.22)', 'display:flex', 'flex-direction:column', 'overflow-y:auto', 'box-sizing:border-box', ].join(';'); card.innerHTML = '

§ Trial ended

' + '

Sign in to keep going.

' + '

' + 'Your 5-minute trial has ended. Sign in to keep going — accounts are free and we save your generated libraries for 45 days.' + '

' + '
' + 'Sign in' + 'Back to home' + '
'; el.appendChild(card); document.body.appendChild(el); // Both links break out of the iframe to the top window so the // user lands on turingdna.com/signup/ (not signup-inside-iframe). ['td-trial-signin', 'td-trial-home'].forEach(function (id) { const a = el.querySelector('#' + id); if (!a) return; a.addEventListener('click', function (e) { e.preventDefault(); const href = a.getAttribute('href'); _redirectToSignup(href); }); }); // Block engine interaction beneath the modal. document.body.style.overflow = 'hidden'; return el; } function _showTrialModal() { if (_modalShown) return; if (_accessToken()) return; // signed in — never show _modalShown = true; const el = _ensureModalEl(); // Trigger CSS transition. requestAnimationFrame(function () { el.style.opacity = '1'; }); } // ─── 4b. Session-expired modal (signed-in user whose JWT was rejected) ─ // Shown when the fetch wrapper sees a 402 or 403 BUT _accessToken() is // truthy. Inline reload-on-click — bypasses the broken auto-reload // loop that commit 50d9d32 killed by handing the decision to the user. let _sessionExpiredShown = false; function _showSessionExpiredModal(signupUrl) { if (_sessionExpiredShown) return; _sessionExpiredShown = true; let el = document.getElementById('td-session-expired-modal'); if (el) { el.style.opacity = '1'; return; } el = document.createElement('div'); el.id = 'td-session-expired-modal'; el.setAttribute('role', 'dialog'); el.setAttribute('aria-modal', 'true'); el.style.cssText = [ 'position:fixed', 'inset:0', 'z-index:99999', 'display:flex', 'align-items:center', 'justify-content:center', 'background:rgba(10,10,10,0.78)', 'backdrop-filter:blur(6px)', '-webkit-backdrop-filter:blur(6px)', 'padding:24px', 'opacity:0', 'transition:opacity 200ms ease', 'font-family:"Source Serif 4",Georgia,serif', 'color:#0A0A0A', ].join(';'); const card = document.createElement('div'); card.style.cssText = [ 'max-width:480px', 'width:100%', 'background:#F7F5F0', 'padding:36px 32px', 'border-radius:4px', 'box-shadow:0 30px 80px rgba(0,0,0,0.45)', ].join(';'); const fallback = signupUrl || 'https://turingdna.com/signin/?from=app'; card.innerHTML = '

§ Session out of sync

' + '

Refresh your session.

' + '

' + 'The engine couldn’t verify your sign-in for this request. Reload to refresh the connection — your work is saved. If it keeps happening, sign in again.' + '

' + '
' + '' + 'Sign in again' + '
'; el.appendChild(card); document.body.appendChild(el); document.body.style.overflow = 'hidden'; // Reload button: top-window reload triggers the wrapper's // getSession + refreshSession path which mints a fresh JWT and // injects it into the iframe URL fragment. If the refresh-token // is also dead, the wrapper falls through to its no-session // branch and the user sees /signin via the pill, no loop. const reloadBtn = card.querySelector('#td-session-expired-reload'); if (reloadBtn) { reloadBtn.addEventListener('click', function () { // Drop the dead token FIRST. Otherwise, if the wrapper re-uses // a cached iframe URL (no fresh #access_token fragment), the // iframe re-reads the same stale token from sessionStorage and // loops on "session out of sync." Cleared → worst case the user // is treated as anonymous and gets the normal sign-in flow, no // dead-token loop. (This is the "reload doesn't fix it" bug.) _clearTokens(); try { if (window.top && window.top !== window.self) { window.top.location.reload(); return; } } catch (_) { /* cross-origin parent — fall through */ } window.location.reload(); }); } // Sign-in fallback: explicit top-window navigation so the user // can choose to re-authenticate from scratch. Clear the stale token so // the fresh sign-in isn't masked by a cached dead session. const signinLink = card.querySelector('#td-session-expired-signin'); if (signinLink) { signinLink.addEventListener('click', function (e) { e.preventDefault(); _clearTokens(); _redirectToSignup(signinLink.getAttribute('href')); }); } requestAnimationFrame(function () { el.style.opacity = '1'; }); } // Tear down the session-expired modal — called when a fresh token // arrives from the parent (silent recovery), so the user never has to // click "Reload" and never loses in-progress work (e.g. a half-done // GenBank upload). function _dismissSessionExpiredModal() { const el = document.getElementById('td-session-expired-modal'); if (el) { try { el.remove(); } catch (_) {} } _sessionExpiredShown = false; try { document.body.style.overflow = ''; } catch (_) {} } // ─── 4c. Live token channel (no-reload session refresh) ────────── // The /app wrapper holds the long-lived Supabase session and rotates // the 1-hour access token in the background. It pushes each fresh JWT // into this iframe via postMessage so our sessionStorage token never // goes stale — the root-cause fix for "session out of sync that a // reload won't clear." Pushing tokens (vs reloading the iframe with a // new URL fragment) keeps the user's current work intact. window.addEventListener('message', function (ev) { // SECURITY: this writes auth tokens, so only accept pushes from the // known wrapper origins. Anything else is ignored outright. if (TRUSTED_PARENT_ORIGINS.indexOf(ev.origin) === -1) return; const data = ev.data; if (!data || data.type !== 'td-token-refresh' || !data.access_token) return; const ok = _stashTokens( data.access_token, data.refresh_token, data.user_id, data.user_email); if (!ok) return; _lastTokenRefreshAt = Date.now(); // A signed-in user can't be mid-trial — clear any stale timer state. try { localStorage.removeItem(TRIAL_START_KEY); } catch (_) {} // If we were showing the "session out of sync" nag, the fresh token // just resolved it — dismiss silently. _dismissSessionExpiredModal(); // The email may only arrive on this push (cached iframe URL w/o hash) — // (re)render the sidebar account row now that we know who's signed in. try { _renderSidebarAccount(); } catch (_) { /* DOM not ready yet */ } }); // Ask the parent wrapper to mint + push a fresh JWT. Carries NO secret // (just a request), so it's safe to broadcast to each trusted origin; // postMessage to a non-matching frame is a silent no-op. function _requestParentTokenRefresh() { try { if (!window.parent || window.parent === window.self) return; TRUSTED_PARENT_ORIGINS.forEach(function (origin) { try { window.parent.postMessage({ type: 'td-token-please' }, origin); } catch (_) { /* origin mismatch — ignore */ } }); } catch (_) { /* no reachable parent — fall through to the modal */ } } // Signed-in user whose JWT was just rejected. Instead of immediately // nagging, ask the wrapper for a fresh token first. If it lands within // the grace window the user continues seamlessly (no modal, no reload); // only if the refresh fails (dead refresh token) do we surface the // session-expired modal so they can re-authenticate. function _onSignedInAuthFailure(signupUrl) { const before = _lastTokenRefreshAt; _requestParentTokenRefresh(); setTimeout(function () { if (_lastTokenRefreshAt > before) return; // silent refresh won _showSessionExpiredModal(signupUrl); }, 1500); } function _armTrialTimer(remainingSeconds) { if (_trialTimerId !== null) { clearTimeout(_trialTimerId); _trialTimerId = null; } if (_accessToken()) return; // signed in — no timer needed if (remainingSeconds <= 0) { _showTrialModal(); return; } // Cap setTimeout delay to avoid integer overflow on very long // remainders and to add a safety check for clock skew. const delayMs = Math.min(remainingSeconds, 7200) * 1000 + 250; _trialTimerId = setTimeout(_showTrialModal, delayMs); } function _captureTrialFromResponse(res) { // Read the server's headers to learn (a) when this visitor's // trial started, (b) how much time is left. Whichever happens // first across signin/timeout fires the modal. try { const startedAt = res.headers.get('X-TD-Trial-Started-At'); const remaining = res.headers.get('X-TD-Trial-Remaining-S'); if (startedAt) { localStorage.setItem(TRIAL_START_KEY, String(parseInt(startedAt, 10) || 0)); } if (remaining !== null) { const rem = Math.max(0, parseInt(remaining, 10) || 0); _armTrialTimer(rem); } } catch (_) { /* headers not readable in some CORS configs — ignore */ } } function _restoreTrialFromStorage() { // If the page reloads, we may not have a fresh /api/run response // yet — restore the timer from localStorage so the modal still // fires on schedule. if (_accessToken()) return; let startedAt; try { startedAt = parseInt(localStorage.getItem(TRIAL_START_KEY) || '0', 10); } catch (_) { startedAt = 0; } if (!startedAt) return; const elapsed = Math.floor(Date.now() / 1000) - startedAt; const remaining = TRIAL_SECONDS - elapsed; _armTrialTimer(remaining); } _restoreTrialFromStorage(); // Full sign-out from inside the engine iframe. The long-lived Supabase // session lives in the /app WRAPPER, not here — so clearing our own // sessionStorage isn't enough: re-entering /app would call getSession(), // re-inject a fresh JWT, and silently sign the user back in. So we ALSO // ask the wrapper to drop its Supabase session (td-signout-please); it // then navigates the top window to /signin. If no wrapper picks that up // (older landing build, or running standalone) we fall back to navigating // ourselves after a short grace window so the user is never stranded. function _fullSignOut() { try { sessionStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(REFRESH_KEY); sessionStorage.removeItem(USER_ID_KEY); sessionStorage.removeItem(USER_EMAIL_KEY); // Clear the engine's session-resume so the next anon visitor // (or a different user signing in on this browser) doesn't // see the prior user's pasted sequence. sessionStorage.removeItem('td_engine_resume'); localStorage.removeItem(TRIAL_START_KEY); } catch (_) { /* ignore */ } let askedParent = false; try { if (window.parent && window.parent !== window.self) { TRUSTED_PARENT_ORIGINS.forEach(function (origin) { try { window.parent.postMessage({ type: 'td-signout-please' }, origin); askedParent = true; } catch (_) { /* origin mismatch — ignore */ } }); } } catch (_) { /* no reachable parent */ } // Give the wrapper a beat to clear the Supabase session + navigate the // top window. If it doesn't (no handler), we navigate ourselves. setTimeout(function () { _redirectToSignup('https://turingdna.com/signin/?from=signout'); }, askedParent ? 1400 : 0); } // ─── 5. Public surface ────────────────────────────────────────── window.TuringAuth = { getToken: _accessToken, getEmail: _userEmail, isSignedIn: function () { return !!_accessToken(); }, signOut: _fullSignOut, }; // ─── 5b. Sidebar account row (foot of the engine's nav rail) ───── // Renders the signed-in user's identity natively at the bottom of the // sidebar (Claude-style), replacing the wrapper's old floating chip. function _renderSidebarAccount() { const signin = document.getElementById('sidebarSignin'); const root = document.getElementById('sidebarAccount'); const email = _userEmail(); const signedIn = !!_accessToken() && !!email; // Anonymous → show the "Sign in" row and tell the /app wrapper to retire // its own floating fallback so the two never both appear once this ships. if (signin) { signin.hidden = signedIn; if (!signedIn) _announceAnonAffordance(); } if (!root) return; if (!signedIn) { root.hidden = true; return; } const nameEl = document.getElementById('acctName'); const emailEl = document.getElementById('acctMenuEmail'); const avatar = document.getElementById('acctAvatar'); if (nameEl) nameEl.textContent = email; if (emailEl) emailEl.textContent = email; if (avatar) avatar.textContent = (email.trim().charAt(0) || '?').toUpperCase(); root.hidden = false; } // Tell the /app wrapper that the engine now renders its own anonymous // "Sign in" row in the sidebar foot, so the wrapper can drop its floating // fallback link (prevents a duplicate once this build is deployed). The // current production wrapper simply ignores an unknown message type. function _announceAnonAffordance() { try { if (!window.parent || window.parent === window.self) return; TRUSTED_PARENT_ORIGINS.forEach(function (origin) { try { window.parent.postMessage({ type: 'td-anon-affordance' }, origin); } catch (_) { /* origin mismatch — ignore */ } }); } catch (_) { /* no reachable parent */ } } function _initSidebarAccount() { const trigger = document.getElementById('acctTrigger'); const menu = document.getElementById('acctMenu'); const signout = document.getElementById('acctSignOut'); if (trigger && menu && !trigger._tdWired) { trigger._tdWired = true; const close = function () { menu.hidden = true; trigger.setAttribute('aria-expanded', 'false'); }; const open = function () { menu.hidden = false; trigger.setAttribute('aria-expanded', 'true'); }; trigger.addEventListener('click', function (e) { e.stopPropagation(); if (menu.hidden) open(); else close(); }); // Click anywhere else closes the menu. document.addEventListener('click', function (e) { if (menu.hidden) return; if (e.target === trigger || trigger.contains(e.target)) return; if (menu.contains(e.target)) return; close(); }); document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && !menu.hidden) close(); }); if (signout) { signout.addEventListener('click', function () { close(); _fullSignOut(); }); } } _renderSidebarAccount(); } // Hook the fetch wrapper from §3 so it also reads trial headers. // We re-wrap rather than mutating the earlier wrapper so the order // of operations stays explicit. const _captureFetch = window.fetch.bind(window); window.fetch = function trialAwareFetch(input, init) { return _captureFetch(input, init).then(function (res) { try { const url = (typeof input === 'string') ? input : (input && input.url) || ''; if ((url.includes('/api/run') || url.indexOf('/api/run') >= 0) && res) { _captureTrialFromResponse(res); } } catch (_) { /* ignore */ } return res; }); }; // ─── 6. Tell the /app wrapper the engine has booted ────────────── // Lets the wrapper drop its cold-start "Waking the engine…" screen as // soon as our UI is up — reliable on iOS, where the cross-origin iframe // load event isn't. Carries no data; posted only to trusted parents. function _announceEngineReady() { try { if (!window.parent || window.parent === window.self) return; TRUSTED_PARENT_ORIGINS.forEach(function (origin) { try { window.parent.postMessage({ type: 'td-engine-ready' }, origin); } catch (_) { /* origin mismatch — ignore */ } }); } catch (_) { /* no reachable parent */ } } function _onReady() { _announceEngineReady(); _initSidebarAccount(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', _onReady); } else { _onReady(); } })();