Skip to content

fix: restore the catalog, verify against OpenWA 0.23.3, and correct plugin defects - #107

Merged
rmyndharis merged 3 commits into
mainfrom
chore/openwa-0233-sync
Aug 25, 2026
Merged

fix: restore the catalog, verify against OpenWA 0.23.3, and correct plugin defects#107
rmyndharis merged 3 commits into
mainfrom
chore/openwa-0233-sync

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

Three things, in one release round because they share the version bump.

The catalog on main is not installable today

Every download URL in plugins.json names a release tag that was never pushed. The host defaults
PLUGIN_CATALOG_URL to this file on main (src/config/configuration.ts), so every marketplace
install answers 404, and the download is fetched before the #sha256= pin is consulted, so the
operator gets no hint about the cause.

plugins.json -> after-hours-v0.2.5/after-hours.zip   HTTP 404
newest tag   -> after-hours-v0.2.4                   HTTP 200

Nothing in CI can see this: catalog:check only proves plugins.json is regenerable from the working
tree. 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.

  • Adds scripts/catalog-live-check.mjs and a catalog-live CI job that fetches every download URL and
    verifies its bytes against the pin. It runs on push to main and nightly, deliberately not on pull
    requests, where a version bump is legitimately red until its tags land.
  • This job will be red on main until 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/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. 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.body for poll
questions, 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 five
plugins gate on type, denying contact and poll. Two constraints, both easy to get backwards and
both covered by tests: unknown stays admitted, because business button and list replies land there;
and no plugin allowlists text, because media captions arrive in body under 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 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.
  • 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-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 and no Chatwoot conversation is re-created.

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 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])+$ 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.
  • 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.

Privacy and access

  • chat-flow no longer logs message bodies, stored menu paths or matched reply text at debug level.
  • 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

  • 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;
    package.mjs checks the host's 200-file limit.
  • Test discovery walks from the repository root, so a test file outside a plugin directory cannot sit
    unrun; loader:check loads bundles from outside the repo tree, where the host's module resolution
    actually applies (verified: injecting an unbundled require now fails the gate).
  • 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.md 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.

Behavior changes worth a reviewer's attention

  • chatwoot-adapter: a queued message now drains only after the plugin has seen an event for its
    session 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 status now requires an admin.
  • faq-bot: the same inbound text is answered at most once every 10 seconds per chat. Keyed on the
    text, 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 has
repeatedly failed to trigger the release workflow), confirm each produced a run, then check
npm run catalog:live goes green.

…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.
@rmyndharis
rmyndharis merged commit b35b076 into main Aug 25, 2026
2 checks passed
@rmyndharis
rmyndharis deleted the chore/openwa-0233-sync branch August 25, 2026 05:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant