diff --git a/buyer/.env.example b/buyer/.env.example index 8c30755..eb7de34 100644 --- a/buyer/.env.example +++ b/buyer/.env.example @@ -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 diff --git a/buyer/src/config.ts b/buyer/src/config.ts index 44358b4..9c512ff 100644 --- a/buyer/src/config.ts +++ b/buyer/src/config.ts @@ -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): 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): BuyerConfig password, sessionSecret, cookieSecure, + pdfEnabled, gatewayPublicUrl, }; } diff --git a/buyer/src/index.ts b/buyer/src/index.ts index f95c014..4e0be24 100644 --- a/buyer/src/index.ts +++ b/buyer/src/index.ts @@ -96,29 +96,36 @@ export async function runMain(env: Record): 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); - // 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. - let pdfJobDb: PdfJobDb | undefined; - try { - pdfJobDb = new PdfJobDb(config.archiveDir); - console.log(`[buyer] pdf job store at ${config.archiveDir}/pdf-jobs.db`); - } catch (err) { - console.error( - `[buyer] failed to open pdf job store at ${config.archiveDir}:`, - err instanceof Error ? err.message : String(err), - ); + 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. + let pdfJobDb: PdfJobDb | undefined; + try { + pdfJobDb = new PdfJobDb(config.archiveDir); + console.log(`[buyer] pdf job store at ${config.archiveDir}/pdf-jobs.db`); + } catch (err) { + console.error( + `[buyer] failed to open pdf job store at ${config.archiveDir}:`, + err instanceof Error ? err.message : String(err), + ); + } + jobStore = new JobStore({ + marketplace, + chain, + walletKey, + indexerUrl: config.indexerUrl, + archive, + caps: pdfCaps, + db: pdfJobDb, + }); + } else { + console.log("[buyer] pdf summarizer disabled (PDF_ENABLED=0)"); } - const jobStore = new JobStore({ - marketplace, - chain, - walletKey, - indexerUrl: config.indexerUrl, - archive, - caps: pdfCaps, - db: pdfJobDb, - }); const app = createApp({ distPath, diff --git a/buyer/src/pdf/routes.ts b/buyer/src/pdf/routes.ts index a89bed9..be9f0c3 100644 --- a/buyer/src/pdf/routes.ts +++ b/buyer/src/pdf/routes.ts @@ -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); diff --git a/tests/unit/buyer-server-config.test.ts b/tests/unit/buyer-server-config.test.ts index a706785..040005a 100644 --- a/tests/unit/buyer-server-config.test.ts +++ b/tests/unit/buyer-server-config.test.ts @@ -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 ──────────────────────────────────────────────────────────