Fixed
- Bulk batch ids are unique per session, not globally. A batch id claimed by one session no longer prevents another session from using the same id — the uniqueness constraint is now scoped to
(session, batchId), matching the per-session lookup, so an explicit cross-session reuse no longer fails with a500. Reusing an id within the same session is still rejected with a clear400. Existing databases are migrated in place. (#531) - A message arriving while a session is being deleted is no longer persisted as an orphan. The inbound-message handler re-checks that the session is still live after its asynchronous processing, so a message that races a session deletion can't leave behind a
messagesrow (which has no cascade) for a session that no longer exists. (#531) - Per-session stats return a consistent
lastActivetimestamp on SQLite and PostgreSQL.GET /stats/sessions/:idpreviously emitted a differenttopChats[].lastActiveformat depending on the database (an ISO date-time on PostgreSQL versus the stored text on SQLite); it is now formatted to a stableYYYY-MM-DD HH:MM:SSon both. (#533) - The uuid id default now works on PostgreSQL 12 and older. Id generation relies on a
gen_random_uuid()column default, which is a core built-in only from PostgreSQL 13; on older servers it lives in thepgcryptoextension. The migration now enablespgcryptofirst, so a fresh deploy against PostgreSQL ≤ 12 no longer fails on startup or first insert. (#533) - The audit-log listing no longer loads the whole table for a large
limit.GET /auditclamps its page size to a maximum of 200, so an oversizedlimitcan't pull the entireaudit_logstable into a single response. (#536) - Migration reverts are idempotent on a synchronize-bootstrapped database. The
baileys_stored_messagesandwebhook_delivery_failuresmigrations now drop their indexes withIF EXISTS, so adown()no longer errors when the named indexes were never created. (#536) - Bulk send always releases its in-flight marker. A batch whose session engine was missing, or that threw mid-processing, previously left a stale entry in an in-memory tracking map; the marker is now released on every exit path. (#536)
Security
- Hook re-entrancy is now blocked for sandboxed plugins too. A plugin running in the worker-thread sandbox could re-fire the hook it was handling by issuing a capability call (for example, sending a message from within a
message:sendinghandler), because the re-entrancy guard did not span the worker boundary — looping the event back into the plugin without bound. The host now runs each worker-initiated capability call inside the in-flight hook context, so such a re-fire is short-circuited exactly as it already was for in-process plugins. (#532) - Docker container teardown is constrained to OpenWA-managed services. The
POST /infra/restartendpoint passed itsprofilesToRemovelist straight to container removal, which resolved containers by a name substring — so an unrecognized or empty profile could stop and remove an unrelated container. Teardown is now restricted to the managed allowlist (postgres,redis,minio) and container resolution requires an exactopenwa-<service>name match. (#534) - Failed API-key authentication attempts are now recorded in the audit log. Rejected or denied keys (invalid, disabled/expired, IP- or session-scope-denied, or insufficient role) previously left no audit entry; the gateway now logs an
api_key_auth_failedevent with the client IP, method, path, and reason, giving administrators a forensic trail for credential probing. Audit logging stays best-effort and never affects the request outcome. (#535) - The SSRF guard blocks the deprecated IPv6 site-local range (
fec0::/10). Webhook and server-side media URLs are now rejected when they resolve intofec0::/10, closing a gap alongside the already-blocked unique-local and link-local ranges. (#536) - Session-scoped MCP tools require a session id before authorization. A session-scoped tool invoked without a session id is now rejected, so a session-restricted API key can't be used to drive such a tool against a session outside its scope. (#536)
- Contact-card vCards are sanitized on both engines. Sending a contact whose name or number contained CR/LF could inject extra vCard fields on the whatsapp-web.js engine; both adapters now build the vCard through one shared sanitizing helper (CR/LF stripped, digits-only
waid). (#537)