/** * Shared A2A authentication + caller-owner resolution (GHSA-jcm5-6wpp-wjj8). * * The JSON-RPC router (/a2a) grew its own authenticate() for GHSA-v54m, but * the REST task routes under /api/a2a/tasks/ had no auth call at all. Both * surfaces now share this single implementation so they cannot drift again: * same REQUIRE_API_KEY posture as /v1, same keyless local-first default, and * a stable owner id (hashed API key) used to scope task visibility. */ import { createHash, timingSafeEqual } from "crypto"; import type { NextRequest } from "next/server"; import { extractApiKey, isValidApiKey } from "@/sse/services/auth"; import { isRequireApiKeyEnabled } from "@/shared/utils/featureFlags"; function tokensMatch(provided: string, expected: string): boolean { const a = Buffer.from(provided); const b = Buffer.from(expected); if (a.length !== b.length) return false; return timingSafeEqual(a, b); } /** * Whether the request may use the A2A surface at all. Mirrors the JSON-RPC * posture: when a client key is required, demand a valid OmniRoute key; * otherwise honor the legacy explicit A2A key; otherwise stay keyless (the * same local-first default as /v1). */ export async function authenticateA2ARequest(req: NextRequest | Request): Promise { const apiKey = extractApiKey(req as NextRequest); if (isRequireApiKeyEnabled()) { return apiKey ? await isValidApiKey(apiKey) : false; } const configuredKey = process.env.OMNIROUTE_API_KEY; if (configuredKey) { return apiKey ? tokensMatch(apiKey, configuredKey) : false; } // No API key required and none configured — allow (keyless local-first). return true; } /** * Owner id for task scoping (GHSA-jcm5-6wpp-wjj8): a stable hash of the * caller's API key, or `undefined` when the call carries no key (keyless * posture — ownerless tasks stay visible to everyone, by design). */ export function resolveA2AOwner(req: NextRequest | Request): string | undefined { const apiKey = extractApiKey(req as NextRequest); if (!apiKey) return undefined; return createHash("sha256").update(apiKey).digest("hex").slice(0, 32); }