eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Pure decision helpers for the signup-first join page (docs/superpowers/specs/
2// 2026-07-31-signup-first-join-design.md): which links lead with the sign-in
3// card, and how the round trip to the home server is stamped so a forged or
4// stale ?su= marker never reads as "signed up". No DOM, no storage — join.js
5// owns those — so this is Node-testable (test/prejoin.test.mjs).
6
7export const PREJOIN_STASH_KEY = "lchat_prejoin";
8
9// A pre-join round trip should be minutes; an hour-old stash is an abandoned
10// one — reading it as a fresh sign-up would put a stale "saved" promise on the
11// welcome card.
12const MAX_AGE_MS = 60 * 60 * 1000;
13
14// offerPrejoin: the lead card shows only to a FRESH person (join/setup kinds —
15// device links and adopts already have an identity), only where a home server
16// exists to sign into, and never in the native embed/wrapper (the apps own
17// their sign-in). signedUp means the round trip already happened.
18export function offerPrejoin({ kind, homeOrigin, embed, native, signedUp }) {
19 if (embed || native || signedUp) return false;
20 if (kind !== "join" && kind !== "setup") return false;
21 return typeof homeOrigin === "string" && homeOrigin.startsWith("http");
22}
23
24// prejoinStash packages what must survive the navigation away: the invite
25// fragment (the secret NEVER travels to the home origin — it waits here, on
26// the instance origin) and the nonce the return marker must echo.
27export function prejoinStash({ frag, nonce, now }) {
28 return JSON.stringify({ v: 1, frag: String(frag || ""), nonce: String(nonce || ""), at: now });
29}
30
31// readPrejoinReturn: ?su=<nonce> marks a return from the home sign-in. It
32// counts only when the nonce matches a fresh stash — anyone can type ?su=1,
33// but only this browser holds the stash minted before leaving.
34export function readPrejoinReturn({ search, storedJSON, now }) {
35 const none = { signedUp: false, frag: "" };
36 const su = new URLSearchParams(search || "").get("su") || "";
37 if (!su) return none;
38 let stash = null;
39 try { stash = JSON.parse(storedJSON || "null"); } catch { return none; }
40 if (!stash || typeof stash.nonce !== "string" || stash.nonce !== su) return none;
41 if (typeof stash.at !== "number" || now < stash.at || now - stash.at > MAX_AGE_MS) return none;
42 return { signedUp: true, frag: String(stash.frag || "") };
43}
44
45// homeSigninURL builds the door: the home page reads #joinret=<url> (a
46// fragment, so the return URL never appears in home's server logs) and sends
47// the person back to it once signed in. Only the nonce rides the return URL.
48export function homeSigninURL(homeOrigin, origin, pathname, nonce) {
49 const ret = origin + pathname + "?su=" + encodeURIComponent(nonce);
50 return homeOrigin.replace(/\/$/, "") + "/#joinret=" + encodeURIComponent(ret);
51}
52