Engineering manualAPI route source
/api/comprehensive/advance
Exact current route handler: hosted-app/app/api/comprehensive/advance/route.js
Methods: GET, POST. Input signals: JSON body, query parameters. Literal response-status signals: 200, 202.
Source: hosted-app/app/api/comprehensive/advance/route.js. Imports: ../../../../lib/comprehensive-pull-queue-runner.mjs, ../../../../lib/comprehensive-pull-queue.mjs, ../../../../lib/queue-continuation.mjs, ../../../../lib/supabase-rest.mjs, next/server. Exact source is shown for inspection; its presence does not prove authentication, validation or live behavior.
All API routes · API and execution contract
import { continueAfterAdvance } from "../../../../lib/queue-continuation.mjs";
// POST/GET /api/comprehensive/advance — the trigger that runs UNATTENDED.
//
// This is the endpoint that finally makes ../route.js's old "there is still no
// cron/queue/worker anywhere in this codebase" note untrue. It advances queued
// comprehensive pulls that nobody is watching: the customer closed the tab, or
// the slice that yielded could not reach anything to hand off to.
//
// THREE CALLERS, ONE BODY OF WORK.
//
// 1. VERCEL CRON — `vercel.json`'s `crons` entry hits GET on this path on its configured
// schedule (the packaged default is daily, not a minute-scale SLA). Vercel signs cron requests with CRON_SECRET when it is set; we
// accept that OR our own x-queue-secret, so the same endpoint serves both
// the platform's scheduler and our own self-invocation.
// 2. SELF-INVOCATION — a slice that yields POSTs here with {pull_id} and the
// shared secret, so the next slice starts in seconds rather than waiting
// out the cron interval. Best-effort: it needs the app to know its own
// public URL (BRAND_STUDIO_PUBLIC_URL or VERCEL_URL).
// 3. A SELF-HOSTED TICKER — instrumentation.js calls the same engine
// IN-PROCESS on an interval, with no HTTP and no secret, because a docker
// deployment has no platform scheduler. See that file.
//
// AUTHORISATION. This endpoint runs work on behalf of users using the
// service-role key, so it must not be callable by the public. It requires the
// shared secret (or Vercel's cron signature) and REFUSES OUTRIGHT when no
// secret is configured — an unauthenticated worker endpoint is worse than an
// absent one, and failing closed means a misconfigured deployment loses the
// cron trigger while keeping the poll trigger, rather than exposing a way for
// strangers to spend the account's provider budget.
//
// Capture uses a fenced lease; terminal billing uses the ledger's unique
// spend-per-pull constraint. Concurrent triggers cannot repeat capture or
// debit a pull twice. Billing retries are paced by polling/the worker sweep,
// never by a new capture claim or an immediate self-handoff loop.
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
// A slice is budgeted at 240s and yields on its own; 300s is the platform
// ceiling that budget sits under.
export const maxDuration = 300;
import { NextResponse, after } from "next/server";
import { supabaseConfigFromEnv } from "../../../../lib/supabase-rest.mjs";
import { queueEnabledFromEnv } from "../../../../lib/comprehensive-pull-queue.mjs";
import {
advanceComprehensivePullSlice,
advanceClaimableComprehensivePulls,
triggerNextSlice
} from "../../../../lib/comprehensive-pull-queue-runner.mjs";
/**
* Fails CLOSED: with neither secret configured, nothing is authorised. See
* this file's header on why that is the right default for a worker endpoint.
*/
const authorizeAdvance = (request, env) => {
const queueSecret = env.BRAND_STUDIO_QUEUE_SECRET || "";
const cronSecret = env.CRON_SECRET || "";
if (!queueSecret && !cronSecret) {
return { ok: false, http_status: 503, reason: "no_queue_secret_configured" };
}
const provided = request.headers.get("x-queue-secret") || "";
if (queueSecret && provided && provided === queueSecret) return { ok: true, via: "queue_secret" };
const authorization = request.headers.get("authorization") || "";
if (cronSecret && authorization === `Bearer ${cronSecret}`) return { ok: true, via: "vercel_cron" };
return { ok: false, http_status: 401, reason: "unauthorized" };
};
const runAdvance = async ({ request, pullId, wait = false }) => {
const env = process.env;
if (!queueEnabledFromEnv(env)) {
return NextResponse.json({ status: "skipped", reason: "queue_disabled" }, { status: 200 });
}
const auth = authorizeAdvance(request, env);
if (!auth.ok) {
return NextResponse.json({ status: "refused", reason: auth.reason }, { status: auth.http_status });
}
const config = supabaseConfigFromEnv(env);
const log = (line) => console.log(`[pull-queue] ${line}`);
// A named pull advances capture or reconciles pending terminal billing;
// an unnamed call sweeps both lanes. The cron takes the second path.
const advance = pullId
? () => advanceComprehensivePullSlice({ pullId, config, env, alphaParking: true, holder: `advance-${auth.via}`, log })
: () => advanceClaimableComprehensivePulls({ config, env, alphaParking: true, holder: `advance-${auth.via}`, log });
const work = () => continueAfterAdvance({ work: advance,
trigger: (id) => triggerNextSlice({ pullId: id, env }),
onHandoff: (row) => log(`handoff ${row.pull_id}: ${row.triggered ? "accepted" : row.reason || "unavailable"}`)
});
// ACCEPT AND DEFER, not accept and block.
//
// A slice takes up to four minutes. If this endpoint ran it before
// responding, the CALLER would be held open for that whole time — and the
// caller is usually a slice that has just yielded and is trying to hand off,
// so the outgoing invocation would stay alive burning its own budget waiting
// for the incoming one. Two invocations alive for one slice's work is the
// opposite of what slicing is for. (Observed on the first live run: the
// handoff log did not appear until the NEXT slice had finished, because the
// trigger was awaiting it.)
//
// So the work is scheduled with after() and the trigger gets an immediate
// 202. Nothing is lost: the outcome is logged, and the only consumer that
// wants it synchronously is a human debugging, who can ask with ?wait=1.
if (wait) {
const result = await work();
return NextResponse.json({ status: "passed", via: auth.via, waited: true, result }, { status: 200 });
}
after(async () => {
try { await work(); } catch (error) { console.error("[pull-queue] advance failed", error); }
});
return NextResponse.json({ status: "accepted", via: auth.via, pull_id: pullId || null }, { status: 202 });
};
export async function POST(request) {
const body = await request.json().catch(() => ({}));
return runAdvance({ request, pullId: String(body?.pull_id || ""), wait: Boolean(body?.wait) });
}
/** GET exists for Vercel cron, which issues GET. */
export async function GET(request) {
const params = new URL(request.url).searchParams;
return runAdvance({ request, pullId: params.get("pull_id") || "", wait: params.get("wait") === "1" });
}