fix(a2a): use a constant-time bearer compare in /api/a2a/tasks (#10544)

* fix(a2a): use a constant-time bearer compare in /api/a2a/tasks

* fix(a2a): drop new Function from tasks-auth test in favor of dynamic import

The regression test for the constant-time bearer compare loaded tokensMatch
and authenticateA2A by regex-extracting their source and eval'ing it via
new Function, which trips the repo's no-new-func/no-implied-eval ESLint
rules (error-level everywhere, including tests). Export both helpers as a
test seam from the route module (mirrors the existing
bridgeSecretMatches/authRouteInternals pattern) and import them directly
in the test instead. Also drops the now-unused eslint-disable directives.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Paco Cartones
2026-08-18 15:52:01 +02:00
committed by GitHub
parent b9cd5ed138
commit 20af3988cf
3 changed files with 116 additions and 3 deletions

View File

@@ -1,3 +1,4 @@
import { timingSafeEqual } from "node:crypto";
import { NextResponse } from "next/server";
import { z } from "zod";
@@ -68,12 +69,32 @@ const delegationSchema = z.object({
.optional(),
});
/** Mesma semântica de auth do JSON-RPC A2A (src/app/a2a/route.ts): Bearer vs OMNIROUTE_API_KEY; aberto se não configurada. */
function authenticateA2A(request: Request): boolean {
/**
* Constant-time comparison of the presented bearer token against the configured
* key. A plain `===` short-circuits on the first differing byte, leaking the
* length of the shared prefix through response timing; `timingSafeEqual` does
* not. It requires equal-length buffers, so mismatched lengths are rejected up
* front (the length itself is not secret).
*
* Exported as a test seam only — not part of the route contract.
*/
export 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);
}
/**
* Mesma semântica de auth do JSON-RPC A2A (src/app/a2a/route.ts): Bearer vs OMNIROUTE_API_KEY; aberto se não configurada.
*
* Exported as a test seam only — not part of the route contract.
*/
export function authenticateA2A(request: Request): boolean {
const configuredKey = process.env.OMNIROUTE_API_KEY;
if (!configuredKey) return true;
const token = (request.headers.get("authorization") || "").replace(/^Bearer\s+/i, "");
return token === configuredKey;
return tokensMatch(token, configuredKey);
}
/**