@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.
@@ -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
- 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.1-beta.0",
3
+ "version": "0.8.1-beta.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },