You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
whatsapp-web.js block and unblock work again. WhatsApp Web removed the contact resolver both calls used, so every id answered an opaque 500; its replacement helpers are modal-driven UI wrappers that block nothing headless, and the server now refuses a phone-keyed block because individual chats are keyed by LID while the library folds every id back to a phone number. An install-time patch resolves through the chat-owning identity and calls the block action directly.
block and unblock accept a privacy id (@lid), the only id a contact without a known phone number has. The blocklist read answers those ids verbatim, so refusing them on the write left such a contact listed as blocked with no way to unblock it; ids that name no individual (group, newsletter, broadcast, free text) are still refused with 400.
whatsapp-web.js deleteChannel classifies a dead browser page as the documented 503 plus an early death signal, like every other channel call; it reached the client directly, so a crash there answered an opaque 500 while the session still reported READY.
Baileys block/unblock answer 400 when WhatsApp cannot map the id between the phone-number and privacy-id dialects (no mapping either way, or an id that is neither). The library refuses those with a Boom the gateway could not classify, so a well-formed request got an opaque 500; whatsapp-web.js already answered 400 for the same cause.
docs/06 states that editing another account's message answers 403, not 500. Both engines raise the refusal as EngineRefusedError, and the published contract already declared 403 with no 500 on that route.
The migration drift gate compares the chain-vs-entity diff against a pinned snapshot of the full statement text instead of classifying statements by shape. On SQLite a new column is applied as a table rebuild and a new index as a bare CREATE INDEX, the same shapes the known column-type drift produces, so the shape filters passed both: an entity change shipped without a migration stayed green and only surfaced as a no such column 500 on a synchronize-disabled deployment.
The plugin config editor derives a per-field id: a hardcoded one collided on any schema with two boolean fields, so the second field's label toggled the first field's checkbox. Six more multi-line labels in MessageTester are associated with their controls.
Dashboard accessibility: 55 form labels are associated with their controls (htmlFor/id, no duplicates or orphans), the muted-text token meets AA on both themes (4.76:1 light, 5.71:1 dark), and the primary button uses dark text on the green (9.0:1, from 1.98:1).
The chain-boot e2e sets MAIN_DATABASE_SYNCHRONIZE=false explicitly: the variable's absence defaults to synchronize=true, so the main connection's migration chain was never actually exercised. The shared delivery recorder also strips the raw error before the persistence spread, and the coverage ignore-pattern list deduplicates.
The direct and queued webhook delivery paths share one POST-and-classify core (postWebhookPayload) and one terminal-failure recorder, instead of two line-for-line copies that an outbox would have tripled.
Test and DB infrastructure: the 23 file-reading specs are excluded from the unit coverage denominators (spec files were counted as 0%-covered source, ~994 lines inflating every floor), an e2e boots the production SQLite schema from scratch through both full migration chains (the path the other suites' synchronize=true never touches), and a drift gate derives the chain-vs-entity diff so a missing index, column, or constraint fails while the known column-type rebuild is pinned as a visible baseline.
The ingress route's rate bound is the per-instance limit alone: the global per-IP medium tier (100/min, below the instance default of 120/min) used to 429 every tenant of a shared-egress-IP provider before the instance bound ever fired.
The in-flight body budget gives each client IP its own share (half the aggregate by default, keyed through TRUSTED_PROXIES): four trickle connections from one source now exhaust only that source's share instead of 503ing every body-bearing request for everyone.
The lifecycle fences (teardown chaining, fail-closed 409, identity-checked initial-status waits, force-destroy eviction) and the status broadcaster (persist-then-mirror, transition de-dup, clear-on-delete) carry their own unit specs instead of being reachable only through the session-service suite's white-box pokes.
Follow-ups: the transient-launch classifier rejects every HttpException up front (the 504 no-retry no longer rests on message wording) and recognizes ECONNRESET; the sendTemplate 404 description names only the template; import-status normalization comments and the parity-fence failure messages state their exact scope.
GET /contacts/:id/phone keeps its documented 400/409 answers (not-started, not-ready) and nulls only genuine lookup failures, logging them at debug; the boundary swallow had absorbed the deliberate errors too.
A transient session-launch failure (dead page at initialize, a database hiccup) gets one bounded retry that keeps the claim held; adopt and boot auto-start used to release the claim and leave the session down until a restart.
The twelve hand-rolled "Session is not started" guards in the session service route through the engine registry's require() (wire contract unchanged), and three routes drop an OpenAPI 404 declaration no code path can produce.
import-data restores an active session status from the backup as disconnected (scoped to claimable rows; a notice counts them), so migrated sessions are startable without a process restart.
The engine parity gate reads any single-quoted throw literal (a parenthesized site like sendText(customPreview) escaped the identifier-only regex), rejects construction sites it cannot see (template literals, variable arguments, literals naming no method), and pins conditional refusals in an explicit list; docs/29 states the refinements.
The Baileys adapter builds one shared host object for its nine delegates instead of nine overlapping closure bags; a new cross-cutting member is added once. No behavior change.
A transient whatsapp-web.js lid-to-phone lookup failure (dead page, rate limit) no longer overwrites a valid stored mapping with a definitive null; the engine method rejects on failure and the HTTP boundary keeps its null-on-failure contract.
Nine whatsapp-web.js group routes answer 404 (GroupNotFoundError) when the id is not a group or is unknown, like the guarded settings writes; they previously threw a bare error that surfaced as an opaque 500.
The cold-reachout budget is charged only after the group engine call resolves; a createGroup that 501s (whatsapp-web.js, always) or an add the engine refuses no longer burns the day's allowance for participants never contacted.
block/unblock refuse ids that do not name a person (400, both engines): whatsapp-web.js silently returned false for a group id (answered 200 "blocked" with nothing blocked) and Baileys surfaced an opaque 500 for an unresolvable jid.
whatsapp-web.js profile, status and channel operations classify a dead browser page as the documented 503 plus an early death signal instead of an opaque 500 while the session still reports READY, matching the split the chat operations already made.
docs/06 repair pass over the Errors lines: the ingress 401 is the signature failure (not an API-key error), label writes have no 404, the catalog product lookup answers 200 empty (not 404), search's 501 names the no-provider case, multi-line Errors blocks were re-joined (no severed sentences, duplicate codes, or · · separators), and the gate now reads wrapped blocks.
docs/14 corrects four hazard-table release labels (instance-config is 0.18.0, the reload 409 is 0.15.0, the group-summary retypes are 0.14.6, Baileys 5xx is 0.14.5), docs/03 drops a duplicated health/, and docs/10's scaling note matches docs/13's "still deploy replicas: 1" stance.
docs/06's audit section scopes the always-null columns correctly (userAgent/statusCode always null; method/path populated on auth-failure, key-lifecycle and queue-board rows), the Chats quote box renders identically under system-dark and explicit dark, and the Logs empty state gives server-filter guidance when only the severity filter is active (13 locales).
The Go, Python and Java clients' media/audio sends declare mentions (only the JS client could type-safely mention on media), and the contract-shape gate compares numeric enum unions for real (member-level, both sides sorted numerically) instead of skipping them.
All five SDKs expose deleteProfilePicture (the contract's DELETE /profile/picture shipped in none of them), and the SDK coverage gate now checks verbs on multi-verb paths, not just path reachability.
The typed SDKs' message-list records declare chatName, author, mediaPath and mediaMimetype (the wire carried all four; every typed client missed them), and the contract-shape gate now maps MessageRecord in all four SDKs, including the Python functional-TypedDict form.
The dashboard's manual WebSocket retry re-registers the message handler on the fresh socket; the handler effect only re-ran on events changes, so a reconnect left the new socket silent while reporting connected.
The Chats page honours the system dark theme: four dark-palette rules (outgoing bubble, document media, quote box, action menus) only matched an explicit data-theme and left the default 'system' theme rendering light popups inside a dark thread.
The Logs page resets to page 1 on a new search and its empty state distinguishes "no matches on this page" (search filters the fetched page only) from "no logs yet".
Chat, channel and status entries in the Chats sidebar are keyboard-activatable (role, focus, Enter/Space) instead of click-only divs, and docs/17 states the accessibility posture honestly (AA target, known label/contrast gaps listed) instead of claiming certified compliance.
docs/06 documents every route-specific status code the contract declares (409/413/415/422/429/501/502/503; 153 missing code mentions across ~90 sections), corrects the catalog routes to Baileys-implements, the profile refusals to 403, scope violations to 401, and the phantom channel 422; a spec now derives the required codes from openapi.json.
docs/06 scopes the audit log honestly: message/webhook actions are never emitted (their tables own that data), the request-actor columns are documented as null, and the OpenAPI example uses a real snake_case action.
docs/30 states the plugin sandbox's memory-kind boundary: the worker heap cap does not cover Buffer/native allocations, which grow host RSS up to the container limit.
docs/25 documents the known wildcard-instance config residue: per-instance isolation covers ingress dispatch only; wildcard/null siblings still merge into the plugin base config.
docs/14's Known Upgrade Hazards table covers every breaking change since 0.12.0 (15 missing rows across 0.14-0.20, including both v0.20.0 config opt-outs); the Redis switch steps now name the real queues.
docs/10 refreshed: the CI table lists the chart job and the full lint/test lanes, the illustrative Dockerfile no longer models the full-/app chown and missing USER the real image rejects, and the scaling note matches the implemented claim/lease design.
message.received / message.sent WebSocket events shed inline media over WEBHOOK_MEDIA_INLINE_MAX_BYTES with the same omitted marker as webhooks; a large blob was broadcast in full to every subscribed socket (and across Redis pub/sub in multi-node).
The chat-media orphan sweep's mediaPath IN (...) lookup is served by a new partial index (WHERE mediaPath IS NOT NULL); it was a full messages-table scan per chunk.
GET /api/health audits a presented-but-invalid API key (API_KEY_AUTH_FAILED) like every other key-validation surface, rate-bounded per IP; probing through the unthrottled health route was invisible to the audit log.
PHP SDK: an empty headers map (webhook create/update), an empty vars map (send-template), and empty per-item variables (send-bulk) encode as JSON {}; they serialized as [], which the gateway rejects for map-typed fields.
JS SDK: the exports map carries per-condition types entries, so a CommonJS TypeScript project under node16-family resolution resolves the CJS declarations instead of failing with TS1479.
The release (tag) workflow runs check:contract-shapes and test:docs like the branch CI, and a spec locks every ci.yml lint/test gate command into the release path; both ran only on branches.
docs/06 qualifies the at-least-once webhook promise with its crash boundary, and the glossary no longer claims a webhook DLQ manual redrive that does not exist.
Both bundled Compose files forward every documented runtime knob; WEBHOOK_SSRF_REDIRECTS, PLUGIN_INSTALL_REQUIRE_PIN and ~75 other .env settings were unreachable in the container. A spec now derives the required list from .env.example.
Ingress delivery ids BullMQ refuses at enqueue (numeric, redrive:<uuid>, 0:-led) are hashed to a legal job id, namespaced by plugin/instance; the old refusal read as a Redis failure and silently degraded the delivery to inline dispatch with no retry.
One precise unique-violation predicate (23505, SQLITE_CONSTRAINT_UNIQUE/_PRIMARYKEY): the old prefix match treated every SQLite constraint failure (FK/NOT NULL/CHECK) as a duplicate, swallowing genuine persistence failures and answering misleading 409s.
A one-time warning fires when a proxied request arrives with an empty TRUSTED_PROXIES: every client then shares one rate-limit bucket keyed on the proxy. The nginx FAQ recipe now tells operators to set it.
Security
The SQLite database files are tightened to owner-only (0600, plus -wal/-shm/-journal) on every boot; they hold plaintext webhook/plugin secrets and were group/world-readable while sibling secret files were 0600.
PUT /sessions/{sessionId}/webhooks/{id} now enforces the same 16-character webhook-secret floor as create; an empty string still clears signing.
The ingress route enforces a second rate-limit window keyed on the client IP (INGRESS_IP_LIMIT, default 1200 per window). Its per-instance window is keyed on the caller-supplied :pluginId/:instanceId, so varying those segments minted a fresh bucket per request and left this unauthenticated route with no effective bound.