type RedirectExtractor = { host: string; exact?: boolean; path: string; param: string; decode?: 'none' | 'uri' | 'form' | 'b64url'; }; const REDIRECT_EXTRACTORS: RedirectExtractor[] = [ { host: 'l.facebook.com', exact: true, path: '/l.php', param: 'u', decode: 'uri' }, { host: 'lm.facebook.com', exact: true, path: '/l.php', param: 'u', decode: 'uri' }, { host: 'linkedin.com', exact: false, path: '/safety/go', param: 'url', decode: 'uri' }, { host: 'youtube.com', exact: false, path: '/redirect', param: 'q', decode: 'uri' }, { host: 'go.bsky.app', exact: true, path: '/redirect', param: 'u', decode: 'uri' }, { host: 'googleadservices.com', exact: false, path: '/pagead/aclk', param: 'adurl', decode: 'uri' }, { host: 'target.georiot.com', exact: true, path: '/Proxy.ashx', param: 'GR_URL', decode: 'uri' }, { host: 'click.linksynergy.com', exact: true, path: '/link', param: 'murl', decode: 'uri' }, ]; function decodeParam(value: string, mode: 'none' | 'uri' | 'form' | 'b64url'): string | null { try { switch (mode) { case 'none': return value; case 'uri': return decodeURIComponent(value); case 'form': return decodeURIComponent(value.replace(/\+/g, ' ')); case 'b64url': return new TextDecoder().decode( Uint8Array.from(atob(value.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0)), ); } } catch { return null; } } function isHttp(s: string): boolean { return /^https?:\/\//i.test(s); } export function unwrapRedirect(url: string): string | null { try { const u = new URL(url); if (u.hostname === 'out.reddit.com') { const target = u.searchParams.get('url'); if (target && isHttp(target)) { const d = decodeURIComponent(target); if (isHttp(d)) return d; } return null; } for (const ex of REDIRECT_EXTRACTORS) { if (ex.exact ? u.hostname !== ex.host : (u.hostname !== ex.host && !u.hostname.endsWith('.' + ex.host))) continue; if (!u.pathname.startsWith(ex.path)) continue; const raw = u.searchParams.get(ex.param); if (!raw) continue; const d = decodeParam(raw, ex.decode ?? 'uri'); if (d && isHttp(d)) return d; } return null; } catch { return null; } } export function fullyUnwrapRedirect(url: string, maxHops: number = 5): { url: string; hops: string[] } { const hops: string[] = []; let cur = url; for (let i = 0; i < maxHops; i++) { const next = unwrapRedirect(cur); if (!next || next === cur) break; try { hops.push(new URL(cur).hostname); } catch {} cur = next; } return { url: cur, hops }; }