import { createHash, timingSafeEqual } from "node:crypto"; import { writeFile } from "node:fs/promises"; import path from "node:path"; /** * POST /api/deploy * * Drops the redeploy flag that scripts/deploy-watch.sh polls for. Requires * `Authorization: Bearer `. * * DEPLOY_TRIGGER_DIR must point at the same directory the watcher reads * (mounted into the container by docker-compose.yml). */ const TRIGGER_DIR = process.env.DEPLOY_TRIGGER_DIR ?? "/deploy-trigger"; const TRIGGER_FILE = path.join(TRIGGER_DIR, "redeploy.flag"); // Hashing both sides gives equal-length buffers, so the comparison is // constant-time regardless of the supplied token's length. function tokenMatches(supplied: string, expected: string): boolean { const a = createHash("sha256").update(supplied).digest(); const b = createHash("sha256").update(expected).digest(); return timingSafeEqual(a, b); } export async function POST(request: Request) { const secret = process.env.DEPLOY_TRIGGER_SECRET; if (!secret) { // Not configured: behave as if the route doesn't exist. return new Response(null, { status: 404 }); } const header = request.headers.get("authorization") ?? ""; const match = /^Bearer\s+(.+)$/i.exec(header); if (!match || !tokenMatches(match[1].trim(), secret)) { return Response.json({ error: "unauthorized" }, { status: 401 }); } try { await writeFile(TRIGGER_FILE, new Date().toISOString() + "\n"); } catch (err) { console.error("[deploy] failed to write trigger file", TRIGGER_FILE, err); return Response.json({ error: "failed to queue redeploy" }, { status: 500 }); } return Response.json({ status: "queued" }, { status: 202 }); }