Defense in depth — every layer, every request

Security

Typst is Turing-complete — that’s what makes it powerful, and what makes isolation non-negotiable. Here’s exactly how the render path, your secrets, and your data are protected.

Render isolation

  • Custom Typst World — package_path disabled, no @preview/* imports.
  • No filesystem access from inside Typst; source and fonts are injected in-memory.
  • 5-second wall-clock timeout per render, enforced around the full compile in a killable worker process.
  • Per-thread seccomp BPF filter, allowlist posture — only the syscalls Tokio + rayon + Rust std + Typst legitimately use (memory, futex, threading, time, entropy, signals, stderr) are permitted. Everything else (exec, fork, the socket family, mount/namespace surface, bpf, ptrace, io_uring, pidfd, kexec, …) returns EPERM. Future kernel additions stay denied by default.
  • Per-thread landlock filesystem ACL — empty rule set, the render thread sees no FS.
  • Compute runs in a Cloudflare Container with cgroups v2 caps on CPU + RAM + PIDs (instance_type basic = 1 GB / 1/4 vCPU).

Authentication

  • API keys: tk_live_… with 256-bit entropy from Web Crypto.
  • Only a BLAKE3 digest is stored; brute-force resistance comes from the 256-bit random secret, not from hash slowness.
  • Constant-time comparison on every request; no fast-path for known prefixes.
  • Display masked everywhere — first 12 + last 4 chars, plaintext shown once.
  • Aging-key banner in the dashboard at 90 days to prompt rotation.

Network & headers

  • HSTS preload, 2-year max-age, includeSubDomains.
  • CSP nonce-based on the dashboard via a Pages Function middleware — per-request 128-bit nonce, no unsafe-inline / unsafe-eval, strict-dynamic propagation; frame-ancestors ‘none’.
  • CORS allow-list on /v1/* — only the first-party dashboard origin (app.paperjet.dev) is reflected. Never Access-Control-Allow-Origin: *.
  • TLS 1.2+ with TLS 1.3 preferred; HTTP automatically upgraded. Cloudflare-managed certs.

Rate limiting & abuse

  • Per-user sliding window in a Durable Object — not per-IP (trivially bypassed).
  • Per-minute limits are surfaced via X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset; monthly quotas use X-RateLimit-Quota-* headers.
  • 429 responses carry Retry-After. Per-IP burst gate on /v1/* auth failures (30/min).
  • Stripe billing webhook verification exists in the code path and must be enabled only after production Stripe secrets and price IDs are configured; outbound render webhooks are signed with X-Paperjet-Signature.

Data & privacy

  • Render output is normally streamed as the response; when Idempotency-Key is used, successful responses can be cached for up to 24 h for safe retry replay.
  • D1 stores metadata and R2 stores templates; deployment/data-location commitments must be verified in the active Cloudflare account before regulated production use.
  • IP addresses are truncated before persistence to reduce personal-data exposure.
  • Email addresses masked in logs (j***@example.com).
  • API key plaintext, secrets, and PDF bytes are not intentionally logged.

Supply chain

  • Exact-pinned dependencies (no ^ / ~) and committed lockfiles are enforced locally.
  • npm audit is currently part of the manual release check. cargo-deny and cargo-audit must be installed before claiming automated Rust coverage.
  • Dependency updates are reviewed manually; there is no CI/CD pipeline by design.

Audit log

Every security-sensitive event is persisted with a 1-year retention, enforced by a daily D1 sweep at 03:00 UTC. Every plan can view its own audit log in the dashboard.

auth.signin.* / auth.signup.*
magic-link requests, sign-ins, and account creation
apikey.created / apikey.revoked
who minted what, when
template.created / template.deleted
mutations to your render assets
webhook.created / webhook.deleted / webhook.disabled
outbound endpoint changes and auto-disable events
webhook.stripe.*
every Stripe event — signature pass / fail
subscription.*
plan changes, cancellations

Reporting a vulnerability

Email [email protected] with the subject prefix [security]. PaperJet is run by a single maintainer — I read every report myself and reply as soon as I can. Please give me a reasonable disclosure window before publishing.

Compliance