npm package: @prombot-ai/prombot-bridge
Description: OpenClaw multi-channel bridge: routes messages between Discord, WhatsApp, Telegram, and PromBot
Latest version: 0.5.0

# bridge

OpenClaw plugin that routes messages between Discord, WhatsApp, Telegram, and PromBot.

**WhatsApp** supports two modes: **Meta WhatsApp Business Cloud API** (Graph, with `phoneNumberId` / `accessToken`) and **OpenClaw’s built-in WhatsApp Web** (Baileys, no Meta credentials). The bridge picks Meta when those fields are set; otherwise it sends through the gateway’s `runtime.channel.outbound` WhatsApp adapter when `channels.whatsapp` is enabled.

## Features

| Feature | Description |
|---|---|
| Routing rules | Match by source channel, user pattern, keywords, and regex |
| Target transforms | Format output for Discord, WhatsApp, Telegram, or PromBot |
| Cross-channel identities | Link users across channels for shared routing and moderation |
| Rate limiting | Sliding-window per (channel, user) with permanent block and manual reset |
| Agent tools | Manage routes, identities, sends, and moderation from OpenClaw tools |

## Installation

```sh
# From npm / ClawHub
openclaw plugins install @prombot-ai/bridge

# Local development from this repository root
openclaw plugins install -l ./bridge
```

## Configuration

Bridge is a plugin, not a channel. Configure channel credentials under top-level `channels`, and keep only bridge routing settings under `plugins.entries.bridge.config`.

```jsonc
{
  "channels": {
    "discord": {
      "token": "Bot ...",
      "publicKey": "..."
    },
    // WhatsApp: either Meta Cloud API (phoneNumberId + accessToken + optional verifyToken)
    // or OpenClaw Web (enabled, dmPolicy, allowFrom, … — no Meta fields; link via `openclaw channels login --channel whatsapp`).
    "whatsapp": {
      "phoneNumberId": "123456789",
      "accessToken": "EAA...",
      "verifyToken": "..."
    },
    "telegram": {
      "botToken": "123456:ABC...",
      "secretToken": "..."
    },
    "prombot": {
      "token": "pb-...",
      "baseUrl": "http://127.0.0.1:3456/v1",
      "webhookSecret": "...",
      "allowFrom": ["user-id-1"],
      "dmSecurity": "allowlist"
    }
  },
  "plugins": {
    "entries": {
      "bridge": {
        "enabled": true,
        "config": {
          "routing": {
            "rules": [
              {
                "id": "prombot-to-telegram",
                "fromChannel": "prombot",
                "fromUserPattern": "*",
                "toChannel": "telegram",
                "toTarget": "prombot",
                "transformFormat": "telegram_html",
                "enabled": true,
                "priority": 10
              }
            ]
          },
          "rateLimit": {
            "windowMs": 60000,
            "maxMessages": 30
          },
          "whatsappTargetAliases": {
            "support": "+15551234567"
          }
        }
      }
    }
  }
}
```

Use `http://127.0.0.1:3456/v1` when running the [PromBot chat](../chat/) web server locally (default port `3456`, path `/v1` via `CHAT_PUBLIC_BASE_URL`). For the included mock API instead, use `http://127.0.0.1:4321/v1`. For a hosted deployment, set `baseUrl` to your public API root (for example `https://prombot.ai/chat`).

When the OpenClaw gateway runs inside WSL2, proxy environment variables such as `HTTP_PROXY` and `HTTPS_PROXY` can interfere with PromBot delivery even when `baseUrl` itself is correct. This bridge now uses direct Node `http` / `https` requests for PromBot outbound calls so local `127.0.0.1` delivery does not depend on proxy bypass rules. If you are debugging with custom scripts from WSL2, either bypass the proxy explicitly or ensure `NO_PROXY` includes `127.0.0.1` and `localhost`.

Bridge reads credentials from top-level `channels.discord`, `channels.whatsapp`, `channels.telegram`, and `channels.prombot`. Legacy fallback config loading still accepts `bridge` at the root and `channels.bridge` for routing settings only, but those shapes may be rejected by newer OpenClaw validators. Use `plugins.entries.bridge.config` for routing going forward.

Optional **`plugins.entries.bridge.config.whatsappTargetAliases`** (see `openclaw.plugin.json`): short name → E.164 (or other WhatsApp target string) for rule `toTarget` values when you use **OpenClaw WhatsApp Web** and cannot put extra keys on `channels.whatsapp` (OpenClaw’s schema is strict). Meta Cloud API mode can keep using `channels.whatsapp.targetAliases` instead.

## WhatsApp: Meta vs OpenClaw Web

| | Meta Cloud API | OpenClaw WhatsApp Web |
|---|---|---|
| Config | `channels.whatsapp.phoneNumberId` + `accessToken` (optional `verifyToken`, `targetAliases`) | `channels.whatsapp` enabled **without** Meta fields (`dmPolicy`, `allowFrom`, … as in OpenClaw docs) |
| Outbound | Bridge calls Meta Graph | Bridge uses `api.runtime.channel.outbound.loadAdapter("whatsapp")` and the same `sendText` path as the official channel |
| Inbound webhook | `GET/POST /bridge/whatsapp/webhook` (verify + Meta payloads) | Not used; inbound is handled by OpenClaw’s WhatsApp channel |
| Aliases in rules | `channels.whatsapp.targetAliases` | Prefer `plugins.entries.bridge.config.whatsappTargetAliases` |

For OpenClaw Web, the destination in routing must satisfy **`resolveWhatsAppOutboundTarget`** (same as the channel plugin): e.g. under **`dmPolicy: "pairing"`** the recipient should appear in `channels.whatsapp.allowFrom`. Link the session with `openclaw channels login --channel whatsapp` (often `--account default`). The bridge **retries briefly** if the error is “No active WhatsApp Web listener” so forwards right after gateway start are less likely to race the Baileys listener registration.

## Webhook URLs

| Channel | Path |
|---|---|
| Discord | `POST /bridge/discord/webhook` |
| WhatsApp | `GET/POST /bridge/whatsapp/webhook` (Meta Cloud API only; omitted when Meta is not configured) |
| Telegram | `POST /bridge/telegram/webhook` |
| PromBot | `POST /bridge/prombot/webhook` |

## Agent Tools

| Tool | Description |
|---|---|
| `bridge_send_message` | Send directly to a channel target |
| `bridge_route_message` | Fan-out through active routing rules |
| `bridge_list_routing_rules` | List all rules |
| `bridge_add_routing_rule` | Create a new rule |
| `bridge_update_routing_rule` | Patch rule fields |
| `bridge_remove_routing_rule` | Delete a rule |
| `bridge_toggle_routing_rule` | Enable or disable a rule |
| `bridge_get_user_identity` | Look up cross-channel identity |
| `bridge_link_user_identities` | Link two channel accounts |
| `bridge_list_user_identities` | List all identities |
| `bridge_remove_user_identity` | Delete an identity |
| `bridge_check_rate_limit` | Query rate-limit status |
| `bridge_reset_rate_limit` | Reset a user's counter |
| `bridge_block_user` | Permanently block a user |

## CLI

```sh
openclaw bridge status
openclaw bridge rules
openclaw bridge identities
```

`openclaw bridge status` reports WhatsApp as configured for **Meta Cloud API**, **OpenClaw WhatsApp Web**, or not configured, depending on `channels.whatsapp`.

## PromBot Routing

Bridge supports PromBot as a native source and destination channel. Inbound PromBot webhooks can match the same routing rules used by the other adapters, and bridge can also send outbound messages directly to PromBot targets.

The PromBot **channel** plugin receives chat traffic at **`POST /prombot/webhook`** (what the web UI and `server.mjs` use). The bridge also exposes **`POST /bridge/prombot/webhook`** for direct testing. The `@prombot-ai/prombot` package **fans out** each accepted `/prombot/webhook` message into the bridge pipeline so routing rules run without a second HTTP hop. Install `@prombot-ai/bridge` where the channel package can resolve it (e.g. `npm install ./bridge` from your OpenClaw project, or a workspace link).

Configure PromBot under top-level `channels.prombot`, then add bridge routing under `plugins.entries.bridge.config`.

```jsonc
{
  "channels": {
    "prombot": {
      "token": "pb-...",
      "baseUrl": "http://127.0.0.1:3456/v1",
      "webhookSecret": "...",
      "allowFrom": ["user-id-1"],
      "dmSecurity": "allowlist"
    }
  },
  "plugins": {
    "entries": {
      "bridge": {
        "enabled": true,
        "config": {
          "routing": {
            "rules": [
              {
                "id": "prombot-to-telegram",
                "fromChannel": "prombot",
                "fromUserPattern": "*",
                "toChannel": "telegram",
                "toTarget": "prombot",
                "transformFormat": "telegram_html",
                "enabled": true,
                "priority": 10
              }
            ]
          }
        }
      }
    }
  }
}
```

With the default `dmSecurity` of `allowlist`, only senders in `allowFrom` can trigger inbound PromBot routing when the allowlist is non-empty. Use `open` while testing if you want to accept any sender.

PromBot user IDs come from inbound webhook `from` values. You can find them from inbound traffic, `openclaw sessions --json`, or `openclaw channels logs --channel prombot`.

## Routing PromBot To Telegram

Add a normal bridge routing rule with `fromChannel: "prombot"` and `toChannel: "telegram"`.

```jsonc
{
  "channels": {
    "telegram": {
      "botToken": "123456:ABC...",
      "secretToken": "..."
    },
    "prombot": {
      "token": "pb-...",
      "baseUrl": "http://127.0.0.1:3456/v1",
      "webhookSecret": "...",
      "dmSecurity": "open"
    }
  },
  "plugins": {
    "entries": {
      "bridge": {
        "enabled": true,
        "config": {
          "routing": {
            "rules": [
              {
                "id": "prombot-dm-to-telegram",
                "fromChannel": "prombot",
                "fromUserPattern": "*",
                "toChannel": "telegram",
                "toTarget": "prombot",
                "transformFormat": "telegram_html",
                "enabled": true,
                "priority": 10
              }
            ]
          }
        }
      }
    }
  }
}
```

For Telegram destinations, `toTarget` is resolved with `channels.telegram.targetAliases` (recommended for short names like `prombot`) or a numeric chat ID / `@username` that Telegram’s `getChat` can resolve. Bare names like `prombot` become `@prombot` unless aliased.

For Discord and Telegram, `toTarget` can use a friendly name when you define it in each channel’s `targetAliases`. For **WhatsApp Meta mode**, use `channels.whatsapp.targetAliases`. For **OpenClaw Web mode**, use `plugins.entries.bridge.config.whatsappTargetAliases` (or put an E.164 / JID directly in `toTarget`). The bridge resolves aliases before sending.

PromBot then posts inbound events directly to bridge at `POST /bridge/prombot/webhook`.

Bridge verifies `X-PromBot-Signature: sha256=<hex>` when `webhookSecret` is configured, applies the PromBot allowlist policy, then forwards matching messages to Telegram or any other configured bridge destination.

## WSL2 Local Debugging

If your chat server or mock PromBot API runs on `127.0.0.1` and the gateway is inside WSL2, verify the following before debugging bridge routing:

1. The target endpoint is reachable directly from WSL2.
2. `NO_PROXY` includes `127.0.0.1` and `localhost` if you use proxy-aware tools.
3. Ad-hoc curl tests use proxy bypass when needed, for example `curl --noproxy '*' http://127.0.0.1:3456/v1/sent`.

Bridge routing itself does not replace the normal PromBot channel reply path. A rule like `fromChannel: "prombot"` and `toChannel: "telegram"` adds a forward to Telegram, but it does not suppress the original PromBot channel's own outbound reply delivery.

## License

Apache 2.0