Pulp Engine Document Rendering
Get started
Release v0.19.0

Pulp Engine v0.19.0 — Release Notes

Auth Hardening, Key Rotation, and Migration Tooling

Summary

v0.19.0 extends the editor authentication model introduced in v0.15.0 with three independent but complementary operator controls, plus a file-to-database migration script, a full OpenAPI schema layer, and a suite of new unit tests.

Auth hardening:

  1. Configurable token TTLEDITOR_TOKEN_TTL_MINUTES lets operators tune session lifetime instead of accepting the fixed 8-hour default. Accepted range: 5–1440 minutes. Values outside the range cause a startup validation error.

  2. Issued-after guardEDITOR_TOKEN_ISSUED_AFTER rejects any editor session token whose issued-at timestamp predates a given UTC datetime. Use this to invalidate all outstanding sessions without rotating API_KEY_EDITOR. Requires an API restart to take effect.

  3. Near-zero-downtime key rotationAPI_KEY_EDITOR_PREVIOUS and API_KEY_ADMIN_PREVIOUS keep the old key active as a verify-only secret during the rotation rollover window, so in-flight editor sessions survive the key change. Previous keys cannot mint new tokens and cannot be used as X-Api-Key. Remove them after EDITOR_TOKEN_TTL_MINUTES elapses.

Token format change: The token format changes from {expiry_ms}.{sig} (pre-v0.19.0) to {iat_ms}.{expiry_ms}.{sig} so the issued-at claim can be verified against EDITOR_TOKEN_ISSUED_AFTER. Old-format tokens are still accepted after upgrading — they expire naturally within one TTL window — unless EDITOR_TOKEN_ISSUED_AFTER is set, in which case all old-format tokens are immediately rejected (their iat is treated as 0).

Mint audit log: Every successful POST /auth/editor-token emits a structured info-level log entry:

{ "event": "editor_token_minted", "keyScope": "editor", "issuedAt": "...", "expiresAt": "..." }

Session expiry UX: When an in-flight API call receives a 401 — indicating the session token has expired or been invalidated — the editor login gate now shows a “Your session has ended” banner and prompts the user to re-enter their key, rather than silently failing.

OpenAPI schema layer: TypeBox schemas are applied to all routes via apps/api/src/schemas/shared.ts. The full machine-readable OpenAPI 3.0 spec is served at GET /docs/json (or GET /docs/yaml); an interactive Swagger UI is at GET /docs. Both endpoints are public.

File-to-DB migration: db:migrate:file-to-db is a new npm script that migrates templates and asset metadata from a file-mode layout (TEMPLATES_DIR + ASSETS_DIR) to a postgres or SQL Server database backend. The script is idempotent — existing records are skipped — and supports --dry-run. See the File-to-DB migration section below.

No database schema changes. No API breaking changes.


New environment variables

VariableDefaultDescription
EDITOR_TOKEN_TTL_MINUTES480Session token lifetime in minutes. Range: 5–1440. Values outside the range cause a startup validation error.
EDITOR_TOKEN_ISSUED_AFTER(unset)UTC ISO-8601 datetime (e.g. 2026-03-24T12:00:00Z). Tokens with an iat before this timestamp are rejected. Requires an API restart to take effect.
API_KEY_EDITOR_PREVIOUS(unset)Old editor key kept during a near-zero-downtime rotation. Verify-only: cannot mint tokens, cannot be used as X-Api-Key. Remove after EDITOR_TOKEN_TTL_MINUTES elapses.
API_KEY_ADMIN_PREVIOUS(unset)Old admin key kept during a near-zero-downtime rotation. Same restrictions as above.

All four are optional and have no effect on existing deployments unless set.


Token format change

FormatVersionStructure
Newv0.19.0+{iat_ms}.{expiry_ms}.{base64url_hmac_sha256}
Oldpre-v0.19.0{expiry_ms}.{base64url_hmac_sha256}

The HMAC payload also changes: new-format tokens sign editor:{iat}:{expiry}; old-format tokens signed editor:{expiry}. The verifier handles both formats automatically.


File-to-DB migration

Operators migrating a file-mode deployment to postgres or SQL Server can now use:

# Dry run — no writes; reports what would be migrated
STORAGE_MODE=postgres \
DATABASE_URL=postgres://user:pass@host:5432/pulp-engine \
TEMPLATES_DIR=/path/to/file-mode-templates \
ASSETS_DIR=/path/to/file-mode-assets \
pnpm --filter @pulp-engine/api db:migrate:file-to-db -- --dry-run

# Live migration
STORAGE_MODE=postgres \
DATABASE_URL=postgres://user:pass@host:5432/pulp-engine \
TEMPLATES_DIR=/path/to/file-mode-templates \
ASSETS_DIR=/path/to/file-mode-assets \
pnpm --filter @pulp-engine/api db:migrate:file-to-db

The script is idempotent — templates and assets already present in the target are skipped, not overwritten. Re-running is safe. Asset binaries are not copied by the script; only metadata from ASSETS_DIR/.assets-index.json is migrated. Copy the binary files separately.

Exit codes: 0 = success, 1 = fatal error, 2 = success with per-record warnings.

See docs/runbook.md § Migrating from file mode to a database backend for the full operator procedure, including a pre-migration checklist and post-migration verification steps.


OpenAPI spec

GET /docs/json returns the full OpenAPI 3.0 spec. GET /docs serves the interactive Swagger UI. Both are public — no X-Api-Key required. Restrict at the network layer in production if the spec should not be externally accessible.


New files

FilePurpose
apps/api/src/schemas/shared.tsTypeBox schemas for all routes — drives OpenAPI spec generation via @fastify/swagger
apps/api/src/scripts/migrate-file-to-db.tsIdempotent file→postgres/SQL Server migration script
apps/api/src/scripts/migrate-file-to-db.test.tsUnit tests for migration helpers and migrateFileToDb()
apps/api/src/__tests__/config-validation.test.tsConfig boundary tests for the v0.19.0 env vars
apps/editor/src/store/editor.store.test.tsUnit tests for undo/redo, history cap, tab isolation, and snapshot symmetry

Changed files

FileChange
apps/api/src/config.tsAdded EDITOR_TOKEN_TTL_MINUTES, EDITOR_TOKEN_ISSUED_AFTER, API_KEY_EDITOR_PREVIOUS, API_KEY_ADMIN_PREVIOUS
apps/api/src/lib/editor-token.tsmintEditorToken embeds iat; verifyEditorToken handles both old and new token formats; notBefore param enables issued-after guard; timing-safe HMAC verify unchanged
apps/api/src/plugins/auth.plugin.tsPrevious keys added to editorCapableSecrets (verify-only); startup warning when rollover vars are active; startup error if a previous key duplicates any active key; notBefore pre-computed from EDITOR_TOKEN_ISSUED_AFTER
apps/api/src/routes/auth/auth.tsMint audit log on every successful POST /auth/editor-token; TypeBox schemas from schemas/shared.ts applied
apps/api/src/routes/templates/index.tsTypeBox schemas applied for OpenAPI documentation
apps/api/src/routes/assets/assets.routes.tsTypeBox schemas applied for OpenAPI documentation
apps/api/src/routes/render/render.tsTypeBox schemas applied for OpenAPI documentation
apps/api/src/server.tsRegisters @fastify/swagger and @fastify/swagger-ui; exposes GET /docs/json, GET /docs/yaml, and GET /docs
apps/api/src/plugins/storage.plugin.tsMinor cleanup
apps/api/package.jsonAdded db:migrate:file-to-db script; added @fastify/swagger and @fastify/swagger-ui to dependencies
apps/api/src/__tests__/editor-session.test.tsExpanded test coverage for previous-key rollover and issued-after guard
apps/editor/src/components/auth/LoginGate.tsxAdded sessionLost state; shows “Your session has ended” banner when AUTH_EXPIRED_EVENT fires
apps/editor/src/components/auth/LoginGate.test.tsxTests for the session-expired banner, retry flow, and api-unavailable state
apps/editor/src/components/canvas/DocumentCanvas.tsxFixed dnd-kit v6 DroppableContainersMap iteration (Map subclass now iterated via .values())
apps/editor/src/editor.csslogin-gate-session-expired and login-gate-api-error CSS classes added
apps/editor/src/lib/auth.tsAuth lib updates aligned with updated token format
apps/editor/src/lib/auth.test.tsAdditional auth lib tests
apps/editor/src/store/editor.store.tsHistory and state management refactor
.env.exampleDocuments all four new auth env vars with usage notes
docs/api-guide.mdUpdated for session token TTL, rotation vars, and issued-after guard
docs/deployment-guide.mdUpdated with new env vars reference and file-to-DB migration section
docs/editor-guide.mdDocuments session expiry UX and re-login flow
docs/runbook.mdAuth secret rotation procedures (A: session invalidation, D: editor key rotation, E: admin key rotation); file-to-DB migration procedure

Upgrade notes

All new env vars are optional. Existing deployments require no changes:

  • EDITOR_TOKEN_TTL_MINUTES — omit to keep the 8-hour default.
  • EDITOR_TOKEN_ISSUED_AFTER — omit unless you need to invalidate all outstanding sessions.
  • API_KEY_EDITOR_PREVIOUS / API_KEY_ADMIN_PREVIOUS — omit unless you are in a key rotation.

Token format transition: Editor sessions minted by v0.18.0 or earlier use the old {expiry}.{sig} format. These are still accepted after upgrading to v0.19.0 as long as EDITOR_TOKEN_ISSUED_AFTER is not set. They expire naturally within the TTL window. No operator action required.

If you set EDITOR_TOKEN_ISSUED_AFTER: all outstanding sessions — both old-format and new-format tokens with an iat before the cutoff — are immediately rejected on the next request. Users see the “Your session has ended” banner and are prompted to re-enter their key.

Startup behaviour with previous keys: if API_KEY_EDITOR_PREVIOUS or API_KEY_ADMIN_PREVIOUS is set, the server emits a warn-level log at startup reminding the operator to remove the var once the rollover window closes.

No database schema changes. No API breaking changes. No new npm dependencies exposed to callers.


What is not in scope (planned follow-up)

  • Per-user session management (distinct tokens per operator)
  • Refresh token / silent session renewal
  • EDITOR_TOKEN_ISSUED_AFTER hot-reload without an API restart
  • Swagger UI restricted behind authentication
  • Multi-architecture Docker image (linux/arm64)