eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

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.
8const MAX_AGE_MS = 60 * 60 * 1000;
9
10// parseJoinReturn pulls the return URL out of "#joinret=<encoded url>".
11export 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.
22export 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.
34export function stashJoinReturn(url, now) {
35 return JSON.stringify({ v: 1, url: String(url), at: now });
36}
37
38export 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