eleven / messenger Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Invite-link fragment codec (see docs/onboarding-and-home-server.md).
2// New links carry a versioned fingerprint — /join/join#e1.<T> or #e1.<T>~<name> —
3// so clients (web paste-to-join, the iOS and Mac apps) can recognise an Eleven
4// invite deterministically and offline, and future format changes can bump the
5// version. Old unprefixed #<T> links parse identically. Pure and framework-free
6// so it's unit-testable in Node (test/invite.test.mjs).
7
8export const INVITE_PREFIX = "e1.";
9
10// The branding key's shape (docs/no-plaintext.md): lowercase base32, minted
11// 10 chars, tolerated 8–32 so the length can be dialed without a format bump.
12// Used to disambiguate the third fragment slot from legacy names containing a
13// raw "~" (encodeURIComponent leaves "~" unescaped; new builders escape it).
14const BRANDING_KEY_RE = /^[a-z2-7]{8,32}$/;
15
16// parseInviteFragment("e1.<T>~<name>~<b>") -> { T, name, b }, name URL-decoded
17// ("" if absent or undecodable), b the branding key when the link carries one
18// (sealed instances put it in every minted link). Accepts unprefixed legacy
19// fragments and the older 1- and 2-slot forms; slot order is fixed (T first)
20// so old parsers keep claiming from new links.
21export function parseInviteFragment(frag) {
22 let f = frag || "";
23 if (f.startsWith(INVITE_PREFIX)) f = f.slice(INVITE_PREFIX.length);
24 const parts = f.split("~");
25 const T = parts[0];
26 let name = parts.slice(1).join("~");
27 let b = "";
28 const last = parts[parts.length - 1];
29 if (parts.length >= 3 && BRANDING_KEY_RE.test(last)) {
30 b = last;
31 name = parts.slice(1, -1).join("~");
32 }
33 return { T, name: safeDecode(name), b };
34}
35
36// buildInviteFragment(T, name?, b?) -> "e1.<T>", "e1.<T>~<encoded name>", or
37// "e1.<T>~<encoded name>~<b>" (name slot kept, possibly empty, whenever b is
38// present). All minters use this, so every new link carries the fingerprint —
39// and, on a sealed instance, the branding key.
40export function buildInviteFragment(T, name = "", b = "") {
41 let f = INVITE_PREFIX + T;
42 if (name || b) f += "~" + encodeURIComponent(name).replace(/~/g, "%7E");
43 if (b) f += "~" + b;
44 return f;
45}
46
47// True when the fragment carries the versioned fingerprint — the deterministic
48// "this is an Eleven invite" signal (an arbitrary URL with /join/ in it isn't).
49export function hasInviteFingerprint(frag) {
50 return (frag || "").startsWith(INVITE_PREFIX);
51}
52
53function safeDecode(s) {
54 try {
55 return decodeURIComponent(s);
56 } catch {
57 return "";
58 }
59}
60