/**
 * Sanitise a `?next=` post-login/registration redirect target.
 *
 * The login/register flow round-trips the page the user was bounced from via
 * `?next=`. Navigating to an attacker-supplied value would be an open
 * redirect, so only same-origin absolute paths are allowed: the value must
 * start with a single `/` and must not be protocol-relative (`//evil.com`)
 * or a backslash trick (`/\evil.com`, which some parsers normalise to `//`).
 *
 * Crucially, the structural prefix checks are not enough on their own:
 * browsers **strip ASCII tab/newline/CR mid-parse**, so `/\t/evil.com`
 * (which starts with a single `/`) collapses to the protocol-relative
 * `//evil.com` once navigated — an open-redirect bypass via `%09`/`%0A`/
 * `%0D`. So we reject any control or whitespace character (and backslashes)
 * *before* the prefix checks. Anything else — absolute URLs, `javascript:`
 * URIs, empty/missing — falls back to the site root.
 */
function hasUnsafeNextChar(s: string): boolean {
    for (let i = 0; i < s.length; i++) {
        const c = s.charCodeAt(i);
        // C0 controls + space (<= 0x20), DEL (0x7f), or backslash (0x5c) —
        // characters a URL parser may strip or treat as a path separator.
        if (c <= 0x20 || c === 0x7f || c === 0x5c) return true;
    }
    // Any remaining Unicode whitespace (NBSP, line/paragraph separators, …).
    return /\s/.test(s);
}

export function safeNext(next: string | null | undefined): string {
    if (typeof next !== 'string' || next.length === 0) return '/';
    if (hasUnsafeNextChar(next)) return '/';
    if (!next.startsWith('/') || next.startsWith('//')) return '/';
    return next;
}
