github rmyndharis/OpenWA v0.16.0

5 hours ago

Added

  • The JavaScript, Python, PHP, Go and Java SDKs expose POST /sessions/:id/chats/pin and chats/mute, the two routes this release adds, so neither ships without a client. muteUntil is epoch milliseconds and null unmutes; both are required, since an omitted value reads as either.
  • POST /sessions/:sessionId/channels/:channelId/owner/transfer hands a channel to a new owner on the Baileys engine; it is irreversible, and the account cannot take the channel back. The whatsapp-web.js engine answers 501.
  • POST /sessions/:sessionId/channels/:channelId/admins/demote demotes a channel admin back to a subscriber on the Baileys engine; neither library has a promote counterpart, so promote from the WhatsApp app. The whatsapp-web.js engine answers 501.
  • POST /sessions/:id/chats/pin pins a chat to the top of the list or unpins it, on both engines; success: false reports WhatsApp's three-pin cap, which only the whatsapp-web.js engine can observe.
  • POST /sessions/:id/chats/mute mutes a chat until an epoch-milliseconds timestamp or unmutes it with an explicit null, on both engines; unlike chats/archive it has no declined outcome, since the change is not keyed to the chat's last message.
  • engine-inventory-parity.spec.ts fails when a docs/29 exposure or event mark no longer matches the adapters: a symbol marked as used must appear in adapter code rather than only in a comment, and an event marked consumed must have a listener behind it.
  • docs/29-engine-capability-matrix.md (docs/29) now covers all 152 Baileys socket methods, all 81 whatsapp-web.js Client methods, all 34 + 31 library events and all seven install-time patches, each mapped to the interface method that uses it or marked unexposed.
  • An onboarding modal the "What's new" probe does not recognise is now logged once with its heading and confirm-button labels (onboarding_dialog_unrecognized), so it can be covered via WWEBJS_ONBOARDING_CONTINUE_LABELS before WhatsApp unlinks the companion. Refs #1072.
  • npm run check:sdk-events compares each typed SDK's webhook event list to the contract, so an event the gateway accepts can no longer be missing from the SDKs.
  • npm run check:sdk-docs compares the SDK design doc and the coverage tables to the shipped client surface, so a client method can no longer ship undocumented.
  • npm run check:sdk-coverage fails when the contract publishes a route no client exposes, which is the direction check:sdk-routes never ran. It reports per SDK, so a route three clients have and two do not is a failure rather than a healthy total, and it reads its exclusion list from sdk/README.md instead of restating it.
  • POST /sessions/:sessionId/calls/link generates a shareable WhatsApp call link on both engines, returning the finished https://call.whatsapp.com/… URL; a WhatsApp-side failure answers 403 rather than a success carrying an empty link.
  • DELETE /sessions/:sessionId/profile/picture removes the account profile picture on both engines; removing one that is already absent is a no-op answering 200, so the call is safe to repeat.
  • docs-29-counts.spec.ts binds every count-shaped claim in docs/29 to the capability matrix, so a figure restated in the intro, the architecture prose, a mermaid node or a section heading cannot drift from the source it restates.
  • npm run check:chart asserts what the rendered chart would do in a cluster, which helm lint and kubeconform cannot: config changes roll the pods, the startup budget exceeds the liveness budget, the metrics selector names one Service.
  • The JavaScript, Python, PHP, Go and Java SDKs expose PUT /sessions/:id/presence, which sets the account's own global presence — not a chat's; the per-chat typing and recording states are unchanged.
  • The JavaScript, Python, PHP, Go and Java SDKs expose the three group membership-request routes — list, approve and reject; omitting the participant list acts on every pending request, and a partial refusal still answers 200 with each outcome in results.
  • The JavaScript, Python, PHP, Go and Java SDKs expose GET /sessions/:sessionId/contacts/blocked as listBlocked (list_blocked in Python), reading back what block and unblock have done. It needs only a read-level key.
  • The JavaScript, Python, PHP, Go and Java SDKs expose POST /sessions/:sessionId/calls/link, which creates a shareable WhatsApp call link. startTime is absolute epoch milliseconds and is required, so a link for right now carries the current timestamp rather than an omitted field.
  • The JavaScript, Python, PHP, Go and Java SDKs expose the two channel administration routes — demote an admin back to a subscriber, and transfer ownership. Both need an OPERATOR key, the transfer is irreversible, and the whatsapp-web.js engine answers 501 for either.

Changed

  • ⚠️ Breaking (behavior). POST /sessions/:sessionId/groups answers 501 on the whatsapp-web.js engine instead of attempting the creation. whatsapp-web.js still declares createGroup, but its page code reaches a WhatsApp Web internal that no longer exists, so every call already failed — as an opaque 500 rather than a stated refusal. Measured against a live session on two WhatsApp Web builds, one auto-resolved and one pinned, with the same TypeError; bare and @c.us-qualified participant ids fail identically. Action required: create groups through the Baileys engine, which is unaffected. The route, its body and its Baileys behaviour are unchanged.

Fixed

  • POST /sessions/:id/chats/pin and chats/mute answer 400 instead of an undeclared 500 on the whatsapp-web.js engine when handed a chat the session cannot resolve. Both routes are new in this release and were the only two members of their family without the resolution guard their neighbours carry, so a stale chatId reached the page and rejected there — No LID for user for a pin, a TypeError from inside _muteUnmuteChat for a mute. An unknown chat is reported as bad input rather than as success: false, which on the pin route already means the three-pin cap refused a real chat. A page-side failure on a chat that does resolve is still a 500, since that is the only signal a renamed WhatsApp Web internal produces. The Baileys engine writes app state without resolving the chat first and still answers success: true.
  • docs/29's architecture diagram counted four whatsapp-web.js patches and one Baileys patch while this release shipped a fifth and a second, and 🔧⁷ marked no cell, so the three whatsapp-web.js participant writes read as working against a stock library. The patch counts are now derived from scripts/ by docs-29-counts.spec.ts, which bound only the interface figures before. docs/09 §9.6 also omitted the two gates added to CI and described the release workflow as a lighter gate than the one it now runs.
  • check:sdk-coverage no longer passes when a client drops a route entirely: a wildcard the client built for a path parameter — contacts/${contactId} — was read as a one-builder family and stood in for every concrete sibling under it, so removing contacts/blocked from a client left the gate green. A wildcard now counts as a family only where the contract publishes no route of that shape.
  • The Go SDK no longer approves or rejects every pending join request when handed an empty participant list: omitempty dropped the empty slice, so []string{} reached the gateway as the bodyless "act on every request" rather than the 400 the other four clients receive. A nil slice still means every request.
  • Approving or rejecting a join request by bare phone number works on the whatsapp-web.js engine instead of answering 500: the participant guard accepts a bare number and the three other participant writes qualify it, but these two routes handed it to the engine unqualified, so it reached the page as an unaddressable id. The same request already worked on Baileys. Refs #1220.
  • A burst of inbound media on the whatsapp-web.js engine no longer loses the media of everything past the eighth, which the Baileys repair in this release left in place on the other engine: its download queue was capped at its active slots too, making admission a constant 8 whatever the batch size. A burst now parks rather than shedding, and only the download waits — a text message never enters the limiter. The wait for a slot is bounded by MEDIA_DOWNLOAD_TIMEOUT_MS, so a burst queued behind a hung page still emits its messages without media rather than waiting on a slot that never frees.
  • A webhook's filters is published as nullable, the sibling of the lastTriggeredAt repair in this release: the field is stored as null for every webhook created without filters and the description offers null as an input, but the schema declared neither, so a client generated from the contract rejected a value the route both sends and accepts. An invariant now fails when any property documented as carrying null does not publish it.
  • The two channel administration routes reject a user id that does not name an individual with 400 instead of handing it to WhatsApp: both bodies accepted any non-empty string, and an id the engine cannot classify was forwarded verbatim, so free text or a group id failed opaquely. A bare phone number is qualified as it is on the group participant writes, and the check sits in the service rather than the body so it holds for any caller.
  • The chart's optional ServiceMonitor now selects on a new openwa.io/scrape-target label, so it scrapes one target per pod instead of two; series previously collected through the headless target stop appearing, so check anything keyed on the service label.
  • The Helm chart's new startup probe stops the kubelet killing a pod that is still booting, allowing 295s (periodSeconds: 5, failureThreshold: 60) where the liveness settings allowed 50. It fires immediately, so a fast boot goes live sooner than before, not later — and it is not limited to auto-start, since the database connect retry alone can consume 30 of the old 50 seconds.
  • A Helm upgrade that changes only env or secretEnv now restarts the pod through ConfigMap and Secret checksums, so the new configuration reaches the running container. With existingSecret set the chart renders no Secret, so that path still needs kubectl rollout restart; values.yaml notes what publishing the Secret checksum as a pod annotation means.
  • A participant id with a recognised domain but a nonsense user-part — NOT A USER@c.us, @c.us — is now rejected with 400 instead of reaching WhatsApp Web as an undiagnosable 500; the same rule backs the mentions validator and the membership-request routes, and a bare phone number is qualified before reaching the Baileys socket. Fixes #1220.
  • Messages that predate the full-text index are indexed on the next boot instead of staying permanently unsearchable, and edits and deletes on them work again; the messages_fts emptiness guard is now a rowid-level completeness check.
  • A WhatsApp-level refusal of a group participant add, remove, promote or demote on the Baileys engine is now 403 instead of an unhandled error; a dropped connection or a timeout is still reported as itself. Refs #1220.
  • Removing, promoting or demoting group participants on the whatsapp-web.js engine now reports who WhatsApp actually acted on: a request naming only non-members answers 403 rather than 500 or a false 200, and a mixed request reports the untouched entries as 404. An installation whose node_modules predates this release keeps the previous reporting. Refs #1220.
  • A group participant id that does not name an individual — free text, a group id, a number too short — is now rejected with 400 naming the offending entries instead of reaching the engine as an undiagnosable 500. The check sits in the service, so the MCP tools are guarded like the REST routes. Refs #1220.
  • Creating a channel on the Baileys engine no longer answers 500 while leaving the channel behind, so a retry no longer leaks an orphan newsletter; a new install-time patch reads the create response defensively and returns the channel's id.
  • A profile-picture lookup that fails on the whatsapp-web.js engine now answers 503 instead of the {"url": null} the route documents as "no picture"; the batch route keeps its best-effort null.
  • Promoting a participant who is already an admin, or demoting one who is already not, answers 200 instead of 500 on the whatsapp-web.js engine; the install-time patch skips a participant whose status already matches, making both operations idempotent.
  • The JavaScript, Python, Go and Java SDKs list group.join_request, an event the gateway has accepted and dispatched all along, so a typed client can subscribe to it without casting past its own type.
  • A webhook's lastTriggeredAt is published as a nullable date-time string instead of an object, so a client generated from the contract no longer rejects both of the values the field actually carries. The response itself is unchanged.
  • The group-list and status routes no longer appear twice in the OpenAPI contract under different path-parameter names, each key carrying one verb of the same URL. GET /sessions/{id}/groups is now {sessionId}, and the status read and delete both take {id} in place of {contactId} and {statusId}. The URLs are unchanged and no request that worked before behaves differently, but a client generated from the contract needs regenerating: the two half-endpoints collapse into one and the parameter names move with them. The status media route keeps {statusId}, since nothing collided there.
  • A bare @SkipThrottle() no longer leaves a route throttled: /api/metrics and the /api/health* probes were rate-limited despite being documented as exempt. All four are also @Public() and now carry no limit — rate-limit them at your proxy if they are internet-facing.
  • The chat-media retention purge and orphan sweep now run while CHAT_MEDIA_ARCHIVE_ENABLED is off, which previously stopped both. Consequence for such a deployment: the sweep now deletes any file under chat-media/ that no message row references, once unreferenced for CHAT_MEDIA_ORPHAN_GRACE_MS (default 1h).
  • A plugin whose code went missing is recoverable through the API again: reinstalling now writes over its surviving ctx.storage directory instead of answering 409, and uninstalling a known-but-unloaded id no longer answers 404.
  • A sticker sent through the Baileys engine no longer reaches WhatsApp as non-WebP bytes labelled image/webp: the adapter converts image/* to a 512×512 WebP, passes genuine WebP through unchanged, and refuses the rest with 400.
  • docs/18-sdk-design.md and the SDK READMEs now list every method the five clients ship: the whole media resource plus 26 methods across nine others were missing, so server-side media conversion, presence subscription, session config and webhook delivery diagnostics read as unavailable.
  • The engine parity check no longer skips optional interface members. probeLiveness?() did not match its member pattern, so the capability matrix could omit any optional method while the check reported green.
  • .env.example and the troubleshooting FAQ no longer name a specific WhatsApp Web build to pin with WWEBJS_WEB_VERSION; the build they named has been withdrawn, and one the registry no longer serves is silently ignored. Both now point at the registry's html/ folder.
  • A WhatsApp Web build that cannot be resolved from the wa-version registry is now reported (web_version_resolve_failed) with the reason, the registry URL and the remedy; an unreachable registry previously continued without a pin and logged nothing.
  • POST /sessions/:id/pairing-code answers 409 while a whatsapp-web.js session is still starting up, instead of a 500 carrying a raw TypeError; a code requested straight after POST /start landed in the Chromium launch window. It is now accepted only from qr_ready.
  • A burst of inbound media messages no longer loses the media of everything past the eighth: the Baileys download queue was capped at its active slots, making admission a constant 8 whatever the batch size. It is now unbounded.
  • The JavaScript SDK reports why a production gateway rejected a request instead of ending its error message in [object Object]; NestJS omits error from the envelope under disableErrorMessages, the default when NODE_ENV=production and VALIDATION_ERROR_DETAIL is unset.
  • Stopping or deleting a session id that does not exist no longer leaves an entry in the teardown-mark set, which nothing could clear and which grew by one for every such 404.
  • A plugin loaded from the legacy plugins directory can be enabled, uninstalled and updated like any other, and its config UI loads; every operation built its path as <plugins.dir>/<id> while the package was elsewhere, so enabling left it in ERROR and uninstall silently kept the code.
  • Auto-starting previously authenticated sessions no longer delays the HTTP listener; NestJS binds the port only after every bootstrap hook finishes, so a host with several sessions kept the port closed past the Helm chart's liveness budget and the Docker HEALTHCHECK.

Security

  • An advisory usage-statistics write no longer persists the whole API-key row: a key deleted mid-request was re-inserted and authenticated again, and a revocation or a narrowing of role, allowlist, IP allowlist or expiry was reverted. The write is now scoped to the two usage columns.
  • .env.example no longer ships ENABLE_SWAGGER=true uncommented: the template also declares NODE_ENV=production, so copying it pinned the opt-in that production withholds and served the schema and running version at /api/docs, which sits outside the API-key guard. Bare-metal operators who already copied it should check their own .env; Docker and Helm are unaffected.

Don't miss a new OpenWA release

NewReleases is sending notifications on new releases.