v0.8.14
Added
-
Plugin search providers (host→plugin search RPC). A sandboxed plugin can now register as a
SearchProviderby callingctx.registerSearchProvider(handler)(e.g.ctx.registerSearchProvider(async (query) => …)) from its worker. The host routesGET /api/searchqueries to the plugin over a new correlatedsearch/search-resultwire 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. WhenSEARCH_PROVIDER=auto(the default), an enabled plugin provider supersedes the built-in database full-text provider;SEARCH_PROVIDER=builtin-ftskeeps the built-in active;SEARCH_PROVIDER=nonedisables 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 existingmessage:persistedhook. -
Search queries are bounded host-side and plugin results are re-scoped.
GET /api/searchnow clampslimit(toSEARCH_LIMIT_MAX) andoffset(toSEARCH_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'stotalis preserved when no out-of-scope hit was stripped, so pagination is unaffected. [#680] -
Ingress events and delivery failures are now retained.
ingress_eventsandintegration_delivery_failuresare pruned pastINGRESS_RETENTION_DAYS(default 90;<=0disables), 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'sMCP_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}HMACcontentTemplateplaceholder 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.jsonstep full-program type-checks test files (previously invisible to CI, sincetsconfig.build.jsonexcludes 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 with501 Not Implementedinstead of a raw500. whatsapp-web.js builds a channel media message and callsmsg.avParams(), a WhatsApp-Web-page method removed in a recent WA Web build, so the send crashed withTypeError: 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 typedChannelMediaNotSupportedError(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 pinWWEBJS_WEB_VERSIONto a cached WA Web build that predates theavParamsremoval. [#673] -
base64media now takes precedence overurlwhen both are provided on a media send. Asend-image/send-video/etc. request carrying both fields previously sent the URL — and because@ValidateIfskipped@IsUrlvalidation onurlwheneverbase64was present, a staleurl(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.buildMediaInputnow prefersbase64, 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 anisHttpUrlcheck, so the change covers both. [#670] -
Fresh Docker Compose dev installs no longer boot-loop with
SQLITE_CANTOPEN..env.exampleshipsDATABASE_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 blankDATABASE_NAMEdefault (matching production),.env.exampledocuments it as PostgreSQL-only, and env-validation rejects a bare SQLite name with a clear boot error. [#677] [#680] -
DATABASE_TYPE=postgrescombined withDATABASE_SYNCHRONIZE=trueis now rejected at boot. On PostgreSQL the data connection runs migrations every boot, sosynchronizeimmediately dropped the search migration's generatedbody_tscolumn 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:errorhook 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=falseis 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=0header 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-filechmod 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-localhosthttp://baseUrl. [#680]