mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 14:22:14 +03:00
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
115 lines
4.5 KiB
JavaScript
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);
|
|
}
|