Pulp Engine Document Rendering
Get started
Release v0.83.0

Release v0.83.0 — Post-audit remediation (SSRF, editor safety, CI evidence)

This release works through the adverse findings of the second post-hardening quality audit. It centralises and fixes the SSRF host blocklist (closing an any-address bypass), makes the editor’s property-panel field staging uniformly undo-safe, adds review-time CI signals without inflating the GitHub Actions budget, fixes two turbo cache-input gaps, and converts the renderer coverage debt into a tracked initiative. Security commits are self-contained and lead the branch so they can be cherry-picked to a patch if needed.

Security

  • SSRF host blocklist centralised; any-address 0.0.0.0 / :: now blocked (F1/F2). Three hand-maintained private-host blocklists had drifted — the PDF render network guard, the HTML image-src guard, and the HTML font-import guard — and the image guard had lost the link-local (fe80::/10) and IPv4-mapped (::ffff:) IPv6 patterns. All three also missed the IPv4/IPv6 any-address forms (0.0.0.0, ::, ::0), which route to loopback on many systems and so bypassed the loopback block the guard advertises. The lists are replaced by one canonical source in @pulp-engine/renderer-common (private-hosts.ts) that includes the any-address forms; the three call sites import it, so they can never drift again. packages/pdf-renderer/src/network-guard.ts stays a thin compatibility wrapper (its isBlockedHostname / isBlockedIpAddress / resolveHostnameIps exports are unchanged), so the API’s outbound safeFetch and webhook SSRF guards — which import from it — inherit the fix automatically.
  • Two additional SSRF edge cases closed (F1). A protocol-relative image src such as //169.254.169.254/x.png carries no : and was previously treated as a safe relative path; it is now resolved against an explicit base and run through the same host check. The PDF render guard’s numeric-IP fast path now re-checks the literal IP with isBlockedIpAddress and aborts, instead of trusting the synchronous URL check to have caught every form. isSafeFontUrl already rejected protocol-relative URLs (new URL() with no base throws) — a regression test now guards that.
  • Scope: these are defence-in-depth guards within the product’s trusted-tenant model; remote resource loading is opt-in (BLOCK_REMOTE_RESOURCES), and hardened production already blocks all remote fetches. The fix removes a real hole in a shipped control regardless.

Editor

  • Uniform, undo-safe property-panel field staging (F3). HeadingNodeProps and SectionNodeProps previously hand-rolled useState + a useEffect that reset local state on every external change, so an undo/redo while editing could clobber an in-progress edit. Both now use the shared, dirty-guarded useStagedField / useImmediateField hooks (the same ones TextNodeProps uses), so external changes are suppressed while a field is dirty and reset only on node switch. New unit tests cover commit-on-blur/Enter, same-node sync, dirty-preservation, and node-switch reset. The two remaining react-hooks/exhaustive-deps suppressions flagged by the audit (BlockPalette, DockedPreview) were reviewed and documented as intentional rather than churned.

Operability & CI

  • Lighter PR-time signal + run-full-ci opt-in (F4). Pull requests now run two cheap review-time jobs — test-sqlserver-smoke (apply migrations to a fresh mssql, the highest-signal-per-minute check for DDL regressions, plus the core store smoke) and test-e2e-smoke (one core editor spec). The full test-sqlserver / test-e2e / test-e2e-auth suites, Docker build+smoke, and the two Windows jobs stay gated to push/release to conserve Actions minutes. To run the whole matrix on a specific PR, add the run-full-ci label (the pull_request trigger now listens for labeled), at which point the two smokes skip to avoid double-spending.
  • Turbo cache-input correctness (F6). schema.prisma is now a build input, so a Prisma schema-only change invalidates the API build cache (it still requires db:generate to regenerate the client). The SDK’s out-of-src __tests__/ directory is now in the test input set. And openapi.json is a scoped build input for @pulp-engine/sdk (via a package-level turbo.json), so a spec-only change can no longer FULL-TURBO cache-hit and ship stale generated SDK types — without invalidating every other package’s build.

Evidence & coverage

  • Renderer coverage debt is now tracked (F5). docs/initiatives/coverage-ratchet.md records the current floors (API 73/63, html-renderer 57/53, pdf-renderer 80/72), the API 74/72 north-star, the cheap-first paydown approach (html-renderer node renderers — explicitly not the type-only dispatcher.ts or the IPC-entrypoint worker.ts), and the rule that floors are ratcheted up in a separate follow-up commit from the CI-Linux number. A first paydown adds direct tests for the previously-0%-covered structural renderers (spacer / page-break / container / columns).
  • Vitest dist-exclude parity (P4). apps/website and packages/sdk-typescript gain the explicit exclude: ['**/node_modules/**','**/dist/**'] the other 22 configs already carry, without narrowing test discovery (the SDK’s __tests__/smoke.test.ts still runs).

Documentation

  • Support policy (P1): “Last reviewed” date refreshed and cross-referenced to the v0.82.0 security release; the support terms are unchanged by it.
  • Evaluator guide (P2): AI template generation is flagged as requiring ANTHROPIC_API_KEY, with POST /templates/generate returning 503 when unset — “unconfigured”, not “broken”.
  • Runbook (P3): a new “Upgrading to v0.82.0 — OIDC id_token Bearer deprecation” section documents what breaks, the 401 operators will see, how to spot raw-id_token callers in the logs, and the /auth/oidc/exchange migration (body field oidcToken).
  • Deployment guide (P5): the multi-tenant section now prominently notes that isolation is row/path-level, not compute-level (a shared Chromium pool in the default child-process render mode), and directs untrusted/semi-trusted tenants to RENDER_MODE=container or socket.

Validation

CI-verified — the release commit is gated by release.yml’s check-ci job, which refuses to build or publish any artifact unless a successful push-event ci.yml run exists for the exact tagged commit on main. That run is the full matrix: ci (lint, typecheck, build, unit + coverage gate), test-file-mode, test-sqlserver, test-e2e, test-e2e-auth, and docker-build-smoke (plus the two Windows jobs). On the pre-merge PR (#76) the light review set was already green: ci, test-file-mode, and the two new PR smokes test-sqlserver-smoke (1m59s) and test-e2e-smoke (2m17s); the run-full-ci opt-in was exercised and correctly flipped the matrix on/off.

Locally verified (by the release author, outside CI):

  • pnpm build / pnpm typecheck (46/46) / pnpm lint (22/22) — green.
  • Per-package suites: renderer-common 113, pdf-renderer 340, html-renderer 344, editor 1405 (2 skipped), website 60, sdk 8; API safe-fetch/webhook guards 28 — all green.
  • Renderer coverage above floor: html-renderer 63.40% lines / 56.52% branches (floor 57/53), pdf-renderer 86.89% / 73.99% (floor 80/72), renderer-common 95.29% / 77.36%.
  • pnpm extract-openapi --checkopenapi.json fresh after the 0.83.0 stamp; pnpm version:check passes across all 7 lockstep surfaces.
  • CI-Linux API coverage on the PR run: 75.24% lines / 65.44% branches (floor 73/63).

Not verified — full SQL Server suite locally (no local SQL Server instance — CI-covered by the test-sqlserver job), the Playwright E2E suites locally (CI-only), and a deployment rehearsal (not performed). PyPI / Go / .NET SDK publishing remains deferred (TypeScript SDK + GHCR images only); see the SDK publishing notes for prior releases.

Upgrade notes

  • No schema changes, no new required environment variables, no API contract changes. This release is drop-in over v0.82.0.
  • Operators: to run the full CI matrix on a specific PR, apply the run-full-ci label.
  • If you maintain a fork or downstream renderer: the private-host SSRF patterns now live in @pulp-engine/renderer-common (PRIVATE_HOST_PATTERNS / isPrivateHostname / isPrivateIpAddress). The @pulp-engine/pdf-renderer/network-guard export surface is unchanged.