Security
- ⚠️ Breaking (config). Production boot now refuses a set
API_MASTER_KEYshorter than 32 characters; unset stays allowed (first boot generates one). Action required: strengthen a short key before upgrading — the boot error names the fix. - Plugin installs over plain
http:now require a#sha256=<hex>fragment, verified fail-closed against the downloaded bytes before anything is installed;https:URLs are unchanged. /api/healthonly includes the runningversionfor callers presenting a valid API key; the endpoint itself stays public.GET /api/infra/export-datano longer exports webhooksecretandheaders; redacted archives restore as unsigned webhooks.- Webhook registration rejects URLs embedding credentials (
user:pass@host) with a400, on create and update. - Boot now warns when
NODE_ENVis unset on a publicly bound listener, and the MCP fallback body parser carries a size limit. - The webhook SSRF guard classifies addresses with
net.BlockListsubnet math and now blocks every IPv6 literal outside the global-unicast range (2000::/3— the reserved space below it, multicast, and the blocks above it that the old prefix list never matched); embedded-IPv6 forms (NAT64, 6to4, mapped) still deliver when the inner address is public, and unrecognized literals still block. - The last-admin guard runs inside the same statement as the write, so demoting, deleting or revoking the last usable admin key is refused even when the requests arrive through different processes.
Added
- Dashboard, Plugins page: installed plugins whose catalog lists a strictly newer version now carry an update chip on their card, and the Install button shows a pending-update count — both driven by a silent on-mount catalog fetch, so an update is visible without opening the Install drawer. The chip opens the drawer's catalog tab pre-filtered to that plugin, where the update flow lives.
Fixed
- Creating a webhook for a nonexistent session answers
404instead of a500. - Postgres boot migrations serialize across replicas: concurrent boots queue on a session-scoped advisory lock instead of racing DDL transactions, and a crashed boot releases its lock automatically.
GET /api/infra/export-datacan no longer silently miss a table: the export/import table set is validated against the entity metadata in both directions, and a spec fails when a new entity ships without a backup decision.scripts/restore.shrefuses to restore over a live database unless--forceis passed.- Dashboard: upload size is pre-checked before reading the file, login reuses the validated role, socket subscriptions are memoised, and restart-flow timers clear on unmount.
- Engine/session lifecycle: a floating
saveCreds()rejection is handled, the listener cleanup list coversgroup.join-request, and duplicated helpers (clampNumber,extFromMimetype,resolveLid) are single-sourced. POST /sessions/:sessionId/stopescalates to a force-destroy when the graceful disconnect fails and answers a retryable502(code: 'SESSION_STOP_INCOMPLETE', session leftdisconnected, no success audit) only when both fail — a wedged browser no longer leaks until the next start. The502is documented in the API reference and all five SDKs.- The status and chat-media stores share one orphaned-file reconciliation sweep, and the integration module reads the engine registry and session table through narrow dependencies instead of importing the session module.
- Removed unused code, dead DTO types and three dev dependencies, plus dead dashboard API helpers.
- The release workflow now runs its two Postgres-gated specs in band, matching the CI job it mirrors: jest's default file-parallelism ran them simultaneously against the one shared postgres service, racing their schema resets — the first v0.19.0 tag attempt failed its own release gate on the boot-migration advisory-lock spec (
relation "messages" already exists) over a tree CI had passed minutes earlier, because the CI job already passes--runInBandfor exactly this reason.
Removed
- ⚠️ Breaking (API).
POST /sessions/:sessionId/messages/send-catalogis removed — it answered501 not supportedon every engine since it shipped. The catalog reads andsend-productare unchanged; the five SDKsendCatalogmethods went with it. - ⚠️ Breaking (API).
PUT /api/settingsis removed — it always answered501. Settings remain readable viaGET /api/settings.
Changed
- The runtime image sets
NODE_ENV=production; it previously ran unset, which several code paths treat as development. The production install step also skips package install scripts and consumes native prebuilds at runtime, leaving the stage toolchain-free and the image roughly 900 MB smaller. - Base image is digest-pinned (
node:22-slim) withnpm@12pinned,@types/nodemoved to^22,whatsapp-web.jspinned exactly, and the backup/restore scripts now ship in the image. - The session routes' path parameter is uniformly
{sessionId}(22 routes previously mixed{id}). The URLs are unchanged — OpenAPI path templates, reference tables and Prometheus route labels respell only. - Major dependency bumps, each landed separately behind the full suite plus a live-Redis queue run: bullmq 6 (with
@nestjs/bullmq11.0.5), ioredis 6 (RESP3 connections by default — no configuration change required), better-sqlite3 13 (N-API prebuilds ship in the package), and https-proxy-agent 9 / socks-proxy-agent 10 for the Baileys proxy path. - Dashboard:
@tanstack/react-tablemoves to v9 — the API keys table migrates to the newuseTable/tableFeaturesregistration model, registering only column visibility. No behavior change; the responsive column hiding works as before. - Internal reorganization behind unchanged public surfaces: the webhook delivery engine, the message send path, and the plugin loader's installer/sandbox each split into dedicated services; the engine interface is composed of fourteen capability slices; the wwebjs adapter delegates lifecycle, reconciliation, stuck-auth and call tracking; the engine capability matrix is derived from the interface with curated exceptions; plugin host services resolve core-defined ports instead of reaching into feature modules.
Documentation
- API reference backfilled (five missing routes plus collection gaps) and 2xx JSON response schemas published for the remaining schemaless operations.
Tests
- Coverage floors ratcheted; new specs for the message send endpoints, catalog, label delegation and session lifecycle edges; e2e wall-clock waits replaced with poll-for-condition.
npm testnow runs the unit lane only: the 22 repo-file drift-gate specs moved tonpm run test:docs, which CI runs as its own step — both lanes together are the former suite, and a gate spec keeps the two lane lists identical. The automation-rule controller gained a direct route spec, and per-scope coverage floors were re-derived around the split.