feat(buyer): PDF_ENABLED kill switch for the PDF summarizer

PDF_ENABLED=0 skips the JobStore/PdfJobDb wiring in runMain, so every
/v1/pdf-* route responds 503 (the pre-existing disabled path). Default
stays enabled; only "0"/"1" accepted, misconfig fails boot loudly.
This commit is contained in:
david
2026-08-21 02:14:32 +02:00
parent c531e4678f
commit 5221815a71
5 changed files with 62 additions and 22 deletions

View File

@@ -42,6 +42,8 @@ NETWORK_ID=0
# Upload a PDF → chunk it → pay suppliers to summarize each chunk (one escrow
# per map + reduce call) → full-book summary. All knobs are optional; defaults
# shown. Tune the caps to bound spend/abuse on mainnet.
# Kill switch: "0" disables the feature entirely (every /v1/pdf-* route 503s):
# PDF_ENABLED=1
#
# Hard caps (a job is refused if it would exceed these):
# PDF_MAX_PAGES=400 # reject books larger than this

View File

@@ -42,6 +42,10 @@
* API key" page knows where to POST /signup. When
* unset, the SPA falls back to deriving "api." +
* current host.
* PDF_ENABLED — "1" (default) serves the PDF book summarizer
* (/v1/pdf-* routes). Set "0" to disable the feature
* entirely: no PDF job store is opened and every
* /v1/pdf-* route responds 503.
*/
const HEX64_RE = /^[0-9a-fA-F]{64}$/;
@@ -62,6 +66,7 @@ export interface BuyerConfig {
password: string;
sessionSecret: string;
cookieSecure: boolean;
pdfEnabled: boolean;
gatewayPublicUrl: string;
}
@@ -148,6 +153,18 @@ export function loadConfig(env: Record<string, string | undefined>): BuyerConfig
cookieSecure = cookieSecureStr === "1";
}
// PDF_ENABLED: defaults to true. Set "0" to disable the PDF book summarizer
// entirely — runMain skips the JobStore wiring and every /v1/pdf-* route
// responds 503. Accept only "0" / "1" to keep boot-time misconfig loud.
let pdfEnabled = true;
const pdfEnabledStr = env.PDF_ENABLED;
if (pdfEnabledStr !== undefined && pdfEnabledStr !== "") {
if (pdfEnabledStr !== "0" && pdfEnabledStr !== "1") {
throw new Error('loadConfig: PDF_ENABLED must be "0" or "1"');
}
pdfEnabled = pdfEnabledStr === "1";
}
// GATEWAY_PUBLIC_URL — optional. Public base URL of the gateway, surfaced to
// the SPA via the boot block (no trailing slash). Empty string means the SPA
// derives the URL from its own host ("api." + location.host).
@@ -167,6 +184,7 @@ export function loadConfig(env: Record<string, string | undefined>): BuyerConfig
password,
sessionSecret,
cookieSecure,
pdfEnabled,
gatewayPublicUrl,
};
}

View File

@@ -96,7 +96,11 @@ export async function runMain(env: Record<string, string | undefined>): Promise<
// PDF book summarizer: one job registry, sharing the live marketplace SDK,
// chain, wallet, and (optional) archive. Caps come from PDF_* env vars.
// PDF_ENABLED=0 skips the whole wiring; createApp then registers every
// /v1/pdf-* route as a 503 stub (feature disabled).
const pdfCaps = loadPdfCaps(env);
let jobStore: JobStore | undefined;
if (config.pdfEnabled) {
// Durable job store (SQLite under ARCHIVE_DIR) so past summaries survive
// restarts and navigation. Best-effort: if it can't open, jobs fall back to
// in-memory only (lost on restart) but the feature still works.
@@ -110,7 +114,7 @@ export async function runMain(env: Record<string, string | undefined>): Promise<
err instanceof Error ? err.message : String(err),
);
}
const jobStore = new JobStore({
jobStore = new JobStore({
marketplace,
chain,
walletKey,
@@ -119,6 +123,9 @@ export async function runMain(env: Record<string, string | undefined>): Promise<
caps: pdfCaps,
db: pdfJobDb,
});
} else {
console.log("[buyer] pdf summarizer disabled (PDF_ENABLED=0)");
}
const app = createApp({
distPath,

View File

@@ -40,7 +40,7 @@ export function registerPdfRoutes(
res,
503,
"service_unavailable",
"PDF summarizer disabled (buyer booted without chain/marketplace/archive deps)",
"PDF summarizer disabled",
);
app.post("/v1/pdf-upload", disabled);
app.all(/^\/v1\/pdf-jobs(\/.*)?$/, disabled);

View File

@@ -87,6 +87,19 @@ describe("loadConfig(env)", () => {
const env = { ...VALID_ENV, COOKIE_SECURE: "true" };
expect(() => loadConfig(env)).toThrow(/COOKIE_SECURE/);
});
it("defaults pdfEnabled to true when PDF_ENABLED is not provided", () => {
expect(loadConfig(VALID_ENV).pdfEnabled).toBe(true);
});
it('honours PDF_ENABLED="0" (summarizer kill switch)', () => {
const config = loadConfig({ ...VALID_ENV, PDF_ENABLED: "0" });
expect(config.pdfEnabled).toBe(false);
});
it("throws when PDF_ENABLED is not 0 or 1", () => {
expect(() => loadConfig({ ...VALID_ENV, PDF_ENABLED: "false" })).toThrow(/PDF_ENABLED/);
});
});
// ─── createApp tests ──────────────────────────────────────────────────────────