Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion PLUGIN-STANDARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>`.
Expand All @@ -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**.
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions after-hours/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions chat-flow/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions chatwoot-adapter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions faq-bot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions group-translate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions gsheets-logger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions http-action/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions supabase-otp-hook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
9 changes: 9 additions & 0 deletions typebot-connector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions voice-transcription/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading