import { createHmac, timingSafeEqual } from "node:crypto"; import type { IdentityUser } from "@/modules/identity/contracts"; import { normalizeIdentityHost } from "@/modules/identity/host"; export type AdminRole = | "owner" | "model_admin" | "billing_admin" | "operations" | "support" | "auditor"; export const adminPermissions = [ "admin.access", "admin.customers.read", "admin.customers.birth_data.read", "admin.users.read", "admin.users.manage_roles", "billing.products.read", "billing.products.write", "billing.products.publish", "billing.orders.read", "billing.adjustments.write", "models.read", "models.write", "models.test", "models.publish", "models.rollback", "ops.flags.write", "audit.read", ] as const; export type AdminPermission = (typeof adminPermissions)[number]; export type AdminAccessResult = | { allowed: true } | { allowed: false; status: 401 | 403 }; export const ADMIN_MFA_PROOF_COOKIE = "jyotisha-admin.mfa"; export const ADMIN_MFA_PROOF_TTL_MS = 10 * 60 * 1_000; type AdminMfaProofContext = { userId: string; sessionId: string; origin: string; }; type AdminMfaProofClaims = AdminMfaProofContext & { version: 1; issuedAt: number; expiresAt: number; }; export type AdminMfaStatus = { required: boolean; enrolled: boolean; verified: boolean; }; export function authorizeAdminAccess( user: IdentityUser | null, permissions: readonly string[], required: AdminPermission, ): AdminAccessResult { if (!user) return { allowed: false, status: 401 }; return permissions.includes(required) ? { allowed: true } : { allowed: false, status: 403 }; } export function isSameOriginAdminMutation( origin: string | null, requestUrl: string, ): boolean { if (!origin) return false; try { return origin === new URL(requestUrl).origin; } catch { return false; } } function singleForwardedValue(value: string | null): string | null { const normalized = value?.trim(); return normalized && !normalized.includes(",") ? normalized : null; } function configuredAdminOrigin(value: string): URL | null { try { const url = new URL(value); const isLocalhost = url.hostname === "localhost" || url.hostname.endsWith(".localhost"); if ( (url.protocol !== "https:" && !(isLocalhost && url.protocol === "http:")) || url.username || url.password || url.pathname !== "/" || url.search || url.hash ) { return null; } return url; } catch { return null; } } export function isTrustedAdminMutationRequest( request: Request, adminOriginValue?: string, ): boolean { const origin = request.headers.get("origin"); const configuredValue = adminOriginValue?.trim(); if (!configuredValue) { return isSameOriginAdminMutation(origin, request.url); } const adminOrigin = configuredAdminOrigin(configuredValue); if (!adminOrigin || origin !== adminOrigin.origin) return false; const hasForwardedHost = request.headers.has("x-forwarded-host"); const hasForwardedProto = request.headers.has("x-forwarded-proto"); if (!hasForwardedHost && !hasForwardedProto) { return isSameOriginAdminMutation(origin, request.url); } const forwardedHostValue = request.headers.get("x-forwarded-host"); const forwardedProtoValue = request.headers.get("x-forwarded-proto"); const host = normalizeIdentityHost(request.headers.get("host")); const forwardedHost = normalizeIdentityHost(forwardedHostValue); const forwardedProto = singleForwardedValue(forwardedProtoValue)?.toLowerCase(); return Boolean( host && forwardedHost && forwardedProto && host === forwardedHost && forwardedHost === adminOrigin.host.toLowerCase() && `${forwardedProto}:` === adminOrigin.protocol, ); } export function resolveAdminMfaStatus( required: boolean, enrolled: boolean, verified: boolean, ): AdminMfaStatus { const currentSessionVerified = enrolled && verified; return { required, enrolled, verified: currentSessionVerified, }; } function signProof( purpose: string, payload: string, proofSecret: string, sessionToken: string, ): Buffer { return createHmac("sha256", proofSecret) .update(purpose) .update("\0") .update(sessionToken) .update("\0") .update(payload) .digest(); } function encodeProof( purpose: string, claims: AdminMfaProofClaims, proofSecret: string, sessionToken: string, ): string { const payload = Buffer.from(JSON.stringify(claims)).toString("base64url"); return `${payload}.${signProof(purpose, payload, proofSecret, sessionToken).toString("base64url")}`; } function decodeProof( proof: string | undefined, purpose: string, proofSecret: string, sessionToken: string, ): Partial | null { if (!proof || proof.length > 2_048) return null; const parts = proof.split("."); if (parts.length !== 2 || parts.some((part) => !/^[A-Za-z0-9_-]+$/.test(part))) return null; const [payload, encodedSignature] = parts; const signature = Buffer.from(encodedSignature, "base64url"); const expected = signProof(purpose, payload, proofSecret, sessionToken); if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) return null; try { return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as Partial; } catch { return null; } } export function issueAdminMfaProof( context: AdminMfaProofContext, proofSecret: string, sessionToken: string, now = Date.now(), ): string { return encodeProof("jyotisha-admin-mfa-v1", { version: 1, ...context, issuedAt: now, expiresAt: now + ADMIN_MFA_PROOF_TTL_MS, } satisfies AdminMfaProofClaims, proofSecret, sessionToken); } export function verifyAdminMfaProof( proof: string | undefined, context: AdminMfaProofContext, proofSecret: string, sessionToken: string, now = Date.now(), ): boolean { const claims = decodeProof( proof, "jyotisha-admin-mfa-v1", proofSecret, sessionToken, ); return claims?.version === 1 && claims.userId === context.userId && claims.sessionId === context.sessionId && claims.origin === context.origin && Number.isSafeInteger(claims.issuedAt) && Number.isSafeInteger(claims.expiresAt) && claims.expiresAt! - claims.issuedAt! === ADMIN_MFA_PROOF_TTL_MS && claims.issuedAt! <= now && now < claims.expiresAt!; }