fix: restore the catalog, verify against OpenWA 0.23.3, and correct plugin defects - #107
Merged
Conversation
…0.23.3 OpenWA 0.23.2 made the Baileys body extractor fill message.body for poll questions, shared event names, business button labels and contact card vCards, matching what the whatsapp-web.js engine has always returned. Five plugins here used a non-empty body as the proxy for "a human typed this", and that change invalidates the proxy on both engines. The vendored contract taught that proxy, so it is corrected first. types/openwa.d.ts now names which shapes still yield an empty body and which no longer do, and requires a body-driven matcher to gate on type. PLUGIN-STANDARD.md carries the same rule for future plugins. - chat-flow, faq-bot, http-action, typebot-connector: deny type 'contact' and 'poll' before the matcher. A vCard is free text that matches ordinary contains and regex rules by accident, and a poll titled with a configured prefix could fire a real request from http-action against the operator's backend. - group-translate: deny 'contact' only. A shared card was posted to the translation backend, echoed back into the group translated, and let language detection pin the sender's language from vCard field names. A poll question is prose and is still translated. - 'unknown' stays admitted everywhere, since business button and list replies land there. No plugin allowlists 'text', so media captions still reach every matcher. - chatwoot-adapter and gsheets-logger relay and log the new text as designed; both READMEs record what operators will see change. Adds scripts/catalog-live-check.mjs and a ci job that fetches every catalog download URL and verifies its sha256 pin. catalog:check only proves plugins.json is regenerable from the working tree, so a version bump merged without its tags leaves every install answering 404 with nothing in CI able to see it. The job runs on push to main and nightly, not on pull requests, where a bump is legitimately red until its tags land. Every plugin cuts a patch release carrying testedOpenWAVersion 0.23.3. The plugin runtime contract itself did not move: src/core/plugins, src/core/hooks and the sandbox sources are byte-identical from v0.20.0 through v0.23.3, and the engine interface gained only optional trailing parameters that no capability verb exposes. Verified statically: typecheck, 596 tests (9 new, covering the denied and the admitted types for each plugin), catalog:check, loader:check, and a rebuild confirming the packaged zips are byte-identical and match their catalog pins. Not verified against a running host. after-hours 0.2.6, chat-flow 1.1.7, chatwoot-adapter 0.9.6, faq-bot 0.2.6, group-translate 1.3.6, gsheets-logger 0.3.8, http-action 0.2.7, supabase-otp-hook 0.3.5, typebot-connector 0.2.7, voice-transcription 1.2.8
…s the catalog A pass over every plugin and the shared tooling, fixing defects that were reachable in normal operation. Grouped by what they cost an operator. Cross-tenant delivery and lost messages: - chatwoot-adapter: the retry queue is plugin-global but a session's config resolves only inside a dispatch for that session, so the timer drain re-posted every queued message with the base account's origin, token, account id and inbox. On a multi-tenant install that put one tenant's customer message into another tenant's helpdesk, and for a chat with no mapping yet it created the conversation in the wrong account and stored that mapping permanently. The drain now relays only for sessions whose own config it has captured, and leaves the rest queued without spending a retry attempt. - supabase-otp-hook: the send was fired and forgotten to stay inside the ingress dispatch budget, which also swallowed instant rejections. Supabase had already been acked and never retries, so those OTPs were gone. The send is raced against a short deadline; a failure inside it fails the delivery so the host retries and dead-letters it, while a merely slow send still finishes in the background. Host-wide performance: - chatwoot-adapter and http-action wrote one storage key per chat and per answered message and pruned none of them. The host re-measures its quota by stat-ing every key on every write, synchronously, on the gateway event loop, so a long-running install slowed every plugin on the host. Both now use sharded buckets. Existing keys are read and migrated on their next write, so nothing is orphaned. Wrong answers: - after-hours: an overnight window such as 22:00-06:00 was evaluated entirely inside its own weekday, so the plugin was silent on a morning nothing had declared open and replied on the morning its window actually covered. A window now belongs to the day it opens on. The test that pinned the old behavior is corrected in the same change. - faq-bot: ambiguous repeated alternations are rejected at parse time. `^([a-z]|[a-z0-9])+$` takes 259 ms against 23 characters and over a minute against 31, and a running regex cannot be interrupted, so one short message pinned the worker and every later message queued behind it. Unambiguous alternations such as `(one|two|three)+` are unaffected. - gsheets-logger: the spreadsheet formula guard checked only the character after a leading sign, so `-1+IMPORTXML(...)` was written unquoted. - chat-flow: flow state is recorded before the message describing it is sent. A write failing after a delivered greeting left the flow unstarted, and with the documented empty trigger that greeted again on every later message. Silent failure: - voice-transcription: the circuit breaker counted the host's shared concurrency rejections as backend failures, so a busy line stopped transcription for the full cooldown with a healthy backend. - after-hours, voice-transcription and supabase-otp-hook gained a healthCheck. The host reports a plugin without one as healthy, so a config edit that stopped parsing, an open breaker and a dropped OTP were all invisible. - group-translate and voice-transcription name a backend URL the host cannot use, instead of failing as silence. Privacy and access: - chat-flow no longer logs message bodies, stored menu paths or matched reply text at debug level. Release 1.1.3 removed one such line and left the rest. - group-translate: `/tr status` is admin-gated (it prints the participant roster, which is the plugin's own access-control state) and `/tr help` is answered at most once a minute per group. Correct floors: - group-translate and voice-transcription declared minOpenWAVersion 0.7.0 while resolving hosts through net.allowConfigHosts, which first shipped in 0.8.0. On a 0.7.x host both installed cleanly and then did nothing. Tooling and contract: - A ci job and `npm run catalog:live` fetch every catalog download URL and verify its sha256 pin, which is the only check that can see an unpushed tag or a stale digest. - The ci build job declares `contents: read` and no longer persists a git credential before running dependency lifecycle scripts. - The catalog's reserved-id check is case-insensitive like the host's, so a mixed-case id can no longer be published and then refused at install; a homepage outside the declared repository is rejected rather than published under this repository's name; package.mjs checks the host's 200-file limit. - Test discovery walks from the repository root, so a test file outside a plugin directory can no longer sit unrun; loader:check loads bundles from outside the repo tree, where the host's resolution actually applies. - types/openwa.d.ts gains the nine manifest fields the host types and this repo sets, narrows `type` to what is installable, and corrects the documented runtime bounds. README and PLUGIN-STANDARD.md correct the capability-error rule, the management-endpoint table (8 routes listed against 14 that exist) and the send-verb names. Verified: typecheck, 628 tests (33 new, each asserting the defect it covers), catalog:check, loader:check, and a rebuild confirming the packaged zips are byte-identical and match their catalog pins. Not verified against a running host.
The reply throttle keyed on the matched rule, so a customer's second question was silently dropped whenever it happened to match the same rule as their first. "berapa harga paket A?" followed by "kalau harga paket B?" answered once, which costs more than the runaway exchange the throttle exists to prevent. A runaway exchange repeats identical text: the other end's autoresponder sends one canned line, this plugin answers, and the same canned line arrives again. Keying on that text stops the loop and leaves distinct questions alone. Also adds the tests for behavior that shipped without one: the voice-transcription health check and delivery-timeout clamp, and the supabase-otp-hook health check, including that a later success clears a recorded failure so the tile does not stay red after one transient error.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three things, in one release round because they share the version bump.
The catalog on
mainis not installable todayEvery
downloadURL inplugins.jsonnames a release tag that was never pushed. The host defaultsPLUGIN_CATALOG_URLto this file onmain(src/config/configuration.ts), so every marketplaceinstall answers 404, and the download is fetched before the
#sha256=pin is consulted, so theoperator gets no hint about the cause.
Nothing in CI can see this:
catalog:checkonly provesplugins.jsonis regenerable from the workingtree. This PR folds forward rather than cutting the missing tags, so exactly one set of tags is ever
pushed and the 0.2.5-era versions simply never existed publicly.
scripts/catalog-live-check.mjsand acatalog-liveCI job that fetches every download URL andverifies its bytes against the pin. It runs on push to
mainand nightly, deliberately not on pullrequests, where a version bump is legitimately red until its tags land.
mainuntil the ten tags are pushed. That is the intended signal.OpenWA 0.23.3
The plugin runtime contract did not move:
src/core/plugins,src/core/hooksand the sandbox sourcesare byte-identical from v0.20.0 through v0.23.3, and the engine interface gained only optional trailing
parameters that no capability verb exposes. All ten manifests pass the v0.23.3 loader.
One change is visible to plugins. v0.23.2 made the Baileys body extractor fill
message.bodyfor pollquestions, shared event names, business button labels and contact-card vCards, matching what
whatsapp-web.js has always returned. Five plugins used a non-empty body as the test for "a human typed
this", which that invalidates on both engines.
The vendored contract taught that idiom, so it is corrected first (
types/openwa.d.ts), then the fiveplugins gate on
type, denyingcontactandpoll. Two constraints, both easy to get backwards andboth covered by tests:
unknownstays admitted, because business button and list replies land there;and no plugin allowlists
text, because media captions arrive inbodyunder their own media type.Defects across the plugins
Grouped by what they cost an operator.
Cross-tenant delivery and lost messages
chatwoot-adapter: the retry queue is plugin-global, but a session's config resolves only inside adispatch for that session, so the timer drain re-posted every queued message with the base account's
origin, token, account id and inbox. On a multi-tenant install that put one tenant's customer message
into another tenant's helpdesk, and for a chat with no mapping yet it created the conversation in the
wrong account and stored that mapping permanently.
supabase-otp-hook: the send was fired and forgotten to stay inside the ingress dispatch budget,which also swallowed instant rejections. Supabase had already been acked and never retries, so those
OTPs were gone. The send is now raced against a short deadline; a failure inside it fails the delivery
so the host retries and dead-letters it, while a merely slow send still finishes in the background.
Host-wide performance
chatwoot-adapterandhttp-actionwrote one storage key per chat and per answered message andpruned none of them. The host re-measures its quota by stat-ing every key on every write,
synchronously, on the gateway event loop, so a long-running install slowed every plugin on the host.
Both now use sharded buckets; existing keys are read and migrated on their next write, so nothing is
orphaned and no Chatwoot conversation is re-created.
Wrong answers
after-hours: an overnight window such as22:00-06:00was evaluated entirely inside its ownweekday, so the plugin was silent on a morning nothing had declared open and replied on the morning
its window actually covers. A window now belongs to the day it opens on.
faq-bot: ambiguous repeated alternations are rejected at parse time.^([a-z]|[a-z0-9])+$takes259 ms against 23 characters and over a minute against 31, and a running regex cannot be interrupted,
so one short message pinned the worker and every later message queued behind it.
gsheets-logger: the spreadsheet formula guard checked only the character after a leading sign, so-1+IMPORTXML(...)was written unquoted.chat-flow: flow state is recorded before the message describing it is sent. A write failing after adelivered greeting left the flow unstarted, and with the documented empty trigger that greeted again
on every later message.
Silent failure
voice-transcription: the circuit breaker counted the host's shared concurrency rejections as backendfailures, so a busy line stopped transcription for the full cooldown with a healthy backend.
after-hours,voice-transcriptionandsupabase-otp-hookgained ahealthCheck. The host reportsa plugin without one as healthy, so a config edit that stopped parsing, an open breaker and a dropped
OTP were all invisible.
Privacy and access
chat-flowno longer logs message bodies, stored menu paths or matched reply text at debug level.group-translate:/tr statusis admin-gated (it prints the participant roster, which is theplugin's own access-control state) and
/tr helpis answered at most once a minute per group.Correct floors
group-translateandvoice-transcriptiondeclaredminOpenWAVersion0.7.0 while resolving hoststhrough
net.allowConfigHosts, which first shipped in 0.8.0. On a 0.7.x host both installed cleanlyand then did nothing.
Tooling and contract
contents: readand no longer persists a git credential before runningdependency lifecycle scripts.
be published and then refused at install; a
homepageoutside the declared repository is rejected;package.mjschecks the host's 200-file limit.unrun;
loader:checkloads bundles from outside the repo tree, where the host's module resolutionactually applies (verified: injecting an unbundled
requirenow fails the gate).types/openwa.d.tsgains the nine manifest fields the host types and this repo sets, narrowstypeto what is installable, and corrects the documented runtime bounds.
README.mdandPLUGIN-STANDARD.mdcorrect the capability-error rule, the management-endpoint table (8 routeslisted against 14 that exist) and the send-verb names.
Behavior changes worth a reviewer's attention
chatwoot-adapter: a queued message now drains only after the plugin has seen an event for itssession since it was last enabled. Until then it stays queued rather than being posted to whichever
Chatwoot the base config names. Documented in the plugin README.
group-translate:/tr statusnow requires an admin.faq-bot: the same inbound text is answered at most once every 10 seconds per chat. Keyed on thetext, not the rule, so two different questions matching one rule are both still answered.
gsheets-logger: a failing flush no longer retries on every inbound message, only on the timer.Verification
npm run typecheck,npm test(631 tests, 44 new),npm run catalog:check,npm run loader:check,and a rebuild confirming the packaged zips are byte-identical and match their catalog pins.
Each new test was checked to fail when its fix is reverted. Several assert the exposure first: the same
vCard that matches an faq-bot rule as plain text must not match it as a contact card.
Not verified against a running OpenWA host. Everything above is static analysis, unit tests and
targeted scenario runs against the real modules. The ingress acceptance path for the two webhook
plugins, and the storage migrations under a live gateway, are unproven here.
After merge
Push the ten tags one at a time (
git push origin <tag>per tag: a single push carrying many tags hasrepeatedly failed to trigger the release workflow), confirm each produced a run, then check
npm run catalog:livegoes green.