import type { AccessToken } from "./store"; export function isTokenExpired(token: AccessToken) { return token.expiresAt && Date.now() > token.expiresAt; } export function extractCode(urlStr: string, redirectUri: string | null): string | null { const url = new URL(urlStr); if (!urlMatchesRedirect(url, redirectUri)) { console.log("[oauth2] URL does not match redirect origin/path; skipping."); return null; } // Prefer query param; fall back to fragment if query lacks it const query = url.searchParams; const queryError = query.get("error"); const queryDesc = query.get("error_description"); const queryUri = query.get("error_uri"); let hashParams: URLSearchParams | null = null; if (url.hash && url.hash.length > 1) { hashParams = new URLSearchParams(url.hash.slice(1)); } const hashError = hashParams?.get("error"); const hashDesc = hashParams?.get("error_description"); const hashUri = hashParams?.get("error_uri"); const error = queryError || hashError; if (error) { const desc = queryDesc || hashDesc; const uri = queryUri || hashUri; let message = `Failed to authorize: ${error}`; if (desc) message += ` (${desc})`; if (uri) message += ` [${uri}]`; throw new Error(message); } const queryCode = query.get("code"); if (queryCode) return queryCode; const hashCode = hashParams?.get("code"); if (hashCode) return hashCode; console.log("[oauth2] Code not found"); return null; } export function urlMatchesRedirect(url: URL, redirectUrl: string | null): boolean { if (!redirectUrl) return true; let redirect: URL; try { redirect = new URL(redirectUrl); } catch { console.log("[oauth2] Invalid redirect URI; skipping."); return false; } const sameProtocol = url.protocol === redirect.protocol; const sameHost = url.hostname.toLowerCase() === redirect.hostname.toLowerCase(); const normalizePort = (u: URL) => (u.protocol === "https:" && (!u.port || u.port === "443")) || (u.protocol === "http:" && (!u.port || u.port === "80")) ? "" : u.port; const samePort = normalizePort(url) === normalizePort(redirect); const normPath = (p: string) => { const withLeading = p.startsWith("/") ? p : `/${p}`; // strip trailing slashes, keep root as "/" return withLeading.replace(/\/+$/g, "") || "/"; }; // Require redirect path to be a prefix of the navigated URL path const urlPath = normPath(url.pathname); const redirectPath = normPath(redirect.pathname); const pathMatches = urlPath === redirectPath || urlPath.startsWith(`${redirectPath}/`); return sameProtocol && sameHost && samePort && pathMatches; }