Something went wrong. Try again.
The logic for serving and storing associations between users and apps
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738const CSS = ` :root { color-scheme: light; --bg: #f3efe4; --ink: #1b1916; --muted: #5c564c; --accent: #0b5d45; --card: #fffdf7; --line: #d9d2c3; } * { box-sizing: border-box; } html, body { margin: 0; padding: 0; } body { font-family: "Iowan Old Style", Palatino, Georgia, serif; background: var(--bg); color: var(--ink); line-height: 1.45; } main { max-width: 36rem; margin: 0 auto; padding: 2.5rem 1.25rem 4rem; } h1 { font-size: 1.75rem; letter-spacing: -0.02em; margin: 0 0 0.5rem; } p { color: var(--muted); margin: 0 0 1rem; } a { color: var(--accent); } button, .btn { appearance: none; border: 0; background: var(--accent); color: #f7fff9; font: 600 0.95rem/1.2 system-ui, sans-serif; padding: 0.7rem 1rem; border-radius: 999px; cursor: pointer; text-decoration: none; display: inline-block; } button.secondary { background: transparent; color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); } button:disabled { opacity: 0.5; cursor: not-allowed; } .wordmark { font-size: 0.8rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); margin-bottom: 1.25rem; } .list { display: grid; gap: 0.6rem; margin: 1.25rem 0; } .account { text-align: left; width: 100%; border-radius: 14px; background: var(--card); color: var(--ink); box-shadow: inset 0 0 0 1px var(--line); padding: 0.85rem 1rem; font-family: system-ui, sans-serif; } .account strong { display: block; font-size: 1rem; } .account span { color: var(--muted); font-size: 0.8rem; } .empty { font-family: system-ui, sans-serif; color: var(--muted); } .actions { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-top: 1.25rem; } .mono { font-family: ui-monospace, monospace; font-size: 0.8rem; word-break: break-all; }`;
/** * JSON literal safe to inline in a `<script>` block. Escaping `<` stops a * value that contains `</script>` from closing the element early. */function jsonForScript(value: unknown): string { return JSON.stringify(value ?? null) .replaceAll("<", "\\u003c") .replaceAll(">", "\\u003e") .replaceAll("\u2028", "\\u2028") .replaceAll("\u2029", "\\u2029");}
function shell(title: string, body: string, extraHead = ""): string { return `<!doctype html><html lang="en"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>${escapeHtml(title)}</title> <style>${CSS}</style> ${extraHead}</head><body>${body}</body></html>`;}
export { landingPage } from "./landing";
export function embedPage(): string { const script = ` const parentOrigin = resolveParentOrigin();
function resolveParentOrigin() { if (location.ancestorOrigins && location.ancestorOrigins[0]) return location.ancestorOrigins[0]; if (document.referrer) { try { return new URL(document.referrer).origin; } catch { return ""; } } return ""; }
function isHttpOrigin(origin) { try { const url = new URL(origin); if (url.protocol === "https:") return true; return url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1"); } catch { return false; } }
function hasAccess() { if (window.top === window) return Promise.resolve(true); if (typeof document.hasStorageAccess === "function") return document.hasStorageAccess(); return Promise.resolve(true); }
async function tryActivateExistingGrant() { if (window.top === window) return true; if (typeof document.requestStorageAccess !== "function") return false; try { await document.requestStorageAccess(); return true; } catch { return false; } }
async function api(method, path, body) { const res = await fetch(path, { method, credentials: "include", headers: body ? { "content-type": "application/json" } : undefined, body: body ? JSON.stringify(body) : undefined, }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.message || res.statusText); return data; }
function showChoose() { window.parent.postMessage({ type: "needs-access" }, parentOrigin); document.body.innerHTML = '<button type="button" id="choose">Continue with an AT Proto account you already use</button>'; document.body.style.margin = "0"; document.body.style.background = "transparent"; const btn = document.getElementById("choose"); btn.style.width = "100%"; btn.addEventListener("click", async () => { // Do not call hasStorageAccess() here — it can consume the user gesture. if (typeof document.requestStorageAccess === "function") { try { await document.requestStorageAccess(); window.location.reload(); return; } catch {} } window.parent.postMessage({ type: "popup" }, parentOrigin); const url = "/choose?parent=" + encodeURIComponent(parentOrigin); window.open(url, "loggedin-choose", "width=440,height=600"); }); }
async function bootSilent() { window.addEventListener("message", onMessage); window.parent.postMessage({ type: "ready" }, parentOrigin); const data = await api("GET", "/xrpc/fyi.loggedin.account.list"); window.parent.postMessage({ type: "accounts", accounts: data.accounts }, parentOrigin); }
async function boot() { if (!parentOrigin || !isHttpOrigin(parentOrigin)) return; if (await hasAccess()) { await bootSilent(); return; } if (await tryActivateExistingGrant()) { window.location.reload(); return; } showChoose(); }
async function onMessage(event) { if (event.data && event.data.type === "picked" && event.data.did && event.source === window) { return; } if (event.data && event.data.type === "picked" && event.data.did && event.origin === location.origin) { window.parent.postMessage({ type: "picked", did: event.data.did }, parentOrigin); return; } if (event.origin !== parentOrigin) return; const type = event.data && event.data.type; const did = event.data && event.data.did; if (type === "heartbeat" && did) { await api("POST", "/xrpc/fyi.loggedin.account.loggedIn", { did, origin: parentOrigin }); const data = await api("GET", "/xrpc/fyi.loggedin.account.list"); window.parent.postMessage({ type: "accounts", accounts: data.accounts }, parentOrigin); } if (type === "logout" && did) { await api("POST", "/xrpc/fyi.loggedin.account.logout", { did, origin: parentOrigin }); const data = await api("GET", "/xrpc/fyi.loggedin.account.list"); window.parent.postMessage({ type: "accounts", accounts: data.accounts }, parentOrigin); } }
boot().catch(() => { if (parentOrigin && isHttpOrigin(parentOrigin)) showChoose(); }); `; return shell( "loggedin.fyi embed", "", `<style>body{background:transparent;padding:0}</style><script>${script}</script>`, );}
export function choosePage(parentOrigin: string): string { const script = ` const PARENT = ${JSON.stringify(parentOrigin)};
async function main() { const res = await fetch("/xrpc/fyi.loggedin.account.list", { credentials: "include" }); const data = await res.json(); const root = document.getElementById("list"); if (!data.accounts || data.accounts.length === 0) { root.innerHTML = '<p class="empty">No accounts saved on this device yet.</p>'; return; } const needed = [...new Set(data.accounts.flatMap((a) => a.origins || []))]; const LABELS = await loadOriginLabels(needed); for (const account of data.accounts) { const btn = document.createElement("button"); btn.className = "account"; btn.type = "button"; const handle = await resolveHandle(account.did); const originNames = account.origins.map((o) => LABELS[o] || o).join(", "); btn.innerHTML = "<strong>" + escapeHtml(handle) + "</strong><span>" + escapeHtml(originNames) + "</span>"; btn.addEventListener("click", () => pick(account.did)); root.appendChild(btn); } }
async function loadOriginLabels(origins) { const labels = {}; if (!origins.length) return labels; try { const qs = origins.map((o) => "origin=" + encodeURIComponent(o)).join("&"); const res = await fetch("/origins?" + qs); const data = await res.json(); for (const row of data.origins || []) labels[row.origin] = row.name; } catch {} return labels; }
function pick(did) { const payload = { type: "picked", did }; if (window.opener) { try { if (window.opener.parent && window.opener.parent !== window.opener) { window.opener.parent.postMessage(payload, PARENT); } else { window.opener.postMessage(payload, PARENT); } } catch { window.opener.postMessage(payload, PARENT); } } window.close(); }
async function resolveHandle(did) { try { const res = await fetch("https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=" + encodeURIComponent(did)); if (!res.ok) return did; const profile = await res.json(); return profile.handle ? "@" + profile.handle : did; } catch { return did; } }
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); }
main(); `; return shell( "Choose an account — loggedin.fyi", `<main> <div class="wordmark">loggedin.fyi</div> <h1>Continue with an account you already use</h1> <p>These DIDs were saved on this device by participating AT Proto apps. Picking one sends it back so the app can start a normal login.</p> <div id="list" class="list"></div> </main> <script>${script}</script>`, );}
export function enrollPage(did: string, returnUrl: string): string { const script = ` const DID = ${JSON.stringify(did)}; const RETURN_URL = ${JSON.stringify(returnUrl)};
document.getElementById("save").addEventListener("click", async () => { const btn = document.getElementById("save"); btn.disabled = true; try { const origin = new URL(RETURN_URL).origin; const res = await fetch("/xrpc/fyi.loggedin.account.loggedIn", { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify({ did: DID, origin }), }); if (!res.ok) throw new Error("save failed"); location.href = RETURN_URL; } catch (err) { btn.disabled = false; document.getElementById("err").textContent = String(err.message || err); } });
document.getElementById("skip").addEventListener("click", () => { location.href = RETURN_URL; }); `; return shell( "Save this account — loggedin.fyi", `<main> <div class="wordmark">loggedin.fyi</div> <h1>Save this account on this device</h1> <p>Other AT Proto apps can then offer it as a login hint. This does not sign you in anywhere.</p> <p class="mono">${escapeHtml(did)}</p> <div class="actions"> <button type="button" id="save">Save</button> <button type="button" id="skip" class="secondary">Not now</button> </div> <p id="err"></p> </main> <script>${script}</script>`, );}
export function savePage(did: string, origin: string): string { const script = ` const DID = ${JSON.stringify(did)}; const ORIGIN = ${JSON.stringify(origin)};
async function run() { const btn = document.getElementById("save"); const err = document.getElementById("err"); try { const res = await fetch("/xrpc/fyi.loggedin.account.loggedIn", { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify({ did: DID, origin: ORIGIN }), }); if (!res.ok) throw new Error("save failed"); if (window.opener) { window.opener.postMessage({ type: "saved", did: DID }, ORIGIN); } window.close(); } catch (e) { btn.disabled = false; btn.style.display = ""; err.textContent = String(e.message || e); } }
document.getElementById("save").addEventListener("click", run); run(); `; return shell( "Saving — loggedin.fyi", `<main> <div class="wordmark">loggedin.fyi</div> <h1>Saving account…</h1> <p class="mono">${escapeHtml(did)}</p> <p id="err"></p> <button type="button" id="save" style="display:none">Retry</button> </main> <script>${script}</script>`, );}
export function savePageLogout(did: string, origin: string): string { const script = ` const DID = ${JSON.stringify(did)}; const ORIGIN = ${JSON.stringify(origin)};
async function run() { try { const res = await fetch("/xrpc/fyi.loggedin.account.logout", { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify({ did: DID, origin: ORIGIN }), }); if (!res.ok) throw new Error("logout failed"); if (window.opener) { window.opener.postMessage({ type: "saved", did: DID, action: "logout" }, ORIGIN); } window.close(); } catch (e) { document.getElementById("err").textContent = String(e.message || e); } }
run(); `; return shell( "Removing — loggedin.fyi", `<main> <div class="wordmark">loggedin.fyi</div> <h1>Removing account…</h1> <p class="mono">${escapeHtml(did)}</p> <p id="err"></p> </main> <script>${script}</script>`, );}
export type NativeStartOptions = { appName: string; callback: string; origin: string; state: string | null; token: string; /** Set for the post-login save flow; omitted for the account chooser. */ did?: string;};
/** * First-party page opened by a native SDK through the shared browser cookie * jar (ASWebAuthenticationSession on iOS, a Custom Tab on Android). The cookie * is attached here, so this page — and only this page — can hand the app a * token for the same session. The token travels on the affirmative action * only; "Not now" returns `denied=1`. */export function nativeStartPage(options: NativeStartOptions): string { const script = ` const CALLBACK = ${jsonForScript(options.callback)}; const ORIGIN = ${jsonForScript(options.origin)}; const STATE = ${jsonForScript(options.state)}; const TOKEN = ${jsonForScript(options.token)}; const SAVE_DID = ${jsonForScript(options.did ?? null)};
function callbackUrl(params) { const url = new URL(CALLBACK); if (STATE !== null) url.searchParams.set("state", STATE); for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value); return url.toString(); }
function finish(params) { location.href = callbackUrl(params); }
function deny() { finish({ denied: "1" }); }
async function api(method, path, body) { const res = await fetch(path, { method, credentials: "include", headers: body ? { "content-type": "application/json" } : undefined, body: body ? JSON.stringify(body) : undefined, }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data.message || res.statusText); return data; }
async function save() { const btn = document.getElementById("primary"); btn.disabled = true; try { await api("POST", "/xrpc/fyi.loggedin.account.loggedIn", { did: SAVE_DID, origin: ORIGIN }); finish({ did: SAVE_DID, token: TOKEN }); } catch (err) { btn.disabled = false; document.getElementById("err").textContent = String(err.message || err); } }
async function chooser() { const list = document.getElementById("list"); try { const data = await api("GET", "/xrpc/fyi.loggedin.account.list"); const accounts = data.accounts || []; if (accounts.length === 0) { list.innerHTML = '<p class="empty">No accounts saved on this device yet. Continue to sign in normally.</p>'; document.getElementById("continue").hidden = false; return; } list.innerHTML = ""; for (const account of accounts) { const btn = document.createElement("button"); btn.type = "button"; btn.className = "account"; btn.innerHTML = "<strong>" + escapeHtml(account.did) + "</strong>" + "<span>" + escapeHtml((account.origins || []).join(", ")) + "</span>"; btn.addEventListener("click", () => finish({ did: account.did, token: TOKEN })); list.appendChild(btn); } document.getElementById("continue").hidden = false; } catch (err) { list.innerHTML = '<p class="empty">' + escapeHtml(String(err.message || err)) + "</p>"; document.getElementById("continue").hidden = false; } }
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); }
document.getElementById("deny").addEventListener("click", deny); if (SAVE_DID) { document.getElementById("primary").addEventListener("click", save); } else { document.getElementById("continue").addEventListener("click", () => finish({ token: TOKEN })); chooser(); } `;
const body = options.did ? `<h1>Remember this account on this device?</h1> <p>${escapeHtml(options.appName)} can let other AT Proto apps on this device offer this account as a login hint. It does not sign you in anywhere else.</p> <p class="mono">${escapeHtml(options.did)}</p> <div class="actions"> <button type="button" id="primary">Remember</button> <button type="button" id="deny" class="secondary">Not now</button> </div>` : `<h1>Continue with an account you already use</h1> <p>These accounts were saved on this device by participating AT Proto apps. Picking one sends it to ${escapeHtml(options.appName)} so it can start a normal login.</p> <div id="list" class="list"><p class="empty">Looking…</p></div> <div class="actions"> <button type="button" id="continue" hidden>Use a different account</button> <button type="button" id="deny" class="secondary">Not now</button> </div>`;
return shell( "loggedin.fyi", `<main> <div class="wordmark">loggedin.fyi</div> ${body} <p id="err"></p> </main> <script>${script}</script>`, );}
export function exampleRendererJs(): string { return `(() => { const BSKY = "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile";
function ensureAccountStyles() { if (document.getElementById("loggedin-account-styles")) return; const style = document.createElement("style"); style.id = "loggedin-account-styles"; style.textContent = [ ".loggedin-account { display: flex; align-items: center; gap: 0.75rem; text-align: left; width: 100%; box-sizing: border-box; cursor: pointer; }", ".loggedin-account-avatar { width: 2.5rem; height: 2.5rem; border-radius: 999px; object-fit: cover; flex-shrink: 0; background: currentColor; opacity: 0.18; }", "img.loggedin-account-avatar { opacity: 1; background: transparent; }", ".loggedin-account-meta { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }", ".loggedin-account-meta strong, .loggedin-account-meta em, .loggedin-account-meta span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }", ".loggedin-account-meta em { font-style: normal; opacity: 0.85; }", ".loggedin-account-meta span { opacity: 0.7; font-size: 0.85em; }", ].join("\\n"); document.head.appendChild(style); }
async function resolveHandle(did) { try { const res = await fetch(BSKY + "?actor=" + encodeURIComponent(did)); if (!res.ok) return { handle: did, displayName: "", avatar: "" }; const p = await res.json(); return { handle: p.handle ? "@" + p.handle : did, displayName: p.displayName || "", avatar: typeof p.avatar === "string" ? p.avatar : "", }; } catch { return { handle: did, displayName: "", avatar: "" }; } }
async function renderAccountList(el, accounts, options) { const onPick = options && options.onPick; const labels = (options && options.originLabels) || {}; ensureAccountStyles(); el.innerHTML = ""; if (!accounts || accounts.length === 0) { el.textContent = "No hinted accounts on this device."; return; } for (const account of accounts) { const profile = await resolveHandle(account.did); const btn = document.createElement("button"); btn.type = "button"; btn.className = "loggedin-account"; const originNames = (account.origins || []).map((o) => labels[o] || o).join(", "); const avatar = profile.avatar ? '<img class="loggedin-account-avatar" src="' + escapeHtml(profile.avatar) + '" alt="" width="40" height="40">' : '<span class="loggedin-account-avatar" aria-hidden="true"></span>'; btn.innerHTML = avatar + '<span class="loggedin-account-meta">' + "<strong>" + escapeHtml(profile.handle) + "</strong>" + (profile.displayName ? "<em>" + escapeHtml(profile.displayName) + "</em>" : "") + "<span>" + escapeHtml(originNames) + "</span>" + "</span>"; btn.addEventListener("click", () => onPick && onPick(account.did, profile.handle)); el.appendChild(btn); } }
let popupMode = false;
function originOf(iframe, fallback) { try { return new URL(iframe.src).origin; } catch { return fallback || ""; } }
function prepareEmbed(iframe) { const current = iframe.getAttribute("allow") || ""; const parts = current.split(";").map((part) => part.trim()).filter(Boolean); if (!parts.includes("storage-access")) parts.push("storage-access"); iframe.setAttribute("allow", parts.join("; ")); return iframe; }
function listenToEmbed(iframe, handlers) { prepareEmbed(iframe); window.addEventListener("message", (event) => { if (event.source === iframe.contentWindow) { const type = event.data && event.data.type; if (type === "accounts" && handlers.onAccounts) handlers.onAccounts(event.data.accounts || [], event.origin); if (type === "unavailable" && handlers.onUnavailable) handlers.onUnavailable(); if (type === "needs-access" && handlers.onNeedsAccess) handlers.onNeedsAccess(); if (type === "popup") { popupMode = true; if (handlers.onPopup) handlers.onPopup(); else if (handlers.onUnavailable) handlers.onUnavailable(); } if (type === "ready" && handlers.onReady) handlers.onReady(); if (type === "picked" && handlers.onPicked) handlers.onPicked(event.data.did); return; } const loggedinOrigin = originOf(iframe, ""); if (loggedinOrigin && event.origin === loggedinOrigin && event.data && event.data.type === "saved" && event.data.did) { if (handlers.onSaved) handlers.onSaved(event.data.did, event.data.action || "save"); } }); }
function mountEmbed(iframe, src, handlers) { prepareEmbed(iframe); listenToEmbed(iframe, handlers); iframe.src = src; }
function listenForPicked(loggedinOrigin, onPicked) { window.addEventListener("message", (event) => { if (event.origin !== loggedinOrigin) return; if (event.data && event.data.type === "picked" && event.data.did) onPicked(event.data.did); }); }
function saveUrl(loggedinOrigin, did, origin, action) { const url = new URL("/save", loggedinOrigin); url.searchParams.set("did", did); url.searchParams.set("origin", origin); if (action === "logout") url.searchParams.set("action", "logout"); return url.toString(); }
function openSavePopup(iframe, did, targetOrigin, action) { const loggedinOrigin = originOf(iframe, targetOrigin); window.open( saveUrl(loggedinOrigin, did, location.origin, action), action === "logout" ? "loggedin-logout" : "loggedin-save", "width=440,height=600" ); }
function heartbeat(iframe, did, targetOrigin) { if (popupMode) { openSavePopup(iframe, did, targetOrigin, "save"); return; } iframe.contentWindow.postMessage({ type: "heartbeat", did }, targetOrigin); }
function logout(iframe, did, targetOrigin) { if (popupMode) { openSavePopup(iframe, did, targetOrigin, "logout"); return; } iframe.contentWindow.postMessage({ type: "logout", did }, targetOrigin); }
function enrollUrl(loggedinOrigin, did, returnUrl) { const url = new URL("/enroll", loggedinOrigin); url.searchParams.set("did", did); url.searchParams.set("return", returnUrl); return url.toString(); }
function escapeHtml(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); }
window.Loggedin = { renderAccountList, prepareEmbed, listenToEmbed, mountEmbed, listenForPicked, heartbeat, logout, enrollUrl, saveUrl, };})();`;}
function escapeHtml(value: string): string { return value .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """);}