@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.
Files changed (37) hide show
  1. package/lib/public/js/app.js +100 -83
  2. package/lib/public/js/components/agents-tab/agent-pairing-section.js +47 -12
  3. package/lib/public/js/components/channels.js +14 -17
  4. package/lib/public/js/components/envars.js +42 -6
  5. package/lib/public/js/components/features.js +6 -12
  6. package/lib/public/js/components/general/use-general-tab.js +10 -5
  7. package/lib/public/js/components/google/use-gmail-watch.js +22 -18
  8. package/lib/public/js/components/google/use-google-accounts.js +23 -23
  9. package/lib/public/js/components/models-tab/use-models.js +20 -4
  10. package/lib/public/js/components/nodes-tab/connected-nodes/user-connected-nodes.js +2 -2
  11. package/lib/public/js/components/nodes-tab/use-nodes-tab.js +13 -9
  12. package/lib/public/js/components/routes/webhooks-route.js +1 -1
  13. package/lib/public/js/components/webhooks/create-webhook-modal/index.js +176 -0
  14. package/lib/public/js/components/webhooks/helpers.js +106 -0
  15. package/lib/public/js/components/webhooks/index.js +148 -0
  16. package/lib/public/js/components/webhooks/request-history/index.js +241 -0
  17. package/lib/public/js/components/webhooks/request-history/use-request-history.js +167 -0
  18. package/lib/public/js/components/webhooks/webhook-detail/index.js +374 -0
  19. package/lib/public/js/components/webhooks/webhook-detail/use-webhook-detail.js +261 -0
  20. package/lib/public/js/components/webhooks/webhook-list/index.js +96 -0
  21. package/lib/public/js/components/webhooks/webhook-list/use-webhook-list.js +30 -0
  22. package/lib/public/js/hooks/use-app-shell-controller.js +59 -6
  23. package/lib/public/js/hooks/use-cached-fetch.js +63 -0
  24. package/lib/public/js/hooks/usePolling.js +45 -7
  25. package/lib/public/js/lib/api-cache.js +88 -0
  26. package/lib/public/js/lib/api.js +64 -1
  27. package/lib/server/db/webhooks/index.js +144 -0
  28. package/lib/server/db/webhooks/schema.js +13 -0
  29. package/lib/server/init/register-server-routes.js +21 -0
  30. package/lib/server/oauth-callback-middleware.js +34 -0
  31. package/lib/server/routes/proxy.js +2 -0
  32. package/lib/server/routes/system.js +50 -2
  33. package/lib/server/routes/webhooks.js +126 -18
  34. package/lib/server/webhook-middleware.js +6 -1
  35. package/lib/server.js +12 -0
  36. package/package.json +1 -1
  37. 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
- app.get("/api/status", async (req, res) => {
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
- res.json({
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
- return { fullUrl, queryStringUrl, authHeaderValue, hasRuntimeToken: !!token };
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 = webhooksDb.getHookSummaries();
115
+ const summaries = getHookSummaries();
95
116
  const summaryByHook = mapSummaryByHook(summaries);
96
- const webhooks = hooks.map((webhook) =>
97
- mergeWebhookAndSummary({
98
- webhook,
99
- summary: summaryByHook.get(webhook.name),
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 = webhooksDb
115
- .getHookSummaries()
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 { name: rawName, destination = null } = req.body || {};
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({ baseUrl, name });
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
- const deletedRequestCount = webhooksDb.deleteRequestsByHook(name);
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 = webhooksDb.getRequests(name, { limit, offset, status });
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 = webhooksDb.getRequestById(name, requestId);
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.8.0",
3
+ "version": "0.8.1-beta.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },