Pulp Engine v0.20.0 — Release Notes
Attributable Editor Sessions and Mutation Audit Logs
Summary
v0.20.0 closes the last major commercial-readiness gap in Pulp Engine’s operator trust model: editor sessions are now attributable, and all write operations produce structured audit log events.
What changed:
- Operators may supply an optional actor label (display name, email, or any identifier) when signing in to the visual editor. The label is cryptographically bound into the HMAC-signed session token via a new 4-part token format.
- The API propagates the verified actor label as
request.actorthrough the full request lifecycle. - Every template and asset write operation emits a structured
info-level audit log event that includes the actor, credential scope, operation name, and resource identifier. - The editor login form gains an optional “Your name or identifier” field.
Security posture: This is audit attribution in an operator-managed model, not per-user authentication. An actor label identifies who claimed to make a change — it does not cryptographically prove a unique human identity beyond possession of the shared signing key (API_KEY_EDITOR or API_KEY_ADMIN). Use it for audit traceability within a trusted team. The actor field must not be used for authorization decisions.
No new environment variables. No database schema changes. No API breaking changes. Fully backward-compatible with v0.19.0 3-part tokens.
Token format extension
| Format | Parts | HMAC payload | Used when |
|---|---|---|---|
| Legacy (pre-v0.19.0) | {expiry}.{sig} | editor:{expiry} | Old tokens — still accepted, cannot mint |
| v0.19.0 | {iat}.{expiry}.{sig} | editor:{iat}:{expiry} | No actor supplied |
| v0.20.0 | {iat}.{expiry}.{actor_b64url}.{sig} | editor:{iat}:{expiry}:{actor_raw} | Actor supplied at login |
The dot-count of the token determines which format is in use. Since base64url never contains dots, the count is unambiguous.
“No actor” is strictly the 3-part format. The server never mints a 4-part token with an empty actor segment; the verifier rejects any 4-part token whose decoded actor is empty. This avoids a second representation of “no actor” and keeps the payload/signature rules unambiguous.
Backward compatibility: 3-part tokens (minted without actor) are identical to v0.19.0 format and continue to work without modification. Existing call sites mintEditorToken(key, now, ttlMs) are unchanged.
Cross-version incompatibility (cluster-upgrade rule): A v0.20.0 actor-bearing (4-part) token presented to a v0.19.0 verifier is correctly rejected (the old verifier treats the actor segment as part of the expiry, producing NaN — token invalid). This is the correct fail-safe. In multi-instance deployments, upgrade all instances to v0.20.0 before users begin supplying actor labels at login. Using the 3-part format during the rollout window is safe across mixed versions.
Actor validation
Server-side validation at mint time (POST /auth/editor-token):
- Whitespace trimmed from both ends.
- Empty after trim → treated as no actor; 3-part token minted;
actor: nullin response. - Length > 200 chars →
400 Bad Request. - Contains control characters (
\x00–\x1for\x7f) →400 Bad Request. - Validation applies to the post-trim value.
- HMAC is computed over raw UTF-8 bytes of the trimmed actor string.
API changes
POST /auth/editor-token — request
{
"key": "<API_KEY_EDITOR or API_KEY_ADMIN>",
"actor": "alice@example.com"
}
actor is optional. Omit it or send a non-empty string (max 200 chars, no control characters). Do not send null — represent “no actor” by omitting the field.
POST /auth/editor-token — response
{
"token": "<opaque session token>",
"expiresAt": "2026-03-24T18:00:00.000Z",
"actor": "alice@example.com"
}
actor is the server-confirmed operator-supplied actor label embedded in the session token, or null if no actor was supplied. Always use the server-confirmed value from the response — never invent or re-derive it client-side.
New error:
| Status | Condition |
|---|---|
400 Bad Request | actor exceeds 200 chars or contains control characters |
Audit log events
editor_token_minted (updated from v0.19.0)
Now includes actor:
{ "event": "editor_token_minted", "keyScope": "editor", "issuedAt": "...", "expiresAt": "...", "actor": "alice@example.com" }
template_mutation (new)
Emitted after every successful template write:
{ "event": "template_mutation", "operation": "create", "templateKey": "my-template", "credentialScope": "editor", "actor": "alice@example.com" }
Operations: create, update, delete, restore.
asset_mutation (new)
Emitted after every successful asset write:
{ "event": "asset_mutation", "operation": "upload", "assetId": "uuid-here", "credentialScope": "editor", "actor": "alice@example.com" }
Operations: upload, delete.
actor: null means the write was performed via direct X-Api-Key auth, or no actor label was supplied at login.
Hygiene: Raw API key values and token strings are never included in log payloads.
Important: actor and resource identifier fields (templateKey, assetId) are intentionally higher-cardinality than operational metrics. They appear in Pino structured log fields only — never as Prometheus metric labels.
Visual editor changes
The editor login form now includes an optional “Your name or identifier” field below the API key field:
type="text",maxLength={200},autoComplete="name", placeholdere.g. alice@example.com- The field is optional — leaving it blank results in no actor attribution.
- The session token stores only the server-confirmed operator-supplied actor label returned by
POST /auth/editor-token. - When the session expires (
AUTH_EXPIRED_EVENT), the actor input is cleared along with the key input.
Changed files
| File | Change |
|---|---|
apps/api/src/lib/editor-token.ts | 4-part token format; actor 4th param on mintEditorToken; verifyEditorToken return type { actor: string | null } | null; strict base64url regex validation |
apps/api/src/plugins/auth.plugin.ts | request.actor decoration and extraction from verified token |
apps/api/src/routes/auth/auth.ts | Actor extraction + validation (trim, length, control chars); mint audit log includes actor; response includes actor |
apps/api/src/schemas/shared.ts | EditorTokenRequestSchema adds optional actor string; EditorTokenResponseSchema adds actor: string | null |
apps/api/src/routes/templates/index.ts | template_mutation audit log on all 4 mutation handlers |
apps/api/src/routes/assets/assets.routes.ts | asset_mutation audit log on upload and delete |
apps/api/src/__tests__/editor-session.test.ts | Return-type migration; actor unit/integration tests; auth-precedence tests; route-level audit log tests |
apps/editor/src/lib/auth.ts | ACTOR_KEY storage constant; getStoredActor(); setStoredToken actor param; clearStoredToken removes actor; EditorTokenResponse adds actor; requestEditorToken spreads actor into body |
apps/editor/src/components/auth/LoginGate.tsx | Optional actor input field; handleLogin passes actor and stores server-confirmed label; handleAuthExpired clears actor input |
apps/editor/src/lib/auth.test.ts | Actor storage tests; requestEditorToken body and response tests |
apps/editor/src/components/auth/LoginGate.test.tsx | Actor field visibility, filled actor, empty actor, AUTH_EXPIRED_EVENT clears actor |
docs/api-guide.md | Updated POST /auth/editor-token request/response; token format table; “Attributed operator sessions” subsection |
docs/editor-guide.md | Actor label field description in Authentication section |
docs/deployment-guide.md | Audit log fields in “Key log fields” table; audit event note; cluster-upgrade note |
docs/runbook.md | New “Audit log events” reference section |
README.md | One-sentence attribution note in the security section |
Upgrade notes
No new environment variables. Existing deployments require no changes.
No database schema changes.
No API breaking changes. The POST /auth/editor-token response now includes actor: null — existing callers that only consume token and expiresAt are unaffected.
Token format: v0.20.0 tokens are 4-part when an actor is supplied, or 3-part (identical to v0.19.0) when not. Old-format tokens continue to be accepted. The verifier handles all three formats.
Multi-instance deployments: If all instances are on v0.19.0, you can upgrade them one at a time. Users should not supply actor labels until all instances are on v0.20.0. Using the 3-part format (empty actor field) during the rollout window is safe.
What is not in scope (planned follow-up)
- Per-user token issuance (distinct tokens per operator account)
- Surfacing confirmed actor label in the editor header (“Signed in as Alice”)
- Structured log query examples / Grafana dashboard for
template_mutationevents - Per-user authentication and RBAC — Pulp Engine remains operator-managed; the actor label does not change the authorization model