From 3aefd7bb06979286fe8fe88913f88302b3f21d0b Mon Sep 17 00:00:00 2001 From: Carmine Zacchia Date: Sun, 13 Sep 2026 23:18:59 +0200 Subject: [PATCH] fix(timestamp): fall back to the default CORS proxy on official domains The timestamp tool requires a CORS proxy: none of the built-in TSA endpoints send Access-Control-Allow-Origin, so the browser blocks the RFC 3161 POST. The runtime only used a proxy when VITE_CORS_PROXY_URL was set, and the official build never set it, leaving timestamping broken on www.bentopdf.com and on the published Docker images. Fall back to the project-operated proxy on the official domains only (bentopdf.com, www.bentopdf.com), matching the origin already allowed in the generated CSP. Self-hosted instances keep requiring their own proxy. --- src/js/logic/digital-sign-pdf.ts | 28 +++++++++++++++++++++++- src/tests/digital-sign-pdf.test.ts | 34 +++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/js/logic/digital-sign-pdf.ts b/src/js/logic/digital-sign-pdf.ts index 59729b5a..9fabc75a 100644 --- a/src/js/logic/digital-sign-pdf.ts +++ b/src/js/logic/digital-sign-pdf.ts @@ -103,8 +103,34 @@ export function parseCombinedPem( * and set VITE_CORS_PROXY_URL environment variable. * * If not set, certificates requiring external chain fetching will fail. + * + * On the official BentoPDF domains we fall back to the project-operated proxy. + * The generated CSP already allows that origin by default (see + * scripts/generate-security-headers.mjs), but the runtime never used it, which + * left the timestamp tool broken on the official HTTPS site. The fallback is + * deliberately scoped to the official domains so self-hosted instances keep + * requiring their own proxy. */ -const CORS_PROXY_URL = import.meta.env.VITE_CORS_PROXY_URL || ''; +const DEFAULT_CORS_PROXY_URL = + 'https://bentopdf-cors-proxy.bentopdf.workers.dev'; + +const OFFICIAL_HOSTNAMES = new Set(['bentopdf.com', 'www.bentopdf.com']); + +function resolveCorsProxyUrl(): string { + const configured = import.meta.env.VITE_CORS_PROXY_URL || ''; + if (configured) { + return configured; + } + + if (typeof window === 'undefined') { + return ''; + } + + const hostname = window.location?.hostname?.toLowerCase() ?? ''; + return OFFICIAL_HOSTNAMES.has(hostname) ? DEFAULT_CORS_PROXY_URL : ''; +} + +const CORS_PROXY_URL = resolveCorsProxyUrl(); /** * Shared secret for signing proxy requests (HMAC-SHA256). diff --git a/src/tests/digital-sign-pdf.test.ts b/src/tests/digital-sign-pdf.test.ts index 799a28b0..d6819a14 100644 --- a/src/tests/digital-sign-pdf.test.ts +++ b/src/tests/digital-sign-pdf.test.ts @@ -143,7 +143,11 @@ describe('timestampPdf', () => { vi.stubEnv('VITE_CORS_PROXY_URL', ''); Object.defineProperty(window, 'location', { configurable: true, - value: { protocol: 'https:', origin: 'https://www.bentopdf.com' }, + value: { + protocol: 'https:', + origin: 'https://selfhost.example.com', + hostname: 'selfhost.example.com', + }, }); vi.resetModules(); const { timestampPdf: freshTimestamp } = @@ -153,4 +157,32 @@ describe('timestampPdf', () => { freshTimestamp(samplePdfBytes, 'http://timestamp.digicert.com') ).rejects.toThrow(/HTTPS page|VITE_CORS_PROXY_URL/); }); + + it('should fall back to the default proxy on official domains', async () => { + vi.stubEnv('VITE_CORS_PROXY_URL', ''); + Object.defineProperty(window, 'location', { + configurable: true, + value: { + protocol: 'https:', + origin: 'https://www.bentopdf.com', + hostname: 'www.bentopdf.com', + }, + }); + vi.resetModules(); + const { timestampPdf: freshTimestamp } = + await import('@/js/logic/digital-sign-pdf'); + + mockSign.mockResolvedValueOnce(new Uint8Array([1])); + await freshTimestamp(samplePdfBytes, 'http://timestamp.digicert.com'); + + const callArg = vi.mocked(PdfSigner).mock.calls[0][0] as { + signdate: { url: string }; + }; + expect(callArg.signdate.url).toMatch( + /^https:\/\/bentopdf-cors-proxy\.bentopdf\.workers\.dev\?url=/ + ); + expect(callArg.signdate.url).toContain( + encodeURIComponent('http://timestamp.digicert.com') + ); + }); });