// S1b (client half) - the emailed accept link. // // `POST /R4/collaboration-team-external-invite-accept` is `Authorizer: NONE` ON PURPOSE: the invitee // has no Akashic account yet - accepting is what creates it - so the emailed token is the only thing // that could authenticate them. That makes this parse the entire client-side surface of the flow. // // The caller sends BOTH values: the email only LOCATES candidate invites (one address may hold // invites to several teams) and the token AUTHENTICATES. Knowing an email is never enough. export type AcceptInviteParams = | { ok: true; token: string; email: string } | { ok: false }; /** * Extract the token and email from the link's query string. * * Refuses rather than guesses: a half-formed link must render a "this link is not valid" screen, not * a request the server will reject for reasons we would then have to explain. * * The email IS normalized (lower-cased/trimmed) because the invite stored it that way and a mail * client may capitalize it. The TOKEN is deliberately NOT trimmed - it is a credential compared in * constant time against a stored digest, and silently reshaping it here could turn a genuine * mismatch into a success, or mask a real one. */ export const parseAcceptInviteParams = (search: string): AcceptInviteParams => { const params = new URLSearchParams(search); const token = params.get('token'); const email = params.get('email'); if (!token || !email || !email.trim()) return { ok: false }; return { ok: true, token, email: email.trim().toLowerCase() }; };