Something went wrong. Try again.
The logic for serving and storing associations between users and apps
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124/** * Bearer token for native SDKs. * * A native app cannot hold the `loggedin_session` cookie, but it can reach the * shared browser cookie jar once, through ASWebAuthenticationSession on iOS or * a Custom Tab on Android. That first-party visit proves possession of the * cookie; in exchange the worker mints this token, which carries the same * session id. Afterwards the app calls XRPC directly with no browser. * * The token is stateless: an HMAC over `{sid, origin, exp}`. Only the worker * can mint one, so unlike a self-asserted device id it cannot be guessed. */
export const DEVICE_TOKEN_HEADER = "X-Loggedin-Token";export const DEVICE_TOKEN_VERSION = "v1";
/** Matches the account row TTL in session.ts — a token never outlives the data. */export const DEVICE_TOKEN_TTL_SEC = 7 * 24 * 60 * 60;
export type DeviceTokenClaims = { sid: string; origin: string; exp: number;};
export type VerifiedToken = | { ok: true; claims: DeviceTokenClaims } | { ok: false; reason: string };
type Payload = { s: string; o: string; e: number;};
const HMAC_PARAMS = { name: "HMAC", hash: "SHA-256" } as const;
export function base64UrlEncode(bytes: Uint8Array): string { let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");}
export function base64UrlDecode(value: string): Uint8Array { const padded = value.replaceAll("-", "+").replaceAll("_", "/"); const binary = atob(padded + "=".repeat((4 - (padded.length % 4)) % 4)); return Uint8Array.from(binary, (char) => char.charCodeAt(0));}
async function signingKey(secret: string): Promise<CryptoKey> { return crypto.subtle.importKey( "raw", new TextEncoder().encode(secret), HMAC_PARAMS, false, ["sign", "verify"], );}
export async function mintDeviceToken( secret: string, claims: { sid: string; origin: string; expiresAt: number },): Promise<string> { if (!secret) throw new Error("device token secret is not configured"); const payload: Payload = { s: claims.sid, o: claims.origin, e: claims.expiresAt }; const body = `${DEVICE_TOKEN_VERSION}.${base64UrlEncode( new TextEncoder().encode(JSON.stringify(payload)), )}`; const key = await signingKey(secret); const signature = await crypto.subtle.sign(HMAC_PARAMS, key, new TextEncoder().encode(body)); return `${body}.${base64UrlEncode(new Uint8Array(signature))}`;}
/** * Verifies the HMAC before parsing the payload, so a forged token never * reaches JSON.parse or the Durable Object namespace. */export async function verifyDeviceToken( secret: string, token: string, nowSec: number = Math.floor(Date.now() / 1000),): Promise<VerifiedToken> { if (!secret) return { ok: false, reason: "device tokens are not configured" }; const parts = token.trim().split("."); if (parts.length !== 3) return { ok: false, reason: "token is malformed" }; const [version, encodedPayload, encodedSignature] = parts; if (version !== DEVICE_TOKEN_VERSION) return { ok: false, reason: "token version is unsupported" };
let signature: Uint8Array; let payloadBytes: Uint8Array; try { signature = base64UrlDecode(encodedSignature); payloadBytes = base64UrlDecode(encodedPayload); } catch { return { ok: false, reason: "token is malformed" }; }
const key = await signingKey(secret); const body = new TextEncoder().encode(`${version}.${encodedPayload}`); const valid = await crypto.subtle.verify(HMAC_PARAMS, key, signature, body); if (!valid) return { ok: false, reason: "token signature is invalid" };
let payload: Payload; try { payload = JSON.parse(new TextDecoder().decode(payloadBytes)) as Payload; } catch { return { ok: false, reason: "token is malformed" }; } if (typeof payload.s !== "string" || typeof payload.o !== "string" || typeof payload.e !== "number") { return { ok: false, reason: "token is malformed" }; } if (payload.e <= nowSec) return { ok: false, reason: "token has expired" };
return { ok: true, claims: { sid: payload.s, origin: payload.o, exp: payload.e } };}
export function bearerToken(header: string | null | undefined): string | null { if (!header) return null; const trimmed = header.trim(); if (!trimmed) return null; const match = /^Bearer\s+(.+)$/i.exec(trimmed); return (match ? match[1] : trimmed).trim() || null;}