Three bugs causing storage preview cache to be ineffective:
1. Cache keys included the `token` auth parameter, so requests using
resource tokens always generated unique keys and never hit cache.
Introduced `cache.params` label for routes to opt-in specific params
into the cache key; preview now declares only the transform params.
2. Cache hits never refreshed `accessedAt` in the DB or the filesystem
file mtime, because `$response->send()` in the init hook skips the
shutdown hook. After 30 days the maintenance job evicted still-active
cache entries, and after the original 30-day filesystem TTL the cache
file expired — causing periodic full re-renders. The cache-hit path
now updates both on the APP_CACHE_UPDATE (24h) interval.
3. `updateDocument` in the preview action passed the full file document
instead of a sparse one when updating `transformedAt`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SessionCreated event now carries only domain data (no isFirstSession)
- Mails listener uses ordered guard clauses, deferring the DB query
until cheaper checks pass
- Drop $user Document allocation in favour of direct array access
- Inline FileName validator and $smtpEnabled into their use sites
- Extract $isBranded to eliminate duplicate APP_BRANDED_EMAIL_BASE_TEMPLATE check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves session alert email side effect out of the account controller
into a dedicated `Mails` listener that reacts to a new `SessionCreated`
bus event. The event is now always dispatched on session creation; the
listener owns all conditional logic (first session, sessionAlerts flag,
email-link sessions, user email presence).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Swap the order of createDocument('sessions') and purgeCachedDocument('users')
in the email/password session creation flow. Previously, the cache was purged
before the session was written, opening a race window in Swoole's async
environment where a concurrent account.get() could re-cache the user with no
sessions, causing sessionVerify to fail with a 401. This matches the correct
ordering already used by the token-based flows (magic URL, OTP, phone).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove cors from inject chain; resolve via getResource() inside
try-catch so DB failures don't cascade when resolving the cors
resource dependency chain (cors -> allowedHostnames -> rule -> DB)
- Use override:true on addHeader to prevent duplicate CORS headers
when init() already set them before the exception was thrown
- Degrades gracefully: if cors resolution fails, error response is
sent without CORS headers (same behavior as before this PR)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Http::error() handler was missing CORS headers, causing browsers to
block error responses (e.g. 403 PROJECT_PAUSED) with a generic CORS
error instead of showing the actual error message. This injects the cors
resource into the error handler and adds CORS headers before sending the
error response, matching the pattern already used in Http::init().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The users.php and messaging.php legacy controllers were missing the
userType field in their log output, creating an inconsistency with the
new audit user type distinction feature. Also adds missing mode field
to users.php logs endpoint.
https://claude.ai/code/session_01J9gKXwbHoLggsGwJi6KUnM
addHeader() already accumulates multiple values for the same key into an
array internally, so calling it once per value is the correct approach.
Comma-joining violates RFC 6265 for Set-Cookie headers.