@eclaw/openclaw-channel 1.0.9 → 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/gateway.d.ts +2 -1
- package/dist/gateway.js +35 -57
- package/dist/index.d.ts +0 -21
- package/dist/index.js +33 -4
- package/dist/webhook-registry.d.ts +19 -0
- package/dist/webhook-registry.js +39 -0
- package/package.json +1 -1
package/dist/gateway.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Gateway lifecycle: start an E-Claw account.
|
|
3
3
|
*
|
|
4
4
|
* 1. Resolve credentials from ctx.account or disk
|
|
5
|
-
* 2.
|
|
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,12 +6,14 @@ 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';
|
|
9
|
+
import { registerWebhookToken, unregisterWebhookToken } from './webhook-registry.js';
|
|
10
10
|
/**
|
|
11
11
|
* Resolve account from ctx.
|
|
12
12
|
*
|
|
13
13
|
* OpenClaw may pass a pre-resolved account object in ctx.account,
|
|
14
14
|
* or an empty config. Fall back to reading openclaw.json from disk.
|
|
15
15
|
*/
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
17
|
function resolveAccountFromCtx(ctx) {
|
|
17
18
|
// Preferred: OpenClaw passes the resolved account in ctx.account
|
|
18
19
|
if (ctx.account?.apiKey) {
|
|
@@ -40,7 +41,8 @@ function resolveAccountFromCtx(ctx) {
|
|
|
40
41
|
* Gateway lifecycle: start an E-Claw account.
|
|
41
42
|
*
|
|
42
43
|
* 1. Resolve credentials from ctx.account or disk
|
|
43
|
-
* 2.
|
|
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)
|
|
44
46
|
* 3. Register callback URL with E-Claw backend
|
|
45
47
|
* 4. Auto-bind entity if not already bound
|
|
46
48
|
* 5. Keep the promise alive until abort signal fires
|
|
@@ -59,7 +61,6 @@ export async function startAccount(ctx) {
|
|
|
59
61
|
// Generate per-session callback token
|
|
60
62
|
const callbackToken = randomBytes(32).toString('hex');
|
|
61
63
|
// Webhook URL: account config > env var > warn
|
|
62
|
-
const webhookPort = parseInt(process.env.ECLAW_WEBHOOK_PORT || '0') || 0;
|
|
63
64
|
const publicUrl = account.webhookUrl?.replace(/\/$/, '')
|
|
64
65
|
|| process.env.ECLAW_WEBHOOK_URL?.replace(/\/$/, '');
|
|
65
66
|
if (!publicUrl) {
|
|
@@ -68,72 +69,49 @@ export async function startAccount(ctx) {
|
|
|
68
69
|
'or set ECLAW_WEBHOOK_URL env var. ' +
|
|
69
70
|
'Example: https://your-openclaw-domain.com');
|
|
70
71
|
}
|
|
71
|
-
//
|
|
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
|
|
72
75
|
const handler = createWebhookHandler(callbackToken, accountId);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
-
req.body = {};
|
|
87
|
-
}
|
|
88
|
-
handler(req, res);
|
|
89
|
-
});
|
|
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}`);
|
|
90
88
|
}
|
|
91
89
|
else {
|
|
92
|
-
|
|
93
|
-
|
|
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;
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
97
103
|
return new Promise((resolve) => {
|
|
98
|
-
server.listen(webhookPort, async () => {
|
|
99
|
-
const addr = server.address();
|
|
100
|
-
const actualPort = typeof addr === 'object' && addr ? addr.port : webhookPort;
|
|
101
|
-
const baseUrl = publicUrl || `http://localhost:${actualPort}`;
|
|
102
|
-
const callbackUrl = `${baseUrl}/eclaw-webhook`;
|
|
103
|
-
console.log(`[E-Claw] Webhook server listening on port ${actualPort}`);
|
|
104
|
-
console.log(`[E-Claw] Callback URL: ${callbackUrl}`);
|
|
105
|
-
try {
|
|
106
|
-
// Register callback with E-Claw backend
|
|
107
|
-
const regData = await client.registerCallback(callbackUrl, callbackToken);
|
|
108
|
-
console.log(`[E-Claw] Registered with E-Claw. Device: ${regData.deviceId}, Entities: ${regData.entities.length}`);
|
|
109
|
-
// Auto-bind entity if not already bound
|
|
110
|
-
const entity = regData.entities.find(e => e.entityId === account.entityId);
|
|
111
|
-
if (!entity?.isBound) {
|
|
112
|
-
console.log(`[E-Claw] Entity ${account.entityId} not bound, binding...`);
|
|
113
|
-
const bindData = await client.bindEntity(account.entityId, account.botName);
|
|
114
|
-
console.log(`[E-Claw] Bound entity ${account.entityId}, publicCode: ${bindData.publicCode}`);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
console.log(`[E-Claw] Entity ${account.entityId} already bound`);
|
|
118
|
-
const bindData = await client.bindEntity(account.entityId, account.botName);
|
|
119
|
-
console.log(`[E-Claw] Retrieved credentials for entity ${account.entityId}`);
|
|
120
|
-
void bindData;
|
|
121
|
-
}
|
|
122
|
-
console.log(`[E-Claw] Account ${accountId} ready!`);
|
|
123
|
-
}
|
|
124
|
-
catch (err) {
|
|
125
|
-
console.error(`[E-Claw] Setup failed for account ${accountId}:`, err);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
// Cleanup on abort
|
|
129
104
|
const signal = ctx.abortSignal;
|
|
130
105
|
if (signal) {
|
|
131
106
|
signal.addEventListener('abort', () => {
|
|
132
107
|
console.log(`[E-Claw] Shutting down account ${accountId}`);
|
|
133
108
|
client.unregisterCallback().catch(() => { });
|
|
134
|
-
|
|
109
|
+
unregisterWebhookToken(callbackToken);
|
|
135
110
|
resolve();
|
|
136
111
|
});
|
|
137
112
|
}
|
|
113
|
+
else {
|
|
114
|
+
resolve();
|
|
115
|
+
}
|
|
138
116
|
});
|
|
139
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
};
|
|
@@ -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
|
+
}
|