@chrysb/alphaclaw 0.8.0 → 0.8.1-beta.1
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/lib/public/js/app.js +100 -83
- package/lib/public/js/components/agents-tab/agent-pairing-section.js +47 -12
- package/lib/public/js/components/channels.js +14 -17
- package/lib/public/js/components/envars.js +42 -6
- package/lib/public/js/components/features.js +6 -12
- package/lib/public/js/components/general/use-general-tab.js +10 -5
- package/lib/public/js/components/google/use-gmail-watch.js +22 -18
- package/lib/public/js/components/google/use-google-accounts.js +23 -23
- package/lib/public/js/components/models-tab/use-models.js +20 -4
- package/lib/public/js/components/nodes-tab/connected-nodes/user-connected-nodes.js +2 -2
- package/lib/public/js/components/nodes-tab/use-nodes-tab.js +13 -9
- package/lib/public/js/components/routes/webhooks-route.js +1 -1
- package/lib/public/js/components/webhooks/create-webhook-modal/index.js +176 -0
- package/lib/public/js/components/webhooks/helpers.js +106 -0
- package/lib/public/js/components/webhooks/index.js +148 -0
- package/lib/public/js/components/webhooks/request-history/index.js +241 -0
- package/lib/public/js/components/webhooks/request-history/use-request-history.js +167 -0
- package/lib/public/js/components/webhooks/webhook-detail/index.js +374 -0
- package/lib/public/js/components/webhooks/webhook-detail/use-webhook-detail.js +261 -0
- package/lib/public/js/components/webhooks/webhook-list/index.js +96 -0
- package/lib/public/js/components/webhooks/webhook-list/use-webhook-list.js +30 -0
- package/lib/public/js/hooks/use-app-shell-controller.js +59 -6
- package/lib/public/js/hooks/use-cached-fetch.js +63 -0
- package/lib/public/js/hooks/usePolling.js +45 -7
- package/lib/public/js/lib/api-cache.js +88 -0
- package/lib/public/js/lib/api.js +64 -1
- package/lib/server/db/webhooks/index.js +144 -0
- package/lib/server/db/webhooks/schema.js +13 -0
- package/lib/server/init/register-server-routes.js +21 -0
- package/lib/server/oauth-callback-middleware.js +34 -0
- package/lib/server/routes/proxy.js +2 -0
- package/lib/server/routes/system.js +50 -2
- package/lib/server/routes/webhooks.js +126 -18
- package/lib/server/webhook-middleware.js +6 -1
- package/lib/server.js +12 -0
- package/package.json +1 -1
- package/lib/public/js/components/webhooks.js +0 -1259
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
2
|
const path = require("path");
|
|
3
|
+
const crypto = require("crypto");
|
|
3
4
|
const { DatabaseSync } = require("node:sqlite");
|
|
4
5
|
const { createSchema } = require("./schema");
|
|
5
6
|
|
|
@@ -43,6 +44,19 @@ const parseJsonText = (value) => {
|
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
|
|
47
|
+
const generateOauthCallbackId = () => crypto.randomBytes(16).toString("hex");
|
|
48
|
+
|
|
49
|
+
const toOauthCallbackModel = (row) => {
|
|
50
|
+
if (!row) return null;
|
|
51
|
+
return {
|
|
52
|
+
callbackId: String(row.callback_id || ""),
|
|
53
|
+
hookName: String(row.hook_name || ""),
|
|
54
|
+
createdAt: row.created_at || null,
|
|
55
|
+
rotatedAt: row.rotated_at || null,
|
|
56
|
+
lastUsedAt: row.last_used_at || null,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
46
60
|
const toRequestModel = (row) => {
|
|
47
61
|
if (!row) return null;
|
|
48
62
|
return {
|
|
@@ -220,6 +234,130 @@ const deleteRequestsByHook = (hookName) => {
|
|
|
220
234
|
return Number(result.changes || 0);
|
|
221
235
|
};
|
|
222
236
|
|
|
237
|
+
const createOauthCallback = ({ hookName }) => {
|
|
238
|
+
const database = ensureDb();
|
|
239
|
+
const normalizedHookName = String(hookName || "").trim();
|
|
240
|
+
if (!normalizedHookName) throw new Error("hookName is required");
|
|
241
|
+
const callbackId = generateOauthCallbackId();
|
|
242
|
+
database
|
|
243
|
+
.prepare(`
|
|
244
|
+
INSERT INTO oauth_callbacks (
|
|
245
|
+
callback_id,
|
|
246
|
+
hook_name
|
|
247
|
+
) VALUES (
|
|
248
|
+
$callback_id,
|
|
249
|
+
$hook_name
|
|
250
|
+
)
|
|
251
|
+
`)
|
|
252
|
+
.run({
|
|
253
|
+
$callback_id: callbackId,
|
|
254
|
+
$hook_name: normalizedHookName,
|
|
255
|
+
});
|
|
256
|
+
const inserted = database
|
|
257
|
+
.prepare(`
|
|
258
|
+
SELECT
|
|
259
|
+
callback_id,
|
|
260
|
+
hook_name,
|
|
261
|
+
created_at,
|
|
262
|
+
rotated_at,
|
|
263
|
+
last_used_at
|
|
264
|
+
FROM oauth_callbacks
|
|
265
|
+
WHERE callback_id = $callback_id
|
|
266
|
+
LIMIT 1
|
|
267
|
+
`)
|
|
268
|
+
.get({ $callback_id: callbackId });
|
|
269
|
+
return toOauthCallbackModel(inserted);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const getOauthCallbackByHook = (hookName) => {
|
|
273
|
+
const database = ensureDb();
|
|
274
|
+
const normalizedHookName = String(hookName || "").trim();
|
|
275
|
+
if (!normalizedHookName) return null;
|
|
276
|
+
const row = database
|
|
277
|
+
.prepare(`
|
|
278
|
+
SELECT
|
|
279
|
+
callback_id,
|
|
280
|
+
hook_name,
|
|
281
|
+
created_at,
|
|
282
|
+
rotated_at,
|
|
283
|
+
last_used_at
|
|
284
|
+
FROM oauth_callbacks
|
|
285
|
+
WHERE hook_name = $hook_name
|
|
286
|
+
LIMIT 1
|
|
287
|
+
`)
|
|
288
|
+
.get({ $hook_name: normalizedHookName });
|
|
289
|
+
return toOauthCallbackModel(row);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const getOauthCallbackById = (callbackId) => {
|
|
293
|
+
const database = ensureDb();
|
|
294
|
+
const normalizedCallbackId = String(callbackId || "").trim();
|
|
295
|
+
if (!normalizedCallbackId) return null;
|
|
296
|
+
const row = database
|
|
297
|
+
.prepare(`
|
|
298
|
+
SELECT
|
|
299
|
+
callback_id,
|
|
300
|
+
hook_name,
|
|
301
|
+
created_at,
|
|
302
|
+
rotated_at,
|
|
303
|
+
last_used_at
|
|
304
|
+
FROM oauth_callbacks
|
|
305
|
+
WHERE callback_id = $callback_id
|
|
306
|
+
LIMIT 1
|
|
307
|
+
`)
|
|
308
|
+
.get({ $callback_id: normalizedCallbackId });
|
|
309
|
+
return toOauthCallbackModel(row);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const rotateOauthCallback = (hookName) => {
|
|
313
|
+
const database = ensureDb();
|
|
314
|
+
const normalizedHookName = String(hookName || "").trim();
|
|
315
|
+
if (!normalizedHookName) throw new Error("hookName is required");
|
|
316
|
+
const existing = getOauthCallbackByHook(normalizedHookName);
|
|
317
|
+
if (!existing) throw new Error("OAuth callback not found");
|
|
318
|
+
const nextCallbackId = generateOauthCallbackId();
|
|
319
|
+
database
|
|
320
|
+
.prepare(`
|
|
321
|
+
UPDATE oauth_callbacks
|
|
322
|
+
SET
|
|
323
|
+
callback_id = $callback_id,
|
|
324
|
+
rotated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
325
|
+
WHERE hook_name = $hook_name
|
|
326
|
+
`)
|
|
327
|
+
.run({
|
|
328
|
+
$callback_id: nextCallbackId,
|
|
329
|
+
$hook_name: normalizedHookName,
|
|
330
|
+
});
|
|
331
|
+
return getOauthCallbackById(nextCallbackId);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const deleteOauthCallback = (hookName) => {
|
|
335
|
+
const database = ensureDb();
|
|
336
|
+
const normalizedHookName = String(hookName || "").trim();
|
|
337
|
+
if (!normalizedHookName) return 0;
|
|
338
|
+
const result = database
|
|
339
|
+
.prepare(`
|
|
340
|
+
DELETE FROM oauth_callbacks
|
|
341
|
+
WHERE hook_name = $hook_name
|
|
342
|
+
`)
|
|
343
|
+
.run({ $hook_name: normalizedHookName });
|
|
344
|
+
return Number(result.changes || 0);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const markOauthCallbackUsed = (callbackId) => {
|
|
348
|
+
const database = ensureDb();
|
|
349
|
+
const normalizedCallbackId = String(callbackId || "").trim();
|
|
350
|
+
if (!normalizedCallbackId) return 0;
|
|
351
|
+
const result = database
|
|
352
|
+
.prepare(`
|
|
353
|
+
UPDATE oauth_callbacks
|
|
354
|
+
SET last_used_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
355
|
+
WHERE callback_id = $callback_id
|
|
356
|
+
`)
|
|
357
|
+
.run({ $callback_id: normalizedCallbackId });
|
|
358
|
+
return Number(result.changes || 0);
|
|
359
|
+
};
|
|
360
|
+
|
|
223
361
|
const pruneOldEntries = (days = 30) => {
|
|
224
362
|
const database = ensureDb();
|
|
225
363
|
const safeDays = Math.max(1, Number.parseInt(String(days || 30), 10) || 30);
|
|
@@ -240,5 +378,11 @@ module.exports = {
|
|
|
240
378
|
getRequestById,
|
|
241
379
|
getHookSummaries,
|
|
242
380
|
deleteRequestsByHook,
|
|
381
|
+
createOauthCallback,
|
|
382
|
+
getOauthCallbackByHook,
|
|
383
|
+
getOauthCallbackById,
|
|
384
|
+
rotateOauthCallback,
|
|
385
|
+
deleteOauthCallback,
|
|
386
|
+
markOauthCallbackUsed,
|
|
243
387
|
pruneOldEntries,
|
|
244
388
|
};
|
|
@@ -18,6 +18,19 @@ const createSchema = (database) => {
|
|
|
18
18
|
CREATE INDEX IF NOT EXISTS idx_webhook_requests_hook_ts
|
|
19
19
|
ON webhook_requests(hook_name, created_at DESC);
|
|
20
20
|
`);
|
|
21
|
+
database.exec(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS oauth_callbacks (
|
|
23
|
+
callback_id TEXT PRIMARY KEY,
|
|
24
|
+
hook_name TEXT NOT NULL UNIQUE,
|
|
25
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
26
|
+
rotated_at TEXT,
|
|
27
|
+
last_used_at TEXT
|
|
28
|
+
);
|
|
29
|
+
`);
|
|
30
|
+
database.exec(`
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_oauth_callbacks_hook_name
|
|
32
|
+
ON oauth_callbacks(hook_name);
|
|
33
|
+
`);
|
|
21
34
|
};
|
|
22
35
|
|
|
23
36
|
module.exports = {
|
|
@@ -17,6 +17,9 @@ const { registerDoctorRoutes } = require("../routes/doctor");
|
|
|
17
17
|
const { registerAgentRoutes } = require("../routes/agents");
|
|
18
18
|
const { registerCronRoutes } = require("../routes/cron");
|
|
19
19
|
const { registerNodeRoutes } = require("../routes/nodes");
|
|
20
|
+
const {
|
|
21
|
+
createOauthCallbackMiddleware,
|
|
22
|
+
} = require("../oauth-callback-middleware");
|
|
20
23
|
|
|
21
24
|
const registerServerRoutes = ({
|
|
22
25
|
app,
|
|
@@ -58,6 +61,12 @@ const registerServerRoutes = ({
|
|
|
58
61
|
getRequestById,
|
|
59
62
|
getHookSummaries,
|
|
60
63
|
deleteRequestsByHook,
|
|
64
|
+
createOauthCallback,
|
|
65
|
+
getOauthCallbackByHook,
|
|
66
|
+
getOauthCallbackById,
|
|
67
|
+
rotateOauthCallback,
|
|
68
|
+
deleteOauthCallback,
|
|
69
|
+
markOauthCallbackUsed,
|
|
61
70
|
watchdog,
|
|
62
71
|
getRecentEvents,
|
|
63
72
|
readLogTail,
|
|
@@ -132,6 +141,8 @@ const registerServerRoutes = ({
|
|
|
132
141
|
restartRequiredState,
|
|
133
142
|
topicRegistry,
|
|
134
143
|
authProfiles,
|
|
144
|
+
watchdog,
|
|
145
|
+
doctorService,
|
|
135
146
|
});
|
|
136
147
|
registerBrowseRoutes({
|
|
137
148
|
app,
|
|
@@ -185,9 +196,18 @@ const registerServerRoutes = ({
|
|
|
185
196
|
getRequestById,
|
|
186
197
|
getHookSummaries,
|
|
187
198
|
deleteRequestsByHook,
|
|
199
|
+
createOauthCallback,
|
|
200
|
+
getOauthCallbackByHook,
|
|
201
|
+
rotateOauthCallback,
|
|
202
|
+
deleteOauthCallback,
|
|
188
203
|
},
|
|
189
204
|
restartRequiredState,
|
|
190
205
|
});
|
|
206
|
+
const oauthCallbackMiddleware = createOauthCallbackMiddleware({
|
|
207
|
+
getOauthCallbackById,
|
|
208
|
+
markOauthCallbackUsed,
|
|
209
|
+
webhookMiddleware,
|
|
210
|
+
});
|
|
191
211
|
registerWatchdogRoutes({
|
|
192
212
|
app,
|
|
193
213
|
requireAuth,
|
|
@@ -233,6 +253,7 @@ const registerServerRoutes = ({
|
|
|
233
253
|
getGatewayUrl,
|
|
234
254
|
SETUP_API_PREFIXES,
|
|
235
255
|
requireAuth,
|
|
256
|
+
oauthCallbackMiddleware,
|
|
236
257
|
webhookMiddleware,
|
|
237
258
|
});
|
|
238
259
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const createOauthCallbackMiddleware = ({
|
|
2
|
+
getOauthCallbackById,
|
|
3
|
+
markOauthCallbackUsed = () => {},
|
|
4
|
+
webhookMiddleware,
|
|
5
|
+
}) => {
|
|
6
|
+
return (req, res) => {
|
|
7
|
+
const callbackId = String(req.params?.id || "").trim();
|
|
8
|
+
if (!callbackId) {
|
|
9
|
+
return res.status(404).json({ error: "Not found" });
|
|
10
|
+
}
|
|
11
|
+
const callback = getOauthCallbackById(callbackId);
|
|
12
|
+
if (!callback?.hookName) {
|
|
13
|
+
return res.status(404).json({ error: "Not found" });
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
markOauthCallbackUsed(callbackId);
|
|
17
|
+
} catch {}
|
|
18
|
+
const originalUrl = String(req.originalUrl || req.url || "");
|
|
19
|
+
const queryIndex = originalUrl.indexOf("?");
|
|
20
|
+
const querySuffix = queryIndex >= 0 ? originalUrl.slice(queryIndex) : "";
|
|
21
|
+
const rewrittenUrl = `/hooks/${callback.hookName}${querySuffix}`;
|
|
22
|
+
req.url = rewrittenUrl;
|
|
23
|
+
req.originalUrl = rewrittenUrl;
|
|
24
|
+
const webhookToken = String(process.env.WEBHOOK_TOKEN || "").trim();
|
|
25
|
+
if (webhookToken) {
|
|
26
|
+
req.headers.authorization = `Bearer ${webhookToken}`;
|
|
27
|
+
}
|
|
28
|
+
return webhookMiddleware(req, res);
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
createOauthCallbackMiddleware,
|
|
34
|
+
};
|
|
@@ -4,6 +4,7 @@ const registerProxyRoutes = ({
|
|
|
4
4
|
getGatewayUrl,
|
|
5
5
|
SETUP_API_PREFIXES,
|
|
6
6
|
requireAuth,
|
|
7
|
+
oauthCallbackMiddleware,
|
|
7
8
|
webhookMiddleware,
|
|
8
9
|
}) => {
|
|
9
10
|
const kOpenClawPathPattern = /^\/openclaw\/.+/;
|
|
@@ -24,6 +25,7 @@ const registerProxyRoutes = ({
|
|
|
24
25
|
proxy.web(req, res, { target: getGatewayUrl() }),
|
|
25
26
|
);
|
|
26
27
|
|
|
28
|
+
app.all("/oauth/:id", oauthCallbackMiddleware);
|
|
27
29
|
app.all(kHooksPathPattern, webhookMiddleware);
|
|
28
30
|
app.all(kWebhookPathPattern, webhookMiddleware);
|
|
29
31
|
|
|
@@ -25,6 +25,8 @@ const registerSystemRoutes = ({
|
|
|
25
25
|
restartRequiredState,
|
|
26
26
|
topicRegistry,
|
|
27
27
|
authProfiles,
|
|
28
|
+
watchdog,
|
|
29
|
+
doctorService,
|
|
28
30
|
}) => {
|
|
29
31
|
let envRestartPending = false;
|
|
30
32
|
const kManagedChannelTokenPattern =
|
|
@@ -465,12 +467,12 @@ const registerSystemRoutes = ({
|
|
|
465
467
|
res.json({ ok: true, changed, restartRequired });
|
|
466
468
|
});
|
|
467
469
|
|
|
468
|
-
|
|
470
|
+
const buildStatusPayload = async () => {
|
|
469
471
|
const configExists = fs.existsSync(`${OPENCLAW_DIR}/openclaw.json`);
|
|
470
472
|
const running = await isGatewayRunning();
|
|
471
473
|
const repo = process.env.GITHUB_WORKSPACE_REPO || "";
|
|
472
474
|
const openclawVersion = openclawVersionService.readOpenclawVersion();
|
|
473
|
-
|
|
475
|
+
return {
|
|
474
476
|
gateway: running
|
|
475
477
|
? "running"
|
|
476
478
|
: configExists
|
|
@@ -481,6 +483,52 @@ const registerSystemRoutes = ({
|
|
|
481
483
|
repo,
|
|
482
484
|
openclawVersion,
|
|
483
485
|
syncCron: getSystemCronStatus(),
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
app.get("/api/status", async (req, res) => {
|
|
490
|
+
const payload = await buildStatusPayload();
|
|
491
|
+
res.json(payload);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
app.get("/api/events/status", async (req, res) => {
|
|
495
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
496
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
497
|
+
res.setHeader("Connection", "keep-alive");
|
|
498
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
499
|
+
res.flushHeaders?.();
|
|
500
|
+
|
|
501
|
+
const writeStatusEvent = async () => {
|
|
502
|
+
try {
|
|
503
|
+
const status = await buildStatusPayload();
|
|
504
|
+
const watchdogStatus =
|
|
505
|
+
typeof watchdog?.getStatus === "function" ? watchdog.getStatus() : null;
|
|
506
|
+
const doctorStatus =
|
|
507
|
+
typeof doctorService?.buildStatus === "function"
|
|
508
|
+
? doctorService.buildStatus()
|
|
509
|
+
: null;
|
|
510
|
+
res.write("event: status\n");
|
|
511
|
+
res.write(
|
|
512
|
+
`data: ${JSON.stringify({
|
|
513
|
+
status,
|
|
514
|
+
watchdogStatus,
|
|
515
|
+
doctorStatus,
|
|
516
|
+
timestamp: new Date().toISOString(),
|
|
517
|
+
})}\n\n`,
|
|
518
|
+
);
|
|
519
|
+
} catch {}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
await writeStatusEvent();
|
|
523
|
+
const statusIntervalId = setInterval(writeStatusEvent, 2000);
|
|
524
|
+
const keepAliveIntervalId = setInterval(() => {
|
|
525
|
+
res.write(": keepalive\n\n");
|
|
526
|
+
}, 15000);
|
|
527
|
+
|
|
528
|
+
req.on("close", () => {
|
|
529
|
+
clearInterval(statusIntervalId);
|
|
530
|
+
clearInterval(keepAliveIntervalId);
|
|
531
|
+
res.end();
|
|
484
532
|
});
|
|
485
533
|
});
|
|
486
534
|
|
|
@@ -46,7 +46,7 @@ const normalizeStatusFilter = (rawStatus) => {
|
|
|
46
46
|
return "all";
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
const buildWebhookUrls = ({ baseUrl, name }) => {
|
|
49
|
+
const buildWebhookUrls = ({ baseUrl, name, oauthCallback = null }) => {
|
|
50
50
|
const fullUrl = `${baseUrl}/hooks/${name}`;
|
|
51
51
|
const token = String(process.env.WEBHOOK_TOKEN || "").trim();
|
|
52
52
|
const queryStringUrl = token
|
|
@@ -55,7 +55,18 @@ const buildWebhookUrls = ({ baseUrl, name }) => {
|
|
|
55
55
|
const authHeaderValue = token
|
|
56
56
|
? `Authorization: Bearer ${token}`
|
|
57
57
|
: "Authorization: Bearer <WEBHOOK_TOKEN>";
|
|
58
|
-
|
|
58
|
+
const callbackId = String(oauthCallback?.callbackId || "").trim();
|
|
59
|
+
return {
|
|
60
|
+
fullUrl,
|
|
61
|
+
queryStringUrl,
|
|
62
|
+
authHeaderValue,
|
|
63
|
+
hasRuntimeToken: !!token,
|
|
64
|
+
oauthCallbackId: callbackId || "",
|
|
65
|
+
oauthCallbackUrl: callbackId ? `${baseUrl}/oauth/${callbackId}` : "",
|
|
66
|
+
oauthCallbackCreatedAt: oauthCallback?.createdAt || null,
|
|
67
|
+
oauthCallbackRotatedAt: oauthCallback?.rotatedAt || null,
|
|
68
|
+
oauthCallbackLastUsedAt: oauthCallback?.lastUsedAt || null,
|
|
69
|
+
};
|
|
59
70
|
};
|
|
60
71
|
|
|
61
72
|
const registerWebhookRoutes = ({
|
|
@@ -67,6 +78,16 @@ const registerWebhookRoutes = ({
|
|
|
67
78
|
shellCmd,
|
|
68
79
|
restartRequiredState,
|
|
69
80
|
}) => {
|
|
81
|
+
const {
|
|
82
|
+
getRequests = () => [],
|
|
83
|
+
getRequestById = () => null,
|
|
84
|
+
getHookSummaries = () => [],
|
|
85
|
+
deleteRequestsByHook = () => 0,
|
|
86
|
+
createOauthCallback: createOauthCallbackEntry = () => null,
|
|
87
|
+
getOauthCallbackByHook: getOauthCallbackByHookEntry = () => null,
|
|
88
|
+
rotateOauthCallback: rotateOauthCallbackEntry = () => null,
|
|
89
|
+
deleteOauthCallback: deleteOauthCallbackEntry = () => 0,
|
|
90
|
+
} = webhooksDb || {};
|
|
70
91
|
const fallbackRestartState = {
|
|
71
92
|
markRequired: () => {},
|
|
72
93
|
getSnapshot: async () => ({ restartRequired: false }),
|
|
@@ -91,14 +112,18 @@ const registerWebhookRoutes = ({
|
|
|
91
112
|
app.get("/api/webhooks", (req, res) => {
|
|
92
113
|
try {
|
|
93
114
|
const hooks = listWebhooks({ fs, constants });
|
|
94
|
-
const summaries =
|
|
115
|
+
const summaries = getHookSummaries();
|
|
95
116
|
const summaryByHook = mapSummaryByHook(summaries);
|
|
96
|
-
const webhooks = hooks.map((webhook) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
const webhooks = hooks.map((webhook) => {
|
|
118
|
+
const oauthCallback = getOauthCallbackByHookEntry(webhook.name);
|
|
119
|
+
return {
|
|
120
|
+
...mergeWebhookAndSummary({
|
|
121
|
+
webhook,
|
|
122
|
+
summary: summaryByHook.get(webhook.name),
|
|
123
|
+
}),
|
|
124
|
+
oauthCallbackEnabled: !!String(oauthCallback?.callbackId || "").trim(),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
102
127
|
res.json({ ok: true, webhooks });
|
|
103
128
|
} catch (err) {
|
|
104
129
|
res.status(500).json({ ok: false, error: err.message });
|
|
@@ -111,12 +136,11 @@ const registerWebhookRoutes = ({
|
|
|
111
136
|
const detail = getWebhookDetail({ fs, constants, name });
|
|
112
137
|
if (!detail)
|
|
113
138
|
return res.status(404).json({ ok: false, error: "Webhook not found" });
|
|
114
|
-
const summary =
|
|
115
|
-
|
|
116
|
-
.find((item) => item.hookName === name);
|
|
139
|
+
const summary = getHookSummaries().find((item) => item.hookName === name);
|
|
140
|
+
const oauthCallback = getOauthCallbackByHookEntry(name);
|
|
117
141
|
const merged = mergeWebhookAndSummary({ webhook: detail, summary });
|
|
118
142
|
const baseUrl = getBaseUrl(req);
|
|
119
|
-
const urls = buildWebhookUrls({ baseUrl, name });
|
|
143
|
+
const urls = buildWebhookUrls({ baseUrl, name, oauthCallback });
|
|
120
144
|
return res.json({
|
|
121
145
|
ok: true,
|
|
122
146
|
webhook: {
|
|
@@ -125,6 +149,11 @@ const registerWebhookRoutes = ({
|
|
|
125
149
|
queryStringUrl: urls.queryStringUrl,
|
|
126
150
|
authHeaderValue: urls.authHeaderValue,
|
|
127
151
|
hasRuntimeToken: urls.hasRuntimeToken,
|
|
152
|
+
oauthCallbackId: urls.oauthCallbackId,
|
|
153
|
+
oauthCallbackUrl: urls.oauthCallbackUrl,
|
|
154
|
+
oauthCallbackCreatedAt: urls.oauthCallbackCreatedAt,
|
|
155
|
+
oauthCallbackRotatedAt: urls.oauthCallbackRotatedAt,
|
|
156
|
+
oauthCallbackLastUsedAt: urls.oauthCallbackLastUsedAt,
|
|
128
157
|
authNote:
|
|
129
158
|
"All hooks use WEBHOOK_TOKEN. Use Authorization: Bearer <token> or x-openclaw-token header.",
|
|
130
159
|
},
|
|
@@ -136,11 +165,22 @@ const registerWebhookRoutes = ({
|
|
|
136
165
|
|
|
137
166
|
app.post("/api/webhooks", async (req, res) => {
|
|
138
167
|
try {
|
|
139
|
-
const {
|
|
168
|
+
const {
|
|
169
|
+
name: rawName,
|
|
170
|
+
destination = null,
|
|
171
|
+
oauthCallback = false,
|
|
172
|
+
} = req.body || {};
|
|
140
173
|
const name = validateWebhookName(rawName);
|
|
141
174
|
const webhook = createWebhook({ fs, constants, name, destination });
|
|
175
|
+
const oauthCallbackRecord = oauthCallback
|
|
176
|
+
? createOauthCallbackEntry({ hookName: name })
|
|
177
|
+
: null;
|
|
142
178
|
const baseUrl = getBaseUrl(req);
|
|
143
|
-
const urls = buildWebhookUrls({
|
|
179
|
+
const urls = buildWebhookUrls({
|
|
180
|
+
baseUrl,
|
|
181
|
+
name,
|
|
182
|
+
oauthCallback: oauthCallbackRecord,
|
|
183
|
+
});
|
|
144
184
|
const syncWarning = await runWebhookGitSync("create", name);
|
|
145
185
|
markRestartRequired("webhooks");
|
|
146
186
|
const snapshot = await getRestartSnapshot();
|
|
@@ -152,6 +192,11 @@ const registerWebhookRoutes = ({
|
|
|
152
192
|
queryStringUrl: urls.queryStringUrl,
|
|
153
193
|
authHeaderValue: urls.authHeaderValue,
|
|
154
194
|
hasRuntimeToken: urls.hasRuntimeToken,
|
|
195
|
+
oauthCallbackId: urls.oauthCallbackId,
|
|
196
|
+
oauthCallbackUrl: urls.oauthCallbackUrl,
|
|
197
|
+
oauthCallbackCreatedAt: urls.oauthCallbackCreatedAt,
|
|
198
|
+
oauthCallbackRotatedAt: urls.oauthCallbackRotatedAt,
|
|
199
|
+
oauthCallbackLastUsedAt: urls.oauthCallbackLastUsedAt,
|
|
155
200
|
},
|
|
156
201
|
restartRequired: snapshot.restartRequired,
|
|
157
202
|
syncWarning,
|
|
@@ -164,6 +209,68 @@ const registerWebhookRoutes = ({
|
|
|
164
209
|
}
|
|
165
210
|
});
|
|
166
211
|
|
|
212
|
+
app.post("/api/webhooks/:name/oauth-callback", (req, res) => {
|
|
213
|
+
try {
|
|
214
|
+
const name = validateWebhookName(req.params.name);
|
|
215
|
+
const detail = getWebhookDetail({ fs, constants, name });
|
|
216
|
+
if (!detail)
|
|
217
|
+
return res.status(404).json({ ok: false, error: "Webhook not found" });
|
|
218
|
+
const existing = getOauthCallbackByHookEntry(name);
|
|
219
|
+
if (existing?.callbackId) {
|
|
220
|
+
return res.status(409).json({
|
|
221
|
+
ok: false,
|
|
222
|
+
error: "OAuth callback alias already exists",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const oauthCallback = createOauthCallbackEntry({ hookName: name });
|
|
226
|
+
const baseUrl = getBaseUrl(req);
|
|
227
|
+
const urls = buildWebhookUrls({ baseUrl, name, oauthCallback });
|
|
228
|
+
return res.status(201).json({
|
|
229
|
+
ok: true,
|
|
230
|
+
oauthCallbackId: urls.oauthCallbackId,
|
|
231
|
+
oauthCallbackUrl: urls.oauthCallbackUrl,
|
|
232
|
+
oauthCallbackCreatedAt: urls.oauthCallbackCreatedAt,
|
|
233
|
+
oauthCallbackRotatedAt: urls.oauthCallbackRotatedAt,
|
|
234
|
+
oauthCallbackLastUsedAt: urls.oauthCallbackLastUsedAt,
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
return res.status(400).json({ ok: false, error: err.message });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
app.post("/api/webhooks/:name/oauth-callback/rotate", (req, res) => {
|
|
242
|
+
try {
|
|
243
|
+
const name = validateWebhookName(req.params.name);
|
|
244
|
+
const detail = getWebhookDetail({ fs, constants, name });
|
|
245
|
+
if (!detail)
|
|
246
|
+
return res.status(404).json({ ok: false, error: "Webhook not found" });
|
|
247
|
+
const oauthCallback = rotateOauthCallbackEntry(name);
|
|
248
|
+
const baseUrl = getBaseUrl(req);
|
|
249
|
+
const urls = buildWebhookUrls({ baseUrl, name, oauthCallback });
|
|
250
|
+
return res.json({
|
|
251
|
+
ok: true,
|
|
252
|
+
oauthCallbackId: urls.oauthCallbackId,
|
|
253
|
+
oauthCallbackUrl: urls.oauthCallbackUrl,
|
|
254
|
+
oauthCallbackCreatedAt: urls.oauthCallbackCreatedAt,
|
|
255
|
+
oauthCallbackRotatedAt: urls.oauthCallbackRotatedAt,
|
|
256
|
+
oauthCallbackLastUsedAt: urls.oauthCallbackLastUsedAt,
|
|
257
|
+
});
|
|
258
|
+
} catch (err) {
|
|
259
|
+
const status = String(err?.message || "").includes("not found") ? 404 : 400;
|
|
260
|
+
return res.status(status).json({ ok: false, error: err.message });
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
app.delete("/api/webhooks/:name/oauth-callback", (req, res) => {
|
|
265
|
+
try {
|
|
266
|
+
const name = validateWebhookName(req.params.name);
|
|
267
|
+
const deletedCount = deleteOauthCallbackEntry(name);
|
|
268
|
+
return res.json({ ok: true, deleted: deletedCount > 0 });
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return res.status(400).json({ ok: false, error: err.message });
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
167
274
|
app.delete("/api/webhooks/:name", async (req, res) => {
|
|
168
275
|
try {
|
|
169
276
|
const name = validateWebhookName(req.params.name);
|
|
@@ -184,7 +291,8 @@ const registerWebhookRoutes = ({
|
|
|
184
291
|
}
|
|
185
292
|
if (!deletion?.removed)
|
|
186
293
|
return res.status(404).json({ ok: false, error: "Webhook not found" });
|
|
187
|
-
|
|
294
|
+
deleteOauthCallbackEntry(name);
|
|
295
|
+
const deletedRequestCount = deleteRequestsByHook(name);
|
|
188
296
|
const syncWarning = await runWebhookGitSync("delete", name);
|
|
189
297
|
markRestartRequired("webhooks");
|
|
190
298
|
const snapshot = await getRestartSnapshot();
|
|
@@ -216,7 +324,7 @@ const registerWebhookRoutes = ({
|
|
|
216
324
|
.status(400)
|
|
217
325
|
.json({ ok: false, error: "Invalid limit/offset" });
|
|
218
326
|
}
|
|
219
|
-
const requests =
|
|
327
|
+
const requests = getRequests(name, { limit, offset, status });
|
|
220
328
|
return res.json({ ok: true, requests });
|
|
221
329
|
} catch (err) {
|
|
222
330
|
return res.status(400).json({ ok: false, error: err.message });
|
|
@@ -230,7 +338,7 @@ const registerWebhookRoutes = ({
|
|
|
230
338
|
if (!isFiniteInteger(requestId) || requestId <= 0) {
|
|
231
339
|
return res.status(400).json({ ok: false, error: "Invalid request id" });
|
|
232
340
|
}
|
|
233
|
-
const request =
|
|
341
|
+
const request = getRequestById(name, requestId);
|
|
234
342
|
if (!request)
|
|
235
343
|
return res.status(404).json({ ok: false, error: "Request not found" });
|
|
236
344
|
return res.json({ ok: true, request });
|
|
@@ -114,6 +114,11 @@ const resolveGatewayPath = ({ pathname, search }) => {
|
|
|
114
114
|
return `${pathname}${search || ""}`;
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
+
const resolveForwardMethod = (method) => {
|
|
118
|
+
if (String(method || "").toUpperCase() === "GET") return "POST";
|
|
119
|
+
return method;
|
|
120
|
+
};
|
|
121
|
+
|
|
117
122
|
const parseJsonSafe = (rawValue) => {
|
|
118
123
|
try {
|
|
119
124
|
return JSON.parse(String(rawValue || "").trim() || "{}");
|
|
@@ -297,7 +302,7 @@ const createWebhookMiddleware = ({
|
|
|
297
302
|
protocol: gateway.protocol,
|
|
298
303
|
hostname: gateway.hostname,
|
|
299
304
|
port: gateway.port,
|
|
300
|
-
method: req.method,
|
|
305
|
+
method: resolveForwardMethod(req.method),
|
|
301
306
|
path: resolveGatewayPath({
|
|
302
307
|
pathname: inboundUrl.pathname,
|
|
303
308
|
search: inboundUrl.search,
|
package/lib/server.js
CHANGED
|
@@ -30,6 +30,12 @@ const {
|
|
|
30
30
|
getRequestById,
|
|
31
31
|
getHookSummaries,
|
|
32
32
|
deleteRequestsByHook,
|
|
33
|
+
createOauthCallback,
|
|
34
|
+
getOauthCallbackByHook,
|
|
35
|
+
getOauthCallbackById,
|
|
36
|
+
rotateOauthCallback,
|
|
37
|
+
deleteOauthCallback,
|
|
38
|
+
markOauthCallbackUsed,
|
|
33
39
|
} = require("./server/db/webhooks");
|
|
34
40
|
const {
|
|
35
41
|
initWatchdogDb,
|
|
@@ -294,6 +300,12 @@ const { isAuthorizedRequest, gmailWatchService } = registerServerRoutes({
|
|
|
294
300
|
getRequestById,
|
|
295
301
|
getHookSummaries,
|
|
296
302
|
deleteRequestsByHook,
|
|
303
|
+
createOauthCallback,
|
|
304
|
+
getOauthCallbackByHook,
|
|
305
|
+
getOauthCallbackById,
|
|
306
|
+
rotateOauthCallback,
|
|
307
|
+
deleteOauthCallback,
|
|
308
|
+
markOauthCallbackUsed,
|
|
297
309
|
watchdog,
|
|
298
310
|
getRecentEvents,
|
|
299
311
|
readLogTail,
|