Releases: rmyndharis/OpenWA
v0.4.4
A reliability and correctness patch. Engine: Baileys reconnect no longer leaks its socket, and a session
keeps its operator config even if the engine plugin fails to enable before onLoad. Templates: names are now
unique per session (deterministic resolve, 409 on duplicate, with a lossless de-duplicating migration).
Tooling: the migration CLI can manage the main (auth/audit) connection, and the Docker image ships procps
so a missing-ps cleanup path can't crash the container. One behavior change to note: PUT /settings
now returns 501 — settings are environment-derived and read-only at runtime — instead of a misleading 200
(no dashboard flow uses the write).
Added
- CLI migration commands for the main (auth/audit) connection. The app runs the main connection as a separate
always-SQLite connection, but the migration CLI only managed the data connection. Newmigration:run:main,
migration:generate:main,migration:show:main, andmigration:revert:mainscripts (plus:prodvariants) manage
it — needed whenMAIN_DATABASE_SYNCHRONIZE=falsedisables boot auto-migration. Purely additive. (#364)
Changed
PUT /settingsnow returns501 Not Implementedinstead of a misleading200. Settings are derived from
environment variables and consumed at boot (andConfigServiceis immutable at runtime), so the previous handler
mutated an in-memory copy and reported success while persisting and applying nothing. The endpoint is now honest
about being read-only;GET /settingsand the ADMIN guard are unchanged, and no dashboard flow uses the write. (#364)
Fixed
- Baileys reconnect no longer leaks the previous socket. An internal (transient-drop) reconnect overwrote the live
socket without tearing the old one down, leaking its WebSocket and event listeners on every reconnect. The previous
socket is now detached and ended before its replacement is created. (#364) - Engine sessions keep operator config when the engine plugin fails to enable. The engine config blob is now also
supplied at plugin construction, sosessionDataPath/executablePath/authDirstill apply if a plugin fails to
enable before itsonLoadruns (they previously dropped silently to defaults). The healthy path is unchanged. (#364) - Template names are unique per session. A composite unique index makes resolve-by-name deterministic and rejects
duplicate names with409 Conflict; a migration losslessly de-duplicates any pre-existing collisions (keeps the
earliest, renames the rest) before adding the index. The{{var}}/{var}template-syntax split is unchanged and
still tracked in #69. (#364) - Container no longer crashes on browser-cleanup paths when
psis missing. The production image is based on
node:22-slim, which omits thepsbinary; cleanup code that shells out tops(e.g. process-tree kills) fails
withspawn ps ENOENT, and that unhandled child-process error can take down the whole Node runtime. The image now
installsprocps. This does not change the underlying browser-init timeout — it only prevents the missing-ps
cleanup failure from being fatal. (#359)
Documentation
- Documented chat-history limits. A new guide explains the difference between the local message-history
endpoint (GET /sessions/:id/messages, reads OpenWA's database) and the bounded live-history endpoint
(GET /sessions/:id/messages/:chatId/history, asks the engine): live history defaults tolimit=50and is
clamped to[1, 100](solimit=999returns 100, not the full account history), and is a recent-history
helper rather than a complete server-side import. (#356)
v0.4.3
A security-hardening and reliability release: outbound-request and storage hardening, plugin/message persistence
fixes, delivery-status and concurrency correctness, and lifecycle robustness — including a force-kill recovery
for stuck sessions and its dashboard button. No breaking changes for a correctly-configured deployment; the
only behavior change to note is that a misconfigured ENGINE_TYPE/STORAGE_TYPE now fails fast at boot instead
of silently falling back to the default.
Added
- Force-kill a stuck session.
POST /sessions/:id/force-kill(OPERATOR) recovers a session whose engine is
wedged and won't respond to a normal stop/delete: the whatsapp-web.js engine SIGKILLs its own Chromium
process directly (never a process-wide kill that could take down other sessions), then best-effort tears the
client down; the Baileys engine ends its socket. The teardown is time-bounded and isolated, and the session is
leftDISCONNECTEDand restartable. (#352) - Dashboard "Kill Stuck" button. Session cards in a
failedstate get a Kill Stuck action that confirms,
then calls the force-kill endpoint above. (#351)
Security
- Outbound webhook and media fetches are pinned to the SSRF-validated IP. The host check and the actual
connection previously resolved DNS independently, leaving a DNS-rebinding window; the connection now reuses
the exact vetted address (preserving the hostname for TLS SNI/Host, with A-record failover) across webhook
delivery (direct/queued/test) and server-side media downloads. (#338) - IPv6 SSRF blocklist closes embedded-IPv4 gaps (6to4
2002::/16, NAT6464:ff9b::/96, IPv4-compatible
::/96); the LibreTranslate plugin client is SSRF-guarded; per-sessionproxyUrlis validated as an
http(s)/socks4/socks5URL. (#344) - Secret/auth hardening. Generated secret files (
data/.env.generated,data/.api-key) are written0600;
an opt-inAPI_KEY_PEPPERhashes API keys with HMAC-SHA256;allowedIpsentries are validated as IPv4/CIDR;
the queue dashboard (Bull Board) auth uses the same trusted-proxy IP model as the API; the production
secret-guard inspects the canonical S3 variables. (#345) - Storage import/key hardening. A
tar.gzimport is bounded against decompression bombs (per-entry byte cap- entry-count cap); storage-key containment is enforced at the backend-agnostic boundary so the S3 path
inherits it; a plugin'sctx.storageis sandbox-contained against..traversal. (#346)
- entry-count cap); storage-key containment is enforced at the backend-agnostic boundary so the S3 path
Fixed
- Webhook subscriptions for session lifecycle events now deliver.
session.status,session.qr,
session.authenticated,session.disconnectedwere accepted on subscribe but never dispatched; they now fire
from the engine lifecycle (the n8n docs are corrected to the real event names). (#335) - Plugin enable/disable and configuration now persist across restarts (they previously updated only
in-memory state while the API reported success). Plugins are not auto-enabled on boot for safety; their saved
configuration is preserved. (#339) - Bulk-sent messages are recorded, their errors no longer leak internal addresses, and a running batch can be
cancelled across instances. (#340) - Forwarded messages on the whatsapp-web.js engine report a real WhatsApp message id, so their delivery
status advances (the syntheticfwd_<id>could never match an ack). (#341) - A late delivery/read receipt is no longer lost (the ack retries once when it arrives before the send's id
is committed); concurrent reactions no longer overwrite each other (serialized per message); a plugin hook
that reports an error no longer has its partial output applied; a failed ack write is logged with context. (#348) - Storage export no longer accumulates copies on the data volume — it writes under
data/exports/with a
TTL sweep and an async read (instead of a synchronous read that blocked the event loop). (#346) WEBHOOK_TIMEOUTis honored on the queued and test delivery paths (not just the deprecated direct one);
graceful shutdown is bounded (a half-open Redis socket can't blockapp.close()); unsupported status/catalog
operations return a consistent501; a misconfiguredENGINE_TYPE/STORAGE_TYPEfails fast at boot. (#350)
Changed
- The
/api/metricsscrape is memoized for a few seconds so back-to-back scrapes don't each run a full
session scan plus aggregates; removed a dead branch in the WebSocket connect handler. (#350)
Documentation
v0.4.2
Bug-fix and hardening release: access-control tightening, session-lifecycle resilience, data-migration
correctness, and a PostgreSQL analytics fix. No breaking changes — existing deployments and the default
(ADMIN) dashboard key are unaffected.
Security
- The well-known development API key is refused in production. With
ALLOW_DEV_API_KEY=true(and no
API_MASTER_KEY), the server seeded the documenteddev-admin-keyas an ADMIN credential in any
environment. Production now fails fast whenALLOW_DEV_API_KEY=true, anddev-admin-keyis rejected as an
API_MASTER_KEY. Development behaviour is unchanged. - Webhook by-id operations and the webhook list are scoped to their session.
GET/PUT/DELETE
/sessions/:sessionId/webhooks/:idand the test endpoint now verify the webhook belongs to the URL's
session (a mismatch returns 404), andGET /webhooksis scoped to the calling key's allowed sessions —
closing cross-session access to another session's webhook configuration. GET /sessionsis scoped to the API key's allowed sessions. A session-restricted key no longer lists
every session.- The audit log and global statistics require ADMIN.
GET /audit,GET /stats/overviewand
GET /stats/messages(cross-session, unscoped reads) now require an ADMIN key. The per-session stats route
is unchanged (already scoped by its session parameter). - Plugin secrets are redacted on read.
GET /pluginsandGET /plugins/:idnow mask config fields a
plugin markssecret(e.g. API keys); updating config preserves the stored secret when the masked value is
submitted back unchanged.
Fixed
- Baileys: inbound and sent messages no longer fail to persist for a recreated session (#319). An
orphaned adapter writing under a stale session id raised a foreign-key error on every message and left the
message store empty (breaking reply/forward/react/delete by id). The store now skips the write for an absent
parent session, logging once instead of erroring per message. import-datano longer silently loses message history. The restore targeted non-existent columns for
themessagesandmessage_batchestables, so every row failed while the endpoint still reported success —
after the destructive delete. Column mapping is corrected for both SQLite and PostgreSQL, and a partial
restore now rolls back and reportsimported: falseinstead of committing a half-wiped database.- Statistics work on a PostgreSQL data database. The time-series and hourly-activity queries used a
SQLite-only date function and returned 500 on PostgreSQL; the date bucketing is now dialect-correct. - Concurrent session start no longer orphans an engine. Two near-simultaneous
POST /sessions/:id/start
for the same session could both create an engine, leaking a Chromium process the lifecycle could never
clean up. The second start is now rejected with a clear error. - A stuck engine teardown no longer wedges a session.
delete()andstop()now time-bound and isolate
the engine teardown, so a hanging Chromium can't prevent the session row from being removed or its status
from being updated. A genuine database failure on delete still surfaces as an error. - Reconnect backoff is bounded. An unvalidated
reconnectBaseDelay/maxReconnectAttemptsin a
session's config could drive an immediate-relaunch storm or an unbounded reconnect loop; the values are now
coerced and clamped (the defaults are unchanged). - Inbound media is size-capped. Media on an inbound message is bounded by
MEDIA_DOWNLOAD_MAX_BYTES
(default 50 MiB; previously this cap applied only to outbound URL sends). Oversized media is dropped — the
message envelope is preserved — instead of being base64-encoded into memory, persisted, and broadcast. reply/forward/react/deleteon a missing message return 404 instead of a generic 500.- Swagger now reports the current API version (it was pinned to an old value).
Documentation
- Added an n8n appointment-booking workflow example and webhook signature-verification examples, and corrected
themessage.receivedwebhook payload field reference.
v0.4.1
Bug-fix release found while verifying v0.4.0 on both engines (whatsapp-web.js and Baileys): the Baileys QR
now renders in the dashboard, a synchronize-created SQLite data DB no longer crashes when adopting
migrations, and graceful shutdown is clean. No API or breaking changes.
Fixed
- Baileys QR code is now scannable from the dashboard. The Baileys engine returned the raw WhatsApp QR
ref string fromGET /sessions/:id/qr, while the dashboard (and the whatsapp-web.js engine) expect a PNG
data URL — so the dashboard's<img>showed a broken image and Baileys sessions could not be linked via
the UI. The Baileys adapter now renders the QR to adata:image/pngURL, matching the whatsapp-web.js
engine's contract (the REST response shape is now consistent across engines). - Adopting migrations over a
synchronize-created SQLite data DB no longer crashes on boot. A data DB
whose schema was created byDATABASE_SYNCHRONIZE=truehas an empty migrations table, so the baseline
migration re-ranCREATE TABLE "sessions"and aborted startup withtable "sessions" already exists. The
baseline migration is now idempotent (it skips when the schema already exists, mirroring the other
migrations), so switching a SQLite data DB from synchronize to migration-managed boots cleanly and the DB
becomes migration-managed going forward (existing rows preserved). Fresh deployments are unaffected. - Graceful shutdown no longer logs "could not find DataSource" on SIGTERM. With two named TypeORM
connections (main+data),@nestjs/typeorm's shutdown hook resolved the default (unnamed) DataSource
token and threwNest could not find DataSource element, leaving the DataSources undestroyed and the
process exiting non-zero. The connection factories now carry theirname, so the shutdown hook resolves
the correct named DataSource and the app shuts down cleanly (exit 0).
Changed
- Internal: the SQLite data-DB configuration comment and a dead
synchronizedefault inapp.module.tsnow
reflect the actual behavior (the data DB is migration-managed by default;DATABASE_SYNCHRONIZE=trueopts
into synchronize). No runtime behavior change.
v0.4.0
Single-port deployment. The API now serves the bundled dashboard SPA itself, and the bundled Traefik
reverse proxy is removed. This is a deployment/packaging change only — there are no API or
application-code changes.
Changed
- BREAKING — single-port dashboard: the API now serves the bundled dashboard SPA. In production the
NestJS API serves the built dashboard from its own port (default2785) via@nestjs/serve-static, so
there is no separate dashboard container and the UI is available by default wherever the API runs./api
and/socket.ioare excluded so they keep returning real API/WebSocket responses. Opt out with
SERVE_DASHBOARD=false. Dev is unchanged:npm run devstill runs the Vite dev server on:2886(HMR)
proxying to the API. Split-origin hosting (dashboard on a separate origin/CDN) still works: build with
VITE_API_URL=<api-origin>and hostdashboard/distanywhere. (#275) - The API's Content-Security-Policy now allows
https://fonts.googleapis.com(style-src) and
https://fonts.gstatic.com(font-src) so the dashboard's webfonts load now that it is served under the
API's CSP. (#275) - BREAKING — removed the bundled Traefik reverse proxy. With the API serving both the UI and the API
on one port, the shipped Traefik service was a single-backend passthrough that added no value (it
terminated no TLS out of the box). Removed thetraefikservice, thetraefik/configs, and the
with-proxyprofile. For TLS / public exposure, put your own reverse proxy (nginx, Caddy, a cloud load
balancer, or a k8s Ingress) in front of the API — seedocs/12-troubleshooting-faq.md. (#276)
Added
npm run build:all(build API + dashboard) andnpm run prod(build then serve) for running the
production build directly without Docker. (#275)
Migration
- The dashboard moved from
:2886(separate nginx container) to the API port:2785. Update bookmarks,
monitoring, and any external reverse-proxy config accordingly. (#275) - The
with-dashboardandwith-proxycompose profiles are removed, and theDASHBOARD_PORT,
PROXY_ENABLED, andDASHBOARD_ENABLEDenv vars are gone (silently ignored if still set).--profile fullnow starts the optional datastores (postgres, redis, minio). If you relied on the bundled Traefik
for TLS, front the API with your own reverse proxy. (#275, #276)
v0.3.0
Engine pluggability and plugin extensibility. OpenWA can now run on a second, browser-free WhatsApp engine
(Baileys) as a peer to whatsapp-web.js, and bot-shaped features can ship as first-party extension plugins
on a scoped capability layer instead of living in core (#265).
⚠️ Breaking (plugin API):PluginContext.getServiceis removed. It was a stub returningundefined
with no real consumers; out-of-tree plugins must migrate to the newctx.messages/ctx.engine
capabilities.
Added
- Baileys engine (
ENGINE_TYPE=baileys) — a second, browser-free WhatsApp engine built on
@whiskeysockets/baileys(WebSocket/Noise protocol, no Chromium), selectable as a peer to the default
whatsapp-web.js engine. It supports linking (QR + pairing code); sending text, media
(image/video/audio/document/sticker), location, and contacts; reply / forward / react /
delete-for-everyone; full group management (create, participants, subject/description, invite codes),
profile pictures, and block/unblock; contacts, chats, and read receipts; and receiving messages with
their media, captions, location, quoted context, reactions, and remote deletes. URL media is fetched
through the same SSRF-guarded path as the default engine. Reply/forward/react/delete are backed by a
per-session persisted message store (baileys_stored_messages, bounded byBAILEYS_MESSAGE_STORE_LIMIT,
default 5000; cleared on logout; CASCADE-deleted with its session).getChatHistoryand
labels/channels/status/catalog remain unsupported (HTTP 501) — Baileys has no on-demand history API, and
the rest are parity with the whatsapp-web.js engine. Config:BAILEYS_AUTH_DIR(default./data/baileys);
proxy is not yet supported on this engine. The engine loads lazily (dynamicimport()only when
selected), so default-engine operators are unaffected and there is no global Node version floor.
(#299, #307, #308, #309, #310, #312) - Plugin capability layer (Tier-2 extension plugins): scoped
ctx.messages(sendText/reply,
routed throughMessageServiceso persistence and the send pipeline are preserved) and read-only
ctx.engine(getGroupInfo/getContacts/getContactById/checkNumberExists/getChats) on
PluginContext, replacing the stubbedgetService. A manifest-declaredsessionsscope is enforced at
the facade before any engine access (default['*']), and a capability call to a dead/unstarted session
fails withPluginCapabilityErrorinstead of a raw error. (#294) HookManagerre-entrancy guard (AsyncLocalStorage): a plugin that sends from inside a hook handler
can no longer recurse into the same event (synchronous re-entry; the asyncmessage:sentecho loop is
documented as out of scope for now). (#294)auto-replyreference extension plugin, first-party and registered disabled by default — enable
it viaPOST /plugins/auto-reply/enableto exercise the capability layer end-to-end. (#294)- Group auto-translation extension plugin — a first-party, disabled-by-default plugin that
auto-translates incoming group messages via LibreTranslate, built entirely on the new capability layer
(supersedes the earlier in-core approach). (#300) - Schema-driven plugin config form (dashboard): the Plugins page now renders an editable config form
for any plugin that exposes aconfigSchema(text / secret / number / boolean / enum), saved via the
existing plugin-config endpoint — previously only the engine plugin had editable fields. (#303) - Spanish (
es) dashboard locale at full parity with English. (#292)
Changed
- Engine config is now opaque per-engine:
EngineFactorypasses only engine-neutral fields
(sessionId/proxyUrl/proxyType) to an engine plugin and supplies engine-specific config (Puppeteer
for whatsapp-web.js) as a blob via the plugin context, so a non-browser engine can be added without the
factory knowing browser fields. No env-var or behavior change for existing deployments. (#296)
Fixed
v0.2.10
Completes the v0.2.9 non-breaking batch with three dashboard/CI follow-ups that belonged to the same
improvement set. No breaking changes.
Fixed
- MessageTester (dashboard) resolves the recipient through the engine, not a hand-built
…@c.usJID:
it calls the check-number endpoint for the engine-canonical chat id and surfaces a clear "not registered
on WhatsApp" message for unknown numbers, instead of silently sending to a guessed id (#265). New
messageTester.notOnWhatsAppstring across all 8 locales. (#279) - Dashboard message bubbles use the engine-neutral
MessageTypevocabulary end-to-end — incoming
websocket/revoked payloads are coerced viaasMessageType(), and an attachment's optimistic bubble is
typed from its MIME (e.g. a PDF isdocument, notapplication), matching the backend normalization
shipped in #270. (#281)
Internal
- CI: bump
docker/setup-qemu-actionv3 → v4 (Node 24), clearing the Node-20 deprecation warning on the
image-build/publish jobs. (#280)
v0.2.9
A reliability, security, and accessibility hardening release — no breaking changes. It tightens RBAC on
write endpoints, patches the ws/qs advisories, makes the busy message path and graceful shutdown
crash-resistant, fixes bulk-message terminal status, finally honors LOG_LEVEL, adds audit-log and
webhook-job retention, and improves dashboard accessibility and load-error states.
⚠️ RBAC tightening (action may be required): write endpoints for groups, contacts, labels, channels,
catalog, and status now require theOPERATORrole. If you used aVIEWERkey for any of these writes,
switch it toOPERATOR(orADMIN). Everything else is backward-compatible.
Security
- Write endpoints for groups, contacts, labels, channels, catalog, and status now require the
OPERATORrole, closing an unintended privilege gap where aVIEWER-role API key could create/leave
groups, manage participants, block contacts, post statuses, send products, and mutate labels. Read
(GET) endpoints remain open to any valid key, matching the message/session controllers.⚠️ If you used aVIEWERkey for any of these write operations, switch it toOPERATOR(orADMIN). - Patched a high-severity
wsadvisory (and a moderateqsDoS) on the live socket.io transport by
bumping in-range deps (ws→8.21.0,engine.io→6.6.9,qs→6.15.2, plus the incidental
re-resolutionsnpm audit fixpulled in) in both the API and dashboard. Lockfile-only — no
package.json/API change. The remaining advisories are build-only (sqlite3→node-gyp→tar)
and require a breakingsqlite3major, deferred.
Added
LOG_LEVELis now honored. It was read into config/compose but never applied (logging was hardcoded
toinfo); the level (error/warn/info/debug/verbose) is now set at bootstrap.- Automatic audit-log retention. Audit logs older than
AUDIT_RETENTION_DAYS(default 90;0disables)
are pruned daily and once at startup — the existingcleanup()was never scheduled, soaudit_logsgrew
without bound.
Fixed
- Bulk-message batch status is now correct on cancel and stop-on-error. A cancelled batch could be
silently reverted toPROCESSING(the final save overwrote theCANCELLEDstatus with the stale
in-memory one), and astopOnErrorabort was reported asCOMPLETEDwhenever at least one message had
already been sent. The terminal status is now re-derived (cancelled →CANCELLEDwith reconciled
counters; stop-on-error →FAILED; otherwiseCOMPLETED/FAILED). - Bulk-message item
typeis now validated against the allowed set (text/image/video/audio/document)
with@IsIn, so an invalid type is rejected up front instead of failing mid-send. - Graceful shutdown is now robust.
onModuleDestroyclears reconnect timers first and destroys engines
in parallel, each isolated and time-bounded — so one hung/throwing Chromium can no longer abort teardown
of the other sessions or stall shutdown. - A session that exhausts its reconnect attempts is now marked
FAILEDwith a reason (surfaced via
lastError) instead of sitting silentlyDISCONNECTEDforever. - BullMQ webhook jobs are auto-evicted (
removeOnComplete/removeOnFailretention) so completed/failed
job payloads no longer accumulate unbounded in Redis (audit M19). - Engine-event handlers no longer risk unhandled promise rejections. Webhook dispatch is now
self-contained (a failed webhook lookup is logged and swallowed, not rejected into the fire-and-forget
callers), theonMessage/onMessageCreatehook chains carry a.catch(), and a process-level
unhandledRejectionbackstop logs (instead of crashing) anything that still slips through. A transient
DB hiccup on the busy message path can no longer drop the event silently or take the process down. - Audit-log writes are best-effort. A failed audit insert is logged and swallowed instead of turning
an otherwise-successful operation (create/delete/start/stop session, etc.) into a500. - Dashboard accessibility: toast notifications are now an ARIA live region (
role="region"/aria-live,
withrole="alert"on error/warning toasts) so screen readers announce success/error feedback, and the
toast close button has an accessible name. The API-key visibility toggles on the Login and API Keys pages
now have state-reflectingaria-labels (show/hide). Newcommon.showApiKey/common.hideApiKeystrings
across all locales. - Dashboard no longer shows a misleading "nothing here" empty state when a list fetch fails. The
Webhooks, API Keys, and Logs pages discarded the query error and rendered the empty state on failure;
they now surface an accessible error banner (role="alert") so the user knows the data failed to load.
v0.2.8
The engine-pluggability release: the whatsapp-web.js delivery-ack, message-type, and JID specifics are
now decoupled behind the neutral engine interface (a different engine, e.g. Baileys, can map its own at
the adapter boundary). Plus dashboard message templates, best-effort @lid → phone resolution, and a
Docker fix for sessions stuck at "authenticating".
⚠️ Breaking for webhook consumers: themessage.received/message.senttypefield is now a
neutral enum — incomingchat→text,ptt→voice,vcard/multi_vcard→contact. Update
any consumer that matched the raw whatsapp-web.js tokens. See Changed below.
Added
- Message templates (dashboard). Manage reusable message templates from a new dashboard page
(create/edit/delete,{{variable}}placeholders), backed by the existingsessions/:id/templates
API, with full i18n across all locales. Thanks @Leslie-23 (#266). - Resolve a
@lidprivacy id to a phone number (#263), engine-neutral via a new
IWhatsAppEngine.resolveContactPhone. On-demand endpointGET /sessions/:id/contacts/:contactId/phone
→{ contactId, phone }(MSISDN digits, ornullwhen the engine can't map it — best-effort, since
@lidexists to hide numbers). Optional inline resolution: setRESOLVE_LID_TO_PHONE=trueto attach
a best-effortsenderPhoneto themessage.receivedwebhook + websocket payload for@lidsenders
(off by default; per-sender lookups are cached). A non-whatsapp-web.js engine implements its own mapping.
Changed
- Message delivery status is now engine-agnostic (engine-pluggability decoupling, #265). The raw whatsapp-web.js
ack integer no longer leaks past the engine adapter — a neutralDeliveryStatus
(pending/sent/delivered/read/failed) flows through the interface, services, webhooks, websocket, and
dashboard, so a non-whatsapp-web.js engine (e.g. Baileys) can map its own delivery codes at the adapter boundary.- The
message.ack/message.failedwebhooks now include a neutralstatusfield. The legacyackinteger
is kept (deprecated) for backward compatibility — new consumers should readstatus. - Dashboard chat delivery ticks now update live over the websocket (the ack push was previously never emitted).
- Minor deprecated-surface deltas: the legacy webhook
ackreports3(not4) for a "played" voice/video receipt,
and a play-after-read no longer emits a secondmessage.ack(both map tostatus: 'read').
- The
- Message
typeis now an engine-neutral enum (engine-pluggability decoupling, #265). Raw whatsapp-web.js
message-type tokens no longer leak past the engine adapter — incoming live/history messages, persisted rows, and the
message.received/message.sentwebhooks now use a neutralMessageType
(text/image/video/audio/voice/document/sticker/location/contact/revoked/unknown), consistent with
outgoing sends. A non-whatsapp-web.js engine maps its own tokens at the adapter boundary.- Webhook contract change (both
message.receivedandmessage.sent): incomingtypewas previously raw — e.g.
chat→text,ptt→voice,vcard→contact. New consumers should expect the neutral enum. - An idempotent startup backfill rewrites existing
messages.typerows to the neutral vocabulary (runs in every DB
mode, including the zero-config SQLite default where data migrations don't), so historical chats render correctly
and message-type stats don't split the same kind across old/new tokens. - Fixes a latent dashboard bug where incoming text (
chat) was mis-styled as media and shown as[chat]in reply previews.
- Webhook contract change (both
- JID construction moved into the engine (engine-pluggability decoupling, #265). The check-number endpoint
(GET /sessions/:id/contacts/check/:number) now returns the engine's canonical chat id via a new
IWhatsAppEngine.getNumberId(number)instead of the controller hand-building a…@c.usJID. As a result the
returnedwhatsappIdis the engine-resolved id and may be normalized — it can differ from the submitted number's
…@c.usform (e.g. a@lididentifier) rather than echoing the input. And status/story
broadcasts are flagged with a neutralisStatusBroadcaston the message payload, so engine-neutral code no longer
matches the engine-specificstatus@broadcastpseudo-JID. A non-whatsapp-web.js engine supplies its own JID scheme.
Fixed
- The
WWEBJS_WEB_VERSION(andWWEBJS_WEB_VERSION_REMOTE_PATH) workaround for sessions stuck at
"authenticating" (#251) is now actually passed through by the Docker Compose files. Theenvironment:
blocks enumerate vars explicitly with noenv_file, so settingWWEBJS_WEB_VERSIONin.envpreviously
never reached the container — making the documented fix a no-op for Compose users. Added the passthrough
(empty default = auto-select, no behavior change when unset) todocker-compose.ymland
docker-compose.dev.yml. (#273) - Refined the Italian (
it) dashboard translations. Thanks @albanobattistella (#272).
v0.2.7
A feature + fix release: typing simulation (anti-ban, on by default), a delete-chat endpoint, and a fix
for duplicate outgoing messages in the dashboard — plus engine-agnostic groundwork and the nginx/
singleton-lock container fixes.
Added
- Typing simulation before single sends (anti-ban), on by default. A text send now shows a "typing…"
indicator and pauses briefly (length-scaled, jittered) before sending, so automated messages don't look
instantaneous. Disable withSIMULATE_TYPING=false; cap the pause withSIMULATE_TYPING_MAX_MS
(default 5000). Exposed engine-agnostically viaIWhatsAppEngine.sendChatStateand a new
POST /sessions/:id/chats/typingendpoint (state:typing|recording|paused). Bulk sends are
unaffected (they keep their owndelayBetweenMessagesthrottle). - The engine API (
GET /infra/engines) and the dashboard Active Engine card now report the underlying
engine library version (e.g.whatsapp-web.js 1.34.7), distinct from the adapter plugin version. - Delete a chat from the chat list via
POST /sessions/:id/chats/delete(e.g. to clear out groups
you've left).OPERATORrole, engine-agnostic DTO. Thanks @tobiasstrebitzer (#261).
Fixed
- Duplicate outgoing messages in the dashboard Chats view. A race between the optimistic placeholder
and the realtimemessage.sentecho could render a sent message twice. Reconciliation is now race-safe.
(Display-only — the recipient always received exactly one message.) - Dashboard (simple nginx image) proxied API/WebSocket requests to a
openwahost that doesn't match the
backend service name;dashboard/nginx.confnow targetsopenwa-apifor both/api/and/socket.io/,
matching the production compose andDockerfile.traefik. Thanks @Abhishekrajpurohit (#259). - The container entrypoint now clears stale Chromium
SingletonLock/SingletonSocket/SingletonCookiefiles
from session profiles on start, so a session can re-launch after an unclean shutdown instead of failing with
"profile appears to be in use by another Chromium process" (exit Code 21). Thanks @Abhishekrajpurohit (#259).
Changed
mark-chat-readchatIdvalidation is now engine-neutral (accepts any engine's JID scheme, e.g. a
Baileys…@s.whatsapp.net) instead of hardcoding the whatsapp-web.js@c.us/@g.us/@lidformat.