"use strict";
/**
 * Module: server/auth
 *
 * Production-safe shared-secret authentication for Dominds.
 *
 * - Dev mode: always disabled.
 * - Prod mode:
 *   - DOMINDS_AUTH_KEY unset: enabled with generated key
 *   - DOMINDS_AUTH_KEY empty string: disabled
 *   - DOMINDS_AUTH_KEY non-empty: enabled with provided key verbatim
 *
 * HTTP: Authorization: Bearer <auth-key>
 * WebSocket: prefers Authorization header, otherwise accepts a token in
 * Sec-WebSocket-Protocol as "dominds-auth.<auth-key>" (plain text).
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeAuthConfig = computeAuthConfig;
exports.isAuthEnabled = isAuthEnabled;
exports.getHttpAuthCheck = getHttpAuthCheck;
exports.getHttpAuthCheckAllowUrlParam = getHttpAuthCheckAllowUrlParam;
exports.getWebSocketAuthCheck = getWebSocketAuthCheck;
exports.formatServerOrigin = formatServerOrigin;
exports.formatAutoAuthUrl = formatAutoAuthUrl;
const crypto = __importStar(require("crypto"));
function computeAuthConfig(params) {
    if (params.mode === 'development')
        return { kind: 'disabled' };
    const raw = params.env.DOMINDS_AUTH_KEY;
    if (raw === undefined) {
        const key = generateAuthKey();
        // Generated key is base64url, which is safe for WebSocket subprotocol tokens.
        return { kind: 'enabled', key, source: 'generated' };
    }
    if (raw === '')
        return { kind: 'disabled' };
    if (!isWebSocketSubprotocolTokenSafe(raw)) {
        throw new Error('DOMINDS_AUTH_KEY must be a plain-text token-safe string (RFC 7230 tchar set) so it can be used in WebSocket subprotocols.');
    }
    return { kind: 'enabled', key: raw, source: 'env' };
}
function isAuthEnabled(auth) {
    return auth.kind === 'enabled';
}
function getHttpAuthCheck(req, auth) {
    if (auth.kind === 'disabled')
        return { kind: 'ok' };
    const header = getSingleHeaderValue(req.headers.authorization);
    if (!header)
        return { kind: 'unauthorized', reason: 'missing' };
    const parsed = parseBearerAuthHeader(header);
    if (!parsed)
        return { kind: 'unauthorized', reason: 'invalid' };
    return constantTimeEqual(parsed.token, auth.key)
        ? { kind: 'ok' }
        : { kind: 'unauthorized', reason: 'invalid' };
}
function getHttpAuthCheckAllowUrlParam(req, auth, requestUrl) {
    if (auth.kind === 'disabled')
        return { kind: 'ok' };
    const header = getSingleHeaderValue(req.headers.authorization);
    if (header) {
        const parsed = parseBearerAuthHeader(header);
        if (!parsed)
            return { kind: 'unauthorized', reason: 'invalid' };
        return constantTimeEqual(parsed.token, auth.key)
            ? { kind: 'ok' }
            : { kind: 'unauthorized', reason: 'invalid' };
    }
    const queryToken = requestUrl.searchParams.get('auth');
    if (typeof queryToken !== 'string' || queryToken === '') {
        return { kind: 'unauthorized', reason: 'missing' };
    }
    return constantTimeEqual(queryToken, auth.key)
        ? { kind: 'ok' }
        : { kind: 'unauthorized', reason: 'invalid' };
}
function getWebSocketAuthCheck(req, auth) {
    if (auth.kind === 'disabled')
        return { kind: 'ok' };
    // Prefer true Authorization header when available (Node clients can set this).
    const header = getSingleHeaderValue(req.headers.authorization);
    if (header) {
        const parsed = parseBearerAuthHeader(header);
        if (parsed && constantTimeEqual(parsed.token, auth.key))
            return { kind: 'ok' };
        return { kind: 'unauthorized', reason: 'invalid' };
    }
    // Browser clients cannot set Authorization; accept an encoded token via subprotocols.
    const protocols = getSingleHeaderValue(req.headers['sec-websocket-protocol']);
    if (!protocols)
        return { kind: 'unauthorized', reason: 'missing' };
    const token = extractAuthKeyFromWebSocketProtocols(protocols);
    if (!token)
        return { kind: 'unauthorized', reason: 'invalid' };
    return constantTimeEqual(token, auth.key)
        ? { kind: 'ok' }
        : { kind: 'unauthorized', reason: 'invalid' };
}
function formatServerOrigin(params) {
    const visibleHost = normalizeHostForUrl(params.host, params.scheme);
    return `${params.scheme}://${visibleHost}:${params.port}`;
}
function formatAutoAuthUrl(params) {
    return `${formatServerOrigin({
        scheme: params.scheme ?? 'http',
        host: params.host,
        port: params.port,
    })}/?auth=${encodeURIComponent(params.authKey)}`;
}
function normalizeHostForUrl(host, scheme) {
    // Binding to 0.0.0.0/:: is common, but not a browsable "host".
    // For HTTPS, preserve the certificate-matched bind host to avoid creating a hostname mismatch.
    if (scheme === 'http' && (host === '0.0.0.0' || host === '::'))
        return 'localhost';
    if (host.includes(':') && !host.startsWith('['))
        return `[${host}]`;
    return host;
}
function generateAuthKey() {
    // 256-bit random, base64url output is URL-safe.
    return crypto.randomBytes(32).toString('base64url');
}
function getSingleHeaderValue(value) {
    if (typeof value === 'string')
        return value;
    if (Array.isArray(value))
        return value[0];
    return undefined;
}
function parseBearerAuthHeader(header) {
    // Exact scheme match. Do not trim or normalize token; token is opaque.
    const prefix = 'Bearer ';
    if (!header.startsWith(prefix))
        return undefined;
    const token = header.slice(prefix.length);
    if (token === '')
        return undefined;
    return { token };
}
function extractAuthKeyFromWebSocketProtocols(headerValue) {
    // Header is a comma-separated list.
    const protocols = headerValue
        .split(',')
        .map((s) => s.trim())
        .filter((s) => s !== '');
    for (const p of protocols) {
        const decoded = decodeAuthKeyProtocol(p);
        if (decoded)
            return decoded;
    }
    return undefined;
}
function decodeAuthKeyProtocol(protocol) {
    const prefix = 'dominds-auth.';
    if (!protocol.startsWith(prefix))
        return undefined;
    const key = protocol.slice(prefix.length);
    if (key === '')
        return undefined;
    if (!isWebSocketSubprotocolTokenSafe(key))
        return undefined;
    return key;
}
function isWebSocketSubprotocolTokenSafe(value) {
    // WebSocket subprotocols are HTTP tokens. Require RFC 7230 "tchar" only.
    // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
    //         "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
    return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value);
}
function constantTimeEqual(a, b) {
    const aBuf = Buffer.from(a, 'utf8');
    const bBuf = Buffer.from(b, 'utf8');
    const maxLen = Math.max(aBuf.length, bBuf.length);
    const aPadded = maxLen === aBuf.length ? aBuf : Buffer.concat([aBuf, Buffer.alloc(maxLen - aBuf.length)]);
    const bPadded = maxLen === bBuf.length ? bBuf : Buffer.concat([bBuf, Buffer.alloc(maxLen - bBuf.length)]);
    const equal = crypto.timingSafeEqual(aPadded, bPadded);
    return equal && aBuf.length === bBuf.length;
}
