Skip to content

Releases: rmyndharis/OpenWA

v0.8.14

Choose a tag to compare

@rmyndharis rmyndharis released this 10 Jul 05:43

Added

  • Plugin search providers (host→plugin search RPC). A sandboxed plugin can now register as a SearchProvider by calling ctx.registerSearchProvider(handler) (e.g. ctx.registerSearchProvider(async (query) => …)) from its worker. The host routes GET /api/search queries to the plugin over a new correlated search / search-result wire protocol (mirroring the existing hook/webhook/health-check bridges), so a plugin owns all of its vendor-specific query logic (Meilisearch, Elasticsearch, Typesense, …) while the core stays backend-agnostic — adding a new backend is a plugin, with no core changes. When SEARCH_PROVIDER=auto (the default), an enabled plugin provider supersedes the built-in database full-text provider; SEARCH_PROVIDER=builtin-fts keeps the built-in active; SEARCH_PROVIDER=none disables search. Searches are bounded by a 10s timeout and fail fast rather than hanging, and a provider is dropped from the registry when its plugin is disabled. This is Part 1 of the plugin-search work (the query RPC + selection policy); the indexing side uses the existing message:persisted hook.

  • Search queries are bounded host-side and plugin results are re-scoped. GET /api/search now clamps limit (to SEARCH_LIMIT_MAX) and offset (to SEARCH_OFFSET_MAX) before dispatching to any provider, so a plugin backend can no longer be asked for an unbounded page; a plugin provider's returned hits are re-filtered host-side against the caller's session scope, mirroring the SQL-enforced built-in provider. The provider's total is preserved when no out-of-scope hit was stripped, so pagination is unaffected. [#680]

  • Ingress events and delivery failures are now retained. ingress_events and integration_delivery_failures are pruned past INGRESS_RETENTION_DAYS (default 90; <=0 disables), matching the existing webhook/audit retention so the tables no longer grow without bound. [#680]

  • MCP auth failures are audited and Bull Board login is throttled. MCP authentication failures are now written to the audit log (parity with the REST ApiKeyGuard), and the Bull Board login endpoint is pre-auth IP-throttled (sharing MCP's MCP_IP_RATE_LIMIT_* settings) to bound credential-guessing floods. [#680]

  • Ingress guardrails. A startup warning now names any unauthenticated (scheme:'none') ingress route a plugin declares; the documented {id} HMAC contentTemplate placeholder is now implemented (previously it silently 401'd deliveries that used it). [#680]

  • CI now type-checks spec files, and the release gate matches CI. A tsc --noEmit -p tsconfig.json step full-program type-checks test files (previously invisible to CI, since tsconfig.build.json excludes specs); the release workflow's test gate now runs the full CI suite (lint + typecheck + jest + e2e + postgres-migration + dashboard) gating both the image and the GitHub Release. [#680]

  • Korean (한국어) dashboard locale. The dashboard language picker now includes Korean alongside the existing locales. Thanks @moduvoice. [#679]

Fixed

  • Sending media to a channel (@newsletter) on the whatsapp-web.js engine now fails fast with 501 Not Implemented instead of a raw 500. whatsapp-web.js builds a channel media message and calls msg.avParams(), a WhatsApp-Web-page method removed in a recent WA Web build, so the send crashed with TypeError: msg.avParams is not a function (upstream wwebjs#201823, unresolved in the current 1.34.7 release). The engine adapter now guards all media sends — image/video/audio/document (which share one send path) and sticker (a separate path that funnels into the same crash for channels) — for a channel recipient and raises a typed ChannelMediaNotSupportedError (501) before reaching the broken upstream code. Text→channel still works; only media is affected. The Baileys engine is unaffected. Workaround for affected media-to-channel sends until the upstream is fixed: switch the session to the Baileys engine, or pin WWEBJS_WEB_VERSION to a cached WA Web build that predates the avParams removal. [#673]

  • base64 media now takes precedence over url when both are provided on a media send. A send-image/send-video/etc. request carrying both fields previously sent the URL — and because @ValidateIf skipped @IsUrl validation on url whenever base64 was present, a stale url (e.g. an example default left in the body, or a client such as the n8n community node that sends both) was fetched unvalidated and could 404, silently shadowing the supplied base64 image. buildMediaInput now prefers base64, aligning the send path with the already-base64-first persisted message metadata. Both engine adapters (whatsapp-web.js and Baileys) already guard the remote fetch behind an isHttpUrl check, so the change covers both. [#670]

  • Fresh Docker Compose dev installs no longer boot-loop with SQLITE_CANTOPEN. .env.example ships DATABASE_NAME=openwa (a PostgreSQL db-name) which, in the SQLite dev compose, became the SQLite file path — SQLite tried to open a bare file on the read-only container rootfs and retried 9× per boot. The dev compose now forwards a blank DATABASE_NAME default (matching production), .env.example documents it as PostgreSQL-only, and env-validation rejects a bare SQLite name with a clear boot error. [#677] [#680]

  • DATABASE_TYPE=postgres combined with DATABASE_SYNCHRONIZE=true is now rejected at boot. On PostgreSQL the data connection runs migrations every boot, so synchronize immediately dropped the search migration's generated body_ts column and broke /search (501) on every restart; the misconfigured combination now fails fast. [#680]

  • Resolved internal IPs are no longer leaked in SSRF-block error messages. The webhook test response, delivery-failure/DLQ lastError, webhook:error hook payloads, and plugin-download errors now show a generic message instead of the resolved internal address (which was a server-side-recon oracle). The full detail is still logged server-side. [#680]

  • STORE_EPHEMERAL_MESSAGES=false is now honored on Baileys history backfill. Previously only the live inbound path enforced the opt-out, so disappearing-message history was silently persisted + indexed on every connect/reconnect despite the operator's opt-out. [#680]

  • Plugin archive extraction is bounded and fails clean. Per-entry and aggregate actual extracted bytes are now capped (a lying size=0 header can no longer inflate unboundedly), and a corrupt/oversized archive returns a 400 instead of an uncaught 500. [#680]

  • WebSocket auth lifecycle. IP-restricted API keys can now connect from an allowed IP (previously the gateway failed-closed and locked out every IP-restricted key), and a revoked/disabled key's active sockets are now evicted. [#680]

  • Misc hardening. The migration CLI honors MAIN_DATABASE_NAME; secret-file chmod failures are logged (a world-readable secret file no longer stays silent after a rewrite on a chmod-unsupported FS); the fully-expanded IPv4-mapped-loopback SSRF classifier gap is closed (latent, defense-in-depth); storage file-list/export traversal is now async + bounded (no longer blocks the event loop); the dashboard and all four SDKs warn on a non-localhost http:// baseUrl. [#680]

v0.8.13

Choose a tag to compare

@rmyndharis rmyndharis released this 09 Jul 05:16

Added

  • Dashboard search panel. The Chats page now has a global search bar in its header — type to search messages across all sessions, see results with highlighted snippets in a dropdown, and click a result to jump to that message in its chat (with cross-session navigation and best-effort message scrolling). Includes a scope toggle (all sessions / current session), load-more pagination, and graceful error states (incl. "search unavailable" when search is disabled). The snippet renderer is XSS-safe (escape-then-highlight, never dangerouslySetInnerHTML). Search UI strings are localized across all 10 supported locales.
  • SDK search resources. The JavaScript, Python, PHP, and Java SDKs now expose a search resource mirroring GET /api/searchclient.search.search({ q, sessionId?, chatId?, … })SearchResults. Typed params and results match the backend contract exactly.

v0.8.12

Choose a tag to compare

@rmyndharis rmyndharis released this 09 Jul 01:11

Fixed

  • Debian Chromium SIGTRAP crash in Kubernetes. The Dockerfile previously installed the OS-level chromium package from Debian 12. When run as a non-root user in strict Docker/Kubernetes environments, this package consistently crashed at startup with Code: null and Trace/breakpoint trap (core dumped). On amd64 the Dockerfile now downloads Chrome for Testing via Puppeteer during the build phase (avoiding the Debian package's SIGTRAP); arm64 keeps Debian's chromium, since Chrome for Testing publishes no linux-arm64 build. Both resolve to a single PUPPETEER_EXECUTABLE_PATH symlink. This resolves the persistent browser launch failures on restricted environments without requiring insecure workarounds like --no-zygote or disabling Seccomp/AppArmor. Thanks @muhfalihr.

Added

  • Global message search across sessions. GET /api/search finds messages across all sessions through an open SearchProvider contract, with a built-in database full-text provider (PostgreSQL tsvector/GIN, SQLite FTS5) as the zero-dependency default — no external service required. Search works out of the box on both SQLite and PostgreSQL and survives repeated dialect switching and export/import round-trips; a non-FTS5 SQLite build skips the index gracefully and the route returns 501 instead of crashing boot. Set SEARCH_ENABLED=false to disable the route and module entirely (the index is DB-maintained per-write regardless, at negligible in-process cost). Advanced backends (typo-tolerance, CJK word-segmentation, large-scale relevance) will be available as provider plugins.
  • message:persisted plugin hook. A new general extension point fired when a message is durably persisted — on outbound send (from MessageService) and on inbound receive (from SessionService, where both engine adapters converge on the single persist) — so plugins can react to persisted messages (e.g. an external search indexer) without coupling to the message/session services. The built-in FTS search provider is DB-synced and does not consume this hook; it exists for plugin providers and general use. Fire-and-forget: a handler error is swallowed and never breaks the send/receive path.
  • Redis authentication via username. Added support for REDIS_USERNAME to configure the Redis cache connection and BullMQ queue connections that require a username. Thanks @muhfalihr.
  • OpenAPI/Swagger snapshot export + enriched, auth-accurate API docs. The published spec (served at /api/docs and exportable via scripts/export-openapi.ts) now mirrors the runtime auth model: @Public() routes (/api/health*, /api/infra/health, and the integration ingress wildcard) are marked as not requiring an X-API-Key, matching what the ApiKeyGuard actually enforces. The webhook events enum now advertises the * subscribe-all wildcard the dispatch layer already honors, and request/response schemas are filled in across the catalog, status, stats, integration-instance, and auth endpoints — notably typing the integration InstanceView (and its IngressUrl entries) as real schemas so generated clients no longer receive opaque objects. The export script pins a hermetic environment (in-memory SQLite, queue/MCP off) so it is safe to run anywhere without touching a real database or opening a Redis connection.

v0.8.11

Choose a tag to compare

@rmyndharis rmyndharis released this 08 Jul 04:03

Added

  • Prometheus counter for terminally-failed webhook deliveries. /api/metrics now exposes openwa_webhook_delivery_failures_total, incremented once per delivery that exhausts all its retries (mirroring the durable webhook_delivery_failures dead-letter record, on both the queued and the queue-disabled direct path). It is an in-process monotonic counter — cheap, real-time, and resetting only on restart, which Prometheus rate()/increase() treat as a normal counter reset — so a COUNT(*) over the retention-pruned failure table isn't queried per scrape. Operators can now alert on webhook failure rate from the scrape instead of tailing the structured log or querying the dead-letter endpoint.

Changed

  • Runtime feature flags are centralized in the config layer. AUTO_START_SESSIONS, STORE_EPHEMERAL_MESSAGES, RESOLVE_LID_TO_PHONE, SIMULATE_TYPING, and SIMULATE_TYPING_MAX_MS now resolve through a single discoverable features.* namespace on ConfigService (backed by one computeFeatureFlags() source of truth) instead of ad-hoc process.env reads scattered across the session and message services. Runtime behavior and defaults are unchanged. The four boolean flags are now validated at boot alongside the existing QUEUE_ENABLED/MCP_ENABLED/SERVE_DASHBOARD checks — ⚠️ behavior change: a deployment booting one of them with a non-canonical value (e.g. SIMULATE_TYPING=1, AUTO_START_SESSIONS=yes) will now fail fast naming the offending key until corrected to true/false/unset, instead of silently falling back to the default.
  • Coverage floors added for the session, webhook, and hook-manager modules. Per-directory Jest coverageThreshold entries now guard src/modules/session, src/modules/webhook, and src/core/hooks (set just below measured, matching the ratcheted floors already in place for the security, auth, engine-adapter, and integration modules), so a large deletion of these modules' tests fails CI instead of passing under the softer global gate.

Fixed

  • Inbound integration (ingress) deliveries now retry with backoff instead of failing on the first error. A queued ingress job was enqueued with no retry policy, so BullMQ ran a single attempt and a transient plugin-handler error (e.g. a 5xx from the sandbox) went straight to the dead-letter table — asymmetric with the webhook queue, which already retries. Ingress jobs now use bounded exponential-backoff retries (default 3 attempts, 5s base delay; configurable via INGRESS_MAX_ATTEMPTS and INGRESS_RETRY_DELAY_MS), and the dead-letter write still fires exactly once, only after the retries are exhausted. ⚠️ Ordering caveat: the per-conversation ordering lock guarantees no two same-conversation dispatches run concurrently, but a retried delivery re-enters the lock after its backoff and can therefore overtake a same-conversation successor that dispatched during the backoff window. Ingress order is best-effort regardless (the provider delivers over unordered HTTP); order-strict plugins must not assume a retried event still arrives in sequence. This trades strict order for throughput — BullMQ releases the worker slot during backoff, where retrying inside the lock would hold it.
  • /infra/status now actively probes the databases (SELECT 1) instead of trusting isInitialized. The Infrastructure panel's database tile read only DataSource.isInitialized, which stays true after a PostgreSQL backend dies (until an explicit .destroy()), so the tile showed the database healthy while it was actually down. The endpoint now runs a short, timeout-bounded SELECT 1 on both connections — the same probe /health/ready uses — so a post-init outage is reflected. SQLite is effectively always up, so this only changes behavior for a genuinely-down external PostgreSQL.
  • The settings panel reports the real docs and base-URL configuration. GET /settings hardcoded enableDocs: true (ignoring ENABLE_SWAGGER, so it claimed the API docs were enabled in production where they are disabled by default) and an http://localhost:<port> apiBaseUrl (ignoring the operator's configured BASE_URL). Both now reflect the real values, and autoReconnect reports the engine's actual default (on) rather than a hardcoded false for a config key that never existed.
  • A reaction no longer clobbers a message's delivery status. applyReaction read the full message row, mutated its reactions metadata, then wrote the whole row back — so a delivery ack (SENTDELIVERED/READ) that committed in the window between that read and write was overwritten with the stale status, permanently regressing the message's state until another ack happened to arrive. The reaction now writes only the metadata column via a scoped UPDATE keyed on (sessionId, waMessageId), so it can never touch status (acks and reactions update disjoint columns). The existing per-message serialization for concurrent reactions is unchanged.
  • PUT /infra/config returns the real HTTP status for a rejected configuration. A validation failure (an unknown engine type, or a value carrying a newline that would inject an extra env var) was caught and returned as HTTP 200 with { saved: false }, so a client branching on the status code alone treated a rejected save as success. Such validation errors now surface as their real 4xx (BadRequestException); a genuine persistence fault (e.g. a disk/permission error while writing the env file) still returns { saved: false } with 200, preserving the dashboard's body.saved handling.
  • Deleting a session no longer orphans its webhooks, templates, or stored Baileys messages on SQLite. webhooks, templates, and baileys_stored_messages declare an ON DELETE CASCADE foreign key to sessions, but the default data engine (SQLite) runs with foreign_keys OFF, so that cascade never fired — a session delete removed only the session, messages, and message_batches rows and left the rest behind indefinitely (orphaned webhooks rows in particular retain their signing secret and any custom headers). delete() now removes all CASCADE-FK child rows explicitly inside the same transaction (children before the parent), which is engine-agnostic — redundant-but-harmless on PostgreSQL, where the real cascade already handles it — and mirrors the ordering the data-restore path already uses.
  • The message:sending moderation gate and message:failed notification now cover every outbound path. Both hooks were wired only into the text sendText method, so a plugin registered on message:sending (the canonical pre-send moderation/compliance gate) saw no image/video/audio/document/sticker/location/contact/poll/reply/forward send and no bulk send at all, and a message:failed plugin saw only text-send failures. Every single sender now passes through a shared pre-send gate (a plugin can block or rewrite the payload) and a shared failure emitter, and BulkMessageService runs the same per-message gate (a block fails just that message, honoring stopOnError) and emits message:failed on a failed batch item. ⚠️ Behavior change: a message:sending plugin now receives — and can block/modify — media, extended, and bulk sends it previously never saw; the hook payload carries a type discriminator (image/video/poll/reply/… ) so a handler can scope its logic per send type, and its error field is sanitized so an SSRF-blocked media fetch does not expose the resolved internal address to plugins. A moderation block on a bulk item fails just that item (honoring stopOnError) without emitting message:failed, matching single-send where a block is a client error, not a delivery failure. message:sent is unchanged (still emitted once from the engine message_create path).
  • Sibling webhooks subscribed to the same event now get distinct idempotency keys. The per-event idempotency key was derived only from the event and its payload, so two different webhook endpoints registered for the same event on the same session received an identical X-OpenWA-Idempotency-Key. A receiver sitting behind both (or a shared dedup store) could drop the second endpoint's delivery as a replay of the first. The key is now salted with the destination webhook's id, so each endpoint is dedup'd independently while retries of the same delivery (including the queue-add→direct fallback) keep their stable key.
  • A session with auto-reconnect turned off no longer reports "reconnection failed after 0 attempts". When a disconnect fired with the reconnect budget set to 0 (auto-reconnect disabled), the session was marked FAILED with the exhausted-retries message and a count of 0 — implying a retry loop had run and failed rather than a feature that is simply off. That case now records an explicit "Auto-reconnect is disabled" reason; the genuine exhausted-retries message (with its real attempt count) is unchanged.
  • A failed inbound integration (ingress) delivery is no longer silently dropped on the inline path. With the queue disabled (or Redis unreachable), an ingress delivery is dispatched inline; if the plugin handler threw, the error was swallowed and the event was stranded — the provider had already received its 202, and the redrive tooling only scans the dead-letter table, which never got a row. The inline path now persists a dead-letter record (the same shape the queued path writes on its final failed attempt), so the event is redrivable. The write is best-effort: a failure to persist it is logged but never turns the 202 into a 5xx (the delivery is already dedup-persisted, so the provider won't re-send).
  • Plugin instance session bindings are re-derived on startup, so a binding lost while the plugin was momentarily unloaded self-heals. Provisioning a plugin instance mirrors its config into the plugin runtime (per-session config + activation) so an ingress handler resolves the right...
Read more

v0.8.10

Choose a tag to compare

@rmyndharis rmyndharis released this 07 Jul 01:20

Added

  • PostgreSQL schema selection via POSTGRES_SCHEMA. OpenWA's tables and the TypeORM migration ledger can now be placed in a dedicated Postgres schema (default public preserves historical behavior). Set POSTGRES_SCHEMA to isolate OpenWA from other apps sharing a database, or to use a managed-Postgres project schema. The schema must already exist (the built-in container creates it; for external Postgres run CREATE SCHEMA <name>; once). SQLite ignores this setting. The dashboard Infrastructure page exposes the field, and the environment variable is validated as a legal, non-reserved Postgres identifier at boot.

Changed

  • OpenAPI/Swagger tag hygiene. Every controller tag is now declared in the API document (ten were used but undeclared), the three Integration Fabric controllers gained @ApiTags, and the tag casing is uniform — so /api/docs groups every endpoint under a described tag instead of leaving some ungrouped.
  • Graceful shutdown now drains on SIGTERM/SIGINT (rolling deploys, docker stop, Ctrl+C), not just on the admin restart endpoint. On a termination signal the app flips readiness to 503 immediately so a load balancer/orchestrator stops routing, keeps serving in-flight requests for a bounded grace window, then tears down and exits deterministically. ⚠️ Behavior change: a docker stop / redeploy now takes up to the grace window (SHUTDOWN_DELAY_MS, default 3s in production, 0 in dev) plus teardown instead of tearing down instantly, and the process now exits 0 on a clean signal. In Docker set stop_grace_periodSHUTDOWN_DELAY_MS + your worst-case teardown (the bundled compose now sets 45s); for Kubernetes set terminationGracePeriodSeconds accordingly. A second signal during the drain forces an immediate exit. The whatsapp-web.js engine no longer lets Puppeteer install its own signal handlers (which previously killed Chromium at signal time / exit(130) before the drain could run).
  • The bundled Docker Compose stack pins its docker-socket-proxy and minio images to explicit tags (they were on :latest) for reproducible, non-drifting deploys, and a Dependabot docker ecosystem was added so base and stack images keep receiving update PRs. A Node >=22 engines floor + .nvmrc were declared, and the transitive install-time Scarf telemetry (via swagger-ui-dist) is disabled.

Fixed

  • The Integration Fabric now works on PostgreSQL. conversation_mappings and integration_delivery_failures declared @PrimaryGeneratedColumn('uuid') ids but their columns were created without a Postgres DEFAULT gen_random_uuid(), so on PostgreSQL every first insert failed with a NOT NULL violation on id — breaking the plugin conversation-mapping upsert (e.g. the Chatwoot handover) and the ingress dead-letter write. SQLite was unaffected because its driver mints the uuid client-side, which is why it went unnoticed. A forward-only migration adds the default (no-op on SQLite). A new CI job now applies the full migration chain against a real PostgreSQL and asserts every generated-uuid primary key has a database default, so this dialect gap can't recur.
  • Indexed webhooks.sessionId. The webhook dispatch path looks up a session's active webhooks by sessionId on every emitted event, so on a busy session this was a full table scan of the webhooks table per event (the foreign-key column carried no index). A cross-dialect index migration — plus the matching entity index — makes the lookup index-backed.
  • Boot now rejects a non-canonical boolean feature flag instead of silently disabling the feature. QUEUE_ENABLED, MCP_ENABLED, and SERVE_DASHBOARD are read with an exact === 'true' / !== 'false' comparison, so a typo (True, 1, yes) or a stray trailing space/CR (a Windows-edited env file forwarded verbatim by docker run --env-file) silently (dis)abled the feature with zero diagnostics. These are now validated at startup and boot fails fast naming the offending key. ⚠️ Behavior change: a deployment currently booting with such a value (e.g. QUEUE_ENABLED=1) will now refuse to start until corrected to true/false/unset — including SERVE_DASHBOARD=0/no, which was silently serving the dashboard and will now correctly disable it once set to false.
  • A fatal uncaught exception is now written to the structured log (with its stack and origin) before the process exits, instead of only a raw stack on stderr that the log pipeline missed. This is observe-only: the crash-and-restart posture is unchanged (the container restart policy still fires and the process never continues on corrupted post-exception state).
  • POST /infra/import-data no longer swallows a genuine database error while clearing tables. The table-clearing step tolerated only a genuinely-absent table but previously used a blanket catch, so an I/O/lock error (or an aborted transaction) could let a restore commit a merged rather than replaced dataset on SQLite. Such errors now surface and roll the whole import back (a real fault returns a 500 carrying the actual cause); the intended tolerance for a missing table is preserved.
  • A session no longer schedules a reconnect while the process is shutting down — a disconnect during the drain window would otherwise launch a fresh Chromium racing the shutdown teardown. The session is left DISCONNECTED (a later start / auto-restore re-initializes it cleanly).
  • Documentation & config accuracy. .env.example now documents PORT (the port the app binds to on bare metal) distinctly from the Compose-only host-published API_PORT, and adds the QUEUE_ENABLED/CACHE_ENABLED toggles. SECURITY.md's supported-versions table and the Java SDK install snippets are refreshed to the current releases. The unused uuid/@types/uuid dependency was removed, and stale "not yet wired" comments on the plugin ingress-manifest validation (which the loader has called since it shipped) were corrected.
  • The bundled Docker Compose stack no longer kills Chromium mid-spawn under multi-session whatsapp-web.js workloads (#636). The per-container pids_limit shipped at 512 since the #243 hardening pass — a fork-bomb guard chosen without accounting for Chromium's multi-process model. whatsapp-web.js runs a full Chromium instance per session (browser + renderer + GPU + zygote + utilities), and WhatsApp Web is itself process-heavy, so ~4 concurrent sessions already approached 512 and the next session's Chromium was killed mid-spawn when fork() returned EAGAIN — surfacing in the API as a Failed to launch the browser process: Code: null launch failure with no useful log (the dbus/crashpad noise in the log is non-fatal). The default is now 2048 (fits ~8–10 sessions with startup-spike headroom), exposed as OPENWA_PIDS_LIMIT for larger fleets. The limit is a cgroup pids.max ceiling, not an allocation — raising it is a no-op for light containers, so this is safe for the baileys engine (single-process, no Chromium, a handful of PIDs regardless). The fork-bomb guard stays finite (-1/unlimited is explicitly discouraged). A new troubleshooting entry distinguishes the three causes of Code: null (PID exhaustion vs OOM-kill vs the XDG/crashpad crash already fixed earlier), since the cause isn't visible in the log without docker stats / dmesg.

v0.8.9

Choose a tag to compare

@rmyndharis rmyndharis released this 06 Jul 10:21

Changed

  • Dashboard <select> elements replaced with a custom dropdown component. The "All Status" filter (Sessions), "All Severities" filter (Logs), and language picker (Login) now use a reusable CustomSelect component that matches the dashboard design system with proper dark/light theming, keyboard navigation (arrows, Home/End, type-ahead, Escape), and responsive behavior. Focus returns to the trigger on close, matching native <select> semantics. Thanks @haseeblodhi1899.
  • The "Install a plugin" modal is wider on desktop (480px → 680px) to give the plugin catalog list more room, while still collapsing to a full-width bottom sheet on small screens.
  • Webhook delivery-failure records are pruned on a retention window. webhook_delivery_failures is an append-only log written on every terminally-failed delivery, so under a receiver outage it grew without bound. It is now pruned to WEBHOOK_FAILURE_RETENTION_DAYS (default 90; set <= 0 to disable) once at startup and daily, mirroring the existing audit-log retention.

Fixed

  • A malformed session id now returns 400 instead of a 500 on PostgreSQL. The session routes validate the :id path param as a UUID at the boundary, so a non-UUID id (a typo or path fuzzing) is rejected with a clean 400 rather than reaching the uuid primary-key column and raising an uncaught cast error that surfaced as a generic 500 — a divergence that only appeared on PostgreSQL (SQLite treated the id as text and returned 404).
  • Baileys API sends now emit message.sent (parity with the whatsapp-web.js engine). The wwjs engine fires this for the account's own sends; the Baileys engine's own socket-sends echo back only as a skipped history-sync upsert, so message.sent webhooks / WebSocket events / the message:sent hook never fired for Baileys API sends. They now fire for text and every media/location/contact/poll/reply/forward send (reactions and deletes excluded).
  • Config & reliability hardening. DATABASE_STATEMENT_TIMEOUT_MS, DATABASE_IDLE_TIMEOUT_MS, and DATABASE_CONNECTION_TIMEOUT_MS are validated at boot — a typo previously reached the config layer as NaN and broke the PostgreSQL pool. An unparseable BODY_SIZE_LIMIT no longer silently disables the request body-size cap (it falls back to the 25 MB default). The channel-messages endpoint no longer forwards NaN to the engine on a non-numeric ?limit. The fire-and-forget session-row writes in the engine callbacks now handle a transient DB fault instead of surfacing an unhandled rejection, and a set of engine-adapter warning logs no longer mislabel their component name.
  • A terminally-failed or un-reinitializable session no longer strands its browser process or wedges at "already started". When an engine reports a terminal error, and when a reconnect attempt's re-initialization throws, the dead or half-built engine is now evicted from the session registry and its Chromium process is force-killed instead of being left in place — previously it kept holding a concurrency slot and caused a later start to be rejected as already running. Deleting a session likewise force-kills its browser (rather than a graceful close that could hang on a wedged Chromium and orphan the process).
  • The dark theme now covers every dashboard surface. A number of components used hardcoded colors instead of the theme's CSS variables, so several surfaces stayed light in dark mode — most visibly the Infrastructure "Database Migrations" card, plus status/severity badges, toasts, danger-hover states, and toggle tracks across most pages. They now use the theme tokens (and translucent semantic fills) so they follow the active theme in both light and dark. A new --info token themes the blue badges (permission, SQLite, info logs, qr-ready pill) that previously had no theme-aware color, and the root <html> background no longer stays white when the dark theme is selected on a light-OS device (visible on overscroll).

v0.8.8

Choose a tag to compare

@rmyndharis rmyndharis released this 05 Jul 15:56

Added

  • Native WhatsApp polls via POST /api/sessions/:sessionId/messages/send-poll: question, 2–12 options and an optional allowMultipleAnswers flag (default single choice), implemented on both engines (whatsapp-web.js Poll, Baileys poll content with selectableCount 1/0). The message history stores the poll question as the body so the log stays readable. Polls are a first-class poll message type end to end — both engines map incoming poll messages to it, so the websocket/webhook events, persisted rows, and dashboard all report poll consistently. Thanks @alejo117.

Changed

Fixed

  • GET /api/sessions/:sessionId/channels/:channelId/messages always returned an empty array on the whatsapp-web.js engine (#625). The adapter called client.getChannelById(), which does not exist in whatsapp-web.js 1.34.x, so every call threw and the error was swallowed into []. Channel messages are now read from the subscribed Channel instance (via getChannels()), and an unknown/unsubscribed channel returns a 404 (ChannelNotFoundError) instead of a silent empty 200 — matching GET /channels/:channelId. Thanks @Header9968.
  • A session whose engine.initialize() fails no longer orphans its browser process. The crash-recovery path in SessionService.start() was tearing down the half-built engine with a graceful destroy(), but a failed initialize() usually means the underlying browser/CDP connection is already broken (e.g. a TargetCloseError: Target closed mid-injection) — destroy() has nothing live to talk to, so it could only time out after 10s via teardownEngineSafely's race, leaving the Chromium process alive and orphaned. Every such crash left one more orphaned process behind, eventually starving the host of memory. It now uses forceDestroy() (the same SIGKILL-the-process recovery POST /:id/force-kill uses), since a failed initialize is the same "possibly-unreachable engine" state that exists for.
  • Authenticated HTTP/HTTPS proxies now work on the whatsapp-web.js engine (#628). Credentials were passed inside --proxy-server, which Chromium ignores, so a proxy with a username/password never authenticated (only IP-authorized proxies worked). The username/password are now handed to whatsapp-web.js's proxyAuthentication (which drives Chromium's page.authenticate) while --proxy-server gets a credential-less URL. SOCKS proxies still cannot be authenticated — Chromium does not support SOCKS proxy authentication at all — so a SOCKS proxy carrying credentials now logs a clear warning instead of failing with an opaque navigation timeout. Thanks @gudge25.

v0.8.7

Choose a tag to compare

@rmyndharis rmyndharis released this 03 Jul 12:28

Added

  • Plugins can canonicalize a chat id via a new ctx.engine.canonicalChatId(sessionId, chatId) capability, gated by the engine:read permission like the other engine reads. It resolves a @lid privacy id to its stable <phone>@c.us form when the mapping is known (best-effort; an unresolved id passes through), letting a plugin key a chat by one identity across WhatsApp's @lid migration. This is the host-side prerequisite for an adapter to keep a contact's conversation from splitting when they migrate to @lid. (#615)

v0.8.6

Choose a tag to compare

@rmyndharis rmyndharis released this 03 Jul 09:56

Fixed

  • The engine.getChatHistory plugin capability (added in 0.8.5) now reaches sandboxed plugins. It was wired only into the host-side context, not the plugin-worker bridge, so a sandboxed plugin's ctx.engine.getChatHistory was undefined and the call failed silently. It is now bridged through the worker capability + router like the other engine reads. Historical messages from the whatsapp-web.js engine also carry location coordinates and quoted-message references now, matching the live message path (previously a backfilled location rendered empty and replies lost their thread link). (#609)

v0.8.5

Choose a tag to compare

@rmyndharis rmyndharis released this 03 Jul 09:15

Added

  • Plugins can read recent chat history via a new ctx.engine.getChatHistory(sessionId, chatId, limit?, includeMedia?) capability, gated by the engine:read permission and the plugin's active-session scope like the other engine reads. The limit is clamped host-side (max 100), and both message directions are returned. This is the host-side prerequisite for an adapter to backfill prior conversation context. (#609)