/**
 * Bearer token middleware for MCP servers.
 *
 * Returns a 401 Response if the Authorization header is missing or does
 * not match the expected token. Returns null on success so callers can
 * fall through to the next handler.
 *
 * - Case-insensitive Authorization header lookup.
 * - "Bearer " prefix is matched case-insensitively, exactly one space.
 * - Constant-time comparison via crypto.timingSafeEqual.
 * - Empty / whitespace-only tokens always fail.
 * - Token value is never logged.
 */

import { timingSafeEqual } from "node:crypto";

const UNAUTHORIZED_BODY = JSON.stringify({
  error: "unauthorized",
  message: "missing or invalid bearer token",
});

function unauthorized(): Response {
  return new Response(UNAUTHORIZED_BODY, {
    status: 401,
    headers: {
      "content-type": "application/json",
      "www-authenticate": 'Bearer realm="mcp"',
    },
  });
}

/**
 * Looks up a header value case-insensitively. The Fetch standard
 * Headers object is already case-insensitive, but we accept the
 * lookup explicitly to make intent obvious.
 */
function getAuthHeader(req: Request): string | null {
  // Headers.get is case-insensitive per the Fetch spec.
  return req.headers.get("authorization");
}

/**
 * Constant-time comparison.
 *
 * Returns false immediately on length mismatch. Token lengths are not
 * considered sensitive (Python's `secrets.compare_digest` does the same)
 * and this avoids a DoS vector where an attacker-supplied oversized
 * Authorization header would force proportional Buffer.alloc / copy
 * work on every request and stall the event loop.
 */
function constantTimeEqual(a: string, b: string): boolean {
  const ab = Buffer.from(a, "utf8");
  const bb = Buffer.from(b, "utf8");
  if (ab.length !== bb.length) return false;
  return timingSafeEqual(ab, bb);
}

/**
 * Validates the Authorization header against the expected bearer token.
 *
 * @returns null on success; a 401 Response on failure.
 */
export function requireBearer(
  req: Request,
  expectedToken: string,
): Response | null {
  // Empty / whitespace-only expected tokens always fail. The caller is
  // expected to enforce token configuration at startup, but defend in
  // depth here too.
  if (!expectedToken || expectedToken.trim().length === 0) {
    return unauthorized();
  }

  const header = getAuthHeader(req);
  if (!header) {
    return unauthorized();
  }

  // Match "Bearer " prefix case-insensitively, exactly one space.
  // We intentionally do not allow tab or multiple spaces.
  if (header.length < 7) {
    return unauthorized();
  }
  const prefix = header.slice(0, 7);
  if (prefix.toLowerCase() !== "bearer ") {
    return unauthorized();
  }

  const token = header.slice(7);
  if (!token || token.trim().length === 0) {
    return unauthorized();
  }

  // Defense in depth: reject obviously-oversized tokens before doing
  // any allocation in the comparator. Our tokens are <= 128 chars; we
  // give a generous ceiling of 4096 to absorb future format changes.
  if (token.length > 4096) {
    return unauthorized();
  }

  if (!constantTimeEqual(token, expectedToken)) {
    return unauthorized();
  }

  return null;
}
