Pulp Engine v0.23.0 — Release Notes
Private asset delivery and per-user named credentials
Two major capabilities land in this release:
- Private asset delivery (
ASSET_ACCESS_MODE=private) — all asset binaries served through an authenticated API proxy; Puppeteer inlines assets server-side for PDF/HTML rendering. No public bucket required. - Per-user named credentials (
EDITOR_USERS_JSON) — each team member gets a personal login key, server-derived identity, and an individual role.createdByattribution is now persistent on template versions and assets.
What changed
New ASSET_ACCESS_MODE configuration (default: public)
ASSET_ACCESS_MODE controls how uploaded asset binaries are delivered. The default public is the existing behaviour — no change for existing deployments.
| Mode | Filesystem | S3 |
|---|---|---|
public (default) | @fastify/static serves /assets/* without auth | Public S3 URL stored in AssetRecord.url; bucket must be publicly readable |
private | GET /assets/:filename requires admin or editor credentials; API streams from disk | API proxies from S3 using its credentials; bucket does NOT need public-read; S3_PUBLIC_URL not required |
Private mode render pipeline:
Instead of Puppeteer fetching /assets/… URLs at render time, the API inlines all referenced assets as base64 data URIs in the HTML before passing it to Puppeteer. No unauthenticated network calls from the renderer.
Private mode editor:
The visual editor fetches /assets/:filename with X-Editor-Token and displays images via a revocable blob URL. No <img src> pointing directly at /assets/ — auth is applied at fetch time.
S3 in private mode: add s3:GetObject to the API credentials in addition to PutObject / DeleteObject. No S3_PUBLIC_URL required.
New EDITOR_USERS_JSON named-user credential registry
Set EDITOR_USERS_JSON to a JSON array of user objects to enable per-user named credentials:
[
{ "id": "alice", "displayName": "Alice Smith", "key": "alice-unique-secret", "role": "editor" },
{ "id": "bob", "displayName": "Bob Jones", "key": "bob-unique-secret", "role": "admin" },
{ "id": "carol", "displayName": "Carol Wu", "key": "carol-secret", "role": "editor",
"tokenIssuedAfter": "2026-03-25T12:00:00Z" }
]
| Field | Required | Description |
|---|---|---|
id | yes | URL-safe unique identifier — used as the actor value in tokens and audit records |
displayName | yes | Human-readable name shown in the editor header |
key | yes | Personal login credential — must be unique; must not duplicate any API_KEY_* value |
role | yes | "editor" or "admin" — maps directly to credential scope |
tokenIssuedAfter | no | ISO-8601 UTC datetime — tokens with iat before this are rejected for this user only (targeted session revocation without global key rotation) |
When EDITOR_USERS_JSON is configured:
POST /auth/editor-tokenaccepts only personal user keys — shared API keys (API_KEY_EDITOR,API_KEY_ADMIN) are rejected with HTTP 401.- The
actorfield in the request body is silently ignored; actor identity is always server-derived from the registry. X-Api-Keymachine access (CI/CD, render pipelines) is unaffected.- Startup fails immediately if
idvalues are duplicated,keyvalues are duplicated, or any user key collides with an activeAPI_KEY_*value.
Absent = shared-key mode (existing behaviour unchanged).
Persistent createdBy attribution
Template version history and asset records now carry a createdBy: string | null field:
- In named-user mode:
createdByis the user’sid— server-derived and cryptographically bound. - In shared-key mode:
createdByis the operator-supplied actor label (ornullif none was provided). - Existing records created before v0.23.0 have
createdBy: null.
Editor identity pill
The visual editor header now shows the authenticated user’s displayName (named-user mode) or actor label (shared-key mode) as an identity pill when a session is active.
API contract changes (additive)
These fields are new. No existing fields were renamed or removed.
| Endpoint | New field | Type | Notes |
|---|---|---|---|
GET /auth/status | identityMode | "shared-key" | "named-users" | Always present |
POST /auth/editor-token | displayName | string | null | null in shared-key mode |
GET /templates/:key/versions | createdBy (per version) | string | null | null for pre-v0.23.0 records |
GET /assets | createdBy (per asset) | string | null | null for pre-v0.23.0 records |
New environment variables
| Variable | Default | Notes |
|---|---|---|
ASSET_ACCESS_MODE | public | public or private |
EDITOR_USERS_JSON | (absent) | JSON array; see deployment guide § Named-User Mode |
Database changes
Postgres: one new migration (add_created_by) adds a nullable created_by column to template_versions and assets. Run pnpm --filter @pulp-engine/api db:deploy as part of the upgrade.
SQL Server: one new migration (002_add_created_by.sql) adds the same nullable created_by columns. Run pnpm --filter @pulp-engine/api db:migrate:sqlserver before starting v0.23.0. The runner applies the migration automatically on both fresh installs and upgrades.
File mode: no migration needed.
Backward compatibility
- No API routes removed or renamed.
ASSET_ACCESS_MODE=publicis the default — no config change required for existing deployments.EDITOR_USERS_JSONabsent → shared-key mode — identical to previous behaviour.createdByis nullable — existing records reportnull.- Postgres migration is a nullable column addition — safe to apply on a live database with no downtime.
Upgrading
- Pull v0.23.0.
- Apply database schema changes:
- Postgres:
pnpm --filter @pulp-engine/api db:deploy - SQL Server:
pnpm --filter @pulp-engine/api db:migrate:sqlserver - File mode: no action needed
- Postgres:
- Restart the API.
Optional configuration changes:
- Add
ASSET_ACCESS_MODE=privatefor authenticated asset delivery. - Add
EDITOR_USERS_JSONto enable per-user named credentials.
See docs/deployment-guide.md § Named-User Mode and § Asset Access Mode for full configuration reference.
Known limitations and caveats
- Named-user cutover: switching from shared-key to named-user mode causes all existing editor sessions to become invalid (shared API keys are rejected). Set
EDITOR_TOKEN_ISSUED_AFTERto the cutover timestamp to cleanly expire outstanding sessions. Plan for all editor users to re-login with their personal keys. - Asset inlining scope: only
/assets/filenamerelative paths are inlined. Absolute URLs (e.g. public S3 URLs from public mode) are left unchanged.