[1.64.0] - 2026-06-28 — Zosma
Added
- API key prefix is configurable. The generated key prefix (
gf_live_…/gf_test_…) is no
longer hard-coded —ApiKeyServicereads the brand fromauth.api_keys.prefix(env
API_KEY_PREFIX, defaultgf). Set it to rebrand keys per app (e.g.lm→lm_live_…). Only the
first 16 chars are stored as the indexed lookup prefix, so keep it short. Backward compatible — the
default reproduces the existinggf_*keys. - API key lifecycle is auditable.
ApiKeyService::create/rotate/revokenow emit framework entity
events (EntityCreatedEvent/EntityUpdatedEventfor theapi_keystable) so an audit consumer can
record who minted, rotated, or revoked a key — previously these went throughModel::save()and
emitted nothing. The event payload is identity only (uuid, name, key_prefix, scopes, …) and
never carries the plaintext or thekey_hash. Best-effort dispatch — a failed audit never breaks
the key operation. Covers every path (HTTP, CLI, anyApiKeyServicecaller).
Fixed
- Webhook list endpoints no longer 500 on the query validator.
WebhookController::listSubscriptions
andlistDeliverieschained->offset()->limit(), but the strict query validator rejects OFFSET
set before LIMIT (OFFSET requires LIMIT to be set). Reordered to->limit()->offset(). WebhookSubscriptioninserts no longer fail on PostgreSQL. The model didn't set
$timestamps = false, so the ORM boundDateTimeImmutablecreated_at/updated_atvalues
(DB-defaulted columns) through the string-bind path, throwing on insert. Now disabled like
WebhookDelivery/ApiKey, letting the DB fill the timestamps.- Webhook UUID columns widened to fit the generated ids.
webhook_subscriptions/
webhook_deliveriesdeclareduuid varchar(12), but ids arewh_sub_/wh_del_+ a 16-char nano
id (23 chars). SQLite ignores the length so it was never caught; PostgreSQL overflowed every insert.
The auto-create path now usesvarchar(32).
These webhook fixes were latent: the framework ships
WebhookControllerbut does not register its
routes, so the endpoints were never exercised against PostgreSQL until an application mounted them.
- Uploaded blob visibility is now persisted.
FileUploader::saveBlobRecord()never wrote the
visibilitycolumn, so every blob fell back to theprivateDB default regardless of the
visibilityrequested at upload — the response echoedpublicbut the row wasprivate, so the
blob then 401'd on retrieval (GET /blobs/{uuid}), breaking any<img>use of a "public" upload.
uploadMedia()now forwards its options tosaveBlobRecord(), which persists the requested
visibility (falling back touploads.default_visibility).