@eclaw/openclaw-channel 1.0.8 → 1.0.10

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/dist/config.js CHANGED
@@ -30,5 +30,6 @@ export function resolveAccount(cfg, accountId) {
30
30
  apiBase: (account?.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
31
31
  entityId: account?.entityId ?? 0,
32
32
  botName: account?.botName,
33
+ webhookUrl: account?.webhookUrl,
33
34
  };
34
35
  }
package/dist/gateway.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Gateway lifecycle: start an E-Claw account.
3
3
  *
4
- * 1. Initialize HTTP client with channel API credentials
5
- * 2. Start a local HTTP server to receive webhook callbacks
4
+ * 1. Resolve credentials from ctx.account or disk
5
+ * 2. Register a per-session handler in the webhook-registry (served by the
6
+ * main OpenClaw gateway HTTP server at /eclaw-webhook — no separate port)
6
7
  * 3. Register callback URL with E-Claw backend
7
8
  * 4. Auto-bind entity if not already bound
8
9
  * 5. Keep the promise alive until abort signal fires
package/dist/gateway.js CHANGED
@@ -1,4 +1,3 @@
1
- import { createServer } from 'node:http';
2
1
  import { randomBytes } from 'node:crypto';
3
2
  import { readFileSync } from 'node:fs';
4
3
  import { join } from 'node:path';
@@ -7,32 +6,51 @@ import { resolveAccount } from './config.js';
7
6
  import { EClawClient } from './client.js';
8
7
  import { setClient } from './outbound.js';
9
8
  import { createWebhookHandler } from './webhook-handler.js';
10
- /** Read full openclaw.json config from disk */
11
- function readFullConfig() {
9
+ import { registerWebhookToken, unregisterWebhookToken } from './webhook-registry.js';
10
+ /**
11
+ * Resolve account from ctx.
12
+ *
13
+ * OpenClaw may pass a pre-resolved account object in ctx.account,
14
+ * or an empty config. Fall back to reading openclaw.json from disk.
15
+ */
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ function resolveAccountFromCtx(ctx) {
18
+ // Preferred: OpenClaw passes the resolved account in ctx.account
19
+ if (ctx.account?.apiKey) {
20
+ return {
21
+ enabled: ctx.account.enabled ?? true,
22
+ apiKey: ctx.account.apiKey,
23
+ apiSecret: ctx.account.apiSecret,
24
+ apiBase: (ctx.account.apiBase ?? 'https://eclawbot.com').replace(/\/$/, ''),
25
+ entityId: ctx.account.entityId ?? 0,
26
+ botName: ctx.account.botName,
27
+ webhookUrl: ctx.account.webhookUrl,
28
+ };
29
+ }
30
+ // Fallback: read config from disk (OpenClaw passes empty config object)
12
31
  const configPath = process.env.OPENCLAW_CONFIG_PATH
13
32
  || join(homedir(), '.openclaw', 'openclaw.json');
33
+ let fullConfig = {};
14
34
  try {
15
- return JSON.parse(readFileSync(configPath, 'utf8'));
16
- }
17
- catch {
18
- return {};
35
+ fullConfig = JSON.parse(readFileSync(configPath, 'utf8'));
19
36
  }
37
+ catch { /* ignore */ }
38
+ return resolveAccount(fullConfig, ctx.accountId ?? ctx.account?.accountId);
20
39
  }
21
40
  /**
22
41
  * Gateway lifecycle: start an E-Claw account.
23
42
  *
24
- * 1. Initialize HTTP client with channel API credentials
25
- * 2. Start a local HTTP server to receive webhook callbacks
43
+ * 1. Resolve credentials from ctx.account or disk
44
+ * 2. Register a per-session handler in the webhook-registry (served by the
45
+ * main OpenClaw gateway HTTP server at /eclaw-webhook — no separate port)
26
46
  * 3. Register callback URL with E-Claw backend
27
47
  * 4. Auto-bind entity if not already bound
28
48
  * 5. Keep the promise alive until abort signal fires
29
49
  */
30
50
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
51
  export async function startAccount(ctx) {
32
- const { accountId } = ctx;
33
- // OpenClaw passes an empty config object to the gateway — read config from disk directly
34
- const fullConfig = readFullConfig();
35
- const account = resolveAccount(fullConfig, accountId);
52
+ const accountId = ctx.accountId ?? ctx.account?.accountId ?? 'default';
53
+ const account = resolveAccountFromCtx(ctx);
36
54
  if (!account.enabled || !account.apiKey) {
37
55
  console.log(`[E-Claw] Account ${accountId} disabled or missing credentials, skipping`);
38
56
  return;
@@ -42,81 +60,58 @@ export async function startAccount(ctx) {
42
60
  setClient(accountId, client);
43
61
  // Generate per-session callback token
44
62
  const callbackToken = randomBytes(32).toString('hex');
45
- // Determine webhook configuration
46
- const webhookPort = parseInt(process.env.ECLAW_WEBHOOK_PORT || '0') || 0;
47
- const publicUrl = process.env.ECLAW_WEBHOOK_URL;
63
+ // Webhook URL: account config > env var > warn
64
+ const publicUrl = account.webhookUrl?.replace(/\/$/, '')
65
+ || process.env.ECLAW_WEBHOOK_URL?.replace(/\/$/, '');
48
66
  if (!publicUrl) {
49
- console.warn('[E-Claw] ECLAW_WEBHOOK_URL not set. Set this to your public-facing URL ' +
50
- 'so E-Claw can send messages to this plugin. Example: https://my-openclaw.example.com');
67
+ console.warn('[E-Claw] Webhook URL not configured. ' +
68
+ 'Run "openclaw configure" and enter your OpenClaw public URL, ' +
69
+ 'or set ECLAW_WEBHOOK_URL env var. ' +
70
+ 'Example: https://your-openclaw-domain.com');
51
71
  }
52
- // Create webhook handler
72
+ // The callback URL points to /eclaw-webhook on the main gateway HTTP server
73
+ const callbackUrl = `${publicUrl || 'http://localhost'}/eclaw-webhook`;
74
+ // Register handler in the per-token registry
53
75
  const handler = createWebhookHandler(callbackToken, accountId);
54
- // Parse JSON body for incoming requests
55
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
- const requestHandler = (req, res) => {
57
- if (req.method === 'POST' && req.url?.startsWith('/eclaw-webhook')) {
58
- let body = '';
59
- req.on('data', (chunk) => { body += chunk.toString(); });
60
- req.on('end', () => {
61
- try {
62
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
- req.body = JSON.parse(body);
64
- }
65
- catch {
66
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
- req.body = {};
68
- }
69
- handler(req, res);
70
- });
76
+ registerWebhookToken(callbackToken, accountId, handler);
77
+ console.log(`[E-Claw] Webhook registered at: ${callbackUrl}`);
78
+ try {
79
+ // Register callback with E-Claw backend
80
+ const regData = await client.registerCallback(callbackUrl, callbackToken);
81
+ console.log(`[E-Claw] Registered with E-Claw. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
82
+ // Auto-bind entity if not already bound
83
+ const entity = regData.entities.find(e => e.entityId === account.entityId);
84
+ if (!entity?.isBound) {
85
+ console.log(`[E-Claw] Entity ${account.entityId} not bound, binding...`);
86
+ const bindData = await client.bindEntity(account.entityId, account.botName);
87
+ console.log(`[E-Claw] Bound entity ${account.entityId}, publicCode: ${bindData.publicCode}`);
71
88
  }
72
89
  else {
73
- res.writeHead(404);
74
- res.end('Not Found');
90
+ console.log(`[E-Claw] Entity ${account.entityId} already bound`);
91
+ const bindData = await client.bindEntity(account.entityId, account.botName);
92
+ console.log(`[E-Claw] Retrieved credentials for entity ${account.entityId}`);
93
+ void bindData;
75
94
  }
76
- };
77
- const server = createServer(requestHandler);
95
+ console.log(`[E-Claw] Account ${accountId} ready!`);
96
+ }
97
+ catch (err) {
98
+ console.error(`[E-Claw] Setup failed for account ${accountId}:`, err);
99
+ unregisterWebhookToken(callbackToken);
100
+ return;
101
+ }
102
+ // Keep the promise alive until abort signal fires
78
103
  return new Promise((resolve) => {
79
- server.listen(webhookPort, async () => {
80
- const addr = server.address();
81
- const actualPort = typeof addr === 'object' && addr ? addr.port : webhookPort;
82
- const baseUrl = publicUrl || `http://localhost:${actualPort}`;
83
- const callbackUrl = `${baseUrl}/eclaw-webhook`;
84
- console.log(`[E-Claw] Webhook server listening on port ${actualPort}`);
85
- console.log(`[E-Claw] Callback URL: ${callbackUrl}`);
86
- try {
87
- // Register callback with E-Claw backend
88
- const regData = await client.registerCallback(callbackUrl, callbackToken);
89
- console.log(`[E-Claw] Registered with E-Claw. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
90
- // Auto-bind entity if not already bound
91
- const entity = regData.entities.find(e => e.entityId === account.entityId);
92
- if (!entity?.isBound) {
93
- console.log(`[E-Claw] Entity ${account.entityId} not bound, binding...`);
94
- const bindData = await client.bindEntity(account.entityId, account.botName);
95
- console.log(`[E-Claw] Bound entity ${account.entityId}, publicCode: ${bindData.publicCode}`);
96
- }
97
- else {
98
- console.log(`[E-Claw] Entity ${account.entityId} already bound`);
99
- // For already-bound entities, we need to get the botSecret
100
- // The bind endpoint returns existing credentials for channel-bound entities
101
- const bindData = await client.bindEntity(account.entityId, account.botName);
102
- console.log(`[E-Claw] Retrieved credentials for entity ${account.entityId}`);
103
- void bindData; // credentials stored in client
104
- }
105
- console.log(`[E-Claw] Account ${accountId} ready!`);
106
- }
107
- catch (err) {
108
- console.error(`[E-Claw] Setup failed for account ${accountId}:`, err);
109
- }
110
- });
111
- // Cleanup on abort
112
104
  const signal = ctx.abortSignal;
113
105
  if (signal) {
114
106
  signal.addEventListener('abort', () => {
115
107
  console.log(`[E-Claw] Shutting down account ${accountId}`);
116
108
  client.unregisterCallback().catch(() => { });
117
- server.close();
109
+ unregisterWebhookToken(callbackToken);
118
110
  resolve();
119
111
  });
120
112
  }
113
+ else {
114
+ resolve();
115
+ }
121
116
  });
122
117
  }
package/dist/index.d.ts CHANGED
@@ -1,24 +1,3 @@
1
- /**
2
- * E-Claw Channel Plugin for OpenClaw.
3
- *
4
- * Installation:
5
- * npm install @eclaw/openclaw-channel
6
- *
7
- * Configuration (config.yaml):
8
- * channels:
9
- * eclaw:
10
- * accounts:
11
- * default:
12
- * apiKey: "eck_..."
13
- * apiSecret: "ecs_..."
14
- * apiBase: "https://eclawbot.com"
15
- * entityId: 0
16
- * botName: "My Bot"
17
- *
18
- * Environment variables:
19
- * ECLAW_WEBHOOK_URL - Public URL for receiving callbacks (required for production)
20
- * ECLAW_WEBHOOK_PORT - Port for webhook server (default: random)
21
- */
22
1
  declare const plugin: {
23
2
  id: string;
24
3
  name: string;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { setPluginRuntime } from './runtime.js';
2
2
  import { eclawChannel } from './channel.js';
3
+ import { dispatchWebhook } from './webhook-registry.js';
3
4
  /**
4
5
  * E-Claw Channel Plugin for OpenClaw.
5
6
  *
@@ -12,15 +13,32 @@ import { eclawChannel } from './channel.js';
12
13
  * accounts:
13
14
  * default:
14
15
  * apiKey: "eck_..."
15
- * apiSecret: "ecs_..."
16
16
  * apiBase: "https://eclawbot.com"
17
17
  * entityId: 0
18
18
  * botName: "My Bot"
19
+ * webhookUrl: "https://your-openclaw-domain.com"
19
20
  *
20
- * Environment variables:
21
- * ECLAW_WEBHOOK_URL - Public URL for receiving callbacks (required for production)
22
- * ECLAW_WEBHOOK_PORT - Port for webhook server (default: random)
21
+ * The plugin registers /eclaw-webhook on the main OpenClaw gateway HTTP server,
22
+ * so no separate port is needed. Set webhookUrl to your OpenClaw public URL
23
+ * (e.g. https://eclaw2.zeabur.app) so E-Claw knows where to push messages.
23
24
  */
25
+ /** Parse JSON body from a raw incoming request */
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ function parseBody(req) {
28
+ return new Promise((resolve) => {
29
+ let body = '';
30
+ req.on('data', (chunk) => { body += chunk.toString(); });
31
+ req.on('end', () => {
32
+ try {
33
+ req.body = JSON.parse(body);
34
+ }
35
+ catch {
36
+ req.body = {};
37
+ }
38
+ resolve();
39
+ });
40
+ });
41
+ }
24
42
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
43
  const plugin = {
26
44
  id: 'eclaw',
@@ -30,6 +48,17 @@ const plugin = {
30
48
  register(api) {
31
49
  console.log('[E-Claw] Plugin loaded');
32
50
  setPluginRuntime(api.runtime);
51
+ // Register /eclaw-webhook on the main OpenClaw gateway HTTP server.
52
+ // Token-based routing is handled in dispatchWebhook() — each account
53
+ // registers its own handler keyed by a random per-session Bearer token.
54
+ api.registerHttpRoute({
55
+ path: '/eclaw-webhook',
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ handler: async (req, res) => {
58
+ await parseBody(req);
59
+ await dispatchWebhook(req, res);
60
+ },
61
+ });
33
62
  api.registerChannel({ plugin: eclawChannel });
34
63
  },
35
64
  };
@@ -47,6 +47,11 @@ export const eclawOnboardingAdapter = {
47
47
  placeholder: 'My Bot',
48
48
  initialValue: resolved.botName ?? '',
49
49
  });
50
+ const webhookUrl = await prompter.text({
51
+ message: 'Webhook URL (your OpenClaw public URL, e.g. https://openclaw.example.com)',
52
+ placeholder: 'https://your-openclaw-domain.com',
53
+ initialValue: resolved.webhookUrl ?? '',
54
+ });
50
55
  const nextCfg = {
51
56
  ...cfg,
52
57
  channels: {
@@ -60,6 +65,7 @@ export const eclawOnboardingAdapter = {
60
65
  apiBase: resolved.apiBase || 'https://eclawbot.com',
61
66
  entityId: Number(entityIdStr),
62
67
  botName: String(botName).trim() || undefined,
68
+ webhookUrl: String(webhookUrl).trim() || undefined,
63
69
  enabled: true,
64
70
  },
65
71
  },
package/dist/types.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface EClawAccountConfig {
6
6
  apiBase: string;
7
7
  entityId: number;
8
8
  botName?: string;
9
+ webhookUrl?: string;
9
10
  }
10
11
  /** Inbound message from E-Claw callback webhook */
11
12
  export interface EClawInboundMessage {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Per-session webhook token registry.
3
+ *
4
+ * Each account generates a random callbackToken when it starts.
5
+ * The token is sent to E-Claw as part of the callback URL registration,
6
+ * and E-Claw echoes it back as `Authorization: Bearer <token>` on every push.
7
+ *
8
+ * The main route handler (registered on the gateway HTTP server) looks up
9
+ * the correct per-account handler by matching the Bearer token.
10
+ */
11
+ type WebhookHandler = (req: any, res: any) => Promise<void>;
12
+ export declare function registerWebhookToken(callbackToken: string, accountId: string, handler: WebhookHandler): void;
13
+ export declare function unregisterWebhookToken(callbackToken: string): void;
14
+ /**
15
+ * Dispatch an incoming webhook request to the correct account handler.
16
+ * Verifies the Bearer token and routes to the matching handler.
17
+ */
18
+ export declare function dispatchWebhook(req: any, res: any): Promise<void>;
19
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Per-session webhook token registry.
3
+ *
4
+ * Each account generates a random callbackToken when it starts.
5
+ * The token is sent to E-Claw as part of the callback URL registration,
6
+ * and E-Claw echoes it back as `Authorization: Bearer <token>` on every push.
7
+ *
8
+ * The main route handler (registered on the gateway HTTP server) looks up
9
+ * the correct per-account handler by matching the Bearer token.
10
+ */
11
+ const registry = new Map();
12
+ export function registerWebhookToken(callbackToken, accountId, handler) {
13
+ registry.set(callbackToken, { accountId, handler });
14
+ }
15
+ export function unregisterWebhookToken(callbackToken) {
16
+ registry.delete(callbackToken);
17
+ }
18
+ /**
19
+ * Dispatch an incoming webhook request to the correct account handler.
20
+ * Verifies the Bearer token and routes to the matching handler.
21
+ */
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ export async function dispatchWebhook(req, res) {
24
+ const authHeader = req.headers?.authorization;
25
+ if (!authHeader?.startsWith('Bearer ')) {
26
+ res.writeHead(401, { 'Content-Type': 'application/json' });
27
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
28
+ return;
29
+ }
30
+ const token = authHeader.slice(7);
31
+ const entry = registry.get(token);
32
+ if (!entry) {
33
+ // Unknown token — likely a stale push after a server restart
34
+ res.writeHead(404, { 'Content-Type': 'application/json' });
35
+ res.end(JSON.stringify({ error: 'Unknown token' }));
36
+ return;
37
+ }
38
+ await entry.handler(req, res);
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eclaw/openclaw-channel",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "E-Claw channel plugin for OpenClaw — AI chat platform for live wallpaper entities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",