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