// Invite-link fragment codec (see docs/onboarding-and-home-server.md). // New links carry a versioned fingerprint — /join/join#e1. or #e1.~ — // so clients (web paste-to-join, the iOS and Mac apps) can recognise an Eleven // invite deterministically and offline, and future format changes can bump the // version. Old unprefixed # links parse identically. Pure and framework-free // so it's unit-testable in Node (test/invite.test.mjs). export const INVITE_PREFIX = "e1."; // The branding key's shape (docs/no-plaintext.md): lowercase base32, minted // 10 chars, tolerated 8–32 so the length can be dialed without a format bump. // Used to disambiguate the third fragment slot from legacy names containing a // raw "~" (encodeURIComponent leaves "~" unescaped; new builders escape it). const BRANDING_KEY_RE = /^[a-z2-7]{8,32}$/; // parseInviteFragment("e1.~~") -> { T, name, b }, name URL-decoded // ("" if absent or undecodable), b the branding key when the link carries one // (sealed instances put it in every minted link). Accepts unprefixed legacy // fragments and the older 1- and 2-slot forms; slot order is fixed (T first) // so old parsers keep claiming from new links. export function parseInviteFragment(frag) { let f = frag || ""; if (f.startsWith(INVITE_PREFIX)) f = f.slice(INVITE_PREFIX.length); const parts = f.split("~"); const T = parts[0]; let name = parts.slice(1).join("~"); let b = ""; const last = parts[parts.length - 1]; if (parts.length >= 3 && BRANDING_KEY_RE.test(last)) { b = last; name = parts.slice(1, -1).join("~"); } return { T, name: safeDecode(name), b }; } // buildInviteFragment(T, name?, b?) -> "e1.", "e1.~", or // "e1.~~" (name slot kept, possibly empty, whenever b is // present). All minters use this, so every new link carries the fingerprint — // and, on a sealed instance, the branding key. export function buildInviteFragment(T, name = "", b = "") { let f = INVITE_PREFIX + T; if (name || b) f += "~" + encodeURIComponent(name).replace(/~/g, "%7E"); if (b) f += "~" + b; return f; } // True when the fragment carries the versioned fingerprint — the deterministic // "this is an Eleven invite" signal (an arbitrary URL with /join/ in it isn't). export function hasInviteFingerprint(frag) { return (frag || "").startsWith(INVITE_PREFIX); } function safeDecode(s) { try { return decodeURIComponent(s); } catch { return ""; } }