fix: CORS headers on early-return error responses + auto-combo validation (#208, #209)

- Added CORS_HEADERS spread to 400/503 responses in chat/completions route
- Added createAutoComboSchema with Zod validation to /api/combos/auto
- Isolated JSON parsing errors with structured 400 response
- Prevented String(err) leakage on 500 errors
This commit is contained in:
diegosouzapw
2026-03-05 15:56:17 -03:00
parent 0d3728efa4
commit 084b206ae6
3 changed files with 52 additions and 11 deletions

View File

@@ -10,6 +10,8 @@
*/
import { NextRequest, NextResponse } from "next/server";
import { createAutoComboSchema } from "@/shared/validation/schemas";
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
// ── In-memory auto-combo store (mirrors open-sse/services/autoCombo/engine.ts) ──
@@ -45,29 +47,45 @@ interface AutoComboConfig {
const autoCombos = new Map<string, AutoComboConfig>();
export async function POST(req: NextRequest) {
let rawBody: unknown;
try {
const body = await req.json();
const { id, name, candidatePool, weights, modePack, budgetCap, explorationRate } = body;
rawBody = await req.json();
} catch {
return NextResponse.json(
{
error: {
message: "Invalid request",
details: [{ field: "body", message: "Invalid JSON body" }],
},
},
{ status: 400 }
);
}
if (!id || !name) {
return NextResponse.json({ error: "id and name required" }, { status: 400 });
try {
const validation = validateBody(createAutoComboSchema, rawBody);
if (isValidationFailure(validation)) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { id, name, candidatePool, weights, modePack, budgetCap, explorationRate } =
validation.data;
const config: AutoComboConfig = {
id,
name,
type: "auto",
candidatePool: candidatePool || [],
weights: weights || DEFAULT_WEIGHTS,
candidatePool,
weights: weights ?? DEFAULT_WEIGHTS,
modePack,
budgetCap,
explorationRate: explorationRate ?? 0.05,
explorationRate,
};
autoCombos.set(id, config);
return NextResponse.json(config, { status: 201 });
} catch (err) {
return NextResponse.json({ error: String(err) }, { status: 500 });
console.log("Error creating auto-combo:", err);
return NextResponse.json({ error: "Failed to create auto-combo" }, { status: 500 });
}
}

View File

@@ -1,4 +1,4 @@
import { CORS_ORIGIN } from "@/shared/utils/cors";
import { CORS_ORIGIN, CORS_HEADERS } from "@/shared/utils/cors";
import { callCloudWithMachineId } from "@/shared/utils/cloud";
import { handleChat } from "@/sse/handlers/chat";
import { initTranslators } from "@omniroute/open-sse/translator/index.ts";
@@ -53,7 +53,7 @@ export async function POST(request) {
detections: result.detections.length,
},
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
{ status: 400, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }
);
}
}
@@ -67,7 +67,7 @@ export async function POST(request) {
code: "SECURITY_002",
},
}),
{ status: 503, headers: { "Content-Type": "application/json" } }
{ status: 503, headers: { ...CORS_HEADERS, "Content-Type": "application/json" } }
);
}

View File

@@ -79,6 +79,29 @@ export const createComboSchema = z.object({
config: comboConfigSchema,
});
// ──── Auto-Combo Schemas ────
const scoringWeightsSchema = z
.object({
quota: z.number().min(0).max(1),
health: z.number().min(0).max(1),
costInv: z.number().min(0).max(1),
latencyInv: z.number().min(0).max(1),
taskFit: z.number().min(0).max(1),
stability: z.number().min(0).max(1),
})
.optional();
export const createAutoComboSchema = z.object({
id: z.string().trim().min(1, "id is required").max(100),
name: z.string().trim().min(1, "name is required").max(200),
candidatePool: z.array(z.string().min(1)).optional().default([]),
weights: scoringWeightsSchema,
modePack: z.string().max(100).optional(),
budgetCap: z.number().positive().optional(),
explorationRate: z.number().min(0).max(1).optional().default(0.05),
});
// ──── Settings Schemas ────
// FASE-01: Removed .passthrough() — only explicitly listed fields are accepted