feat(http-action): safe REST action bot plugin (0.1.0)#34
Merged
Conversation
Trigger safe REST API requests from WhatsApp commands and map JSON responses back to chat. One request per message, one reply. Safe by default: fixed https origin (allowConfigHosts, required), server-relative path validation, dangerous-header blocklist, CRLF rejection, prototype-safe templating with URL-encoded path segments and JSON-safe POST bodies, 256 KiB response cap, and an off-dispatch handler so a slow upstream never stalls the inbound hook. 86 tests (node:test); passes typecheck, catalog:check, build, and the loader contract. Order-status, stock-lookup, and ticket-creation use cases run end-to-end through the real message path. Dedup/cooldown hardening is deferred to a follow-up.
Storage-backed idempotency (claim, fail-closed, 3-day TTL) so a redelivered message id never double-fires; a throttled prune keeps ctx.storage from growing unbounded. In-memory per-chat cooldown (allowCooldown, fail-open, LRU-capped) rate-limits a single chat. Both gates run in handleMessage before the upstream call. Updated the 0.1.0 changelog to cover the full MVP. 106 tests; typecheck, catalog:check, build, and the three end-to-end use cases still pass.
Review-driven fixes: - Enroll http-action in tsconfig include so the CI typecheck actually validates it; it had been excluded, masking a type error. FetchResponse.statusText is now optional to match PluginNetResponse. - Reject CR/LF/NUL in rendered header values (a templated attacker field could otherwise inject headers), reject '..' path segments, reject a query string in baseUrl, and screen apiKeyHeader against the dangerous- header blocklist. - Rework dedup into a read-only hasSeen check plus a mark written only after a successful send, so a transient send failure retries on redelivery instead of being dropped; the marker is presence-based and cooldown runs before any mark. - Route request/prune failures through logger.error so the Error context is kept, surface the parsed body to notFound/error templates, avoid splitting a surrogate pair when truncating, and strip C0 control chars from rendered replies. 381 tests; typecheck (now covering http-action), catalog:check, build, and the three end-to-end use cases all pass.
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.
Summary
Adds
http-action, a plugin that triggers a safe REST API request from a WhatsApp command and maps the JSON response back to chat — one request per message, one reply. It connects WhatsApp to an existing HTTPS API without webhook middleware: e.g.cek-order INV-001→GET {baseUrl}/orders/INV-001→ a reply rendered from the response.Behavior
A configured trigger (
exact/prefix, optional case-sensitivity) on an inbound message selects an action, builds a request to a fixed HTTPS origin, fetches it throughctx.net.fetch, parses the JSON body, and renders a reply template. SupportsGETand JSONPOST(with abodyTemplate), Bearer / API-key / no auth, query params, and per-actionreplyTemplate/notFoundTemplate/errorTemplate.Safety posture
baseUrlis required, HTTPS, credential-free, and declared inallowConfigHosts(a code-side default would be invisible to the net gate, so it is forbidden). The request path is server-relative only — protocol-relative//, absolute URLs, fragments, and control chars are rejected — and each substituted segment is URL-encoded, so a message cannot inject a path segment or change the host.eval, no free regex, no loops. Prototype keys (__proto__/constructor/prototype) are rejected in template paths; POST bodies are JSON-escaped and re-parsed before send; dangerous headers (hop-by-hop,x-forwarded-*) and CRLF are blocked. Response bodies are capped at 256 KiB.{ continue: true }immediately and floats the fetch/render/send, so a slow upstream never stalls the inbound hook.Structure
config.ts(validation) ·url-template.ts(prototype-safe rendering) ·client.ts(fixed-origin fetch + response cap) ·matcher.ts(triggers + arg parsing) ·reliability.ts(dedup + cooldown + prune) ·index.ts(lifecycle + off-dispatch handler). Secrets (authToken) are markedsecret: trueinconfigSchemaso the dashboard masks and round-trips them.Verification
npm test(373 tests, all green),npm run typecheck,npm run catalog:check,node package.mjs http-action(zip + sha256), and the loader-contract instantiation all pass. Three sample use cases — order status, stock lookup, ticket creation — run end-to-end through the real message path.Notes
development.minOpenWAVersion/testedOpenWAVersionare intentionally unset pending install and runtime testing on a real OpenWA instance; both will be filled from that test before moving tobeta.