VERIPDFSDK

VeriPDF Architecture

Technical architecture of the VeriPDF SDK, covering the Rust/WASM core engine, package structure, verification pipeline, plugin system, and build pipeline.


Monorepo Structure

veripdf/
├── packages/
│   ├── core/                        # @trexolab/verifykit-core — Rust/WASM verification engine
│   │   ├── Cargo.toml               # Rust dependencies (rsa, ecdsa, x509-cert, etc.)
│   │   ├── build.sh                 # wasm-pack build script
│   │   ├── src/                     # Rust source
│   │   │   ├── lib.rs               # WASM entry point, #[wasm_bindgen] exports
│   │   │   ├── types.rs             # Rust type definitions (serde-serializable)
│   │   │   ├── error.rs             # Error types
│   │   │   ├── parse/
│   │   │   │   ├── pdf_parser.rs    # Raw PDF signature extraction from bytes
│   │   │   │   ├── pdf_metadata.rs  # Document metadata + permissions
│   │   │   │   └── pdf_utils.rs     # PDF string decoding, date parsing, Flate decompress
│   │   │   ├── verify/
│   │   │   │   ├── signature_verifier.rs  # Main verification orchestrator
│   │   │   │   ├── cms_verification.rs    # CMS/PKCS#7 signature validation
│   │   │   │   └── revocation_checker.rs  # CRL/OCSP URL extraction
│   │   │   ├── certs/
│   │   │   │   ├── trust_store.rs         # Trusted cert store (119 embedded DER certs)
│   │   │   │   ├── cert_processing.rs     # Certificate info extraction, OID names
│   │   │   │   └── trusted_certs.rs       # Loads embedded DER certificates at init
│   │   │   ├── crypto/
│   │   │   │   ├── hash.rs                # SHA-1, SHA-256, SHA-384, SHA-512
│   │   │   │   ├── signature.rs           # RSA, ECDSA (P-256/P-384/P-521), Ed25519
│   │   │   │   └── asn1_helpers.rs        # ASN.1/DER parsing utilities
│   │   │   └── helpers/
│   │   │       └── format_utils.rs        # buf2hex, sha256hex
│   │   ├── certs/
│   │   │   └── der/                 # 119 embedded AATL root CA certificates (DER format)
│   │   ├── js/                      # TypeScript wrapper over WASM
│   │   │   ├── index.ts             # Public API exports (createVerifier, verifyPdf, etc.)
│   │   │   ├── types.ts             # TypeScript interface definitions
│   │   │   └── wasm-loader.ts       # Platform-aware WASM initialization
│   │   ├── pkg/                     # wasm-pack output (generated)
│   │   ├── tsdown.config.ts         # Build config (ESM + CJS + DTS)
│   │   └── package.json
│   │
│   ├── react/                       # @trexolab/verifykit-react — React UI + plugin system
│   │   ├── src/
│   │   │   ├── index.ts             # All exports
│   │   │   ├── context.tsx          # VeriPdfProvider, useVeriPdfConfig
│   │   │   ├── store.ts             # ViewerStore — pub-sub reactive store
│   │   │   ├── plugin-types.ts      # ViewerPlugin, PluginContext interfaces
│   │   │   ├── plugin-host.tsx      # Plugin lifecycle, slot collection, ShortcutManager
│   │   │   ├── polyfills.ts         # PDF.js 5.x Map polyfills (main thread)
│   │   │   ├── worker-utils.ts      # Worker URL resolution + polyfill injection
│   │   │   ├── appearance-swapper.ts # Signature widget icon replacement
│   │   │   ├── hooks/
│   │   │   │   ├── useVerification.ts  # Main verification state hook
│   │   │   │   ├── useContainerSize.ts # Responsive breakpoint hook
│   │   │   │   ├── useViewerStore.ts   # Store selector hooks
│   │   │   │   └── usePinchZoom.ts     # Touch pinch-zoom gesture
│   │   │   ├── core-viewer/
│   │   │   │   ├── Viewer.tsx          # User-facing wrapper
│   │   │   │   ├── CoreViewer.tsx      # Core viewer (~300 lines)
│   │   │   │   └── PageRenderer.tsx    # Page rendering (canvas, text layer, annotations)
│   │   │   ├── plugins/               # 24 individual plugins
│   │   │   │   ├── default-layout/    # Meta-plugin composing all plugins
│   │   │   │   ├── toolbar/           # Toolbar with slot system
│   │   │   │   ├── zoom/             # Zoom controls, Ctrl+wheel
│   │   │   │   ├── page-navigation/  # Page nav, shortcuts
│   │   │   │   ├── rotation/         # Rotation
│   │   │   │   ├── search/           # Text search
│   │   │   │   ├── print/            # Print via iframe
│   │   │   │   ├── download/         # Download via blob URL
│   │   │   │   ├── theme/            # Light/dark toggle
│   │   │   │   ├── fullscreen/       # Fullscreen mode
│   │   │   │   ├── selection/        # Cursor tools
│   │   │   │   ├── scroll-mode/      # Scroll modes
│   │   │   │   ├── spread-mode/      # Page spreads
│   │   │   │   ├── signature/        # Signature panel
│   │   │   │   ├── sidebar/          # Left sidebar
│   │   │   │   ├── thumbnail/        # Thumbnail tab
│   │   │   │   ├── bookmark/         # Bookmark tab
│   │   │   │   ├── attachment/       # Attachment tab
│   │   │   │   ├── highlight/        # Text highlighting
│   │   │   │   ├── open-file/        # File open + drag-drop
│   │   │   │   ├── properties/       # Document properties modal
│   │   │   │   ├── shortcut-help/    # Keyboard shortcuts help
│   │   │   │   ├── context-menu/     # Right-click menu
│   │   │   │   └── more-menu/        # Overflow menu
│   │   │   ├── viewer/               # Legacy viewer components
│   │   │   ├── signature/            # Signature UI components
│   │   │   ├── certificate/          # Certificate chain viewer
│   │   │   ├── ui/                   # Shared UI primitives (Radix-based)
│   │   │   ├── styles/               # CSS theme variables
│   │   │   ├── helpers/              # Status config, check builders, formatters
│   │   │   ├── i18n/                 # Translation system
│   │   │   └── download-utils.ts     # PEM/DER export, blob download
│   │   ├── vite.config.ts
│   │   └── package.json
│   │
│   ├── vanilla/                      # @trexolab/verifykit-vanilla — UMD/ESM drop-in
│   │   ├── src/
│   │   │   └── index.tsx             # VeriPdf.create() imperative API + event bus
│   │   ├── vite.config.ts
│   │   └── package.json
│   │
│   └── plugin-revocation/            # @trexolab/verifykit-plugin-revocation — Online CRL/OCSP
│       ├── src/
│       │   └── index.ts              # revocationPlugin() factory
│       ├── tsdown.config.ts
│       └── package.json
│
├── docs/                             # Documentation
│   ├── README.md                     # Documentation hub
│   ├── getting-started.md            # Setup guides
│   ├── api-reference.md              # Complete API reference
│   ├── examples.md                   # Practical examples
│   └── architecture.md               # This file
│
├── package.json                      # Workspace root
└── tsconfig.json                     # TypeScript project references

Package Dependency Graph

@trexolab/verifykit-plugin-revocation ──> @trexolab/verifykit-core
@trexolab/verifykit-react ──────────────> @trexolab/verifykit-core
@trexolab/verifykit-vanilla ────────────> @trexolab/verifykit-react ──> @trexolab/verifykit-core

External Dependencies

@trexolab/verifykit-core
├── (Rust/WASM — no npm runtime dependencies)
└── Rust crates (compiled into WASM):
    ├── rsa, ecdsa, p256, p384, p521, ed25519-dalek   (cryptographic verification)
    ├── sha1, sha2, digest                              (hash algorithms)
    ├── x509-cert, x509-parser, cms, der, const-oid     (ASN.1 / X.509 / CMS parsing)
    ├── flate2                                           (PDF stream decompression)
    ├── wasm-bindgen, serde, serde_json                  (WASM interop)
    └── time, thiserror, regex-lite, pem, hex            (utilities)

@trexolab/verifykit-react
├── @trexolab/verifykit-core
├── pdfjs-dist 5.5.207       (PDF rendering — pinned, see below)
├── pdf-lib                   (appearance swapping for signature widgets)
├── @radix-ui/*               (accessible dialog, tabs, accordion, separator)
├── lucide-react              (icons)
└── react 19+, react-dom 19+ (peer dependencies)

@trexolab/verifykit-vanilla
├── @trexolab/verifykit-react
├── react 19.2.0              (bundled — not a peer dep)
└── react-dom 19.2.0          (bundled)

@trexolab/verifykit-plugin-revocation
└── @trexolab/verifykit-core

Rust/WASM Core Architecture

The @trexolab/verifykit-core package is a Rust library compiled to WebAssembly via wasm-pack. All cryptographic operations -- signature validation, hash computation, certificate chain building, ASN.1 parsing -- run entirely in WASM. The JavaScript layer is a thin wrapper that:

  1. Initializes the WASM module (wasm-loader.ts)
  2. Converts inputs (ArrayBuffer, File, URL string) to Uint8Array for WASM
  3. Rehydrates ISO 8601 date strings from WASM output into JavaScript Date objects
  4. Runs plugin-based revocation checking as a post-processing step

Why Rust/WASM

AspectPrevious JS EngineRust/WASM Engine
CryptoWeb Crypto API + pkijs + asn1js + @peculiar/x509Built-in Rust crates (rsa, ecdsa, sha2, x509-cert)
Dependencies~8 npm runtime deps0 npm runtime deps
PerformanceBaseline3.7x faster
Trust storePEM strings parsed at runtime119 DER certificates compiled into WASM binary
Node.jsRequired @peculiar/webcrypto polyfillWorks natively (WASM runs in Node.js)
Bundle~180 KB (minified JS + deps)~650 KB (.wasm) + ~5 KB (JS wrapper)

WASM Loading Pipeline

1. createVerifier() or verifyPdf() called
2. initWasm() imports ../pkg/veripdf_core_wasm (dynamic import)
3. wasm-pack glue code loads .wasm via import.meta.url
4. WASM module initializes, trust store loads 119 DER certs
5. Module cached in memory — subsequent calls are instant

The WASM module is a singleton. Multiple createVerifier() calls share the same underlying WASM instance. Each verifier can have different trust store configurations, which are applied via wasm.set_trust_store_config().

Supported Algorithms

CategoryAlgorithms
SignatureRSA (PKCS#1 v1.5, PSS), ECDSA (P-256, P-384, P-521), Ed25519
HashSHA-1 (legacy), SHA-256, SHA-384, SHA-512, MD5 (detection only)
CertificateX.509 v3, CMS/PKCS#7 SignedData

Config Hierarchy (Vanilla -> React -> Core)

Options set in @trexolab/verifykit-vanilla flow through to the React provider and core engine:

VeriPdfOptions (vanilla)           VeriPdfConfig (react)           VeriPdfCoreConfig (core)
─────────────────────              ──────────────────              ─────────────────────

.trustStore                    ->  .trustStore                 ->  .trustStore
.plugins                       ->  .plugins                    ->  .plugins
.theme                         ->  .theme
.workerSrc                     ->  .worker.src
.locale                        ->  .locale
.translations                  ->  .translations
.features                      ->  defaultLayoutPlugin({ disable: ... })
                                   (inverted: features.X = false
                                    -> disable.X = true)

The vanilla wrapper creates a defaultLayoutPlugin() internally and maps features flags to disable flags (inverted logic). Core verification config (trustStore, plugins) passes through unchanged.


Verification Pipeline

Each PDF signature is validated with 8 independent checks:

CheckWhat It Verifies
integrityCheckDocument bytes match the signed range (tampering detection)
signatureCheckCryptographic signature is mathematically valid
certificateChainCheckCertificate chains to a trusted root CA
expiryCheckCertificates were valid at signing time
timestampCheckRFC 3161 timestamp is present and valid
revocationCheckCertificate not revoked (CRL/OCSP)
algorithmCheckAlgorithms meet minimum strength (no SHA-1, RSA >= 2048)
ekuCheckExtended Key Usage permits document signing

Verification Flow

1. PDF Parsing (pdf_parser.rs)
   ├── Scan PDF bytes for signature dictionaries
   ├── Extract ByteRange (bytes covered by signature)
   ├── Extract Contents (DER-encoded CMS blob)
   └── Detect visibility, field names, permissions, deleted signatures

2. CMS Verification (cms_verification.rs)
   ├── Parse CMS/PKCS#7 SignedData structure
   ├── Validate cryptographic signature (RSA/ECDSA/Ed25519)
   ├── Check certificate chain against trust store (119 AATL root CAs)
   ├── Validate certificate expiry, key usage, EKU
   └── Check algorithm strength

3. Timestamp Verification
   ├── Extract RFC 3161 timestamp token
   ├── Verify timestamp signature
   └── Extract TSA certificate info

4. Revocation Checking (revocation_checker.rs)
   ├── Extract embedded CRL/OCSP data from signature
   └── Optional: online checking via @trexolab/verifykit-plugin-revocation (JS post-processing)

5. Document Integrity
   ├── Check if ByteRange covers whole document
   ├── Detect DSS (Document Security Store) for LTV data
   └── Identify deleted/tampered signatures

6. Results Assembly
   ├── WASM returns JSON with ISO 8601 date strings
   ├── JS wrapper rehydrates dates to Date objects
   ├── Plugin-based revocation updates applied (if plugins configured)
   └── Final VerificationResult returned

Status Aggregation

The 8 individual checks aggregate into overallStatus:

StatusMeaning
validAll checks pass
warningMinor issues (no timestamp, algorithm concerns)
invalidSignature broken, certificate untrusted, or document modified
unknownCould not determine (missing data)

PAdES Conformance

The engine detects PAdES conformance levels:

LevelDescription
B-BBasic signature (CMS signed data)
B-TWith trusted timestamp
B-LTWith long-term validation data (CRL/OCSP embedded)
B-LTAWith archive timestamp for long-term archival

Plugin System

VeriPDF has two separate plugin systems:

Core Plugins (Verification)

Extend the verification engine. Implemented as VeriPdfPlugin:

interface VeriPdfPlugin {
  name: string
  setup?: (ctx: PluginContext) => void | Promise<void>
  trustStore?: TrustStoreConfig
  revocation?: {
    checkCRL?: (cert, urls) => Promise<RevocationCheckResult>
    checkOCSP?: (cert, issuer, urls) => Promise<RevocationCheckResult>
  }
  inputResolver?: (input: PdfInput) => Promise<ArrayBuffer | null>
}

Core plugins can:

  • Add trusted root certificates (merge or replace the built-in AATL store)
  • Provide online revocation checking (CRL/OCSP via @trexolab/verifykit-plugin-revocation)
  • Resolve custom input types (fetch from cloud storage, decrypt buffers)

The revocation plugin runs as a JavaScript post-processing step after WASM verification completes. For each signature whose revocationCheck.status is "unknown", the plugin performs live OCSP/CRL lookups and updates the result.

Viewer Plugins (React UI)

Extend the React viewer. Implemented as ViewerPlugin:

interface ViewerPlugin {
  name: string
  dependencies?: string[]
  composedPlugins?: ViewerPlugin[]
  install?(ctx: PluginContext): void
  onDocumentLoad?(e: DocumentLoadEvent): void
  onDocumentUnload?(): void
  onPageChange?(e: PageChangeEvent): void
  onZoomChange?(e: ZoomChangeEvent): void
  onRotationChange?(e: RotationChangeEvent): void
  destroy?(): void
  renderToolbarSlot?: Partial<ToolbarSlots>
  sidebarTabs?: SidebarTabDefinition[]
  renderOverlay?: (props) => React.ReactNode
  renderRightPanel?: (props) => React.ReactNode
  renderPageOverlay?: (props) => React.ReactNode
}

Plugin Lifecycle (Viewer)

1. Plugin factory called:     const zoom = zoomPlugin()
2. Passed to Viewer:          <Viewer plugins={[zoom]} />
3. CoreViewer creates store:  const store = createViewerStore()
4. PluginHost resolves:       resolvePlugins(plugins) -> flat, deduped list
5. install() called:          zoom.install({ store, registerShortcut, ... })
6. Document loads:            zoom.onDocumentLoad({ document, numPages })
7. User interacts:            zoom.api.zoomIn() -> store.update({ scale: 1.35 })
8. Component unmounts:        zoom.destroy() -> cleanup

PluginHost

Manages the full viewer plugin lifecycle:

  1. Resolve -- Flatten composed plugins (meta-plugins like defaultLayoutPlugin), deduplicate by name
  2. Install -- Call install(ctx) on each plugin in order
  3. Collect UI -- Gather toolbar slots, sidebar tabs, overlays, and page overlays from all plugins
  4. Dispatch events -- Route lifecycle events to all plugins
  5. Destroy -- Clean up all plugins on unmount

defaultLayoutPlugin

A meta-plugin that composes all 22+ viewer plugins into a batteries-included experience:

const layout = defaultLayoutPlugin({
  disable: { print: true, download: true },
  toolbar: { transform: (slots) => { delete slots.Print; return slots } },
})

Disabled plugins are never instantiated, so they do not contribute to bundle size with proper tree-shaking.

The result exposes APIs for all sub-plugins:

layout.zoom.zoomIn()
layout.navigation.goToPage(5)
layout.sidebar.toggle('thumbnails')
layout.search.open()
layout.theme.toggleTheme()
layout.signature.togglePanel()

ViewerStore

A lightweight pub-sub reactive store that replaces prop drilling across the component tree. Plugins read and write state through this store, and React components subscribe via useViewerStoreKey() for selective re-rendering.

State Shape

CategoryKeys
Documentdocument, fileBuffer, fileName, loadState, errorMessage
NavigationcurrentPage, totalPages
Zoomscale, fitMode
Rotationrotation
LayoutscrollMode, spreadMode, cursorTool
ThemethemeMode
FullscreenisFullscreen
Verificationsignatures, unsignedFields, verificationStatus
UI panelssidebarOpen, sidebarTab, sigPanelOpen, findOpen
PasswordpasswordNeeded, passwordError

Key Features

  • Key-level subscriptions -- Subscribe to individual keys. A zoom change does not re-render page navigation components.
  • Batch updates -- Multiple update() calls inside batch() fire listeners only once.
  • Shallow merge -- Only changed keys notify subscribers.
  • Zero dependencies -- No Zustand, Redux, or external state library.

Toolbar Slot System

Plugins contribute to named toolbar slots. The toolbarPlugin collects all contributions and renders them in a 3-section grid layout:

| LEFT                    | CENTER                                    | RIGHT                  |
| SearchPopover           | GoToPreviousPage  CurrentPageInput        | Download Print         |
|                         | NumberOfPages GoToNextPage                | ThemeToggle Fullscreen |
|                         | ZoomOut Zoom ZoomIn Rotate                | MoreMenu               |

The transform API lets consumers remove, reorder, or add custom slots:

toolbarPlugin({
  transform: (slots) => {
    delete slots.Print
    delete slots.Download
    slots.MyButton = ({ store }) => <button>Custom</button>
    return slots
  },
})

Build Pipeline

1. @trexolab/verifykit-core               (wasm-pack -> .wasm + glue JS, then tsdown -> ESM + CJS + DTS)
2. @trexolab/verifykit-plugin-revocation   (tsdown -> ESM + CJS + DTS)
3. @trexolab/verifykit-react               (Vite lib mode -> ESM + CJS + CSS + DTS)
4. @trexolab/verifykit-vanilla             (Vite lib mode -> UMD + ESM + CSS)

Build all packages: npm run build

Core Build Steps

1. build.sh runs wasm-pack build --target web --release
2. wasm-pack outputs to pkg/:
   ├── veripdf_core_wasm_bg.wasm    (~650 KB, optimized with LTO)
   ├── veripdf_core_wasm.js         (glue code)
   └── veripdf_core_wasm.d.ts       (WASM type declarations)
3. tsdown compiles js/ (TypeScript wrapper) to dist/:
   ├── index.mjs                    (ESM entry)
   ├── index.cjs                    (CJS entry)
   └── index.d.mts                  (TypeScript declarations)

The WASM binary is compiled with:

  • opt-level = 3 (maximum optimization)
  • lto = true (link-time optimization)
  • codegen-units = 1 (single codegen unit for smaller output)
  • strip = true (strip debug symbols)

Browser and Node.js Support

Browser Support

BrowserMinimum VersionNotes
Chrome109+Full support
Firefox115+Full support
Safari16.4+Full support
Edge109+Full support (Chromium-based)

Required browser capabilities:

  • WebAssembly (instantiation and streaming)
  • Web Workers (ES module workers)
  • ES2022+ features (private class fields, structuredClone)
  • CSS custom properties

No IE11, legacy Edge (EdgeHTML), or polyfill path is provided.

Node.js Support

RuntimeMinimum VersionNotes
Node.js20.19.0+WASM runs natively, no polyfills needed

The WASM engine works identically in Node.js. Unlike the previous JS engine, there is no need for @peculiar/webcrypto or any crypto polyfill -- all crypto operations are compiled into the WASM binary.


Worker Architecture

PDF.js uses a Web Worker for CPU-intensive parsing and rendering.

Zero-Config Auto-Resolution

1. resolveWorkerUrl()
   ├── Read pdfjsLib.version  ->  "5.5.207"
   ├── Construct CDN URL      ->  "https://unpkg.com/pdfjs-dist@5.5.207/build/pdf.worker.min.mjs"
   └── Fallback               ->  "/pdf.worker.min.mjs"

2. createPolyfillWorkerUrl(resolvedUrl)
   ├── Prepend Map polyfills (getOrInsertComputed, getOrInsert)
   ├── Use dynamic import() to load the real worker as ES module
   ├── Wrap everything in a Blob  ->  blob:http://...
   └── Cache the Blob URL for reuse

3. Set pdfjsLib.GlobalWorkerOptions.workerSrc = blobUrl

Why Polyfill Injection is Needed

pdfjs-dist 5.5+ uses Map.prototype.getOrInsertComputed, a TC39 Stage 2 proposal not yet available in browsers. The worker runs in a separate JS context, so main-thread polyfills do not apply. The Blob URL wrapper injects polyfills into the worker's global scope.

Pinned pdfjs-dist Version

pdfjs-dist is pinned to 5.5.207 (exact, no semver range). Reasons:

  1. Unstable TC39 polyfills. New versions may add additional Stage 2/3 API calls not covered by the polyfill set.
  2. Worker/library version coupling. The worker and main library must match exactly.
  3. CMap and font asset compatibility. Version bumps can change these assets.

Before upgrading, verify that polyfills in polyfills.ts and worker-utils.ts still cover all non-standard APIs.


Theme System

CSS custom properties on :root with a data-theme attribute:

:root {
  --bg: #ffffff;
  --fg: #1a1a1a;
  --primary: #0072c6;
  /* 100+ variables */
}
 
:root[data-theme="dark"] {
  --bg: #1d1d1d;
  --fg: #e0e0e0;
  --primary: #4da6ff;
}

The VeriPdfProvider sets data-theme on <html>. The themePlugin toggles it at runtime. All library styles are scoped to .veripdf-root to avoid conflicts with host application styles.


Security Considerations

Worker Isolation

The PDF.js Web Worker runs in a separate JavaScript context. Polyfills and security patches applied to the main thread have no effect inside the worker. VeriPDF injects polyfills directly into the worker via a Blob URL wrapper.

Blob URL and CSP

Applications with a Content Security Policy must allow:

Content-Security-Policy: worker-src 'self' blob:; script-src 'self' blob:;

If the worker is fetched from a CDN, the CDN origin must be permitted in connect-src:

Content-Security-Policy: connect-src 'self' https://unpkg.com;

Self-host the worker to avoid CDN allowlisting.

WASM and CSP

WebAssembly compilation requires 'wasm-eval' or 'unsafe-eval' in script-src on some browsers. Alternatively, use 'wasm-unsafe-eval':

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

Trust Store in Memory

The 119 embedded AATL root CA certificates and any additional certificates provided via trustStore config are held in the WASM linear memory for the lifetime of the module. They are not written to disk or persisted to browser storage.

No External Data Transmission

VeriPDF does not send document data, verification results, or telemetry to external servers. The only network requests are:

  1. Worker CDN fetch -- A single import() to download the PDF.js worker (avoidable by self-hosting).
  2. Revocation checking (opt-in) -- When @trexolab/verifykit-plugin-revocation is installed, it makes direct connections to CA infrastructure (CRL distribution points, OCSP responders).

All verification runs entirely client-side in the WASM engine.


Known Limitations

Single Theme per Page

The theme system sets data-theme on <html>, making it global. Multiple VeriPdfProvider instances with different theme modes will conflict. Use a single provider or the same theme mode for all viewers.

Worker URL is a Global Singleton

The generated Blob URL is cached globally and set on pdfjsLib.GlobalWorkerOptions.workerSrc. All PDF.js instances share the same worker URL. Revoking it breaks worker creation for all viewers.

Appearance Swapping Bundle Cost

The appearance swapper imports pdf-lib (~300 KB minified) unconditionally. Tree-shaking does not eliminate it because the import is unconditional in the React package.

No Incremental Verification

Verification processes all signatures in a single pass. There is no API to verify a single signature in isolation.

CMap and Font File Hosting

PDF.js requires CMap and standard font files for CJK text and certain form fields. These files are not bundled by VeriPDF -- consumers must copy them from pdfjs-dist and serve them.