@vama/openclaw 2026.5.5-1 → 2026.5.5-4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # @vama/openclaw
2
+
3
+ Connect your own [OpenClaw](https://openclaw.ai) agent to **Vama** direct messages — the BotFather-style "bring your own claw" flow. This plugin adds a `vama` channel that talks to Vama's BotHub, so an agent you run appears natively in Vama DMs.
4
+
5
+ > **Full setup guide:** https://web.vama.com/connect-guide
6
+
7
+ ## Install the channel
8
+
9
+ The Vama channel ships **bundled** in Vama-provided OpenClaw builds (for example, agents created from inside the Vama app), so if you use one of those you can skip straight to **Get a token** below.
10
+
11
+ If you run **stock OpenClaw** from upstream, install the Vama channel first. It is published to both npm and ClawHub; npm is the default:
12
+
13
+ ```bash
14
+ openclaw plugins install @vama/openclaw
15
+ ```
16
+
17
+ Prefer ClawHub? Use the `clawhub:` spec instead:
18
+
19
+ ```bash
20
+ openclaw plugins install clawhub:@vama/openclaw
21
+ ```
22
+
23
+ Either one pulls the published plugin so `vama` becomes available to `openclaw onboard` and the `channels.vama` config block below. The channel requires OpenClaw `>=2026.5.12`.
24
+
25
+ ## Get a token (Bring your own claw)
26
+
27
+ If you run your own OpenClaw gateway and just want to connect it to Vama — the BotFather-style flow — mint a token straight from the Vama app:
28
+
29
+ 1. On stock OpenClaw, install the channel first (see **Install the channel**). Vama-provided builds already bundle it.
30
+ 2. In Vama, go to **Settings → Agents → Connect an agent** (the "Bring your own claw" page).
31
+ 3. Optionally name the agent, then click **Create agent**. Vama shows a one-time **agent token** and **webhook secret**, plus a ready-to-paste `channels.vama` config block.
32
+ 4. Copy the config into `~/.openclaw/openclaw.json` (see below).
33
+ 5. Fill in `webhookUrl` with your gateway's public URL and start the gateway with `openclaw gateway run` — it registers the URL with BotHub automatically on every start (see **Receiving messages**).
34
+
35
+ Each token is shown **once**. If you lose one, use **Regenerate token** on that agent — the old token stops working immediately. **Delete agent** disconnects the claw and removes that agent from your Vama DMs.
36
+
37
+ > You can connect **as many agents as you like** — each **Create agent** provisions a new, independent agent with its own token. Run a separate OpenClaw gateway (or a separate `accounts` entry — see **Multi-account**) per agent.
38
+
39
+ ## CLI onboarding (alternative)
40
+
41
+ Instead of minting a token in the app, you can auto-provision from the CLI:
42
+
43
+ ```bash
44
+ openclaw onboard
45
+ ```
46
+
47
+ Select **Vama** from the channel list. The wizard prompts for your Vama username, auto-provisions a bot via BotHub, and configures webhook settings. Then start the gateway:
48
+
49
+ ```bash
50
+ openclaw gateway run
51
+ ```
52
+
53
+ ## Manual configuration
54
+
55
+ Set the following in `~/.openclaw/openclaw.json`:
56
+
57
+ ```json
58
+ {
59
+ "channels": {
60
+ "vama": {
61
+ "enabled": true,
62
+ "botToken": "<bot_token from provisioning>",
63
+ "webhookSecret": "<webhook_secret from provisioning>",
64
+ "webhookUrl": "https://<your-public-host>/vama/events",
65
+ "webhookPort": 3001,
66
+ "webhookPath": "/vama/events",
67
+ "webhookHost": "127.0.0.1"
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## Receiving messages (webhook reachability)
74
+
75
+ BotHub delivers inbound messages to your gateway over an HTTP **webhook**, so BotHub must be able to reach your gateway's webhook listener from the internet. Two things make that work:
76
+
77
+ 1. **A public HTTPS URL that forwards to the local listener.** The listener binds to `webhookHost`:`webhookPort``webhookPath` (default `127.0.0.1:3001/vama/events`). If your machine isn't directly reachable, any tunnel or reverse proxy works, e.g.:
78
+
79
+ ```bash
80
+ cloudflared tunnel --url http://localhost:3001
81
+ # prints something like https://random-words.trycloudflare.com
82
+ ```
83
+
84
+ 2. **Registering that URL with BotHub.** Set it as `channels.vama.webhookUrl` (include the `/vama/events` path). The gateway registers it with BotHub **automatically every time it starts** — no manual API calls. If your tunnel URL changes (quick tunnels are ephemeral), update `webhookUrl` and restart the gateway. For a set-and-forget setup, use a stable URL (named Cloudflare tunnel, Tailscale Funnel, or a reverse proxy on a domain you own).
85
+
86
+ The `openclaw onboard` wizard also prompts for this URL, registers it immediately, and saves it to `webhookUrl` for you.
87
+
88
+ Verify end-to-end with:
89
+
90
+ ```bash
91
+ openclaw channels status --probe
92
+ ```
93
+
94
+ The probe checks both that your token is valid **and** that a webhook URL is registered with BotHub. If registration is missing it fails with instructions — a bot in that state shows **"Awaiting claw"** in Vama and cannot receive messages.
95
+
96
+ ### WebSocket delivery (no public URL needed)
97
+
98
+ BotHub can also deliver events over an **outbound WebSocket** (`GET /v1/bot/ws`) — the gateway dials out, so there's nothing to expose: no tunnel, no reverse proxy, works behind any NAT. The gateway probes for it automatically at startup (`transport` defaults to `"auto"`) and falls back to webhook delivery when the bot doesn't have WebSocket enabled server-side. When the socket is connected, BotHub prefers it and uses the registered webhook only as fallback. The probe also accepts a WebSocket-enabled bot without a registered webhook URL.
99
+
100
+ ## Configuration reference
101
+
102
+ | Key | Type | Default | Description |
103
+ | ---------------- | ------- | ---------------- | --------------------------------------------------------------------------------------------------------- |
104
+ | `enabled` | boolean | `false` | Enable/disable the Vama channel |
105
+ | `botToken` | string | — | Bot authentication token from provisioning |
106
+ | `webhookSecret` | string | — | HMAC secret for webhook signature verification |
107
+ | `webhookUrl` | string | — | Public URL of your webhook listener. Auto-registered with BotHub at every gateway start. |
108
+ | `transport` | string | `"auto"` | Inbound delivery: `"auto"` (WebSocket when BotHub allows it, else webhook), `"webhook"`, or `"websocket"` |
109
+ | `webhookPort` | integer | `3001` | Local port for the webhook listener |
110
+ | `webhookPath` | string | `"/vama/events"` | URL path for webhook events |
111
+ | `webhookHost` | string | `"127.0.0.1"` | Bind address for the webhook listener |
112
+ | `dmPolicy` | string | `"open"` | DM access policy: `"open"`, `"pairing"`, or `"allowlist"` |
113
+ | `allowFrom` | array | `[]` | Vama user IDs allowed to message the bot |
114
+ | `textChunkLimit` | integer | `10000` | Max characters per outbound message |
115
+ | `bothubUrl` | string | _(canonical)_ | Override the BotHub API base URL. Only needed for self-hosted BotHub deployments. |
116
+
117
+ ## Access control
118
+
119
+ Control who can message the bot with `channels.vama.dmPolicy`:
120
+
121
+ - **`open`** (default) — any Vama user can DM the bot.
122
+ - **`pairing`** — unknown users get a pairing code to request approval.
123
+ - **`allowlist`** — only users listed in `channels.vama.allowFrom` can DM the bot.
124
+
125
+ ```json
126
+ {
127
+ "channels": {
128
+ "vama": {
129
+ "dmPolicy": "allowlist",
130
+ "allowFrom": ["user_alice", "user_bob"]
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Webhook security
137
+
138
+ BotHub signs every webhook delivery with HMAC-SHA256. OpenClaw verifies the signature using the `webhookSecret` from provisioning.
139
+
140
+ Headers sent by BotHub:
141
+
142
+ - `X-BotHub-Signature` — `sha256=<hex HMAC>`
143
+ - `X-BotHub-Timestamp` — Unix seconds
144
+ - `X-BotHub-Event` — Event type (e.g. `message.create`)
145
+ - `X-BotHub-Delivery-ID` — Unique delivery identifier
146
+
147
+ Signatures older than 5 minutes are rejected to prevent replay attacks.
148
+
149
+ ## Multi-account
150
+
151
+ For multiple bot accounts, use the `accounts` map:
152
+
153
+ ```json
154
+ {
155
+ "channels": {
156
+ "vama": {
157
+ "enabled": true,
158
+ "bothubUrl": "https://bothub.example.com",
159
+ "accounts": {
160
+ "staging": {
161
+ "botToken": "<staging_token>",
162
+ "webhookSecret": "<staging_secret>",
163
+ "webhookPort": 3002,
164
+ "name": "Staging Bot"
165
+ },
166
+ "production": {
167
+ "botToken": "<prod_token>",
168
+ "webhookSecret": "<prod_secret>",
169
+ "webhookPort": 3003,
170
+ "name": "Production Bot"
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ ```
177
+
178
+ Named accounts inherit top-level settings (like `bothubUrl`) and can override them individually.
179
+
180
+ ## Capabilities
181
+
182
+ | Feature | Supported |
183
+ | ----------------- | -------------------- |
184
+ | Direct messages | Yes |
185
+ | Threads (replies) | Yes |
186
+ | Media attachments | No (text-only in v1) |
187
+ | Reactions | No |
188
+ | Message editing | No |
189
+ | Groups/channels | No |
190
+
191
+ ## Troubleshooting
192
+
193
+ - **Vama shows "Awaiting claw" even though the gateway is running**: no webhook URL is registered with BotHub. Set `channels.vama.webhookUrl` to your public URL and restart the gateway — it registers automatically. `openclaw channels status --probe` reports this state explicitly ("no webhook URL is registered with BotHub").
194
+ - **Bot not responding**: run `openclaw channels status --probe`. It verifies both the token and webhook registration. Also check the gateway log for `webhook registered with BotHub` (or a registration error) at startup.
195
+ - **Worked, then stopped after a tunnel restart**: ephemeral tunnel URLs change on restart. Update `webhookUrl` to the new URL and restart the gateway, or switch to a stable tunnel/domain.
196
+ - **Signature verification failed**: ensure `webhookSecret` matches the value from provisioning. Re-provision if needed.
197
+ - **Connection test fails during onboarding**: verify the `bothubUrl` is correct and reachable from your gateway host.
198
+ - **Messages dropped**: check gateway logs for `dmPolicy` blocks. If using `allowlist`, verify the sender's user ID is in `channels.vama.allowFrom`.
199
+
200
+ ## License
201
+
202
+ See the Vama OpenClaw distribution for license terms.
@@ -1,4 +1,4 @@
1
- import { a as dispatchStarted, i as dispatchEnded } from "./probe-B2hFOc2Y.js";
1
+ import { a as dispatchStarted, i as dispatchEnded } from "./monitor-CHFjRu2J.js";
2
2
  //#region extensions/vama/src/subagent-keepalive-hooks.ts
3
3
  function registerVamaSubagentKeepaliveHooks(api) {
4
4
  api.on("subagent_spawned", () => {
package/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { n as monitorVamaProvider, r as sendMessageVama, t as probeVama } from "./probe-B2hFOc2Y.js";
2
- import { t as registerVamaSubagentKeepaliveHooks } from "./api-C0vtNv5b.js";
1
+ import { n as probeVama, r as sendMessageVama, t as monitorVamaProvider } from "./monitor-CHFjRu2J.js";
2
+ import { t as registerVamaSubagentKeepaliveHooks } from "./api-lhR0QgC_.js";
3
3
  export { monitorVamaProvider, probeVama, registerVamaSubagentKeepaliveHooks, sendMessageVama };
@@ -1,5 +1,5 @@
1
1
  import { a as provisionBot, i as createBotHubClient, n as attachmentHintFromExtension } from "./client-AsD46gcK.js";
2
- import { c as resolveVamaAccount, d as buildBaseChannelStatusSummary, f as createDefaultChannelRuntimeState, l as DEFAULT_ACCOUNT_ID$1, n as monitorVamaProvider, o as listVamaAccountIds, r as sendMessageVama, s as resolveDefaultVamaAccountId, t as probeVama, u as PAIRING_APPROVED_MESSAGE } from "./probe-B2hFOc2Y.js";
2
+ import { c as buildBaseChannelStatusSummary, d as resolveDefaultVamaAccountId, f as resolveVamaAccount, l as createDefaultChannelRuntimeState, n as probeVama, o as DEFAULT_ACCOUNT_ID$1, r as sendMessageVama, s as PAIRING_APPROVED_MESSAGE, t as monitorVamaProvider, u as listVamaAccountIds } from "./monitor-CHFjRu2J.js";
3
3
  import { t as getVamaRuntime } from "./runtime-w-1oL50p.js";
4
4
  import { jsonResult, readStringOrNumberParam, readStringParam } from "openclaw/plugin-sdk/channel-actions";
5
5
  import { extractToolSend } from "openclaw/plugin-sdk/tool-send";
@@ -367,6 +367,15 @@ const vamaPlugin = {
367
367
  minimum: 1
368
368
  },
369
369
  webhookHost: { type: "string" },
370
+ webhookUrl: { type: "string" },
371
+ transport: {
372
+ type: "string",
373
+ enum: [
374
+ "auto",
375
+ "webhook",
376
+ "websocket"
377
+ ]
378
+ },
370
379
  dmPolicy: {
371
380
  type: "string",
372
381
  enum: [
@@ -399,7 +408,16 @@ const vamaPlugin = {
399
408
  type: "integer",
400
409
  minimum: 1
401
410
  },
402
- webhookHost: { type: "string" }
411
+ webhookHost: { type: "string" },
412
+ webhookUrl: { type: "string" },
413
+ transport: {
414
+ type: "string",
415
+ enum: [
416
+ "auto",
417
+ "webhook",
418
+ "websocket"
419
+ ]
420
+ }
403
421
  }
404
422
  }
405
423
  }
@@ -625,13 +643,24 @@ const vamaPlugin = {
625
643
  }
626
644
  }
627
645
  })).trim();
628
- if (publicWebhookUrl) try {
629
- await createBotHubClient(resolveVamaAccount({ cfg: next })).registerWebhook({ url: publicWebhookUrl });
630
- await prompter.note(`Webhook registered: ${publicWebhookUrl}`, "Vama webhook registration");
631
- } catch (err) {
632
- await prompter.note(`Webhook registration failed: ${String(err)}\nYou can register later via the BotHub API.`, "Vama webhook registration");
633
- }
634
- else await prompter.note("Skipped webhook registration. Run the setup wizard again once your gateway is publicly reachable to register a webhook URL.", "Vama webhook registration");
646
+ if (publicWebhookUrl) {
647
+ next = {
648
+ ...next,
649
+ channels: {
650
+ ...next.channels,
651
+ vama: {
652
+ ...next.channels?.vama,
653
+ webhookUrl: publicWebhookUrl
654
+ }
655
+ }
656
+ };
657
+ try {
658
+ await createBotHubClient(resolveVamaAccount({ cfg: next })).registerWebhook({ url: publicWebhookUrl });
659
+ await prompter.note(`Webhook registered: ${publicWebhookUrl}`, "Vama webhook registration");
660
+ } catch (err) {
661
+ await prompter.note(`Webhook registration failed: ${String(err)}\nThe gateway will retry automatically at startup (channels.vama.webhookUrl is saved).`, "Vama webhook registration");
662
+ }
663
+ } else await prompter.note("Skipped webhook registration. Once your gateway is publicly reachable, set channels.vama.webhookUrl to its public URL (e.g. https://your-host/vama/events) — it registers automatically at startup.", "Vama webhook registration");
635
664
  const account = resolveVamaAccount({ cfg: next });
636
665
  try {
637
666
  const probe = await probeVama(account);
@@ -1,2 +1,2 @@
1
- import { t as vamaPlugin } from "./channel-plugin-api-CcZ_y9pT.js";
1
+ import { t as vamaPlugin } from "./channel-plugin-api-YGbkWmVM.js";
2
2
  export { vamaPlugin };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { n as monitorVamaProvider, r as sendMessageVama, t as probeVama } from "./probe-B2hFOc2Y.js";
1
+ import { n as probeVama, r as sendMessageVama, t as monitorVamaProvider } from "./monitor-CHFjRu2J.js";
2
2
  import { n as setVamaRuntime } from "./runtime-w-1oL50p.js";
3
- import { t as registerVamaSubagentKeepaliveHooks } from "./api-C0vtNv5b.js";
4
- import { t as vamaPlugin } from "./channel-plugin-api-CcZ_y9pT.js";
3
+ import { t as registerVamaSubagentKeepaliveHooks } from "./api-lhR0QgC_.js";
4
+ import { t as vamaPlugin } from "./channel-plugin-api-YGbkWmVM.js";
5
5
  import "./runtime-api.js";
6
6
  //#region extensions/vama/index.ts
7
7
  const channelEntry = {
@@ -3,6 +3,11 @@ import { t as getVamaRuntime } from "./runtime-w-1oL50p.js";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
5
5
  import * as http from "node:http";
6
+ import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
7
+ import path from "node:path";
8
+ import { WebSocket, fetch } from "undici";
9
+ import { homedir } from "node:os";
10
+ import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
6
11
  import { createDedupeCache } from "openclaw/plugin-sdk/dedupe-runtime";
7
12
  import { installRequestBodyLimitGuard } from "openclaw/plugin-sdk/webhook-request-guards";
8
13
  import { logTypingFailure } from "openclaw/plugin-sdk/channel-logging";
@@ -10,34 +15,8 @@ import { PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk/channel-plugin-com
10
15
  import { createReplyPrefixContext, createTypingCallbacks } from "openclaw/plugin-sdk/channel-message";
11
16
  import { createPersistentDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
12
17
  import { buildBaseChannelStatusSummary, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
13
- import { DEFAULT_ACCOUNT_ID, DEFAULT_ACCOUNT_ID as DEFAULT_ACCOUNT_ID$1, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
14
- import path from "node:path";
15
- import { homedir } from "node:os";
16
- import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
17
18
  import * as crypto from "node:crypto";
18
19
  import { createHash } from "node:crypto";
19
- //#region extensions/vama/src/host-pairing-access.ts
20
- /** Scope pairing store operations to one channel/account pair for plugin-facing helpers. */
21
- function createScopedPairingAccess(params) {
22
- const resolvedAccountId = normalizeAccountId(params.accountId);
23
- return {
24
- accountId: resolvedAccountId,
25
- readAllowFromStore: () => params.core.channel.pairing.readAllowFromStore({
26
- channel: params.channel,
27
- accountId: resolvedAccountId
28
- }),
29
- readStoreForDmPolicy: (provider, accountId) => params.core.channel.pairing.readAllowFromStore({
30
- channel: provider,
31
- accountId: normalizeAccountId(accountId)
32
- }),
33
- upsertPairingRequest: (input) => params.core.channel.pairing.upsertPairingRequest({
34
- channel: params.channel,
35
- accountId: resolvedAccountId,
36
- ...input
37
- })
38
- };
39
- }
40
- //#endregion
41
20
  //#region extensions/vama/src/accounts.ts
42
21
  function listConfiguredAccountIds(cfg) {
43
22
  const accounts = (cfg.channels?.vama)?.accounts;
@@ -77,6 +56,9 @@ function resolveVamaAccount(params) {
77
56
  const webhookSecret = merged.webhookSecret?.trim() || void 0;
78
57
  const webhookSecretFile = merged.webhookSecretFile?.trim() || void 0;
79
58
  const bothubUrl = merged.bothubUrl?.trim() || "https://bothub.vama.com";
59
+ const webhookUrl = merged.webhookUrl?.trim() || void 0;
60
+ const rawTransport = merged.transport?.trim();
61
+ const transport = rawTransport === "auto" || rawTransport === "webhook" || rawTransport === "websocket" ? rawTransport : void 0;
80
62
  return {
81
63
  accountId,
82
64
  enabled,
@@ -86,10 +68,34 @@ function resolveVamaAccount(params) {
86
68
  webhookSecret,
87
69
  webhookSecretFile,
88
70
  bothubUrl,
71
+ webhookUrl,
72
+ transport,
89
73
  config: merged
90
74
  };
91
75
  }
92
76
  //#endregion
77
+ //#region extensions/vama/src/host-pairing-access.ts
78
+ /** Scope pairing store operations to one channel/account pair for plugin-facing helpers. */
79
+ function createScopedPairingAccess(params) {
80
+ const resolvedAccountId = normalizeAccountId(params.accountId);
81
+ return {
82
+ accountId: resolvedAccountId,
83
+ readAllowFromStore: () => params.core.channel.pairing.readAllowFromStore({
84
+ channel: params.channel,
85
+ accountId: resolvedAccountId
86
+ }),
87
+ readStoreForDmPolicy: (provider, accountId) => params.core.channel.pairing.readAllowFromStore({
88
+ channel: provider,
89
+ accountId: normalizeAccountId(accountId)
90
+ }),
91
+ upsertPairingRequest: (input) => params.core.channel.pairing.upsertPairingRequest({
92
+ channel: params.channel,
93
+ accountId: resolvedAccountId,
94
+ ...input
95
+ })
96
+ };
97
+ }
98
+ //#endregion
93
99
  //#region extensions/vama/src/dedup.ts
94
100
  const DEDUP_TTL_MS = 1440 * 60 * 1e3;
95
101
  const MEMORY_MAX_SIZE = 1e3;
@@ -703,6 +709,335 @@ async function handleVamaMessage(params) {
703
709
  }
704
710
  }
705
711
  //#endregion
712
+ //#region extensions/vama/src/ws.ts
713
+ /**
714
+ * WebSocket transport for inbound BotHub events.
715
+ *
716
+ * BotHub exposes `GET /v1/bot/ws` (Authorization: Bot <token>). When a bot
717
+ * has `websocket_enabled` set server-side, BotHub prefers delivering events
718
+ * over the live WebSocket and only falls back to the registered webhook when
719
+ * no connection is up. Frames are the exact same JSON envelopes as webhook
720
+ * POST bodies (`{type, timestamp, bot_id, data}`) — no HMAC headers, since
721
+ * authentication already happened at the upgrade.
722
+ *
723
+ * Why this exists: a connected WebSocket needs NO public URL — no tunnel,
724
+ * no reverse proxy, works behind any NAT. That makes it the easiest possible
725
+ * "bring your own claw" transport. The flag is off by default server-side,
726
+ * so this client is written to probe first and fall back to webhook-only
727
+ * silently — the day BotHub enables WebSocket for a bot, the very next
728
+ * gateway start picks it up with zero config changes.
729
+ *
730
+ * Server behavior this client is built against (BotHub `internal/ws/hub.go`):
731
+ * - server pings every 30s, expects a pong within 60s (undici auto-pongs)
732
+ * - client frames are ignored (bots send via REST) — we never send
733
+ * - a new connection for the same bot replaces the old one server-side
734
+ */
735
+ /** Reconnect schedule. Index advances per consecutive failure, clamps at the tail. */
736
+ const DEFAULT_BACKOFF_MS = [
737
+ 1e3,
738
+ 2e3,
739
+ 5e3,
740
+ 1e4,
741
+ 3e4
742
+ ];
743
+ function resolveVamaWsUrl(bothubUrl) {
744
+ const base = new URL(bothubUrl);
745
+ base.protocol = base.protocol === "http:" ? "ws:" : "wss:";
746
+ base.pathname = `${base.pathname.replace(/\/$/, "")}/v1/bot/ws`;
747
+ base.search = "";
748
+ base.hash = "";
749
+ return base.toString();
750
+ }
751
+ /**
752
+ * Plain authed GET against the WS endpoint to learn whether an upgrade can
753
+ * ever succeed, without burning a real upgrade attempt. The server checks
754
+ * token auth and `websocket_enabled` BEFORE upgrading, so a non-upgrade GET
755
+ * deterministically distinguishes "would work" (400/426 from the upgrader
756
+ * rejecting a plain GET) from "will never work" (403 disabled / 401 / 404).
757
+ *
758
+ * Throws on transport errors — callers treat those as retryable.
759
+ */
760
+ async function checkVamaWebSocketSupport(account, deps = {}) {
761
+ const fetchImpl = deps.fetchImpl ?? fetch;
762
+ const url = new URL(account.bothubUrl ?? "https://bothub.vama.com");
763
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/v1/bot/ws`;
764
+ const res = await fetchImpl(url.toString(), {
765
+ method: "GET",
766
+ headers: { Authorization: `Bot ${account.botToken ?? ""}` }
767
+ });
768
+ const body = await res.text().catch(() => "");
769
+ if (res.status === 400 || res.status === 426) return { supported: true };
770
+ if (res.status === 403) {
771
+ let code;
772
+ try {
773
+ code = JSON.parse(body).error;
774
+ } catch {}
775
+ return {
776
+ supported: false,
777
+ reason: code === "websocket_disabled" ? "disabled" : "http_403"
778
+ };
779
+ }
780
+ if (res.status === 401) return {
781
+ supported: false,
782
+ reason: "unauthorized"
783
+ };
784
+ if (res.status === 404) return {
785
+ supported: false,
786
+ reason: "unsupported"
787
+ };
788
+ return {
789
+ supported: false,
790
+ reason: `http_${res.status}`
791
+ };
792
+ }
793
+ function sleep$1(ms, abortSignal) {
794
+ return new Promise((resolve) => {
795
+ if (ms <= 0 || abortSignal?.aborted) {
796
+ resolve();
797
+ return;
798
+ }
799
+ const timer = setTimeout(() => {
800
+ abortSignal?.removeEventListener("abort", onAbort);
801
+ resolve();
802
+ }, ms);
803
+ const onAbort = () => {
804
+ clearTimeout(timer);
805
+ resolve();
806
+ };
807
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
808
+ });
809
+ }
810
+ /**
811
+ * Starts the WebSocket transport loop. Never throws; the loop ends either on
812
+ * abort, or when the preflight rules WS out (caller stays on webhook
813
+ * delivery — BotHub falls back server-side automatically).
814
+ */
815
+ function startVamaWebSocket(opts) {
816
+ const { account, onEvent, log, error } = opts;
817
+ const accountId = account.accountId;
818
+ const mode = opts.mode ?? "auto";
819
+ const backoff = opts.backoffMs ?? DEFAULT_BACKOFF_MS;
820
+ const WebSocketCtor = opts.deps?.webSocketCtor ?? WebSocket;
821
+ let state = "connecting";
822
+ let active;
823
+ const abort = new AbortController();
824
+ if (opts.abortSignal?.aborted) abort.abort();
825
+ else opts.abortSignal?.addEventListener("abort", () => abort.abort(), { once: true });
826
+ const aborted = () => abort.signal.aborted;
827
+ const connectOnce = () => new Promise((resolve) => {
828
+ const ws = new WebSocketCtor(resolveVamaWsUrl(account.bothubUrl ?? "https://bothub.vama.com"), { headers: { Authorization: `Bot ${account.botToken ?? ""}` } });
829
+ active = ws;
830
+ let opened = false;
831
+ let settled = false;
832
+ const settle = () => {
833
+ if (!settled) {
834
+ settled = true;
835
+ active = void 0;
836
+ resolve({ opened });
837
+ }
838
+ };
839
+ ws.addEventListener("open", () => {
840
+ opened = true;
841
+ state = "open";
842
+ log(`vama[${accountId}]: WebSocket connected to BotHub — events arrive in real time`);
843
+ });
844
+ ws.addEventListener("message", (ev) => {
845
+ let parsed;
846
+ try {
847
+ parsed = JSON.parse(String(ev.data));
848
+ } catch {
849
+ error(`vama[${accountId}]: ignoring malformed WebSocket frame`);
850
+ return;
851
+ }
852
+ try {
853
+ onEvent(parsed);
854
+ } catch (err) {
855
+ error(`vama[${accountId}]: WebSocket event handler error: ${String(err)}`);
856
+ }
857
+ });
858
+ ws.addEventListener("error", () => {});
859
+ ws.addEventListener("close", () => settle());
860
+ });
861
+ const loop = async () => {
862
+ let failures = 0;
863
+ while (!aborted()) {
864
+ let preflight;
865
+ try {
866
+ preflight = await checkVamaWebSocketSupport(account, opts.deps);
867
+ } catch (err) {
868
+ failures += 1;
869
+ const delay = backoff[Math.min(failures - 1, backoff.length - 1)];
870
+ if (failures === 1) log(`vama[${accountId}]: WebSocket preflight failed (${err instanceof Error ? err.message : String(err)}); retrying in ${Math.round(delay / 1e3)}s`);
871
+ await sleep$1(delay, abort.signal);
872
+ continue;
873
+ }
874
+ if (!preflight.supported) {
875
+ if (mode === "websocket") error(`vama[${accountId}]: transport is set to "websocket" but BotHub refused (${preflight.reason}). ` + (preflight.reason === "disabled" ? "WebSocket delivery is not enabled for this bot — falling back to webhook delivery. Remove transport or set it to \"auto\"." : "Falling back to webhook delivery."));
876
+ else if (preflight.reason !== "disabled") log(`vama[${accountId}]: WebSocket unavailable (${preflight.reason}); using webhook delivery`);
877
+ state = "fallback";
878
+ return;
879
+ }
880
+ if (aborted()) break;
881
+ state = "connecting";
882
+ const { opened } = await connectOnce();
883
+ if (aborted()) break;
884
+ if (opened) {
885
+ failures = 0;
886
+ log(`vama[${accountId}]: WebSocket closed; reconnecting`);
887
+ } else failures += 1;
888
+ const delay = opened ? backoff[0] : backoff[Math.min(failures - 1, backoff.length - 1)];
889
+ await sleep$1(Math.round(delay * (.8 + Math.random() * .4)), abort.signal);
890
+ }
891
+ state = "stopped";
892
+ };
893
+ return {
894
+ state: () => state,
895
+ done: loop().catch((err) => {
896
+ state = "stopped";
897
+ error(`vama[${accountId}]: WebSocket transport loop crashed: ${String(err)}`);
898
+ }),
899
+ stop: () => {
900
+ abort.abort();
901
+ try {
902
+ active?.close();
903
+ } catch {}
904
+ }
905
+ };
906
+ }
907
+ //#endregion
908
+ //#region extensions/vama/src/probe.ts
909
+ const probeCache = /* @__PURE__ */ new Map();
910
+ const PROBE_CACHE_TTL_MS = 600 * 1e3;
911
+ const MAX_PROBE_CACHE_SIZE = 64;
912
+ async function probeVama(account) {
913
+ if (!account.botToken) return {
914
+ ok: false,
915
+ error: "missing credentials (botToken)"
916
+ };
917
+ const cacheKey = account.accountId;
918
+ const cached = probeCache.get(cacheKey);
919
+ if (cached && cached.expiresAt > Date.now()) return cached.result;
920
+ try {
921
+ const me = await createBotHubClient(account).getMe();
922
+ const botId = me.bot_id;
923
+ const registeredUrl = me.webhook_url?.trim() || void 0;
924
+ if (!registeredUrl) {
925
+ let wsEnabled = false;
926
+ if (account.transport !== "webhook") try {
927
+ wsEnabled = (await checkVamaWebSocketSupport(account)).supported;
928
+ } catch {}
929
+ if (!wsEnabled) return {
930
+ ok: false,
931
+ botId,
932
+ error: "bot token is valid, but no webhook URL is registered with BotHub — Vama cannot deliver messages to this gateway (the agent shows \"Awaiting claw\"). Set channels.vama.webhookUrl to your gateway's public URL (e.g. https://your-host/vama/events) and restart the gateway, or re-run `openclaw onboard`."
933
+ };
934
+ const wsResult = {
935
+ ok: true,
936
+ botId,
937
+ wsEnabled: true
938
+ };
939
+ probeCache.set(cacheKey, {
940
+ result: wsResult,
941
+ expiresAt: Date.now() + PROBE_CACHE_TTL_MS
942
+ });
943
+ return wsResult;
944
+ }
945
+ const result = {
946
+ ok: true,
947
+ botId,
948
+ webhookUrl: registeredUrl
949
+ };
950
+ probeCache.set(cacheKey, {
951
+ result,
952
+ expiresAt: Date.now() + PROBE_CACHE_TTL_MS
953
+ });
954
+ if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
955
+ const oldest = probeCache.keys().next().value;
956
+ if (oldest !== void 0) probeCache.delete(oldest);
957
+ }
958
+ return result;
959
+ } catch (err) {
960
+ return {
961
+ ok: false,
962
+ error: err instanceof Error ? err.message : String(err)
963
+ };
964
+ }
965
+ }
966
+ function clearProbeCache() {
967
+ probeCache.clear();
968
+ }
969
+ //#endregion
970
+ //#region extensions/vama/src/register.ts
971
+ /**
972
+ * Outer retry schedule for startup webhook registration. The BotHub
973
+ * client already retries transport errors per call (fetchWithRetry);
974
+ * these delays cover application-level failures around gateway boot —
975
+ * e.g. the tunnel in front of the listener still coming up, or BotHub
976
+ * rejecting the URL while DNS for a freshly-minted tunnel propagates.
977
+ */
978
+ const DEFAULT_RETRY_DELAYS_MS = [5e3, 15e3];
979
+ function sleep(ms, abortSignal) {
980
+ return new Promise((resolve) => {
981
+ if (abortSignal?.aborted) {
982
+ resolve();
983
+ return;
984
+ }
985
+ const timer = setTimeout(() => {
986
+ abortSignal?.removeEventListener("abort", onAbort);
987
+ resolve();
988
+ }, ms);
989
+ const onAbort = () => {
990
+ clearTimeout(timer);
991
+ resolve();
992
+ };
993
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
994
+ });
995
+ }
996
+ /**
997
+ * Register `account.webhookUrl` with BotHub (PUT /v1/bot/webhook).
998
+ *
999
+ * Called fire-and-forget from the monitor once the local webhook server
1000
+ * is listening (ordering matters: BotHub replays buffered messages
1001
+ * synchronously on registration, so the listener must be up first).
1002
+ *
1003
+ * Never throws — a registration failure must not take down the channel
1004
+ * (outbound sends still work; the probe surfaces the broken inbound
1005
+ * path). Returns true when registration succeeded.
1006
+ */
1007
+ async function registerVamaWebhook(opts) {
1008
+ const { account, log, error, abortSignal } = opts;
1009
+ const url = account.webhookUrl;
1010
+ if (!url) return false;
1011
+ const accountId = account.accountId;
1012
+ const webhookPath = account.config?.webhookPath ?? "/vama/events";
1013
+ try {
1014
+ const parsed = new URL(url);
1015
+ if (parsed.pathname !== webhookPath) log(`vama[${accountId}]: webhookUrl path "${parsed.pathname}" differs from webhookPath "${webhookPath}" — BotHub will POST to ${url}; make sure that forwards to the local listener path ${webhookPath}`);
1016
+ } catch {
1017
+ error(`vama[${accountId}]: webhookUrl is not a valid URL: ${url} — skipping registration`);
1018
+ return false;
1019
+ }
1020
+ const delays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
1021
+ let lastErr;
1022
+ for (let attempt = 0; attempt <= delays.length; attempt++) {
1023
+ if (abortSignal?.aborted) return false;
1024
+ try {
1025
+ await createBotHubClient(account).registerWebhook({ url });
1026
+ log(`vama[${accountId}]: webhook registered with BotHub: ${url}`);
1027
+ clearProbeCache();
1028
+ return true;
1029
+ } catch (err) {
1030
+ lastErr = err;
1031
+ if (attempt < delays.length) {
1032
+ log(`vama[${accountId}]: webhook registration attempt ${attempt + 1} failed (${err instanceof Error ? err.message : String(err)}); retrying in ${Math.round(delays[attempt] / 1e3)}s`);
1033
+ await sleep(delays[attempt], abortSignal);
1034
+ }
1035
+ }
1036
+ }
1037
+ error(`vama[${accountId}]: webhook registration failed: ${lastErr instanceof Error ? lastErr.message : String(lastErr)}. BotHub cannot deliver messages until this succeeds (the agent shows "Awaiting claw" in Vama). Check that ${url} is a public HTTPS URL that forwards to the local listener, then restart the gateway.`);
1038
+ return false;
1039
+ }
1040
+ //#endregion
706
1041
  //#region extensions/vama/src/webhook.ts
707
1042
  const MAX_TIMESTAMP_AGE_S = 300;
708
1043
  /**
@@ -820,6 +1155,20 @@ async function monitorVamaProvider(opts = {}) {
820
1155
  const webhookPath = account.config?.webhookPath ?? "/vama/events";
821
1156
  const host = account.config?.webhookHost ?? "127.0.0.1";
822
1157
  log(`vama[${accountId}]: starting webhook server on ${host}:${port}, path ${webhookPath}...`);
1158
+ const dispatchEvent = (event, replayId) => {
1159
+ if (event.type === "message.create") {
1160
+ const messageEvent = event.data;
1161
+ handleVamaMessage({
1162
+ cfg,
1163
+ event: messageEvent,
1164
+ runtime: opts.runtime,
1165
+ accountId,
1166
+ replayId
1167
+ }).catch((err) => {
1168
+ error(`vama[${accountId}]: error handling message: ${String(err)}`);
1169
+ });
1170
+ } else log(`vama[${accountId}]: ignoring event type=${event.type}`);
1171
+ };
823
1172
  const server = http.createServer();
824
1173
  server.on("request", (req, res) => {
825
1174
  if (isRateLimited(`${accountId}:${req.socket.remoteAddress ?? "unknown"}`, Date.now())) {
@@ -875,28 +1224,17 @@ async function monitorVamaProvider(opts = {}) {
875
1224
  error(`vama[${accountId}]: failed to parse webhook body`);
876
1225
  return;
877
1226
  }
878
- const eventType = event.type;
879
- const deliveryId = req.headers["x-bothub-delivery-id"];
880
1227
  const replayId = req.headers["x-replay-id"];
881
- if (eventType === "message.create") {
882
- const messageEvent = event.data;
883
- handleVamaMessage({
884
- cfg,
885
- event: messageEvent,
886
- runtime: opts.runtime,
887
- accountId,
888
- replayId
889
- }).catch((err) => {
890
- error(`vama[${accountId}]: error handling message: ${String(err)}`);
891
- });
892
- } else log(`vama[${accountId}]: ignoring event type=${eventType} delivery=${deliveryId}`);
1228
+ dispatchEvent(event, replayId);
893
1229
  }).catch((err) => {
894
1230
  if (!guard.isTripped()) error(`vama[${accountId}]: webhook handler error: ${String(err)}`);
895
1231
  guard.dispose();
896
1232
  });
897
1233
  });
1234
+ let wsHandle;
898
1235
  return new Promise((resolve, reject) => {
899
1236
  const cleanup = () => {
1237
+ wsHandle?.stop();
900
1238
  server.close();
901
1239
  };
902
1240
  const handleAbort = () => {
@@ -912,6 +1250,20 @@ async function monitorVamaProvider(opts = {}) {
912
1250
  opts.abortSignal?.addEventListener("abort", handleAbort, { once: true });
913
1251
  server.listen(port, host, () => {
914
1252
  log(`vama[${accountId}]: webhook server listening on ${host}:${port}`);
1253
+ registerVamaWebhook({
1254
+ account,
1255
+ log,
1256
+ error,
1257
+ abortSignal: opts.abortSignal
1258
+ });
1259
+ if (account.transport !== "webhook") wsHandle = startVamaWebSocket({
1260
+ account,
1261
+ onEvent: (event) => dispatchEvent(event),
1262
+ log,
1263
+ error,
1264
+ abortSignal: opts.abortSignal,
1265
+ mode: account.transport === "websocket" ? "websocket" : "auto"
1266
+ });
915
1267
  });
916
1268
  server.on("error", (err) => {
917
1269
  error(`vama[${accountId}]: webhook server error: ${err}`);
@@ -922,38 +1274,4 @@ async function monitorVamaProvider(opts = {}) {
922
1274
  });
923
1275
  }
924
1276
  //#endregion
925
- //#region extensions/vama/src/probe.ts
926
- const probeCache = /* @__PURE__ */ new Map();
927
- const PROBE_CACHE_TTL_MS = 600 * 1e3;
928
- const MAX_PROBE_CACHE_SIZE = 64;
929
- async function probeVama(account) {
930
- if (!account.botToken) return {
931
- ok: false,
932
- error: "missing credentials (botToken)"
933
- };
934
- const cacheKey = account.accountId;
935
- const cached = probeCache.get(cacheKey);
936
- if (cached && cached.expiresAt > Date.now()) return cached.result;
937
- try {
938
- const result = {
939
- ok: true,
940
- botId: (await createBotHubClient(account).getMe()).bot_id
941
- };
942
- probeCache.set(cacheKey, {
943
- result,
944
- expiresAt: Date.now() + PROBE_CACHE_TTL_MS
945
- });
946
- if (probeCache.size > MAX_PROBE_CACHE_SIZE) {
947
- const oldest = probeCache.keys().next().value;
948
- if (oldest !== void 0) probeCache.delete(oldest);
949
- }
950
- return result;
951
- } catch (err) {
952
- return {
953
- ok: false,
954
- error: err instanceof Error ? err.message : String(err)
955
- };
956
- }
957
- }
958
- //#endregion
959
- export { dispatchStarted as a, resolveVamaAccount as c, buildBaseChannelStatusSummary as d, createDefaultChannelRuntimeState as f, dispatchEnded as i, DEFAULT_ACCOUNT_ID$1 as l, monitorVamaProvider as n, listVamaAccountIds as o, sendMessageVama as r, resolveDefaultVamaAccountId as s, probeVama as t, PAIRING_APPROVED_MESSAGE as u };
1277
+ export { dispatchStarted as a, buildBaseChannelStatusSummary as c, resolveDefaultVamaAccountId as d, resolveVamaAccount as f, dispatchEnded as i, createDefaultChannelRuntimeState as l, probeVama as n, DEFAULT_ACCOUNT_ID$1 as o, sendMessageVama as r, PAIRING_APPROVED_MESSAGE as s, monitorVamaProvider as t, listVamaAccountIds as u };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@vama/openclaw",
3
- "version": "2026.5.5-1",
3
+ "version": "2026.5.5-4",
4
4
  "description": "OpenClaw Vama channel plugin via BotHub",
5
+ "homepage": "https://web.vama.com/connect-guide",
5
6
  "repository": {
6
7
  "type": "git",
7
8
  "url": "https://github.com/VamaSingapore/openclaw"
@@ -10,14 +11,6 @@
10
11
  "dependencies": {
11
12
  "undici": "8.2.0"
12
13
  },
13
- "peerDependencies": {
14
- "openclaw": ">=2026.5.12"
15
- },
16
- "peerDependenciesMeta": {
17
- "openclaw": {
18
- "optional": true
19
- }
20
- },
21
14
  "openclaw": {
22
15
  "extensions": [
23
16
  "./index.ts"
@@ -53,6 +46,15 @@
53
46
  },
54
47
  "files": [
55
48
  "dist/**",
56
- "openclaw.plugin.json"
57
- ]
49
+ "openclaw.plugin.json",
50
+ "README.md"
51
+ ],
52
+ "peerDependencies": {
53
+ "openclaw": ">=2026.5.12"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "openclaw": {
57
+ "optional": true
58
+ }
59
+ }
58
60
  }