mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-04 22:15:07 +03:00
Compare commits
1 Commits
fix/video-
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3945a724c |
1
changelog.d/fixes/12605-openapi-tiers-public-creds.md
Normal file
1
changelog.d/fixes/12605-openapi-tiers-public-creds.md
Normal file
@@ -0,0 +1 @@
|
||||
- **CI:** the OpenAPI security-tier gate now mirrors `isAlwaysProtectedPath()` in full — it also reads `ALWAYS_PROTECTED_API_PATTERNS`, so the pattern-gated credential routes (`/api/providers/{id}/{claude,codex}-auth/{export,apply-local}`, GHSA-5926-2w35-7h4q) no longer report as unannotated. (#12605)
|
||||
@@ -106,6 +106,11 @@ function parsePatterns(name) {
|
||||
const LOCAL_ONLY_PREFIXES = parsePrefixes("LOCAL_ONLY_API_PREFIXES");
|
||||
const LOCAL_ONLY_PATTERNS = parsePatterns("LOCAL_ONLY_API_PATTERNS");
|
||||
const ALWAYS_PROTECTED_PATHS = parsePrefixes("ALWAYS_PROTECTED_API_PATHS");
|
||||
// isAlwaysProtectedPath() is ALSO two-armed (paths || patterns) — reading only the
|
||||
// path array repeated, on this half, the very bug #12350 fixed on the LOCAL_ONLY
|
||||
// half: the pattern-gated credential routes (…/{claude,codex}-auth/{export,
|
||||
// apply-local}, #12600) read as unannotated even though they are protected.
|
||||
const ALWAYS_PROTECTED_PATTERNS = parsePatterns("ALWAYS_PROTECTED_API_PATTERNS");
|
||||
|
||||
if (
|
||||
LOCAL_ONLY_PREFIXES.length === 0 ||
|
||||
@@ -135,6 +140,15 @@ function coveredByLocalOnly(pathStr) {
|
||||
return matchesPrefix(concrete) || LOCAL_ONLY_PATTERNS.some((re) => re.test(concrete));
|
||||
}
|
||||
|
||||
/** Mirror of routeGuard.isAlwaysProtectedPath() — both arms, same order. */
|
||||
function coveredByAlwaysProtected(pathStr) {
|
||||
const concrete = concretize(pathStr);
|
||||
return (
|
||||
ALWAYS_PROTECTED_PATHS.some((p) => concrete === p || concrete.startsWith(`${p}/`)) ||
|
||||
ALWAYS_PROTECTED_PATTERNS.some((re) => re.test(concrete))
|
||||
);
|
||||
}
|
||||
|
||||
const raw = yaml.load(fs.readFileSync(OPENAPI_PATH, "utf-8"));
|
||||
const paths = raw.paths || {};
|
||||
const errors = [];
|
||||
@@ -151,16 +165,11 @@ for (const [pathStr, methods] of Object.entries(paths)) {
|
||||
);
|
||||
}
|
||||
|
||||
if (spec["x-always-protected"] === true) {
|
||||
const matchesPath = ALWAYS_PROTECTED_PATHS.some(
|
||||
(p) => pathStr === p || pathStr.startsWith(`${p}/`)
|
||||
if (spec["x-always-protected"] === true && !coveredByAlwaysProtected(pathStr)) {
|
||||
errors.push(
|
||||
`${method.toUpperCase()} ${pathStr}: has x-always-protected but is NOT covered by ` +
|
||||
`ALWAYS_PROTECTED_API_PATHS or ALWAYS_PROTECTED_API_PATTERNS`
|
||||
);
|
||||
if (!matchesPath) {
|
||||
errors.push(
|
||||
`${method.toUpperCase()} ${pathStr}: has x-always-protected but is NOT in ` +
|
||||
`ALWAYS_PROTECTED_API_PATHS [${ALWAYS_PROTECTED_PATHS.join(", ")}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
* never turn into a second failure on the response path.
|
||||
*/
|
||||
import { saveCallLog, saveRequestUsage } from "@/lib/usageDb";
|
||||
import { redactVideoTranscriptFieldsForLog } from "@/lib/guardrails/videoBridgeSnapshotRedaction";
|
||||
|
||||
export interface RejectedRequestUsageInput {
|
||||
status: number;
|
||||
@@ -83,12 +82,7 @@ export async function recordRejectedRequestUsage(input: RejectedRequestUsageInpu
|
||||
duration,
|
||||
tokens: {},
|
||||
error: error || null,
|
||||
// #12150 P2 item 7: this request was rejected BEFORE the guardrail chain ran
|
||||
// (circuit-breaker-open / combo-exhausted), so the video-bridge guardrail
|
||||
// never redacted the transcript. Redact defensively here — a no-op clone for
|
||||
// any non-video body, structured field substitution (never bypassable by cue
|
||||
// content) for a video one. See videoBridgeSnapshotRedaction.ts.
|
||||
requestBody: requestBody == null ? requestBody : redactVideoTranscriptFieldsForLog(requestBody),
|
||||
requestBody,
|
||||
comboName,
|
||||
comboStepId,
|
||||
comboExecutionKey,
|
||||
|
||||
@@ -136,68 +136,6 @@ test("combo-exhausted rejection persists the client request body for dashboard i
|
||||
});
|
||||
});
|
||||
|
||||
// #12150 P2 item 7: recordRejectedRequestUsage persists the raw client body for
|
||||
// a request rejected BEFORE the guardrail chain runs (circuit-breaker-open /
|
||||
// combo-exhausted), so the video-bridge guardrail never got a chance to redact
|
||||
// the transcript. The body is persisted defensively through
|
||||
// redactVideoTranscriptFieldsForLog, so a rejected video request's stored log
|
||||
// never retains the raw transcript cues.
|
||||
test("#12150 P2 item 7: a rejected request's persisted body has its video transcript redacted", async () => {
|
||||
const SECRET = "top secret cue text";
|
||||
await recordRejectedRequestUsage({
|
||||
status: 503,
|
||||
model: "default",
|
||||
requestedModel: "default",
|
||||
provider: "-",
|
||||
endpoint: "/v1/chat/completions",
|
||||
error: "[503] Pipeline gate rejected",
|
||||
apiKeyId: "key-video-reject",
|
||||
apiKeyName: "video-reject-test",
|
||||
correlationId: "corr-video-reject",
|
||||
startTime: Date.now() - 10,
|
||||
requestBody: {
|
||||
model: "default",
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "look at this video" },
|
||||
{
|
||||
type: "input_video",
|
||||
video_url: "https://example.com/clip.mp4",
|
||||
transcript: { cues: [{ text: SECRET, startSeconds: 0, endSeconds: 2 }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
let rejected: { id: string } | undefined;
|
||||
for (let i = 0; i < 50 && !rejected; i++) {
|
||||
const logs = await callLogs.getCallLogs({});
|
||||
const list = (logs.logs ?? logs) as Array<{ apiKeyName?: string | null }>;
|
||||
const found = (list ?? []).find((l) => l.apiKeyName === "video-reject-test");
|
||||
if (found) rejected = found as unknown as { id: string };
|
||||
else await new Promise((r) => setTimeout(r, 10));
|
||||
}
|
||||
assert.ok(rejected, "expected a call_logs row for the rejected video request");
|
||||
|
||||
const detail = await callLogs.getCallLogById(rejected.id);
|
||||
assert.ok(detail, "expected to load the call log detail");
|
||||
assert.equal(
|
||||
JSON.stringify(detail!.requestBody).includes(SECRET),
|
||||
false,
|
||||
"the rejected request's persisted body must not retain the raw video transcript"
|
||||
);
|
||||
const transcriptField = (
|
||||
detail!.requestBody as {
|
||||
messages: Array<{ content: Array<{ transcript?: unknown }> }>;
|
||||
}
|
||||
).messages[0].content[1].transcript;
|
||||
assert.equal(transcriptField, "[redacted-video-transcript]");
|
||||
});
|
||||
|
||||
test("combo-exhausted rejection without a request body still logs cleanly (no request body available)", async () => {
|
||||
await recordRejectedRequestUsage({
|
||||
status: 503,
|
||||
|
||||
Reference in New Issue
Block a user