import { HEADER_ALLOWLIST, HEADER_CANONICAL, type HeaderRule, type PiApi, } from "../types.ts"; export interface MergeHeadersInput { api: PiApi | null; /** From defaults/headers.json + provider-headers.json rules */ rules: HeaderRule[]; /** Per-dbId overrides (already extracted) */ overrideHeaders?: Record; /** * When true, skip api-matched default/user rules entirely. * Used by fingerprint:"none" so only explicit overrideHeaders apply. */ skipRules?: boolean; /** Variable substitutions e.g. {codexVersion} */ vars?: Record; debug?: boolean; onReject?: (name: string, reason: string) => void; } /** Case-insensitive merge with allowlist filter. Higher priority sources last. */ export function mergeHeaders(input: MergeHeadersInput): Record { const acc = new Map(); // lower-name → value const apply = (headers: Record | undefined, source: string) => { if (!headers) return; for (const [rawName, rawVal] of Object.entries(headers)) { const lower = rawName.toLowerCase(); if (!HEADER_ALLOWLIST.has(lower)) { input.onReject?.(rawName, `not in allowlist (source=${source})`); continue; } if (typeof rawVal !== "string" || !rawVal.trim()) continue; acc.set(lower, substitute(rawVal, input.vars ?? {})); } }; // 1. rules matching api (unless fingerprint:none / skipRules) if (input.api && !input.skipRules) { for (const rule of input.rules) { if (rule.apis.map((a) => a.toLowerCase()).includes(input.api.toLowerCase())) { apply(rule.headers, `rule:${rule.name}`); } } } // 2. per-provider overrides win apply(input.overrideHeaders, "providerOverrides"); const out: Record = {}; for (const [lower, value] of acc) { const canon = HEADER_CANONICAL[lower] ?? lower; out[canon] = value; } return out; } function substitute(template: string, vars: Record): string { return template.replace(/\{(\w+)\}/g, (_, key: string) => vars[key] ?? `{${key}}`); } /** Filter an arbitrary header map to the allowlist only. */ export function filterAllowlisted( headers: Record, onReject?: (name: string) => void, ): Record { const out: Record = {}; for (const [k, v] of Object.entries(headers)) { const lower = k.toLowerCase(); if (!HEADER_ALLOWLIST.has(lower)) { onReject?.(k); continue; } out[HEADER_CANONICAL[lower] ?? k] = v; } return out; }