Skip to content

Hardening rollup: net-contract compliance, Chatwoot echo race fix, full i18n, and tooling#46

Merged
rmyndharis merged 9 commits into
mainfrom
feat/plugin-hardening
Jul 23, 2026
Merged

Hardening rollup: net-contract compliance, Chatwoot echo race fix, full i18n, and tooling#46
rmyndharis merged 9 commits into
mainfrom
feat/plugin-hardening

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

Summary

A cross-repo hardening and tooling pass. Each commit is scoped to one concern; the highlights:

  • gsheets-logger v0.3.0 — outbound HTTP now goes through ctx.net.fetch (host-proxied, SSRF-guarded) instead of the raw worker fetch, with net:fetch + net.allow pinned to the two fixed Google hosts and minOpenWAVersion 0.7.0. Client tests now cover token caching, 401 re-authentication, and malformed token responses.
  • chatwoot-adapter v0.5.6 — fixes an echo race: the message_created webhook could be processed between the adapter's outgoing post and its echo-marker write (inbound/backfill and the webhook path hold different per-chat locks), re-sending a backfilled or mirrored message to WhatsApp. The post + marker write now hold a conversation-scoped lock that the webhook dedup also takes. Deterministic regression test included.
  • supabase-otp-hook v0.2.0 — resolves the canonical chat id via engine.canonicalChatId before sending (bounded by a 2 s timeout, falling back to the phone-derived id), so OTPs land correctly for contacts under privacy-id addressing.
  • Full dashboard i18n — every plugin now ships name/description/config titles in all eight locales; catalog.mjs gains manifest gates (explicit sessionScoped, testedOpenWAVersion required for stable) and the catalog test asserts stable plugins carry complete i18n.
  • Duplicated helpers pinned — chat-lock, multipart, and the LRU cooldown copies are unified and guarded by a drift test that fails if any copy diverges; the voice-transcription multipart copy gains the CR/LF header sanitization the others already had.
  • Toolingnpm test auto-discovers plugin test files (new plugins no longer need package.json edits, and scripts/*.test.mjs finally runs), tsconfig includes are glob-based, engines.node >= 22 is declared, a test:coverage script uses Node's built-in coverage, and packaging no longer needs the external zip CLI (internal zero-dependency STORE writer, byte-deterministic).
  • Docs — PLUGIN-STANDARD.md records the observed runtime contract (res.body-only fetch responses, ~5 s hook budget, allowConfigHosts fail-fast, boot-time INSTALLED reset); PluginNetResponse.text()/json()/arrayBuffer() are marked @deprecated; READMEs document @lid-related known limitations and Chatwoot mapping-storage growth.

Test plan

  • npm test — 422/422 passing (was 406 before this branch; new coverage for the echo race, Sheets client, canonical-id resolution incl. timeout, zip writer round-trip, drift guard, and discovery guard)
  • npm run typecheck — clean
  • npm run catalog:check — catalog in sync
  • npm run build — all 10 plugins package under the 5 MB limit with the internal zip writer; output verified against system unzip -t
  • Node built-in coverage reports ~94.6% lines

Note: gsheets-logger v0.3.0 changes its network path — smoke-install on a live instance before tagging the release.

…t gates

Backfill manifest i18n (name, description, config titles) for http-action,
typebot-connector, and voice-transcription across all eight dashboard
locales, and declare sessionScoped explicitly in after-hours and faq-bot.

catalog.mjs now hard-fails on a missing sessionScoped field and on a stable
plugin without testedOpenWAVersion, and warns on missing/partial i18n. The
catalog test asserts every stable plugin ships a complete locale set.
scripts/run-tests.mjs finds every plugin directory by its manifest.json
(the same rule as the catalog) and runs its tests, so a new plugin is
covered by npm test without edits to package.json. scripts/*.test.mjs now
runs too — the catalog test was previously never executed. A guard test
fails if a plugin directory ships without tests.

tsconfig include/exclude is now glob-based for the same reason, and
package.json declares engines.node >= 22 (the toolchain target) plus a
test:coverage script using Node's built-in coverage.
Replace the external zip CLI in package.mjs with a small STORE-method
writer (scripts/zip-store.mjs) so packaging works on any platform without
extra tooling. Archives are byte-deterministic (fixed timestamps) and
covered by round-trip parse tests; output verified against system unzip.
chat-lock, multipart, and the LRU cooldown helper existed as near-verbatim
copies across plugins (plugins ship as self-contained zips, so copying is
deliberate). The copies are now identical and pinned by
scripts/shared-copies.test.mjs, which fails the suite if they ever drift
apart — a fix in one copy must be applied to all.

The multipart copy in voice-transcription gains the CR/LF header
sanitization the other copies already had, and the cooldown helper moves
to a canonical cooldown.ts imported by after-hours, faq-bot, and
http-action.
….2.0)

The OTP target chat id was built straight from the phone number
(<digits>@c.us), which can miss contacts addressed by a privacy id. The
handler now resolves the canonical chat id via engine.canonicalChatId
when the host provides it (engine:read), bounded by a 2 s timeout so a
wedged engine bridge falls back to the phone-derived id instead of
stalling the ingress job into a retry and a duplicate OTP.
SheetsClient now takes an injected NetFetch bound to ctx.net.fetch
(host-proxied, SSRF-guarded) instead of the raw worker fetch, bringing
the plugin inside the v0.7 net contract: permissions [net:fetch] with
net.allow pinned to the two fixed Google hosts, minOpenWAVersion 0.7.0.
Responses are read via res.body per the sandbox contract. Client tests
now cover token caching, re-authentication after a 401, and malformed
token responses.
… (v0.5.6)

The adapter marked its own outgoing Chatwoot posts as seen only after the
POST returned, but the message_created echo webhook could be processed
inside that window — and the inbound/backfill paths hold a different
per-chat lock than the webhook dedup, so nothing serialized the two. An
echo landing in the gap was relayed to WhatsApp as a duplicate of a
backfilled or mirrored message.

The post and its marker write now hold a conversation-scoped lock that
the webhook dedup also takes (innermost on every path, so no lock-order
cycle). Includes a regression test that drives the echo into the window
deterministically, plus README notes on mapping-storage growth.
PLUGIN-STANDARD.md gains a runtime contract (observed) section covering
the res.body-only fetch response, the ~5 s hook budget, allowConfigHosts
fail-fast, and the boot-time INSTALLED reset, plus the manifest gates now
enforced by the catalog. PluginNetResponse.text()/json()/arrayBuffer()
are marked @deprecated since the sandbox runtime does not provide them.

The after-hours, faq-bot, and chat-flow READMEs document that per-chat
state keyed by chat id can be orphaned when a contact migrates between
@lid and @c.us addressing.
@rmyndharis
rmyndharis merged commit 7a1f1d3 into main Jul 23, 2026
1 check passed
@rmyndharis
rmyndharis deleted the feat/plugin-hardening branch July 23, 2026 10:06
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