CI (SIAX Cloud) / quality (push) Successful in 51s
Security Scan (SIAX Cloud) / security (push) Successful in 26s
CI (SIAX Cloud) / security (push) Successful in 28s
CI (SIAX Cloud) / integration (push) Successful in 1m1s
CI (SIAX Cloud) / integration (pull_request) Successful in 30s
CI (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / quality (pull_request) Successful in 48s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 22s
Tenant isolation (security-critical): every app/api/0pt/** route reading
tenant_id from client-controlled query/body never cross-checked it against
the token middleware.ts already verified (x-0pt-verified-tenant). Adds a
shared assertTenantMatchesVerified guard and wires it into all 20+ affected
routes. Also closes two full-leak-by-omission bugs found in this pass
(documents/capture and lakehouse/mappings GETs returned every tenant's rows
when tenant_id was simply omitted) and two "no tenant check at all" reads
(rate-policies/[id], lakehouse/mappings/[id]) reachable by ID alone.
Duplicate-delivery idempotency (correctness-critical): POST /api/orders now
honors Idempotency-Key (sql/006, replay-or-409-on-conflict, Postgres +
in-memory); the Python orchestrator's POST /orders/{id}/run — the actual
delivery trigger — is now idempotent under an asyncio lock, closing a real
concurrent-request race, not just the sequential-retry case.
Engine-duplication resolution: adaptive_router.py's docstring wrongly
invited a bypass ("see b0ts for scraping") contradicting the product's own
compliance stance; corrected, and given real (additive, not escalating)
block-signal tracking so a rate-limited/blocked source is excluded from the
plan instead of hammered. Also fixes a previously-undiscovered production
bug: the Python engine's only call into the TS quality bridge
(X-Internal-Token) was unconditionally 401'd by middleware.ts, which never
checked that credential — the advisory quality bridge was unreachable in any
environment enforcing auth.
Restore drill: executed for real against throwaway Docker Postgres
containers (not simulated). Found and fixed two real blockers along the way:
sql/004 referenced opt_datasets, which no migration ever created (chain was
never actually runnable end to end); lib/db.ts hardcoded ssl:"require",
making any non-TLS Postgres (incl. compose.dev.yaml's own local-dev
container) unreachable. After both fixes: full migration chain applies
cleanly, pg_dump/pg_restore round-trip is byte-identical (row counts +
content checksums), and real application code (lib/orders-store.ts,
SourceRegistryService's Postgres repository) reads the restored data back
correctly, including cross-tenant isolation still holding.
82 new tests (55 TS + 27 Python), all passing; full existing suite (291 TS +
55 Python) still green; typecheck clean on every changed/added file.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
55 lines
2.6 KiB
TypeScript
55 lines
2.6 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { createOrder, listOrders, IdempotencyKeyConflictError, type OrderConfig } from "@/lib/orders-store";
|
|
import { requireRouteAuth } from "@/src/auth/require-route-auth";
|
|
|
|
// POST /api/orders (docs/04) — create a draft order from a configurator config.
|
|
// Intentionally anonymous: a prospect drafts/estimates a job before signing in
|
|
// to pay the deposit (see docs/01, the configurator flow). Only reading back
|
|
// the order queue (GET, below) is a data-exposure risk.
|
|
//
|
|
// P014: honors an optional `Idempotency-Key` header (the convention this
|
|
// skill's own docs already name -- "Idempotency-Key on order/payment POSTs")
|
|
// so a retried submission (double-click, dropped response, proxy replay)
|
|
// returns the ORIGINAL order (200) instead of creating a duplicate (was:
|
|
// no idempotency mechanism at all -- every retry created a new row, P014
|
|
// recon "Duplicate delivery idempotent: NOT MET"). Omitting the header keeps
|
|
// the pre-P014 behavior exactly: every call creates a fresh order.
|
|
export async function POST(req: NextRequest) {
|
|
let body: { flow?: string; config?: OrderConfig };
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ error: { code: "bad_json", message: "Invalid JSON" } }, { status: 400 });
|
|
}
|
|
if (!body || typeof body !== "object") {
|
|
return NextResponse.json({ error: { code: "bad_json", message: "Invalid JSON body" } }, { status: 400 });
|
|
}
|
|
const flow = body.flow || "custom";
|
|
const config = body.config || {};
|
|
const idempotencyKey = req.headers.get("idempotency-key") ?? undefined;
|
|
try {
|
|
const { order, replayed } = await createOrder(flow, config, idempotencyKey);
|
|
return NextResponse.json(
|
|
{ order_id: order.order_id, status: order.status, deposit_ore: order.deposit_ore, total_ore: order.total_ore },
|
|
{ status: replayed ? 200 : 201 },
|
|
);
|
|
} catch (err: unknown) {
|
|
if (err instanceof IdempotencyKeyConflictError) {
|
|
return NextResponse.json({ error: { code: "idempotency_key_conflict", message: err.message } }, { status: 409 });
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// GET /api/orders — list all orders (queue + financial aggregates). This is
|
|
// outside middleware.ts's matcher (/api/0pt/:path* only), so auth is enforced
|
|
// here directly via the same fail-closed Zitadel verifier — see
|
|
// src/auth/require-route-auth.ts for why the request is re-verified rather
|
|
// than trusting a forwarded header.
|
|
export async function GET(req: NextRequest) {
|
|
const auth = await requireRouteAuth(req);
|
|
if (auth.response) return auth.response;
|
|
const orders = await listOrders();
|
|
return NextResponse.json(orders);
|
|
}
|