Files
OmniRoute/scripts/check/check-openapi-security-tiers.mjs
diegosouzapw c622709e46 fix(ci): mirror isLocalOnlyPath in the security-tier gate and rebaseline four merged-growth file caps
Two base-reds on release/v3.8.51 (#12581), both drained at the source.

1) check:openapi-security-tiers reported six CORRECTLY annotated routes as
   unprotected and demanded the removal of their x-loopback-only annotation —
   pushing the fix in the unsafe direction. The gate re-reads routeGuard.ts as
   text (it cannot import the module: routeGuard pulls the server runtime and
   the gate runs on plain node), but it only read the FIRST half of
   isLocalOnlyPath():

     LOCAL_ONLY_API_PREFIXES.some(...) || LOCAL_ONLY_API_PATTERNS.some(...)

   so every route gated by a regex (/api/providers/volcengine-plan/connect/*)
   or by an imported constant (VNC_ROUTE_PREFIX, which the text parse turned
   into the literal string "VNC_ROUTE_PREFIX") looked open. Proven with
   isLocalOnlyPath() at runtime: all six return true; the control
   /api/providers/{id}/refresh stays false.

   New scripts/check/routeGuardConstants.mjs reads BOTH arrays, resolves
   imported identifiers by following the import, and THROWS on an unresolvable
   token instead of silently degrading it into a literal. Its array scanner is
   hand-rolled because regex literals carry the brackets and commas a
   \[([^\]]+)\] capture plus a naive comma split break on ([^/] and {1,3}).
   The reverse pass (missing-annotation warnings) now uses the same predicate.

2) check:file-size: four frozen files grew past their cap through merged PRs —
   chat.ts +10 (#12427/#12503 video-transcript redaction, derived from the
   post-guardrail payload at the single dispatch point) and stream.ts /
   accountFallback.ts / codex.ts +17 total (#12179 hot-path regex hoisting,
   bounded caches, quadratic-buffering fix). All cohesive at existing
   chokepoints; rebaselined with the rationale recorded in the baseline file.

Refs #12581
2026-09-03 09:25:02 -03:00

115 lines
4.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Cross-references openapi.yaml x-loopback-only / x-always-protected annotations
* against the compile-time constants in src/server/authz/routeGuard.ts.
*
* Fails if any YAML annotation disagrees with the routeGuard.ts constants.
*/
import fs from "node:fs";
import path from "node:path";
import * as yaml from "js-yaml";
import { isLocalOnlyDocPath, readRegexArray, readStringArray } from "./routeGuardConstants.mjs";
const ROOT = process.cwd();
const OPENAPI_PATH = path.join(ROOT, "docs", "openapi.yaml");
const ROUTE_GUARD_PATH = path.join(ROOT, "src", "server", "authz", "routeGuard.ts");
const guardSrc = fs.readFileSync(ROUTE_GUARD_PATH, "utf-8");
// Both halves of isLocalOnlyPath(): the flat prefixes AND the regex patterns.
// Reading only the prefixes reported regex-gated and imported-constant routes
// as unprotected (see routeGuardConstants.mjs).
let LOCAL_ONLY_PREFIXES;
let LOCAL_ONLY_PATTERNS;
let ALWAYS_PROTECTED_PATHS;
try {
LOCAL_ONLY_PREFIXES = readStringArray(guardSrc, "LOCAL_ONLY_API_PREFIXES", { root: ROOT });
LOCAL_ONLY_PATTERNS = readRegexArray(guardSrc, "LOCAL_ONLY_API_PATTERNS");
ALWAYS_PROTECTED_PATHS = readStringArray(guardSrc, "ALWAYS_PROTECTED_API_PATHS", { root: ROOT });
} catch (err) {
console.error(`[openapi-security-tiers] FAIL — ${err.message}`);
process.exit(1);
}
if (LOCAL_ONLY_PREFIXES.length === 0 || ALWAYS_PROTECTED_PATHS.length === 0) {
console.error("[openapi-security-tiers] FAIL — could not parse routeGuard.ts constants");
process.exit(1);
}
const localOnlyGuards = { prefixes: LOCAL_ONLY_PREFIXES, patterns: LOCAL_ONLY_PATTERNS };
const raw = yaml.load(fs.readFileSync(OPENAPI_PATH, "utf-8"));
const paths = raw.paths || {};
const errors = [];
for (const [pathStr, methods] of Object.entries(paths)) {
if (!methods || typeof methods !== "object") continue;
for (const [method, spec] of Object.entries(methods)) {
if (!["get", "post", "put", "patch", "delete"].includes(method) || !spec) continue;
if (spec["x-loopback-only"] === true) {
if (!isLocalOnlyDocPath(pathStr, localOnlyGuards)) {
errors.push(
`${method.toUpperCase()} ${pathStr}: has x-loopback-only but is NOT covered by ` +
`LOCAL_ONLY_API_PREFIXES [${LOCAL_ONLY_PREFIXES.join(", ")}] ` +
`nor by LOCAL_ONLY_API_PATTERNS [${LOCAL_ONLY_PATTERNS.join(", ")}]`
);
}
}
if (spec["x-always-protected"] === true) {
const matchesPath = ALWAYS_PROTECTED_PATHS.some(
(p) => pathStr === p || pathStr.startsWith(`${p}/`)
);
if (!matchesPath) {
errors.push(
`${method.toUpperCase()} ${pathStr}: has x-always-protected but is NOT in ` +
`ALWAYS_PROTECTED_API_PATHS [${ALWAYS_PROTECTED_PATHS.join(", ")}]`
);
}
}
}
}
// Reverse pass: every YAML path that falls under a LOCAL_ONLY prefix should
// carry `x-loopback-only: true` on every method, otherwise external API
// consumers have no signal that the route is loopback-restricted. Closes the
// "new spawn-capable route added without annotation" regression class.
//
// Currently reported as warnings (non-fatal) because the v3.8.4 release ships
// with a known annotation gap on /api/services/* and /api/cli-tools/runtime/*
// that will be patched in a follow-up doc-only PR. Promote to errors once the
// backlog is cleared.
const reverseWarnings = [];
for (const [pathStr, methods] of Object.entries(paths)) {
if (!methods || typeof methods !== "object") continue;
if (!isLocalOnlyDocPath(pathStr, localOnlyGuards)) continue;
for (const [method, spec] of Object.entries(methods)) {
if (!["get", "post", "put", "patch", "delete"].includes(method) || !spec) continue;
if (spec["x-loopback-only"] !== true) {
reverseWarnings.push(
`${method.toUpperCase()} ${pathStr}: is LOCAL_ONLY per routeGuard ` +
`but is missing x-loopback-only: true annotation`
);
}
}
}
if (reverseWarnings.length > 0) {
console.warn(
`[openapi-security-tiers] WARN — ${reverseWarnings.length} LOCAL_ONLY paths missing x-loopback-only annotation (non-fatal, follow-up doc PR):`
);
reverseWarnings.forEach((w) => console.warn(` - ${w}`));
}
if (errors.length === 0) {
console.log("[openapi-security-tiers] PASS — all security tier annotations match routeGuard.ts");
process.exit(0);
} else {
console.error(`[openapi-security-tiers] FAIL — ${errors.length} annotation mismatches:`);
errors.forEach((e) => console.error(` - ${e}`));
process.exit(1);
}