fix(ci): route validation, CodeQL alerts, Docker workflow
- Add Zod schemas + validateBody() to 5 routes missing validation: model-combo-mappings (POST, PUT), webhooks (POST, PUT), openapi/try (POST) - Fix 6 polynomial-redos CodeQL alerts in provider.ts and chatCore.ts by replacing (?:^|/) alternation patterns with segment-based matching - Fix insecure-randomness in acp/manager.ts (crypto.randomUUID) - Fix shell-command-injection in prepublish.mjs (JSON.stringify) - Upgrade docker/setup-buildx-action from v3 to v4 (Node.js 20 deprecation) CI check:route-validation:t06 PASS (176/176 routes validated) Tests: 926/926 pass
This commit is contained in:
@@ -29,7 +29,7 @@ jobs:
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
|
||||
@@ -80,7 +80,8 @@ export function shouldUseNativeCodexPassthrough({
|
||||
if (provider !== "codex") return false;
|
||||
if (sourceFormat !== FORMATS.OPENAI_RESPONSES) return false;
|
||||
const normalizedEndpoint = String(endpointPath || "").replace(/\/+$/, "");
|
||||
return /(?:^|\/)responses(?:\/.*)?$/i.test(normalizedEndpoint);
|
||||
const segments = normalizedEndpoint.split("/");
|
||||
return segments.includes("responses");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,7 +223,7 @@ export async function handleChatCore({
|
||||
|
||||
const endpointPath = String(clientRawRequest?.endpoint || "");
|
||||
const sourceFormat = detectFormatFromEndpoint(body, endpointPath);
|
||||
const isResponsesEndpoint = /(?:^|\/)responses(?:\/.*)?$/i.test(endpointPath);
|
||||
const isResponsesEndpoint = /\/responses(?=\/|$)/i.test(endpointPath) || /^responses(?=\/|$)/i.test(endpointPath);
|
||||
const nativeCodexPassthrough = shouldUseNativeCodexPassthrough({
|
||||
provider,
|
||||
sourceFormat,
|
||||
|
||||
@@ -41,15 +41,15 @@ function buildAnthropicCompatibleUrl(baseUrl) {
|
||||
export function detectFormatFromEndpoint(body, endpointPath = "") {
|
||||
const path = String(endpointPath || "");
|
||||
|
||||
if (/(?:^|\/)responses(?:\/.*)?$/i.test(path)) {
|
||||
if (/\/responses(?=\/|$)/i.test(path) || /^responses(?=\/|$)/i.test(path)) {
|
||||
return "openai-responses";
|
||||
}
|
||||
|
||||
if (/(?:^|\/)messages(?:\/.*)?$/i.test(path)) {
|
||||
if (/\/messages(?=\/|$)/i.test(path) || /^messages(?=\/|$)/i.test(path)) {
|
||||
return "claude";
|
||||
}
|
||||
|
||||
if (/(?:^|\/)(?:chat\/completions|completions)(?:\/.*)?$/i.test(path)) {
|
||||
if (/\/(?:chat\/completions|completions)(?=\/|$)/i.test(path) || /^(?:chat\/completions|completions)(?=\/|$)/i.test(path)) {
|
||||
return "openai";
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ if (existsSync(mitmSrc)) {
|
||||
writeFileSync(tmpTsconfigPath, JSON.stringify(mitmTsconfig, null, 2));
|
||||
|
||||
try {
|
||||
execSync(`npx tsc -p ${tmpTsconfigPath}`, { cwd: ROOT, stdio: "inherit" });
|
||||
execSync("npx tsc -p " + JSON.stringify(tmpTsconfigPath), { cwd: ROOT, stdio: "inherit" });
|
||||
console.log(" ✅ MITM utilities compiled to app/src/mitm/");
|
||||
} catch (err) {
|
||||
console.warn(" ⚠️ MITM compile warning (non-fatal):", err.message);
|
||||
|
||||
@@ -4,12 +4,22 @@
|
||||
* DELETE — Delete a mapping
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { NextResponse } from "next/server";
|
||||
import {
|
||||
updateModelComboMapping,
|
||||
deleteModelComboMapping,
|
||||
getModelComboMappingById,
|
||||
} from "@/lib/localDb";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
|
||||
const updateMappingSchema = z.object({
|
||||
pattern: z.string().min(1).max(500).optional(),
|
||||
comboId: z.string().min(1).optional(),
|
||||
priority: z.number().int().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
});
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
@@ -27,15 +37,13 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id:
|
||||
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const rawBody = await request.json();
|
||||
const validation = validateBody(updateMappingSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const mapping = await updateModelComboMapping(id, {
|
||||
pattern: body.pattern,
|
||||
comboId: body.comboId,
|
||||
priority: body.priority,
|
||||
enabled: body.enabled,
|
||||
description: body.description,
|
||||
});
|
||||
const mapping = await updateModelComboMapping(id, validation.data);
|
||||
|
||||
if (!mapping) {
|
||||
return NextResponse.json({ error: "Mapping not found" }, { status: 404 });
|
||||
|
||||
@@ -4,8 +4,18 @@
|
||||
* POST — Create a new mapping
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getModelComboMappings, createModelComboMapping } from "@/lib/localDb";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
|
||||
const createMappingSchema = z.object({
|
||||
pattern: z.string().min(1, "Pattern is required").max(500),
|
||||
comboId: z.string().min(1, "ComboId is required"),
|
||||
priority: z.number().int().optional().default(0),
|
||||
enabled: z.boolean().optional().default(true),
|
||||
description: z.string().max(1000).optional().default(""),
|
||||
});
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
@@ -21,21 +31,19 @@ export async function GET() {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.pattern || typeof body.pattern !== "string") {
|
||||
return NextResponse.json({ error: "Missing or invalid 'pattern' field" }, { status: 400 });
|
||||
}
|
||||
if (!body.comboId || typeof body.comboId !== "string") {
|
||||
return NextResponse.json({ error: "Missing or invalid 'comboId' field" }, { status: 400 });
|
||||
const rawBody = await request.json();
|
||||
const validation = validateBody(createMappingSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const { data } = validation;
|
||||
const mapping = await createModelComboMapping({
|
||||
pattern: body.pattern.trim(),
|
||||
comboId: body.comboId,
|
||||
priority: typeof body.priority === "number" ? body.priority : 0,
|
||||
enabled: body.enabled !== false,
|
||||
description: body.description || "",
|
||||
pattern: data.pattern.trim(),
|
||||
comboId: data.comboId,
|
||||
priority: data.priority,
|
||||
enabled: data.enabled,
|
||||
description: data.description,
|
||||
});
|
||||
|
||||
return NextResponse.json({ mapping }, { status: 201 });
|
||||
|
||||
@@ -3,21 +3,26 @@
|
||||
* POST — forwards a request to a local endpoint and returns the result
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
|
||||
const tryRequestSchema = z.object({
|
||||
method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]).optional().default("GET"),
|
||||
path: z.string().min(1, "Path is required").startsWith("/", "Path must start with /"),
|
||||
headers: z.record(z.string()).optional().default({}),
|
||||
body: z.any().optional(),
|
||||
});
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { method = "GET", path, headers = {}, body: reqBody } = body;
|
||||
|
||||
if (!path || typeof path !== "string") {
|
||||
return NextResponse.json({ error: "Missing 'path' field" }, { status: 400 });
|
||||
const rawBody = await request.json();
|
||||
const validation = validateBody(tryRequestSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
|
||||
// Only allow requests to local endpoints for security
|
||||
if (!path.startsWith("/")) {
|
||||
return NextResponse.json({ error: "Path must start with /" }, { status: 400 });
|
||||
}
|
||||
const { method, path, headers, body: reqBody } = validation.data;
|
||||
|
||||
// Build the target URL using the incoming request's origin
|
||||
const origin = request.headers.get("x-forwarded-proto")
|
||||
|
||||
@@ -5,8 +5,18 @@
|
||||
* DELETE — Delete webhook
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getWebhook, updateWebhookRecord, deleteWebhook } from "@/lib/localDb";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
|
||||
const updateWebhookSchema = z.object({
|
||||
url: z.string().url("Invalid URL format").max(2000).optional(),
|
||||
events: z.array(z.string()).optional(),
|
||||
secret: z.string().max(500).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
}).passthrough();
|
||||
|
||||
export async function GET(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
@@ -24,8 +34,13 @@ export async function GET(_: Request, { params }: { params: Promise<{ id: string
|
||||
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const webhook = updateWebhookRecord(id, body);
|
||||
const rawBody = await request.json();
|
||||
const validation = validateBody(updateWebhookSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const webhook = updateWebhookRecord(id, validation.data);
|
||||
if (!webhook) {
|
||||
return NextResponse.json({ error: "Webhook not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
@@ -4,8 +4,17 @@
|
||||
* POST — Create a new webhook
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getWebhooks, createWebhook } from "@/lib/localDb";
|
||||
import { validateBody, isValidationFailure } from "@/shared/validation/helpers";
|
||||
|
||||
const createWebhookSchema = z.object({
|
||||
url: z.string().url("Invalid URL format").max(2000),
|
||||
events: z.array(z.string()).optional().default(["*"]),
|
||||
secret: z.string().max(500).optional(),
|
||||
description: z.string().max(1000).optional().default(""),
|
||||
});
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
@@ -26,24 +35,18 @@ export async function GET() {
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.url || typeof body.url !== "string") {
|
||||
return NextResponse.json({ error: "Missing or invalid 'url' field" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
try {
|
||||
new URL(body.url);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid URL format" }, { status: 400 });
|
||||
const rawBody = await request.json();
|
||||
const validation = validateBody(createWebhookSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const { data } = validation;
|
||||
const webhook = createWebhook({
|
||||
url: body.url,
|
||||
events: body.events || ["*"],
|
||||
secret: body.secret,
|
||||
description: body.description || "",
|
||||
url: data.url,
|
||||
events: data.events,
|
||||
secret: data.secret,
|
||||
description: data.description,
|
||||
});
|
||||
|
||||
return NextResponse.json({ webhook }, { status: 201 });
|
||||
|
||||
@@ -47,7 +47,7 @@ export class AcpManager extends EventEmitter {
|
||||
args: string[] = [],
|
||||
env: Record<string, string> = {}
|
||||
): AcpSession {
|
||||
const sessionId = `acp-${agentId}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const sessionId = `acp-${agentId}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
||||
|
||||
const child = spawn(binary, args, {
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
|
||||
Reference in New Issue
Block a user