Something went wrong. Try again.
FlockOff is a mass-block tool for the Atmosphere flockoff.app
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192import 'server-only'
/** * Typed app-state carried through the OAuth flow. * * The oauth-client stores this server-side as `appState` (keyed by its own * generated nonce) and hands it back from client.callback() — it never rides * the wire as the OAuth state parameter. Still treat it as untrusted on the * way back out: parse defensively and re-validate the return path. * * Scope upgrades carry the DID that initiated the upgrade so the callback can * reject a confused-account flow (user switches accounts on the PDS consent * screen mid-upgrade — without this check the callback would silently swap * the iron session to the other account). */
export type OAuthAppState = { kind: 'scope-upgrade'; did: string; returnTo: string }
export function serializeAppState(state: OAuthAppState): string { return JSON.stringify(state)}
export function parseAppState(raw: string | null): OAuthAppState | null { if (!raw) return null try { const parsed = JSON.parse(raw) as Partial<OAuthAppState> if ( parsed?.kind === 'scope-upgrade' && typeof parsed.did === 'string' && parsed.did.startsWith('did:') && typeof parsed.returnTo === 'string' ) { return { kind: 'scope-upgrade', did: parsed.did, returnTo: parsed.returnTo } } return null } catch { return null }}
function safeDecodePath(pathname: string): string { try { return decodeURIComponent(pathname) } catch { return pathname }}
/** * Normalize a user-supplied return path to a safe same-origin relative path. * Anything suspicious falls back to '/'. Applied at the upgrade route (before * embedding in state), at the callback (after parsing state), and at the * bridge (defense in depth). */export function normalizeReturnTo(value: string | null | undefined): string { if (!value) return '/'
// Bound size so oversized values can't bloat OAuth state or logs if (value.length > 2048) return '/'
// Relative paths only; '//' is protocol-relative (open redirect) if (!value.startsWith('/') || value.startsWith('//')) return '/'
// Reject backslashes and encoded slash/backslash in the *path* portion — // browsers, proxies, and routers normalize these inconsistently, and a // redirect helper should stay conservative. The query string is exempt: // it doesn't affect routing, and legitimate return paths carry encoded // URLs there (e.g. /list?url=https%3A%2F%2F...). const pathPortion = value.split('?', 1)[0].split('#', 1)[0] const lowerPath = pathPortion.toLowerCase() if (pathPortion.includes('\\') || lowerPath.includes('%2f') || lowerPath.includes('%5c')) return '/'
const appUrl = process.env.NEXT_PUBLIC_APP_URL! let parsed: URL try { parsed = new URL(value, appUrl) } catch { return '/' }
if (parsed.origin !== new URL(appUrl).origin) return '/'
// Never redirect into API routes. URL.pathname preserves percent-encoding // (e.g. '/%61pi/' survives as-is but routes as '/api/'), so check the // decoded form too. if (parsed.pathname.startsWith('/api/') || safeDecodePath(parsed.pathname).startsWith('/api/')) { return '/' }
return `${parsed.pathname}${parsed.search}${parsed.hash}`}