import type { FastifyInstance } from "fastify"; import { z } from "zod"; import type { Db } from "./db.js"; import type { AppConfig } from "./config.js"; import { getProductBySlug } from "./domain/product.js"; import { ContentConflict } from "./domain/offerContent.js"; import { OrchestratorError, inspectProduct, prepareOperation, decideOperation, getOperation, listOperations, interpretDirective, actionRegistry } from "./domain/orchestrator.js"; import { prepareOperationSchema, operationDecisionSchema, directiveSchema } from "./domain/orchestrationContracts.js"; export function registerOrchestratorRoutes(app: FastifyInstance, db: Db, cfg: AppConfig): void { app.register(async (scope) => { scope.addHook("preHandler", async (req, reply) => { reply.header("Cache-Control", "private, no-store"); if (!req.user) return reply.code(401).send({ error: "Wymagane zalogowanie." }); if (req.method !== "GET") { const origin = req.headers.origin, site = req.headers["sec-fetch-site"]; let matchingOrigin = !origin; if (origin) { try { matchingOrigin = new URL(origin).host === req.headers.host; } catch { matchingOrigin = false; } } if (site === "cross-site" || (site !== "same-origin" && !matchingOrigin)) return reply.code(403).send({ error: "Niedozwolone źródło żądania." }); } }); scope.setErrorHandler((err, _req, reply) => { if (err instanceof z.ZodError) return reply.code(400).send({ error: "Nieprawidłowy zakres operacji. Sprawdź wymagane pola i format danych." }); if (err instanceof OrchestratorError) return reply.code(err.statusCode).send({ error: err.message }); if (err instanceof ContentConflict) return reply.code(409).send({ error: err.message }); return reply.code(500).send({ error: "Nie udało się obsłużyć orkiestratora. Odśwież stan; nie ponawiaj operacji z nowym kluczem bez sprawdzenia historii." }); }); const productId = (slug: string) => { const product = getProductBySlug(db, slug); if (!product) throw new OrchestratorError("Nie znaleziono produktu.", 404); return product.id; }; const base = "/api/products/:slug/orchestrator"; scope.get(base, async (req) => { const id = productId((req.params as { slug: string }).slug); const monitor = db.prepare("SELECT checked_at FROM orchestrator_inspections WHERE product_id=?").get(id) as { checked_at: string } | undefined; return { inspection: inspectProduct(db, cfg, id), operations: listOperations(db, id), registry: actionRegistry, monitoredAt: monitor?.checked_at ?? null }; }); scope.get(`${base}/operations`, async (req) => { const query = z.object({ before: z.string().max(160).optional() }).strict().parse(req.query); return { operations: listOperations(db, productId((req.params as { slug: string }).slug), query.before) }; }); scope.get(`${base}/operations/:id`, async (req) => { const params = req.params as { slug: string; id: string }; return getOperation(db, productId(params.slug), params.id); }); scope.post(`${base}/directives`, { bodyLimit: 8192 }, async (req) => { const body = directiveSchema.parse(req.body); return interpretDirective(db, productId((req.params as { slug: string }).slug), body.text); }); scope.post(`${base}/operations`, { bodyLimit: 32768 }, async (req, reply) => { const body = prepareOperationSchema.parse(req.body); const operation = prepareOperation(db, cfg, productId((req.params as { slug: string }).slug), body, req.user!); return reply.code(200).send(operation); }); scope.post(`${base}/operations/:id/decision`, { bodyLimit: 8192 }, async (req) => { const params = req.params as { slug: string; id: string }; return decideOperation(db, cfg, productId(params.slug), params.id, operationDecisionSchema.parse(req.body), req.user!); }); }); }