← back to Govarbitrage

src/lib/fleet-sso.ts

45 lines

// Fleet single-sign-on cookie ("aafleet") — a stateless HMAC token shared
// across every *.agentabrams.com site, issued by the fleet-sso service
// (/var/www/fleet-sso/server.mjs). Format: "v1.<exp>.<sig>" where
//   sig = HMAC-SHA256(FLEET_SSO_SECRET, "v1." + exp)  (hex)
// and exp is a unix-seconds expiry. Because the cookie's Domain is
// ".agentabrams.com", auctions already receives it — validating it here lets a
// single fleet login carry into this app with no second sign-in.
//
// This mirrors the verifier's valid() exactly and is Edge-safe (Web Crypto,
// no Node `crypto`), so it runs inside the Next.js middleware.

export const FLEET_SSO_COOKIE = "aafleet";

const TOKEN_RE = /^v1\.(\d+)\.([0-9a-f]{64})$/;

function toHex(buf: ArrayBuffer): string {
  return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
}

// Constant-time-ish compare of two equal-length hex strings.
function safeEqualHex(a: string, b: string): boolean {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}

export async function fleetSsoOk(token: string | undefined | null): Promise<boolean> {
  const secret = process.env.FLEET_SSO_SECRET;
  if (!secret || !token) return false;
  const m = TOKEN_RE.exec(token);
  if (!m) return false;
  const exp = parseInt(m[1], 10);
  if (!(exp > Math.floor(Date.now() / 1000))) return false; // expired
  const key = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(secret),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode("v1." + m[1]));
  return safeEqualHex(toHex(mac), m[2]);
}