Something went wrong. Try again.
The logic for serving and storing associations between users and apps
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576import { parseExactOrigin } from "./origins";
/** Schemes that could run script or read local files if we redirected to them. */const DANGEROUS_SCHEMES = new Set(["javascript:", "data:", "vbscript:", "file:", "blob:"]);
const MAX_CALLBACK_LENGTH = 512;const MAX_STATE_LENGTH = 256;
export type NativeStartQuery = | { ok: true; origin: string; callback: string; state: string | null } | { ok: false; reason: string };
/** * `GET /app/start?origin=&callback=&state=` * * `callback` is checked for shape here and for registration against the app's * OAuth client metadata by the caller. Both checks must pass before the worker * hands a token to that URL. */export function parseNativeStartQuery(url: URL): NativeStartQuery { const origin = url.searchParams.get("origin") ?? ""; const callback = url.searchParams.get("callback") ?? ""; const state = url.searchParams.get("state");
if (!origin) return { ok: false, reason: "origin is required" }; const parsedOrigin = parseExactOrigin(origin); if (!parsedOrigin.ok) return { ok: false, reason: parsedOrigin.reason };
const parsedCallback = parseCallback(callback); if (!parsedCallback.ok) return { ok: false, reason: parsedCallback.reason };
if (state !== null && state.length > MAX_STATE_LENGTH) { return { ok: false, reason: "state is too long" }; }
return { ok: true, origin: parsedOrigin.origin, callback: parsedCallback.callback, state };}
function parseCallback( callback: string,): { ok: true; callback: string } | { ok: false; reason: string } { if (!callback) return { ok: false, reason: "callback is required" }; if (callback.length > MAX_CALLBACK_LENGTH) return { ok: false, reason: "callback is too long" };
let url: URL; try { url = new URL(callback); } catch { return { ok: false, reason: "callback is not a URL" }; } if (DANGEROUS_SCHEMES.has(url.protocol.toLowerCase())) { return { ok: false, reason: "callback scheme is not allowed" }; } if (url.hash) return { ok: false, reason: "callback must not contain a fragment" }; return { ok: true, callback };}
/** * A callback is only trusted when the app already published it in * `redirect_uris`. Exact match: we append our own parameters afterwards, so * prefix or host matching would let a registered URI authorize a longer, * attacker-chosen one. */export function isRegisteredCallback(callback: string, redirectUris: string[] | undefined): boolean { if (!redirectUris || redirectUris.length === 0) return false; return redirectUris.includes(callback);}
export function buildCallbackUrl(callback: string, params: Record<string, string>): string { const url = new URL(callback); for (const [key, value] of Object.entries(params)) { url.searchParams.set(key, value); } return url.toString();}