@chrysb/alphaclaw 0.8.1-beta.0 → 0.8.1-beta.2
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/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 +386 -0
- package/lib/public/js/components/webhooks/webhook-detail/use-webhook-detail.js +277 -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/lib/api.js +35 -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 +19 -0
- package/lib/server/oauth-callback-middleware.js +34 -0
- package/lib/server/routes/proxy.js +2 -0
- 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
|
@@ -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
|
|
|
@@ -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,
|