diff --git a/PLUGIN-STANDARD.md b/PLUGIN-STANDARD.md index f25c589..3e27e3a 100644 --- a/PLUGIN-STANDARD.md +++ b/PLUGIN-STANDARD.md @@ -117,6 +117,34 @@ or for a non-session-attributed event). **Read `ctx.config` inside your hook han right slice; reading it at load/lifecycle time yields the base. No plugin code is needed — the operator sets overrides via the dashboard/API. +#### Author requirement: re-resolve config inside the hook + +Because `ctx.config` is a **getter** that returns a different value per firing session, **caching it at +`onEnable` and reading the cache in the hook silently ignores per-session overrides** — the cache holds +the base `*` config. Two patterns honor overrides correctly: + +- **Per-event re-parse (simplest).** Call your `parseConfig(ctx.config)` inside the hook handler, not at + `onEnable`. This is the recommended pattern for plugins whose config is plain values (strings, numbers, + booleans, rule lists). Keep a `parseConfig(ctx.config)` at `onEnable` too as fail-fast validation so a + bad base config surfaces in the dashboard immediately, but don't keep the *result* as hook state. +- **Config-signature caching (for stateful coordinators).** If your hook reads a *stateful object* built + from config — a client with a circuit breaker, a connection pool, a coordinator — a naive per-event + rebuild resets that state on every message (e.g. the circuit breaker never trips). Instead, compute a + stable signature of the coordinator-affecting config fields per event and rebuild **only when the + signature changes**. Two messages from sessions with the same resolved config reuse the same + coordinator (its breaker/state survives); a per-session override that changes a field triggers one + rebuild on the next hook fire. + +> ⚠️ **Anti-pattern.** Do NOT store `this.config = parseConfig(ctx.config)` at `onEnable` and read +> `this.config` in the hook. This breaks per-session overrides and has been the source of multiple bugs +> (PRs #38, #39). Re-parse per event, or use signature-caching for a stateful coordinator. + +If your plugin **cannot** support per-session config — typically because it holds a single shared sink +(e.g. one buffer, one queue) that can't attribute work to a session at flush time — that is an +acceptable design choice, but it **must be documented** in the plugin's README **Compatibility** section +under a `### Per-session config` heading, with the reason and any workaround (e.g. "run one instance per +session"). See the per-plugin README convention below. + **Reserved ids** (cannot be used): `whatsapp-web.js`, `baileys`, `auto-reply`, `translation`. **Package limits** (enforced by OpenWA at install): ≤ 5 MB compressed, ≤ 200 files, ≤ 20 MB uncompressed. **Ship compiled JS** — the loader `require()`s `main`; build with `node package.mjs `. @@ -132,7 +160,9 @@ sets overrides via the dashboard/API. 5. **Setup** — prerequisites in numbered steps. 6. **Install** — `curl` examples (upload zip, set config, enable) and/or the Releases download. 7. **Configuration** — a table: key · required · default · description. -8. **Compatibility** — version-specific behavior and known caveats. +8. **Compatibility** — version-specific behavior and known caveats. Include a `### Per-session config` + subsection stating whether per-session config overrides are supported (and any caveat — see + [Per-session config](#per-session-config-v07) above). 9. **Security** — the threat model relevant to this plugin. 10. **Changelog** — link to `CHANGELOG.md`. 11. **License**. diff --git a/README.md b/README.md index 5fdd628..eab020f 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,27 @@ The table above is generated from each plugin's `manifest.json` + `CHANGELOG.md` (and mirrored in [`plugins.json`](./plugins.json)). See [PLUGIN-STANDARD.md](./PLUGIN-STANDARD.md) for the metadata standard every plugin follows. +## Per-session config support + +OpenWA's `sessionScoped` plugins (the default) may carry per-session config **overrides** set via the +dashboard — so two WhatsApp sessions under one plugin instance can run different settings. A plugin +honors overrides only if it re-reads `ctx.config` inside its hook (not a cached snapshot from enable). +The table below is the status for each plugin in this repo. See each plugin's README **Compatibility → +Per-session config** for details and caveats. + +| Plugin | Per-session config | Notes | +| ------ | :----------------: | ----- | +| [`after-hours`](./after-hours) | ✅ Supported | All fields per session; takes effect on next message. | +| [`chat-flow`](./chat-flow) | ✅ Supported | All fields per session; flow state is per `(session, chat)`. | +| [`chatwoot-adapter`](./chatwoot-adapter) | ✅ Supported | All fields per session — first-class multi-tenant shape. | +| [`faq-bot`](./faq-bot) | ✅ Supported | All fields per session (different rule sets per number). | +| [`group-translate`](./group-translate) | ⚠️ Supported, with caveat | Config-signature caching; multi-backend isolation needs one instance per session. | +| [`gsheets-logger`](./gsheets-logger) | ❌ Not supported | Single-buffer single-sink design; use one instance per session. | +| [`http-action`](./http-action) | ✅ Supported | All fields per session (different endpoints/action sets). | +| [`supabase-otp-hook`](./supabase-otp-hook) | ✅ Supported | All fields per session; applies to the instance's bound session. | +| [`typebot-connector`](./typebot-connector) | ✅ Supported | All fields per session; flow state is per `(session, chat)`. | +| [`voice-transcription`](./voice-transcription) | ⚠️ Supported, with caveat | Config-signature caching; multi-backend isolation needs one instance per session. | + **On the roadmap:** an automatic closing-greeting plugin for new leads. Want something else? [Open an issue](https://github.com/rmyndharis/OpenWA-plugins/issues) or [contribute one](#contributing). ## Installing a plugin diff --git a/after-hours/README.md b/after-hours/README.md index 9b2effb..5a1e6ee 100644 --- a/after-hours/README.md +++ b/after-hours/README.md @@ -86,6 +86,13 @@ and upload it in the dashboard **Plugins → Install** (or the **Catalog** tab). Targets OpenWA **≥ 0.6.2** (sandboxed runtime with `Intl` timezone data and `onConfigChange` forwarding, so schedule edits apply live without a re-enable). +### Per-session config + +**Supported.** Every config field (`schedule`, `timezone`, `awayMessage`, `cooldownSec`, +`respondInGroups`) may be overridden per WhatsApp session via the dashboard; an override takes effect on +the next inbound message (config is re-read per event). For example, two sessions can have different +business hours and away messages. + ## Security The plugin declares only `messages:send` and makes no outbound network calls. The away message is the diff --git a/chat-flow/README.md b/chat-flow/README.md index 5e2612a..60aac64 100644 --- a/chat-flow/README.md +++ b/chat-flow/README.md @@ -107,6 +107,14 @@ Targets OpenWA **≥ 0.7.0** — relies on per-session config resolution (`sessi `onConfigChange`, and the `configUi` sandboxed config editor. Uses only `ctx.messages.reply`, `ctx.storage`, `ctx.logger`, and `ctx.config`. +### Per-session config + +**Supported.** Every config field (`trigger`, `greeting`, `respondInGroups`, the `options` tree) may +be overridden per WhatsApp session via the dashboard; an override takes effect on the next inbound +message (config is re-read per event). For example, two sessions can run different menu trees. Flow +state is keyed per `(session, chat)` in storage, so flows for sessions with different menus never +interfere. + ## Security Flow state lives in `ctx.storage`, keyed per `(session, chat)` and expiring after 15 minutes — no diff --git a/chatwoot-adapter/README.md b/chatwoot-adapter/README.md index 11374aa..f365c8a 100644 --- a/chatwoot-adapter/README.md +++ b/chatwoot-adapter/README.md @@ -115,6 +115,16 @@ public host is added to the outbound allowlist). To use a self-hosted Chatwoot: `RESOLVE_LID_TO_PHONE=true` in OpenWA. - **Chatwoot** — account-level webhooks with timestamped HMAC signing (see Setup). +### Per-session config + +**Supported.** Every config field (`baseUrl`, `apiToken`, `accountId`, `inboxId`, `relayGroups`, +`relayMedia`, `relayOwnMessages`, `backfillLimit`, `backfillAllOnce`) may be overridden per WhatsApp +session via the dashboard; an override takes effect on the next inbound message or webhook delivery +(config is re-read per event). This is the **first-class multi-tenant shape**: bind one instance per +WhatsApp session, each pointing at a different Chatwoot account/inbox, and the session-scoped mapping +store isolates them. The per-session `baseUrl` is auto-added to the outbound allowlist via +`allowConfigHosts`. + ## Security - The outbound HTTP allowlist admits only your configured Chatwoot host; OpenWA's SSRF guard still blocks diff --git a/faq-bot/README.md b/faq-bot/README.md index 0560e22..5f991c7 100644 --- a/faq-bot/README.md +++ b/faq-bot/README.md @@ -88,6 +88,13 @@ Targets OpenWA **≥ 0.6.1** (sandboxed plugin runtime). Live config edits (`PUT immediately on builds that forward `onConfigChange` to sandboxed plugins (the #430 follow-ups); on v0.6.0/v0.6.1 a disable + re-enable is needed after changing rules. +### Per-session config + +**Supported.** Every config field (`rules`, `fallbackReply`, `fallbackCooldownSec`, +`respondInGroups`) may be overridden per WhatsApp session via the dashboard; an override takes effect on +the next inbound message (config is re-read per event). For example, two sessions can run different FAQ +rule sets. + ## Security `regex` patterns are operator-authored (trusted) and tested against at most the first 1000 characters diff --git a/group-translate/README.md b/group-translate/README.md index 77b48b6..13f4880 100644 --- a/group-translate/README.md +++ b/group-translate/README.md @@ -95,6 +95,20 @@ Then, in the group, an admin runs `/tr on`. Or install the packaged `.zip` from Targets OpenWA **≥ 0.7.0** — outbound HTTP uses the v0.7 `ctx.net.fetch` capability. Declares `messages:send`, `engine:read` (for admin checks), and `net:fetch`. +### Per-session config + +**Supported, with a caveat.** Every config field may be overridden per WhatsApp session via the +dashboard; an override that changes a coordinator-affecting field (e.g. `libretranslateUrl`, +`libretranslateApiKey`, `timeoutMs`, `commandPrefix`, `minLength`, `maxLength`, `denyReply`) takes +effect on the next inbound message. The plugin uses config-signature caching: the coordinator (and the +LibreTranslate client's circuit breaker) is reused across messages with the same resolved config, and +rebuilt only when the signature changes. + +**Caveat for multi-backend deployments:** when two sessions point at *different* LibreTranslate +instances, they share one coordinator slot and clobber each other's circuit-breaker state on every +message alternation. For full per-backend isolation, run **one plugin instance per session** (bind each +to a single WhatsApp session via the dashboard's session scope). + ## Security Outbound translate calls go **exclusively** through the host's SSRF-guarded `ctx.net.fetch`; there is no diff --git a/gsheets-logger/README.md b/gsheets-logger/README.md index 2d962d5..3e7bc09 100644 --- a/gsheets-logger/README.md +++ b/gsheets-logger/README.md @@ -209,13 +209,13 @@ version-dependent: v0.6.0/v0.6.1, a config change needs a disable + re-enable, and a non-graceful exit (SIGKILL/OOM) can drop rows buffered since the last flush (≤ `flushIntervalSec`). -### Per-session spreadsheet routing is not supported +### Per-session config -The plugin holds a **single** in-memory buffer and a single Sheets client built from the base `*` config -at `onEnable`. Rows from **every** WhatsApp session land in that one configured sheet. A per-session -config override that points at a different `spreadsheetId` / `serviceAccountJson` is **not** honored — the -buffer cannot attribute a row to a session at flush time, so routing per session is a design-level change, -not a config one. +**Not supported.** The plugin holds a **single** in-memory buffer and a single Sheets client built from +the base `*` config at `onEnable`. Rows from **every** WhatsApp session land in that one configured sheet. +A per-session config override that points at a different `spreadsheetId` / `serviceAccountJson` is **not** +honored — the buffer cannot attribute a row to a session at flush time, so routing per session is a +design-level change, not a config one. If you need to log different sessions to different sheets, run **one plugin instance per session** (bind each to a single WhatsApp session via the dashboard's session scope), or run multiple OpenWA instances. diff --git a/http-action/README.md b/http-action/README.md index 2c38fb7..d8f37bb 100644 --- a/http-action/README.md +++ b/http-action/README.md @@ -105,6 +105,14 @@ Status is **development**. It targets OpenWA **≥ 0.8.7** (`allowConfigHosts` + instance before being marked `beta`. Live config edits apply on the next inbound message (config is re-read per event). +### Per-session config + +**Supported.** Every config field (`baseUrl`, auth fields, `actions`, `timeoutMs`, `cooldownSeconds`, +`respondInGroups`) may be overridden per WhatsApp session via the dashboard; an override takes effect on +the next inbound message (config is re-read per event). For example, two sessions can target different +API endpoints with different action sets. The per-session `baseUrl` is auto-added to the outbound +allowlist via `allowConfigHosts`. + ## Security - **Fixed origin.** `baseUrl` must be HTTPS with no embedded credentials, query string, or fragment, and diff --git a/supabase-otp-hook/README.md b/supabase-otp-hook/README.md index 967e0f2..87b0888 100644 --- a/supabase-otp-hook/README.md +++ b/supabase-otp-hook/README.md @@ -131,6 +131,15 @@ Session scope (which session sends) is also set at instance mint time, not in th - **Supabase** — HTTP Send SMS hook with Standard Webhooks signing. SQL (Postgres function) hook variant not supported. - **WhatsApp** — may rate-limit or require an approved business template for business-initiated messages. Adjust `messageTemplate` to match your approved wording. +### Per-session config + +**Supported.** Every config field (`appName`, `messageTemplate`, `fallbackSessionId`, `debug`) may be +overridden per WhatsApp session via the dashboard; an override takes effect on the next delivery (config +is re-read per delivery). For example, two sessions can brand the OTP message differently. Note: this +plugin's inbound channel is the Supabase webhook (not a WhatsApp message), so a per-session override +applies to the session the instance is bound to (`sessionScope`) — bind each instance to a session, or +set `fallbackSessionId`. + ## Security - Webhook secret stored as the instance secret (masked on dashboard reads after mint). diff --git a/typebot-connector/README.md b/typebot-connector/README.md index cfa36c8..312cdd0 100644 --- a/typebot-connector/README.md +++ b/typebot-connector/README.md @@ -103,6 +103,15 @@ and upload it in the dashboard **Plugins → Install** (or the **Catalog** tab). - `payment` steps and non-renderable embeds can't be shown on WhatsApp and get a short fallback message. Streaming AI blocks are resolved server-side into normal text bubbles. +### Per-session config + +**Supported.** Every config field (`apiHost`, `publicId`, `apiToken`, `respondInGroups`, +`sessionTimeoutMinutes`, `passContactVariables`, `mediaHost`) may be overridden per WhatsApp session +via the dashboard; an override takes effect on the next inbound message (config is re-read per event). +For example, two sessions can connect to different Typebot bots. The per-session `apiHost`/`mediaHost` +is auto-added to the outbound allowlist via `allowConfigHosts`. Flow state is keyed per `(session, chat)` +in storage, so flows across sessions never interfere. + ## Security - **Outbound HTTP is allow-listed.** Calls go through the host's SSRF-guarded `ctx.net.fetch`; only diff --git a/voice-transcription/README.md b/voice-transcription/README.md index 3eb1311..5a0ccca 100644 --- a/voice-transcription/README.md +++ b/voice-transcription/README.md @@ -136,6 +136,20 @@ curl -X POST http://localhost:2785/plugins/voice-transcription/enable \ engine re-fire can still double-call STT). For exactly-once, structured-event delivery, a future core `message.transcription` event would be the upgrade path. +### Per-session config + +**Supported, with a caveat.** Every config field may be overridden per WhatsApp session via the +dashboard; an override that changes a coordinator-affecting field (e.g. `sttBaseUrl`, `sttApiKey`, +`model`, `language`, `timeoutMs`, delivery fields, `chatDelivery`, `enabledMessageTypes`, +`maxSizeBytes`, `maxPerHour`, `provider`) takes effect on the next inbound message. The plugin uses +config-signature caching: the coordinator (and the STT provider's circuit breaker) is reused across +messages with the same resolved config, and rebuilt only when the signature changes. + +**Caveat for multi-backend deployments:** when two sessions point at *different* STT backends, they +share one coordinator slot and clobber each other's circuit-breaker state on every message +alternation. For full per-backend isolation, run **one plugin instance per session** (bind each to a +single WhatsApp session via the dashboard's session scope). + ## Security - **Outbound HTTP is allow-listed.** Calls go through the host's SSRF-guarded `ctx.net.fetch`; only hosts in