Security & redaction
DRM permission enforcement, watermarking, audit logging, structural redaction. No CSS overlay redaction — the engine refuses to burn unless a structural adapter is registered.
DRMEngine
Permission gate. Doesn't encrypt — that's a function of secure mode + the underlying PDF.
import { DRMEngine } from '@trupdf/viewer';
const drm = new DRMEngine({
permissions: new Set(['view', 'print']),
expiresAt: '2026-12-31T23:59:59Z',
maxOpens: 10,
});
drm.enforce('print'); // throws if not in the set
drm.enforce('copy'); // throws → host catches → snack 'Copy blocked'Permissions: view, print, copy, edit, export, annotate,
redact, sign. DRMPolicy also takes expiresAt (ISO timestamp)
and maxOpens.
Secure mode
secureMode: true on DocViewer does the following:
- No DOM text layer — selection by browser is impossible.
- No
getTextContent()— APIs that would expose text are gated. - DOM-level blocks — Ctrl+C / Cut / Drag / right-click context.
- Selection overlays suppressed.
- AI Explain Selection / Explain Page short-circuit with a typed error; Document Summary falls back to the in-browser OCR route.
- Screen reader access via
@trupdf/a11ystructural reading order (live region) instead of the text layer.
<DocViewer source="…" licenseManager={license} secureMode />Watermark
applyWatermark(input, options) draws diagonal repeating text into
the page content stream, not as a /Annot. The watermark survives
flattening and renders in every compliant viewer.
import { applyWatermark } from '@trupdf/viewer';
const watermarked = await applyWatermark(bytes, {
text: 'CONFIDENTIAL',
fontSize: 60, // default 60
rotation: 30, // degrees, default 30
opacity: 0.18, // default 0.18
color: { r: 0.5, g: 0.5, b: 0.5 },
pages: [1, 3, 5], // 1-indexed; omit for all pages
});For the trial-tier auto-watermark, WatermarkOverlay is instantiated
automatically by LicenseManager and painted onto every render +
burned into every save.
RedactionEngine
Structural removal of text objects from the PDF byte stream, audited
and non-cancellable. The engine refuses to burn if no structural
adapter is registered. bindPdfboxRedactionAdapter binds the
document-service redaction adapter (see
the TruPDF document service below);
custom backends register their own adapter instead.
import { RedactionEngine, AuditLogger, bindPdfboxRedactionAdapter } from '@trupdf/viewer';
// Call once at app boot.
bindPdfboxRedactionAdapter({
endpoint: process.env.NEXT_PUBLIC_TRUPDF_DOCSERVICE_URL!,
});
const audit = new AuditLogger();
const redactor = new RedactionEngine();
const result = await redactor.apply(
srcBytes,
[
{
page: 4,
x: 100,
y: 200,
width: 80,
height: 14,
exemptionCode: 'FOIA b6',
},
],
{ audit },
);
console.log(result.objectsRemoved); // count of text-showing ops removedThe audit entry is appended before the bytes are returned. If the audit append throws, the burn aborts and the original bytes are left untouched.
The TruPDF document service
The reference structural-removal backend — a self-hosted container that powers redaction burns. The burn pipeline runs a two-pass content-stream rewrite:
- Pass 1 — tag every text-showing operator whose glyph bbox intersects a redaction rect.
- Pass 2 — rewrite the page content stream replacing those
operators with
[] TJ.
A solid black rectangle is drawn on top as the visual indicator.
docker compose --profile phase8 up -dCustom redaction adapter
Hosts that prefer their own redaction backend can register a custom
structural adapter via registerStructuralRedactionAdapter:
import { registerStructuralRedactionAdapter } from '@trupdf/viewer';
registerStructuralRedactionAdapter(async () => ({
async removeIntersectingObjects(bytes, marks) {
// structurally remove text-showing operators intersecting marks
return { bytes: rewritten, removed: opsRemoved };
},
}));From the viewer
ViewerEngine.applyRedactions() collects every redaction-mark
annotation, calls the engine, replaces the loaded document with the
burned bytes, and removes the marks from the manager.
const result = await engine.applyRedactions({ actor: currentUser });Audit log
Append-only event log. Events are frozen; subscribers forward to
backend storage via the appended event.
import { AuditLogger } from '@trupdf/viewer';
const audit = new AuditLogger();
audit.on('appended', (entry) =>
fetch('/audit', {
method: 'POST',
body: JSON.stringify(entry),
}),
);Event kinds recorded (the union grows without breaking consumers — the log is append-only):
redaction.applied, redaction.failed, signature.applied,
signature.verified, signature.failed, compare.run,
document.opened, document.printed, document.exported,
ai.query, ai.response, ai.cache_hit, ai.error,
ai.extraction.completed, ai.parsing.completed.
Each entry carries { id, at, kind, payload, actor? }; query past
entries with audit.listByKind(kind).
License gating
RedactionEngine calls gate.require('redaction') in its
constructor; trial tokens cannot burn redactions. UIs can hide the
"Redact" button with gate.has('redaction') ahead of time.