// Pure helpers for the pre-join sign-in return (the signup-first join flow): // a join page sends its visitor here with #joinret=, and once signed in // the first vault render sends them straight back. These helpers are the // guard on that redirect — Node-tested (home/test/joinreturn.test.mjs). // A sign-in should take minutes; an hour-old return intent is abandoned, and // honouring it would teleport a later sign-in in this tab somewhere unexpected. const MAX_AGE_MS = 60 * 60 * 1000; // parseJoinReturn pulls the return URL out of "#joinret=". export function parseJoinReturn(hash) { const m = /^#joinret=(.+)$/.exec(hash || ""); if (!m) return ""; try { return decodeURIComponent(m[1]); } catch { return ""; } } // validJoinReturn: only ever send a signed-in person to an https /join/ page // (http allowed for localhost dev). Instance hosts are arbitrary custom // domains, so no host allowlist is possible — the signed-out status line // displays the destination host instead, and the path/scheme pin means the // worst a crafted link can do is land someone on a join page they can close. export function validJoinReturn(url) { let u; try { u = new URL(url); } catch { return false; } const local = u.hostname === "localhost" || u.hostname === "127.0.0.1"; if (u.protocol !== "https:" && !(u.protocol === "http:" && local)) return false; if (u.username || u.password) return false; return u.pathname.startsWith("/join/"); } // The sessionStorage shape. Parked (not held in a variable) because the anchor // doors (Apple/Google/…) leave this page and re-enter it — a fragment or a // variable wouldn't survive the trip; tab-scoped sessionStorage does. export function stashJoinReturn(url, now) { return JSON.stringify({ v: 1, url: String(url), at: now }); } export function readJoinReturnStash(json, now) { let s = null; try { s = JSON.parse(json || "null"); } catch { return ""; } if (!s || typeof s.url !== "string" || typeof s.at !== "number") return ""; if (now < s.at || now - s.at > MAX_AGE_MS) return ""; return validJoinReturn(s.url) ? s.url : ""; }