Skip to content

Releases: rmyndharis/OpenWA

v0.4.4

19 Jun 18:31
63ba9b6

Choose a tag to compare

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. New migration:run:main,
    migration:generate:main, migration:show:main, and migration:revert:main scripts (plus :prod variants) manage
    it — needed when MAIN_DATABASE_SYNCHRONIZE=false disables boot auto-migration. Purely additive. (#364)

Changed

  • PUT /settings now returns 501 Not Implemented instead of a misleading 200. Settings are derived from
    environment variables and consumed at boot (and ConfigService is 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 /settings and 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, so sessionDataPath/executablePath/authDir still apply if a plugin fails to
    enable before its onLoad runs (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 with 409 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 ps is missing. The production image is based on
    node:22-slim, which omits the ps binary; cleanup code that shells out to ps (e.g. process-tree kills) fails
    with spawn ps ENOENT, and that unhandled child-process error can take down the whole Node runtime. The image now
    installs procps. 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 to limit=50 and is
    clamped to [1, 100] (so limit=999 returns 100, not the full account history), and is a recent-history
    helper rather than a complete server-side import. (#356)

v0.4.3

19 Jun 14:37
735fa69

Choose a tag to compare

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
    left DISCONNECTED and restartable. (#352)
  • Dashboard "Kill Stuck" button. Session cards in a failed state 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, NAT64 64:ff9b::/96, IPv4-compatible
    ::/96); the LibreTranslate plugin client is SSRF-guarded; per-session proxyUrl is validated as an
    http(s)/socks4/socks5 URL. (#344)
  • Secret/auth hardening. Generated secret files (data/.env.generated, data/.api-key) are written 0600;
    an opt-in API_KEY_PEPPER hashes API keys with HMAC-SHA256; allowedIps entries 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.gz import 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's ctx.storage is sandbox-contained against .. traversal. (#346)

Fixed

  • Webhook subscriptions for session lifecycle events now deliver. session.status, session.qr,
    session.authenticated, session.disconnected were 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 synthetic fwd_<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_TIMEOUT is 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 block app.close()); unsupported status/catalog
    operations return a consistent 501; a misconfigured ENGINE_TYPE/STORAGE_TYPE fails fast at boot. (#350)

Changed

  • The /api/metrics scrape 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

  • Added a phone-number pairing example. (#343)
  • Documented the webhook idempotencyKey/deliveryId fields (body + X-OpenWA-* headers) and the dedup rule;
    corrected the .env.example rate-limit variable names (RATE_LIMIT_MEDIUM_TTL/_LIMIT, in milliseconds). (#350)

v0.4.2

19 Jun 06:53

Choose a tag to compare

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 documented dev-admin-key as an ADMIN credential in any
    environment. Production now fails fast when ALLOW_DEV_API_KEY=true, and dev-admin-key is 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/:id and the test endpoint now verify the webhook belongs to the URL's
    session (a mismatch returns 404), and GET /webhooks is scoped to the calling key's allowed sessions —
    closing cross-session access to another session's webhook configuration.
  • GET /sessions is 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/overview and
    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 /plugins and GET /plugins/:id now mask config fields a
    plugin marks secret (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-data no longer silently loses message history. The restore targeted non-existent columns for
    the messages and message_batches tables, 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 reports imported: false instead 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() and stop() 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 / maxReconnectAttempts in 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 / delete on 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
    the message.received webhook payload field reference.

v0.4.1

18 Jun 13:16

Choose a tag to compare

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 from GET /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 a data:image/png URL, 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 by DATABASE_SYNCHRONIZE=true has an empty migrations table, so the baseline
    migration re-ran CREATE TABLE "sessions" and aborted startup with table "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 threw Nest could not find DataSource element, leaving the DataSources undestroyed and the
    process exiting non-zero. The connection factories now carry their name, 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 synchronize default in app.module.ts now
    reflect the actual behavior (the data DB is migration-managed by default; DATABASE_SYNCHRONIZE=true opts
    into synchronize). No runtime behavior change.

v0.4.0

18 Jun 10:17

Choose a tag to compare

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 (default 2785) 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.io are excluded so they keep returning real API/WebSocket responses. Opt out with
    SERVE_DASHBOARD=false. Dev is unchanged: npm run dev still 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 host dashboard/dist anywhere. (#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 the traefik service, the traefik/ configs, and the
    with-proxy profile. For TLS / public exposure, put your own reverse proxy (nginx, Caddy, a cloud load
    balancer, or a k8s Ingress) in front of the API — see docs/12-troubleshooting-faq.md. (#276)

Added

  • npm run build:all (build API + dashboard) and npm 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-dashboard and with-proxy compose profiles are removed, and the DASHBOARD_PORT,
    PROXY_ENABLED, and DASHBOARD_ENABLED env vars are gone (silently ignored if still set). --profile full now 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

18 Jun 09:42

Choose a tag to compare

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.getService is removed. It was a stub returning undefined
with no real consumers; out-of-tree plugins must migrate to the new ctx.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 by BAILEYS_MESSAGE_STORE_LIMIT,
    default 5000; cleared on logout; CASCADE-deleted with its session). getChatHistory and
    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 (dynamic import() 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 through MessageService so persistence and the send pipeline are preserved) and read-only
    ctx.engine (getGroupInfo / getContacts / getContactById / checkNumberExists / getChats) on
    PluginContext, replacing the stubbed getService. A manifest-declared sessions scope is enforced at
    the facade before any engine access (default ['*']), and a capability call to a dead/unstarted session
    fails with PluginCapabilityError instead of a raw error. (#294)
  • HookManager re-entrancy guard (AsyncLocalStorage): a plugin that sends from inside a hook handler
    can no longer recurse into the same event (synchronous re-entry; the async message:sent echo loop is
    documented as out of scope for now). (#294)
  • auto-reply reference extension plugin, first-party and registered disabled by default — enable
    it via POST /plugins/auto-reply/enable to 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 a configSchema (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: EngineFactory passes 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

  • Dashboard stops polling for a QR code once its session is connected, and the dev Docker Compose setup
    proxies the dashboard to the API service correctly. (#311)
  • Italian locale: the message-template strings are now fully translated. (#301)

v0.2.10

17 Jun 07:05

Choose a tag to compare

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.us JID:
    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.notOnWhatsApp string across all 8 locales. (#279)
  • Dashboard message bubbles use the engine-neutral MessageType vocabulary end-to-end — incoming
    websocket/revoked payloads are coerced via asMessageType(), and an attachment's optimistic bubble is
    typed from its MIME (e.g. a PDF is document, not application), matching the backend normalization
    shipped in #270. (#281)

Internal

  • CI: bump docker/setup-qemu-action v3 → v4 (Node 24), clearing the Node-20 deprecation warning on the
    image-build/publish jobs. (#280)

v0.2.9

17 Jun 06:44

Choose a tag to compare

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 the OPERATOR role. If you used a VIEWER key for any of these writes,
switch it to OPERATOR (or ADMIN). Everything else is backward-compatible.

Security

  • Write endpoints for groups, contacts, labels, channels, catalog, and status now require the
    OPERATOR role
    , closing an unintended privilege gap where a VIEWER-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 a VIEWER key for any of these write operations, switch it to OPERATOR (or ADMIN).

  • Patched a high-severity ws advisory (and a moderate qs DoS) 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-resolutions npm audit fix pulled in) in both the API and dashboard. Lockfile-only — no
    package.json/API change. The remaining advisories are build-only (sqlite3node-gyptar)
    and require a breaking sqlite3 major, deferred.

Added

  • LOG_LEVEL is now honored. It was read into config/compose but never applied (logging was hardcoded
    to info); 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; 0 disables)
    are pruned daily and once at startup — the existing cleanup() was never scheduled, so audit_logs grew
    without bound.

Fixed

  • Bulk-message batch status is now correct on cancel and stop-on-error. A cancelled batch could be
    silently reverted to PROCESSING (the final save overwrote the CANCELLED status with the stale
    in-memory one), and a stopOnError abort was reported as COMPLETED whenever at least one message had
    already been sent. The terminal status is now re-derived (cancelled → CANCELLED with reconciled
    counters; stop-on-error → FAILED; otherwise COMPLETED/FAILED).
  • Bulk-message item type is 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. onModuleDestroy clears 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 FAILED with a reason (surfaced via
    lastError) instead of sitting silently DISCONNECTED forever.
  • BullMQ webhook jobs are auto-evicted (removeOnComplete/removeOnFail retention) 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), the onMessage/onMessageCreate hook chains carry a .catch(), and a process-level
    unhandledRejection backstop 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 a 500.
  • Dashboard accessibility: toast notifications are now an ARIA live region (role="region"/aria-live,
    with role="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-reflecting aria-labels (show/hide). New common.showApiKey/common.hideApiKey strings
    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

17 Jun 02:06

Choose a tag to compare

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: the message.received/message.sent type field is now a
neutral enum — incoming chattext, pttvoice, vcard/multi_vcardcontact. 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 existing sessions/:id/templates
    API, with full i18n across all locales. Thanks @Leslie-23 (#266).
  • Resolve a @lid privacy id to a phone number (#263), engine-neutral via a new
    IWhatsAppEngine.resolveContactPhone. On-demand endpoint GET /sessions/:id/contacts/:contactId/phone
    { contactId, phone } (MSISDN digits, or null when the engine can't map it — best-effort, since
    @lid exists to hide numbers). Optional inline resolution: set RESOLVE_LID_TO_PHONE=true to attach
    a best-effort senderPhone to the message.received webhook + websocket payload for @lid senders
    (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 neutral DeliveryStatus
    (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.failed webhooks now include a neutral status field. The legacy ack integer
      is kept (deprecated) for backward compatibility — new consumers should read status.
    • Dashboard chat delivery ticks now update live over the websocket (the ack push was previously never emitted).
    • Minor deprecated-surface deltas: the legacy webhook ack reports 3 (not 4) for a "played" voice/video receipt,
      and a play-after-read no longer emits a second message.ack (both map to status: 'read').
  • Message type is 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.sent webhooks now use a neutral MessageType
    (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.received and message.sent): incoming type was previously raw — e.g.
      chattext, pttvoice, vcardcontact. New consumers should expect the neutral enum.
    • An idempotent startup backfill rewrites existing messages.type rows 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.
  • 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.us JID. As a result the
    returned whatsappId is the engine-resolved id and may be normalized — it can differ from the submitted number's
    …@c.us form (e.g. a @lid identifier) rather than echoing the input. And status/story
    broadcasts are flagged with a neutral isStatusBroadcast on the message payload, so engine-neutral code no longer
    matches the engine-specific status@broadcast pseudo-JID. A non-whatsapp-web.js engine supplies its own JID scheme.

Fixed

  • The WWEBJS_WEB_VERSION (and WWEBJS_WEB_VERSION_REMOTE_PATH) workaround for sessions stuck at
    "authenticating" (#251) is now actually passed through by the Docker Compose files. The environment:
    blocks enumerate vars explicitly with no env_file, so setting WWEBJS_WEB_VERSION in .env previously
    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) to docker-compose.yml and
    docker-compose.dev.yml. (#273)
  • Refined the Italian (it) dashboard translations. Thanks @albanobattistella (#272).

v0.2.7

16 Jun 16:19
e00fa90

Choose a tag to compare

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 with SIMULATE_TYPING=false; cap the pause with SIMULATE_TYPING_MAX_MS
    (default 5000). Exposed engine-agnostically via IWhatsAppEngine.sendChatState and a new
    POST /sessions/:id/chats/typing endpoint (state: typing | recording | paused). Bulk sends are
    unaffected (they keep their own delayBetweenMessages throttle).
  • 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). OPERATOR role, engine-agnostic DTO. Thanks @tobiasstrebitzer (#261).

Fixed

  • Duplicate outgoing messages in the dashboard Chats view. A race between the optimistic placeholder
    and the realtime message.sent echo 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 openwa host that doesn't match the
    backend service name; dashboard/nginx.conf now targets openwa-api for both /api/ and /socket.io/,
    matching the production compose and Dockerfile.traefik. Thanks @Abhishekrajpurohit (#259).
  • The container entrypoint now clears stale Chromium SingletonLock/SingletonSocket/SingletonCookie files
    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-read chatId validation 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/@lid format.