VerifyKitv0.13.3

Deployment

A production deployment guide for applications using the VerifyKit SDK.


Production Checklist

Before deploying to production, verify the following:

  • WASM loads successfully (no CSP or bundler errors)
  • PDF.js worker is configured and loading
  • CMap and standard font files are accessible (if using non-Latin PDFs)
  • Revocation proxy endpoint has correct CORS headers (if using @trexolab/verifykit-plugin-revocation)
  • Bundle size is acceptable for your performance targets
  • Trust store is configured with any custom root CAs your PDFs require

Content Security Policy (CSP)

The VerifyKit core engine runs WebAssembly. If your application uses a Content Security Policy, you must allow WASM execution in the script-src directive.

Content-Security-Policy: script-src 'self' 'wasm-unsafe-eval';

The wasm-unsafe-eval directive allows WebAssembly compilation and instantiation without allowing arbitrary JavaScript eval(). This is the recommended approach and is supported in all modern browsers.

Alternative (older browsers)

If you need to support older browsers that do not recognize wasm-unsafe-eval, use:

Content-Security-Policy: script-src 'self' 'unsafe-eval';

Warning: unsafe-eval also permits JavaScript eval(), which is less secure. Use wasm-unsafe-eval when possible.

Common CSP Errors

If WASM fails to load, you will see errors like:

Refused to compile or instantiate WebAssembly module because 'unsafe-eval'
is not an allowed source of script in the following Content Security Policy...

or:

CompileError: WebAssembly.instantiate(): Wasm code generation disallowed by embedder

Both indicate that your CSP needs the wasm-unsafe-eval (or unsafe-eval) directive.


PDF.js Worker

The VerifyKit viewer uses PDF.js to render PDF pages, offloading page rendering to a Web Worker.

workerUrl is required

The pdf.js library is bundled into @trexolab/verifykit-react, but the worker is not — it must be fetched at runtime from a URL you supply. workerUrl is therefore a required config option on both the React and Vanilla packages; the provider throws if it is missing. There is no automatic default.

The worker must match the bundled pdf.js version (5.5.207) exactly:

tsx
<VerifyKitProvider config={{
  workerUrl: 'https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs',
}}>

Self-hosting the worker

If your CSP blocks external scripts, or the deployment cannot reach a CDN at runtime, put the worker in your own static assets. pdf.js is bundled inside the SDK, so there is no node_modules/pdfjs-dist to copy from — download the pinned version (pdfjs-dist@5.5.207, legacy build) as a build step:

bash
curl -o static/pdf.worker.min.mjs \
  https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs
bash
# or from the npm registry instead of a CDN
npm pack pdfjs-dist@5.5.207 --pack-destination /tmp \
  && tar -xzOf /tmp/pdfjs-dist-5.5.207.tgz package/legacy/build/pdf.worker.min.mjs \
     > static/pdf.worker.min.mjs
tsx
import { VerifyKitProvider } from '@trexolab/verifykit-react'
 
<VerifyKitProvider config={{ workerUrl: '/static/pdf.worker.min.mjs' }}>
  {/* ... */}
</VerifyKitProvider>

The version has to match the bundled library exactly, so re-run the download whenever you upgrade VerifyKit — a mismatch is a hard failure, and the console names both versions when it happens. See Installation → PDF.js Worker Setup for the full explanation.


CMap and Standard Font Files

PDF documents using CJK (Chinese, Japanese, Korean) fonts or certain legacy encodings require CMap files for correct text rendering. PDFs may also reference the 14 standard PDF fonts.

Default Behavior

The viewer loads CMap and standard font files from the Mozilla CDN by default. This works for most deployments without configuration.

Self-Hosting

If your application cannot reach external CDNs (e.g., air-gapped environments, strict CSP):

  1. Copy the CMap files from node_modules/@trexolab/verifykit-react/cmaps/ to a public directory (e.g., /static/cmaps/).
  2. Copy the standard fonts from node_modules/@trexolab/verifykit-react/standard_fonts/ to a public directory (e.g., /static/standard_fonts/).
  3. Download the worker into the same tree, as in Self-hosting the worker above. It ships in neither the SDK package nor your node_modules, so this step is not optional for an air-gapped deployment — and the file must be pdfjs-dist@5.5.207, legacy build.
  4. Configure the viewer — every one of these paths is now your own origin:
tsx
<VerifyKitProvider config={{
  workerUrl: '/static/pdf.worker.min.mjs',
  cMapUrl: '/static/cmaps/',
  standardFontDataUrl: '/static/standard_fonts/',
}}>
  {/* ... */}
</VerifyKitProvider>

Bundle Size

The verification engine is about 1.1 MB and is the largest thing @trexolab/verifykit-core ships — but it is a .wasm file rather than part of the JavaScript, so it is not in your JS bundle at all. Your bundler emits it as a hashed asset, the browser compiles it while it downloads, and it is cached apart from the JS, which changes far more often. What follows is about the JavaScript.

Tree Shaking

All VerifyKit packages support tree shaking. If you only use verifyPdf(), unused exports like extractPdfMetadata() and utility functions will be eliminated by your bundler.

Code Splitting

For client-side applications, dynamically import VerifyKit so the WASM binary is not included in the initial page load:

tsx
// Load VerifyKit only when needed
const { verifyPdf } = await import('@trexolab/verifykit-core')
const result = await verifyPdf(buffer)

The React viewer already code-splits pdf.js for you: it is loaded lazily, inside the effects that use it, so it lands in its own chunk rather than your initial bundle — and never runs on the server.


Next.js

Since 0.6.0, nothing special is required. Import the viewer directly, in a Server Component or a Client Component:

tsx
// app/page.tsx — a Server Component. No 'use client', no next/dynamic.
import { VerifyKitProvider, Viewer } from '@trexolab/verifykit-react'
import '@trexolab/verifykit-react/styles.css'
 
export default function Page() {
  return (
    <VerifyKitProvider config={{ workerUrl: '/pdf.worker.min.mjs' }}>
      <Viewer />
    </VerifyKitProvider>
  )
}

The package ships a 'use client' directive, so it is its own client boundary — you do not need a wrapper file. It server-renders and hydrates cleanly under both Turbopack and webpack, in dev and in production builds.

Add 'use client' to your file only when that file itself uses hooks — e.g. when you call useVerification() to drive loading and read results:

tsx
'use client'
import { VerifyKitProvider, Viewer, useVerification, defaultLayoutPlugin } from '@trexolab/verifykit-react'

Before 0.6.0 the viewer had to be wrapped in dynamic(() => import('./Viewer'), { ssr: false }), because importing it evaluated pdf.js at module scope and threw DOMMatrix is not defined under Node. That is fixed — but ssr: false still works if you have it in place, so upgrading does not force a change.

Serving the WASM from somewhere else

The engine is loaded from wherever your bundler emitted it, which is normally what you want. setWasmUrl() overrides that — for serving it from a CDN, from an origin your CSP already allows, or from a path fixed by an air-gapped deployment:

ts
import { setWasmUrl } from '@trexolab/verifykit-core'
 
// Call before createVerifier() / verifyPdf() — the engine reads this once.
setWasmUrl('https://cdn.example.com/verifykit/verifykit_core_wasm_bg.wasm')

The file to copy is verifykit_core_wasm_bg.wasm, from pkg/ inside @trexolab/verifykit-core. Serve it as application/wasm: browsers refuse to stream-compile any other content type and fall back to a slower path, with a console warning naming the type they got.

createVerifier({ wasmUrl }) does the same thing for a single verifier, and the vanilla build takes wasmUrl in VerifyKit.create().


CORS for Revocation Proxy

If you use @trexolab/verifykit-plugin-revocation, the plugin makes POST requests from the browser to your revocation proxy endpoint. The proxy must return appropriate CORS headers.

Required CORS Headers

Access-Control-Allow-Origin: <your-app-origin>
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: Content-Type

Next.js API Route

The handleRevocation() handler from @trexolab/verifykit-plugin-revocation/handler works automatically when mounted on the same origin as your frontend (the recommended setup). If you need cross-origin access, add CORS headers manually in your server framework:

ts
// app/api/revocation/route.ts
import { handleRevocation } from '@trexolab/verifykit-plugin-revocation/handler'
 
const handler = handleRevocation()
 
export async function POST(req: Request) {
  const res = await handler(req)
  res.headers.set('Access-Control-Allow-Origin', 'https://myapp.example.com')
  return res
}

SSRF Protection

The revocation proxy fetches external URLs (CRL distribution points and OCSP responder URLs) embedded in certificates. To prevent Server-Side Request Forgery (SSRF) attacks, the handler includes a urlFilter option:

ts
export const POST = handleRevocation({
  urlFilter: (url) => {
    const parsed = new URL(url)
    // Only allow HTTP/HTTPS to public internet
    if (!['http:', 'https:'].includes(parsed.protocol)) return false
    // Block internal networks
    if (parsed.hostname === 'localhost') return false
    if (parsed.hostname.startsWith('127.')) return false
    if (parsed.hostname.startsWith('10.')) return false
    if (parsed.hostname.startsWith('192.168.')) return false
    return true
  },
})

The default urlFilter allows URLs whose hostname starts with crl. or ocsp., contains .crl. or .ocsp., or whose path ends with .crl. Only HTTP and HTTPS protocols are permitted. Override it if your CA endpoints do not match these patterns.


CDN Deployment (Vanilla JS)

The @trexolab/verifykit-vanilla package produces a UMD bundle that can be loaded directly from a CDN or self-hosted:

CDN Usage

html
<script src="https://verifykit.trexolab.com/cdn/verifykit.umd.js"></script>
<link rel="stylesheet" href="https://verifykit.trexolab.com/cdn/verifykit.css" />
 
<div id="viewer" style="height: 100vh"></div>
 
<script>
  VerifyKit.create(document.getElementById('viewer'), {
    workerUrl: 'https://unpkg.com/pdfjs-dist@5.5.207/legacy/build/pdf.worker.min.mjs',
  })
</script>

Self-Hosting

To self-host the UMD bundle:

  1. Copy the built files from packages/vanilla/dist/:

    • verifykit.umd.js — the main UMD bundle
    • verifykit.css — the viewer styles
  2. Serve them from your static file server or CDN.

  3. Reference them in your HTML:

html
<script src="/static/verifykit.umd.js"></script>
<link rel="stylesheet" href="/static/verifykit.css" />

Subresource Integrity (SRI)

When loading from a CDN, use SRI hashes to ensure the file has not been tampered with:

html
<script
  src="https://cdn.example.com/verifykit.umd.js"
  integrity="sha384-<hash>"
  crossorigin="anonymous"
></script>

Generate the hash with:

bash
cat verifykit.umd.js | openssl dgst -sha384 -binary | openssl base64 -A

AIA Network Implications

AIA (Authority Information Access) certificate chain resolution is enabled by default (enableAIA: true). When enabled, the core engine may make HTTP requests to CA-hosted URLs to fetch missing intermediate certificates. In production:

  • Ensure outbound HTTP access is available if AIA is enabled.
  • For air-gapped or restricted network environments, disable AIA: enableAIA: false.
  • AIA requests are cached in memory for the lifetime of the verifier instance.

Environment-Specific Notes

Next.js

  • No ssr: false and no next/dynamic needed as of v0.6.0 — see Next.js above. Import the viewer directly, including from a Server Component. Verified under Turbopack and webpack, dev and production builds.
  • No config.resolve.alias.canvas = false workaround is needed: VerifyKit renders via PDF.js's browser canvas and never imports the Node canvas package.
  • The revocation proxy API route works with both Pages Router (pages/api/revocation.ts) and App Router (app/api/revocation/route.ts).
  • If using the Edge runtime, note that WASM support varies. The default Node.js runtime is recommended.

Vite

  • No special configuration is needed. Vite resolves the .wasm from the reference the engine makes and emits it as an asset, without vite-plugin-wasm.
  • For production builds, Vite will tree-shake unused exports automatically.

Webpack

  • No special configuration is needed as of v0.3.1+. Remove any asyncWebAssembly experiment configuration from older setups.

Node.js (Server-Side)

  • Requires Node.js >= 20.19.0.
  • WASM loads from the filesystem automatically. No HTTP server is needed for the WASM binary.
  • For large PDFs, increase memory with --max-old-space-size=4096.