Skip to content

v0.8.17

Latest

Choose a tag to compare

@rmyndharis rmyndharis released this 13 Jul 00:39

Added

  • AuditAction emit-coverage gate. A structural test now fails the build when an AuditAction
    enum value is neither emitted at a real call site nor registered (with a reason) in a new
    intentionally-unemitted registry. A declared audit event can no longer silently exist with no
    emission site, and a new action cannot land without either wiring it or documenting why it is held
    back. The registry is also checked for stale entries (an action that is in fact emitted) and empty
    reasons, so it cannot decay into a dumping ground.

  • Operator-tunable HTTP server timeouts. The gateway now pins requestTimeout,
    headersTimeout, and keepAliveTimeout on its HTTP server explicitly (previously Node's
    implicit defaults), exposed via REQUEST_TIMEOUT_MS / HEADERS_TIMEOUT_MS /
    KEEPALIVE_TIMEOUT_MS and logged at boot. Defaults match Node 22 (300s / 65s / 5s);
    headersTimeout is normalized to stay above keepAliveTimeout (Node requires it), and the three
    are validated as positive integers at boot.

  • Committed OpenAPI snapshot + CI sync gate. The gateway's OpenAPI document is now committed as
    openapi.json (generated from the NestJS Swagger decorators via npm run openapi:export) and a CI
    check (npm run openapi:check) fails when a controller/DTO change lands without regenerating it, so
    the machine-readable contract can never silently drift from the code. SDK/API consumers now have a
    versioned artifact at the repo root.

  • Pre-release boot smoke on amd64 + arm64. Cutting a release tag now runs the just-published
    image on both linux/amd64 and linux/arm64 (via QEMU) and polls the dependency-free
    /api/health/live endpoint before the GitHub Release is created — so a runtime-only boot regression
    on one architecture (a clean build can still produce a native-dep/Chromium SIGTRAP on arm64) cannot
    ship under a release. The Release job waits on the new boot-smoke job.

  • SBOM attestation on published images. Each image built by CI and on release now carries an
    in-toto SBOM attestation alongside the SLSA provenance that docker/build-push-action already
    generates by default. Both are verifiable with
    docker buildx imagetools inspect ghcr.io/rmyndharis/openwa:<tag>. Provenance is now also pinned
    explicitly (provenance: true) so the attestation pair is self-documenting rather than reliant on
    the action default.

  • HTTP RED metrics. The /api/metrics endpoint now exposes
    http_requests_total{method,route,status} and an http_request_duration_seconds histogram (per
    route), recorded by a global interceptor. Route labels use the Express route pattern (bounded —
    /api/sessions/:id, not the raw URL) with a Controller#handler fallback, and /api/health +
    /api/metrics are not counted. Conventional unprefixed names so a generic RED dashboard or a 5xx
    error-rate alert matches them directly.

  • Request correlation ids (X-Request-ID). Every inbound request now carries an id that
    propagates through the whole request via AsyncLocalStorage, so each JSON log line and each
    audit-log metadata blob stamps it — a request can be traced end-to-end. A valid client-supplied
    X-Request-ID (alphanumeric + dash, ≤128 chars) is echoed; anything else (including a CRLF
    header-injection attempt) is replaced with a generated UUID. The id is also set on the response.

  • Engine capability matrix + drift gate. A committed, source-verified matrix
    (src/engine/engine-capability-matrix.ts) records, for every IWhatsAppEngine method on each
    engine (whatsapp-web.js default, Baileys), whether it is supported or not-available — and for
    the not-available ones, the root cause: adapter-gap (the underlying library supports it, OpenWA
    just hasn't wired it — fixable) vs library-limitation (no first-class library API), with the
    cited library symbol as evidence. A drift gate fails when a method's throw-availability changes.
    docs/engine-capability-matrix.md inventories the unwired adapter-gaps as a prioritized capability
    backlog.

  • Delete-for-me on the Baileys engine. deleteMessage(…, forEveryone=false) now performs a
    delete-for-me via Baileys' chatModify({ deleteForMe }) instead of returning 501. Revoke-for-
    everyone (forEveryone=true) was already wired; this completes deleteMessage on the Baileys
    engine for the most common delete mode.

  • Status posts on the whatsapp-web.js engine. postTextStatus, postImageStatus, and
    postVideoStatus now work on whatsapp-web.js (the default engine) — they route through
    sendMessage('status@broadcast', …) (text styling via extra: { backgroundColor, fontStyle };
    media via MessageMedia + caption). Previously these returned 501 on the default engine despite
    the library supporting them; the stale "blocked upstream, #455" guard is removed (#455 is a closed
    feature request, and whatsapp-web.js 1.34.7 ships a real status-send path). Caveat: the library has
    no status-recipient arg, so StatusPostOptions.recipients is not honored on this engine (it
    broadcasts to the account's status-privacy audience; a one-time warning is logged). The Baileys
    engine continues to honor recipients.

  • Chat labels on the Baileys engine. addLabelToChat and removeLabelFromChat now work on
    the Baileys engine — 1:1 to sock.addChatLabel(chatId, labelId) / sock.removeChatLabel(chatId, labelId) instead of returning 501. WhatsApp-Business-only (rejects on personal accounts). Label
    listing (getLabels / getLabelById / getChatLabels) remains unavailable on Baileys (no
    first-class library API — see docs/engine-capability-matrix.md).

  • Status delete on the whatsapp-web.js engine. deleteStatus(statusId) now works on
    whatsapp-web.js via client.revokeStatusMessage(statusId) instead of returning 501 — completing
    the status lifecycle (post + delete) on the default engine. Own-status only (the library revokes
    the caller's own status posts).

  • Read contact stories on the whatsapp-web.js engine. getContactStatuses() and
    getContactStatus(contactId) now return contact "stories" (24h status posts) via
    whatsapp-web.js getBroadcasts() / getBroadcastById() flattened to Status[] (contact via
    broadcast.getContact(), type from MessageTypes, 24h TTL) instead of stubbing to []. The
    Baileys engine still cannot read stories — fetchStatus returns the about text, not stories
    (documented as a library limitation).

  • Channel lookup / subscribe / unsubscribe on the Baileys engine. getChannelById(id),
    subscribeToChannel(inviteCode), and unsubscribeFromChannel(id) now work via Baileys
    newsletterMetadata (mapped to Channel with optional fields), newsletterFollow (subscribe,
    resolving invite→jid first), and newsletterUnfollow (unsubscribe, 1:1). getChannelById on
    Baileys resolves ANY channel by jid (richer than the whatsapp-web.js subscribed-list lookup).
    getChannelMessages remains unsupported — newsletterFetchMessages returns a raw BinaryNode with
    no library parser, so it stays a documented gap rather than an unverified walk.

  • Bounded webhook fan-out. An event matching N webhooks now delivers at most
    WEBHOOK_DISPATCH_CONCURRENCY (default 16) concurrently instead of opening N outbound sockets at
    once; the rest queue and run as slots free. Per-webhook isolation (Promise.allSettled) is
    unchanged. The shared ConcurrencyLimiter was also promoted from engine/adapters to
    common/utils (it has no engine-specific logic), so the webhook module no longer imports across the
    engine boundary.

  • Optional Redis-backed rate-limit storage. When REDIS_ENABLED=true, API rate-limit counters
    now persist to Redis (a new RedisThrottlerStorage implementing @nestjs/throttler v6's
    ThrottlerStorage), so limits aggregate across replicas behind a load balancer instead of being
    per-process. Default off (single-node deployments gain nothing and it adds a connection dependency).
    Fail-OPEN on Redis error — rate limiting is a secondary control, and fail-closed would self-DoS the
    API.

Fixed

  • Silent delivery failure for 1:1 Baileys sends to LID-migrated contacts (ack 463). On the
    Baileys engine, a 1:1 send addressed by phone (<phone>@c.us / <phone>@s.whatsapp.net) to a
    contact WhatsApp has migrated to LID addressing was rejected server-side with ack error 463
    (NackCallerReachoutTimelocked / "missing tctoken" — the privacy token is stored and honored
    under the LID), while the same send addressed to the contact's <lid>@lid delivered. Because
    Baileys generates the message id locally, the API still returned a messageId, so the
    non-delivery was silent to the caller. The adapter now resolves phone-dialect 1:1 chat ids to
    the contact's LID at the send boundary via sock.signalRepository.lidMapping.getLIDForPN (the
    same mapping the Baileys send path consults), applied in sendTextMessage, sendContent (all
    media/location/contact/poll sends), and sendChatState. Groups, broadcast, already-@lid, and
    unmapped ids pass through unchanged (non-migrated contacts behave identically), and resolution
    is best-effort: any lookup error falls back to the phone jid. The disappearing-timer lookup
    still resolves under the LID, since getEphemeralExpiration already keys on the raw, engine,
    and neutral forms of the jid. Thanks @isaacmendes. [#717]

  • Diagnosable failure for a stale browser profile after a binary-changing upgrade. Upgrading
    across the v0.8.12 amd64 browser-binary switch (Debian Chromium → Chrome for Testing, #663) — or any
    later change to the Chromium/Chrome binary — can leave an already-authenticated whatsapp-web.js
    session's persistent browser profile incompatible with the new binary: on the next start the page
    context is destroyed during injection and the engine fails with Puppeteer's opaque
    Execution context was destroyed, which reads like a Puppeteer bug and gave no hint that the stale
    profile was the cause. The whatsapp-web.js adapter now detects that error in its initialize()
    catch and logs an advisory pointing the operator at the remedy (delete the session profile dir and
    re-scan); the error still propagates unchanged, so existing failure handling is unaffected. The
    profile is not auto-recovered — a tainted profile is not safely portable across Chromium major
    versions (clearing only the cache subdirs is insufficient), so a one-time re-authentication is
    required. [#708]

  • OpenAPI export script under current env validation. scripts/export-openapi.ts had been broken
    since the SQLite DATABASE_NAME file-path validation tightened (it pinned an in-memory data DB,
    which that rule rejects). The data connection now uses a temp-dir SQLite file that is removed on
    exit, so the snapshot generator runs hermetically again.