@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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { ActionButton } from "../../action-button.js";
|
|
4
|
+
import { Badge } from "../../badge.js";
|
|
5
|
+
import { ConfirmDialog } from "../../confirm-dialog.js";
|
|
6
|
+
import { showToast } from "../../toast.js";
|
|
7
|
+
import { formatDateTime } from "../helpers.js";
|
|
8
|
+
import { RequestHistory } from "../request-history/index.js";
|
|
9
|
+
import { useWebhookDetail } from "./use-webhook-detail.js";
|
|
10
|
+
|
|
11
|
+
const html = htm.bind(h);
|
|
12
|
+
|
|
13
|
+
export const WebhookDetail = ({
|
|
14
|
+
selectedHookName = "",
|
|
15
|
+
onBackToList = () => {},
|
|
16
|
+
onRestartRequired = () => {},
|
|
17
|
+
onOpenFile = () => {},
|
|
18
|
+
}) => {
|
|
19
|
+
const { state, actions } = useWebhookDetail({
|
|
20
|
+
selectedHookName,
|
|
21
|
+
onBackToList,
|
|
22
|
+
onRestartRequired,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
authMode,
|
|
27
|
+
selectedWebhook,
|
|
28
|
+
selectedWebhookManaged,
|
|
29
|
+
selectedDeliveryAgentName,
|
|
30
|
+
selectedDeliveryChannel,
|
|
31
|
+
webhookUrl,
|
|
32
|
+
oauthCallbackUrl,
|
|
33
|
+
hasOauthCallback,
|
|
34
|
+
webhookUrlWithQueryToken,
|
|
35
|
+
authHeaderValue,
|
|
36
|
+
bearerTokenValue,
|
|
37
|
+
effectiveAuthMode,
|
|
38
|
+
activeCurlCommand,
|
|
39
|
+
deleting,
|
|
40
|
+
showDeleteConfirm,
|
|
41
|
+
deleteTransformDir,
|
|
42
|
+
sendingTestWebhook,
|
|
43
|
+
rotatingOauthCallback,
|
|
44
|
+
showRotateOauthConfirm,
|
|
45
|
+
} = state;
|
|
46
|
+
|
|
47
|
+
return html`
|
|
48
|
+
<div class="space-y-4">
|
|
49
|
+
<div class="bg-surface border border-border rounded-xl p-4 space-y-4">
|
|
50
|
+
<div>
|
|
51
|
+
<h2 class="font-semibold text-sm">
|
|
52
|
+
${selectedWebhook?.path || `/hooks/${selectedHookName}`}
|
|
53
|
+
</h2>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
${hasOauthCallback
|
|
57
|
+
? null
|
|
58
|
+
: html`<div class="bg-black/20 border border-border rounded-lg p-3 space-y-4">
|
|
59
|
+
${selectedWebhookManaged
|
|
60
|
+
? null
|
|
61
|
+
: html`
|
|
62
|
+
<div class="space-y-2">
|
|
63
|
+
<p class="text-xs text-gray-500">Auth mode</p>
|
|
64
|
+
<div class="flex items-center gap-2">
|
|
65
|
+
<button
|
|
66
|
+
class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
|
|
67
|
+
"headers"
|
|
68
|
+
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
69
|
+
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
70
|
+
onclick=${() => actions.setAuthMode("headers")}
|
|
71
|
+
>
|
|
72
|
+
Headers
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
|
|
76
|
+
"query"
|
|
77
|
+
? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
|
|
78
|
+
: "border-border text-gray-400 hover:text-gray-200"}"
|
|
79
|
+
onclick=${() => actions.setAuthMode("query")}
|
|
80
|
+
>
|
|
81
|
+
Query string
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
`}
|
|
86
|
+
<div class="space-y-2">
|
|
87
|
+
<p class="text-xs text-gray-500">Webhook URL</p>
|
|
88
|
+
<div class="flex items-center gap-2">
|
|
89
|
+
<input
|
|
90
|
+
type="text"
|
|
91
|
+
readonly
|
|
92
|
+
value=${effectiveAuthMode === "query"
|
|
93
|
+
? webhookUrlWithQueryToken
|
|
94
|
+
: webhookUrl}
|
|
95
|
+
class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
|
|
96
|
+
/>
|
|
97
|
+
<button
|
|
98
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
|
|
99
|
+
onclick=${async () => {
|
|
100
|
+
try {
|
|
101
|
+
await navigator.clipboard.writeText(
|
|
102
|
+
effectiveAuthMode === "query"
|
|
103
|
+
? webhookUrlWithQueryToken
|
|
104
|
+
: webhookUrl,
|
|
105
|
+
);
|
|
106
|
+
showToast("Webhook URL copied", "success");
|
|
107
|
+
} catch {
|
|
108
|
+
showToast("Could not copy URL", "error");
|
|
109
|
+
}
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
Copy
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
${selectedWebhookManaged
|
|
117
|
+
? null
|
|
118
|
+
: effectiveAuthMode === "headers"
|
|
119
|
+
? html`
|
|
120
|
+
<div class="space-y-2">
|
|
121
|
+
<p class="text-xs text-gray-500">Auth headers</p>
|
|
122
|
+
<div class="flex items-center gap-2">
|
|
123
|
+
<input
|
|
124
|
+
type="text"
|
|
125
|
+
readonly
|
|
126
|
+
value=${authHeaderValue}
|
|
127
|
+
class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
|
|
128
|
+
/>
|
|
129
|
+
<button
|
|
130
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
|
|
131
|
+
onclick=${async () => {
|
|
132
|
+
try {
|
|
133
|
+
await navigator.clipboard.writeText(
|
|
134
|
+
bearerTokenValue,
|
|
135
|
+
);
|
|
136
|
+
showToast("Bearer token copied", "success");
|
|
137
|
+
} catch {
|
|
138
|
+
showToast("Could not copy bearer token", "error");
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
>
|
|
142
|
+
Copy
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
`
|
|
147
|
+
: html`
|
|
148
|
+
<p class="text-xs text-yellow-300">
|
|
149
|
+
Always use auth headers when possible. Query string is
|
|
150
|
+
less secure.
|
|
151
|
+
</p>
|
|
152
|
+
`}
|
|
153
|
+
</div>`}
|
|
154
|
+
|
|
155
|
+
${selectedWebhookManaged || !hasOauthCallback
|
|
156
|
+
? null
|
|
157
|
+
: html`
|
|
158
|
+
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
159
|
+
<div class="flex items-center gap-2">
|
|
160
|
+
<p class="text-xs text-gray-500">OAuth Callback URL</p>
|
|
161
|
+
${hasOauthCallback
|
|
162
|
+
? html`<${Badge} tone="neutral">OAuth alias</${Badge}>`
|
|
163
|
+
: null}
|
|
164
|
+
</div>
|
|
165
|
+
<div class="flex items-center gap-2">
|
|
166
|
+
<input
|
|
167
|
+
type="text"
|
|
168
|
+
readonly
|
|
169
|
+
value=${hasOauthCallback ? oauthCallbackUrl : "Not enabled"}
|
|
170
|
+
class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
|
|
171
|
+
/>
|
|
172
|
+
${hasOauthCallback
|
|
173
|
+
? html`
|
|
174
|
+
<button
|
|
175
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
|
|
176
|
+
onclick=${async () => {
|
|
177
|
+
try {
|
|
178
|
+
await navigator.clipboard.writeText(
|
|
179
|
+
oauthCallbackUrl,
|
|
180
|
+
);
|
|
181
|
+
showToast("OAuth callback URL copied", "success");
|
|
182
|
+
} catch {
|
|
183
|
+
showToast("Could not copy URL", "error");
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
Copy
|
|
188
|
+
</button>
|
|
189
|
+
`
|
|
190
|
+
: null}
|
|
191
|
+
</div>
|
|
192
|
+
<div class="flex items-center justify-start gap-3 flex-wrap">
|
|
193
|
+
<div class="flex items-center gap-2">
|
|
194
|
+
<button
|
|
195
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary disabled:opacity-60"
|
|
196
|
+
onclick=${() => {
|
|
197
|
+
if (rotatingOauthCallback) return;
|
|
198
|
+
actions.setShowRotateOauthConfirm(true);
|
|
199
|
+
}}
|
|
200
|
+
disabled=${rotatingOauthCallback}
|
|
201
|
+
>
|
|
202
|
+
${rotatingOauthCallback ? "Rotating..." : "Rotate"}
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
<p class="text-xs text-yellow-300">
|
|
206
|
+
Keep this URL private. Rotate if exposed.
|
|
207
|
+
</p>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
`}
|
|
211
|
+
|
|
212
|
+
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
213
|
+
<p class="text-xs text-gray-500">Deliver to</p>
|
|
214
|
+
<p class="text-xs text-gray-200 font-mono ">
|
|
215
|
+
${selectedDeliveryAgentName}${" "}
|
|
216
|
+
<span class="text-xs text-gray-500 font-mono"
|
|
217
|
+
>(${selectedDeliveryChannel})</span
|
|
218
|
+
>
|
|
219
|
+
</p>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
223
|
+
<p class="text-xs text-gray-500">Test webhook</p>
|
|
224
|
+
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
|
|
225
|
+
<input
|
|
226
|
+
type="text"
|
|
227
|
+
readonly
|
|
228
|
+
value=${activeCurlCommand}
|
|
229
|
+
class="h-8 w-full sm:flex-1 sm:min-w-0 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono overflow-x-auto scrollbar-hidden"
|
|
230
|
+
/>
|
|
231
|
+
<div class="grid grid-cols-2 gap-2 w-full sm:w-auto sm:flex sm:items-center">
|
|
232
|
+
<button
|
|
233
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary w-full sm:w-auto sm:shrink-0"
|
|
234
|
+
onclick=${async () => {
|
|
235
|
+
try {
|
|
236
|
+
await navigator.clipboard.writeText(activeCurlCommand);
|
|
237
|
+
showToast("curl command copied", "success");
|
|
238
|
+
} catch {
|
|
239
|
+
showToast("Could not copy curl command", "error");
|
|
240
|
+
}
|
|
241
|
+
}}
|
|
242
|
+
>
|
|
243
|
+
Copy
|
|
244
|
+
</button>
|
|
245
|
+
<button
|
|
246
|
+
class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary w-full sm:w-auto sm:shrink-0 disabled:opacity-60"
|
|
247
|
+
onclick=${actions.handleSendTestWebhook}
|
|
248
|
+
disabled=${sendingTestWebhook}
|
|
249
|
+
>
|
|
250
|
+
${sendingTestWebhook ? "Sending..." : "Send"}
|
|
251
|
+
</button>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<div class="bg-black/20 border border-border rounded-lg p-3">
|
|
257
|
+
<div class="flex items-center gap-2 text-xs text-gray-300">
|
|
258
|
+
<span class="text-gray-500">Transform:</span>
|
|
259
|
+
${selectedWebhook?.transformPath
|
|
260
|
+
? html`<button
|
|
261
|
+
type="button"
|
|
262
|
+
class="ac-tip-link flex-1 min-w-0 truncate block text-left font-mono"
|
|
263
|
+
title=${selectedWebhook.transformPath}
|
|
264
|
+
onclick=${() => onOpenFile(selectedWebhook.transformPath)}
|
|
265
|
+
>
|
|
266
|
+
${selectedWebhook.transformPath}
|
|
267
|
+
</button>`
|
|
268
|
+
: html`<code class="flex-1 min-w-0 truncate block">—</code>`}
|
|
269
|
+
<span
|
|
270
|
+
class=${`ml-auto inline-flex items-center gap-1 px-1.5 py-0.5 rounded border font-sans ${
|
|
271
|
+
selectedWebhook?.transformExists
|
|
272
|
+
? "border-green-500/30 text-green-300 bg-green-500/10"
|
|
273
|
+
: "border-yellow-500/30 text-yellow-300 bg-yellow-500/10"
|
|
274
|
+
}`}
|
|
275
|
+
>
|
|
276
|
+
<span class="font-sans text-sm leading-none">
|
|
277
|
+
${selectedWebhook?.transformExists ? "✓" : "!"}
|
|
278
|
+
</span>
|
|
279
|
+
${selectedWebhook?.transformExists ? null : html`<span>missing</span>`}
|
|
280
|
+
</span>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<div class="flex items-center justify-between gap-3">
|
|
285
|
+
<p class="text-xs text-gray-600">
|
|
286
|
+
Created: ${formatDateTime(selectedWebhook?.createdAt)}
|
|
287
|
+
</p>
|
|
288
|
+
${selectedWebhookManaged
|
|
289
|
+
? null
|
|
290
|
+
: html`<${ActionButton}
|
|
291
|
+
onClick=${() => {
|
|
292
|
+
if (deleting) return;
|
|
293
|
+
actions.setDeleteTransformDir(true);
|
|
294
|
+
actions.setShowDeleteConfirm(true);
|
|
295
|
+
}}
|
|
296
|
+
disabled=${deleting}
|
|
297
|
+
loading=${deleting}
|
|
298
|
+
tone="danger"
|
|
299
|
+
size="sm"
|
|
300
|
+
idleLabel="Delete"
|
|
301
|
+
loadingLabel="Deleting..."
|
|
302
|
+
className="shrink-0 px-2.5 py-1"
|
|
303
|
+
/>`}
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
${selectedWebhookManaged
|
|
308
|
+
? html`
|
|
309
|
+
<div class="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3">
|
|
310
|
+
<p class="text-xs text-yellow-200">
|
|
311
|
+
This webhook is managed by Gmail Watch setup and cannot be
|
|
312
|
+
deleted or edited from this page.
|
|
313
|
+
</p>
|
|
314
|
+
</div>
|
|
315
|
+
`
|
|
316
|
+
: null}
|
|
317
|
+
<${RequestHistory}
|
|
318
|
+
selectedHookName=${selectedHookName}
|
|
319
|
+
selectedWebhook=${selectedWebhook}
|
|
320
|
+
effectiveAuthMode=${effectiveAuthMode}
|
|
321
|
+
webhookUrl=${webhookUrl}
|
|
322
|
+
webhookUrlWithQueryToken=${webhookUrlWithQueryToken}
|
|
323
|
+
bearerTokenValue=${bearerTokenValue}
|
|
324
|
+
/>
|
|
325
|
+
<${ConfirmDialog}
|
|
326
|
+
visible=${showRotateOauthConfirm &&
|
|
327
|
+
!!selectedHookName &&
|
|
328
|
+
!selectedWebhookManaged &&
|
|
329
|
+
hasOauthCallback}
|
|
330
|
+
title="Rotate OAuth callback?"
|
|
331
|
+
message="Rotating will generate a new callback URL and immediately invalidate the current URL."
|
|
332
|
+
confirmLabel="Rotate callback URL"
|
|
333
|
+
confirmLoadingLabel="Rotating..."
|
|
334
|
+
confirmLoading=${rotatingOauthCallback}
|
|
335
|
+
cancelLabel="Cancel"
|
|
336
|
+
onCancel=${() => {
|
|
337
|
+
if (rotatingOauthCallback) return;
|
|
338
|
+
actions.setShowRotateOauthConfirm(false);
|
|
339
|
+
}}
|
|
340
|
+
onConfirm=${actions.handleRotateOauthCallback}
|
|
341
|
+
/>
|
|
342
|
+
<${ConfirmDialog}
|
|
343
|
+
visible=${showDeleteConfirm &&
|
|
344
|
+
!!selectedHookName &&
|
|
345
|
+
!selectedWebhookManaged}
|
|
346
|
+
title="Delete webhook?"
|
|
347
|
+
message=${`This removes "/hooks/${selectedHookName}" from openclaw.json.`}
|
|
348
|
+
details=${html`
|
|
349
|
+
<div class="rounded-lg border border-border bg-black/20 p-3">
|
|
350
|
+
<label class="flex items-center gap-2 text-xs text-gray-300 select-none">
|
|
351
|
+
<input
|
|
352
|
+
type="checkbox"
|
|
353
|
+
checked=${deleteTransformDir}
|
|
354
|
+
onInput=${(event) =>
|
|
355
|
+
actions.setDeleteTransformDir(!!event.target.checked)}
|
|
356
|
+
/>
|
|
357
|
+
Also delete <code>hooks/transforms/${selectedHookName}</code>
|
|
358
|
+
</label>
|
|
359
|
+
</div>
|
|
360
|
+
`}
|
|
361
|
+
confirmLabel="Delete webhook"
|
|
362
|
+
confirmLoadingLabel="Deleting..."
|
|
363
|
+
confirmLoading=${deleting}
|
|
364
|
+
cancelLabel="Cancel"
|
|
365
|
+
onCancel=${() => {
|
|
366
|
+
if (deleting) return;
|
|
367
|
+
actions.setDeleteTransformDir(true);
|
|
368
|
+
actions.setShowDeleteConfirm(false);
|
|
369
|
+
}}
|
|
370
|
+
onConfirm=${actions.handleDeleteConfirmed}
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
373
|
+
`;
|
|
374
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import {
|
|
3
|
+
deleteWebhook,
|
|
4
|
+
fetchAgents,
|
|
5
|
+
fetchWebhookDetail,
|
|
6
|
+
rotateWebhookOauthCallback,
|
|
7
|
+
} from "../../../lib/api.js";
|
|
8
|
+
import { usePolling } from "../../../hooks/usePolling.js";
|
|
9
|
+
import { showToast } from "../../toast.js";
|
|
10
|
+
import { formatAgentFallbackName } from "../helpers.js";
|
|
11
|
+
|
|
12
|
+
export const useWebhookDetail = ({
|
|
13
|
+
selectedHookName = "",
|
|
14
|
+
onBackToList = () => {},
|
|
15
|
+
onRestartRequired = () => {},
|
|
16
|
+
}) => {
|
|
17
|
+
const [authMode, setAuthMode] = useState("headers");
|
|
18
|
+
const [deleting, setDeleting] = useState(false);
|
|
19
|
+
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
20
|
+
const [deleteTransformDir, setDeleteTransformDir] = useState(true);
|
|
21
|
+
const [rotatingOauthCallback, setRotatingOauthCallback] = useState(false);
|
|
22
|
+
const [showRotateOauthConfirm, setShowRotateOauthConfirm] = useState(false);
|
|
23
|
+
const [sendingTestWebhook, setSendingTestWebhook] = useState(false);
|
|
24
|
+
|
|
25
|
+
const detailPoll = usePolling(
|
|
26
|
+
async () => {
|
|
27
|
+
if (!selectedHookName) return null;
|
|
28
|
+
const data = await fetchWebhookDetail(selectedHookName);
|
|
29
|
+
return data.webhook || null;
|
|
30
|
+
},
|
|
31
|
+
10000,
|
|
32
|
+
{ enabled: !!selectedHookName },
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const agentsPoll = usePolling(fetchAgents, 20000);
|
|
36
|
+
const agents = Array.isArray(agentsPoll.data?.agents) ? agentsPoll.data.agents : [];
|
|
37
|
+
const agentNameById = useMemo(
|
|
38
|
+
() =>
|
|
39
|
+
new Map(
|
|
40
|
+
agents.map((agent) => [
|
|
41
|
+
String(agent?.id || "").trim(),
|
|
42
|
+
String(agent?.name || "").trim() || formatAgentFallbackName(agent?.id),
|
|
43
|
+
]),
|
|
44
|
+
),
|
|
45
|
+
[agents],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const selectedWebhook = detailPoll.data;
|
|
49
|
+
const selectedWebhookManaged = Boolean(selectedWebhook?.managed);
|
|
50
|
+
const selectedDeliveryAgentId =
|
|
51
|
+
String(selectedWebhook?.agentId || "main").trim() || "main";
|
|
52
|
+
const selectedDeliveryAgentName =
|
|
53
|
+
agentNameById.get(selectedDeliveryAgentId) ||
|
|
54
|
+
formatAgentFallbackName(selectedDeliveryAgentId);
|
|
55
|
+
const selectedDeliveryChannel =
|
|
56
|
+
String(selectedWebhook?.channel || "last").trim() || "last";
|
|
57
|
+
|
|
58
|
+
const webhookUrl = selectedWebhook?.fullUrl || `.../hooks/${selectedHookName}`;
|
|
59
|
+
const oauthCallbackUrl = String(selectedWebhook?.oauthCallbackUrl || "").trim();
|
|
60
|
+
const hasOauthCallback = !!oauthCallbackUrl;
|
|
61
|
+
const webhookUrlWithQueryToken =
|
|
62
|
+
selectedWebhook?.queryStringUrl ||
|
|
63
|
+
`${webhookUrl}${webhookUrl.includes("?") ? "&" : "?"}token=<WEBHOOK_TOKEN>`;
|
|
64
|
+
|
|
65
|
+
const derivedTokenFromQuery = useMemo(() => {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = new URL(webhookUrlWithQueryToken);
|
|
68
|
+
return String(parsed.searchParams.get("token") || "").trim();
|
|
69
|
+
} catch {
|
|
70
|
+
return "";
|
|
71
|
+
}
|
|
72
|
+
}, [webhookUrlWithQueryToken]);
|
|
73
|
+
|
|
74
|
+
const authHeaderValue =
|
|
75
|
+
selectedWebhook?.authHeaderValue ||
|
|
76
|
+
(derivedTokenFromQuery
|
|
77
|
+
? `Authorization: Bearer ${derivedTokenFromQuery}`
|
|
78
|
+
: "Authorization: Bearer <WEBHOOK_TOKEN>");
|
|
79
|
+
const bearerTokenValue = authHeaderValue.startsWith("Authorization: ")
|
|
80
|
+
? authHeaderValue.slice("Authorization: ".length)
|
|
81
|
+
: authHeaderValue;
|
|
82
|
+
|
|
83
|
+
const webhookTestPayload = useMemo(() => {
|
|
84
|
+
if (
|
|
85
|
+
String(selectedHookName || "")
|
|
86
|
+
.trim()
|
|
87
|
+
.toLowerCase() === "gmail"
|
|
88
|
+
) {
|
|
89
|
+
return {
|
|
90
|
+
payload: {
|
|
91
|
+
account: "test@gmail.com",
|
|
92
|
+
messages: [
|
|
93
|
+
{
|
|
94
|
+
id: "test-message-1",
|
|
95
|
+
from: "alerts@example.com",
|
|
96
|
+
to: ["test@gmail.com"],
|
|
97
|
+
subject: "Test Gmail webhook event",
|
|
98
|
+
snippet:
|
|
99
|
+
"This is a simulated Gmail message payload for webhook testing.",
|
|
100
|
+
receivedAt: new Date().toISOString(),
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
source: "manual-test",
|
|
108
|
+
message: `This is a test of the ${selectedHookName || "webhook"} webhook.`,
|
|
109
|
+
};
|
|
110
|
+
}, [selectedHookName]);
|
|
111
|
+
|
|
112
|
+
const webhookTestPayloadJson = JSON.stringify(webhookTestPayload);
|
|
113
|
+
const curlCommandHeaders =
|
|
114
|
+
`curl -X POST "${webhookUrl}" ` +
|
|
115
|
+
`-H "Content-Type: application/json" ` +
|
|
116
|
+
`-H "${authHeaderValue}" ` +
|
|
117
|
+
`-d '${webhookTestPayloadJson}'`;
|
|
118
|
+
const curlCommandQuery =
|
|
119
|
+
`curl -X POST "${webhookUrlWithQueryToken}" ` +
|
|
120
|
+
`-H "Content-Type: application/json" ` +
|
|
121
|
+
`-d '${webhookTestPayloadJson}'`;
|
|
122
|
+
|
|
123
|
+
const effectiveAuthMode = selectedWebhookManaged ? "headers" : authMode;
|
|
124
|
+
const activeCurlCommand =
|
|
125
|
+
effectiveAuthMode === "query" ? curlCommandQuery : curlCommandHeaders;
|
|
126
|
+
|
|
127
|
+
const refreshDetail = useCallback(() => {
|
|
128
|
+
detailPoll.refresh();
|
|
129
|
+
agentsPoll.refresh();
|
|
130
|
+
}, [agentsPoll.refresh, detailPoll.refresh]);
|
|
131
|
+
|
|
132
|
+
const handleSendTestWebhook = useCallback(async () => {
|
|
133
|
+
if (!selectedHookName || sendingTestWebhook) return;
|
|
134
|
+
setSendingTestWebhook(true);
|
|
135
|
+
const requestUrl =
|
|
136
|
+
effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
|
|
137
|
+
const headers = { "Content-Type": "application/json" };
|
|
138
|
+
if (effectiveAuthMode === "headers") {
|
|
139
|
+
headers.Authorization = bearerTokenValue;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(requestUrl, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers,
|
|
145
|
+
body: webhookTestPayloadJson,
|
|
146
|
+
});
|
|
147
|
+
const bodyText = await response.text();
|
|
148
|
+
let body = null;
|
|
149
|
+
try {
|
|
150
|
+
body = bodyText ? JSON.parse(bodyText) : null;
|
|
151
|
+
} catch {
|
|
152
|
+
body = null;
|
|
153
|
+
}
|
|
154
|
+
const errorMessage =
|
|
155
|
+
body?.ok === false
|
|
156
|
+
? body?.error || "Webhook rejected"
|
|
157
|
+
: !response.ok
|
|
158
|
+
? body?.error || bodyText || `HTTP ${response.status}`
|
|
159
|
+
: "";
|
|
160
|
+
if (errorMessage) {
|
|
161
|
+
showToast(`Test webhook failed: ${errorMessage}`, "error");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
showToast("Test webhook sent", "success");
|
|
165
|
+
} catch (err) {
|
|
166
|
+
showToast(err.message || "Could not send test webhook", "error");
|
|
167
|
+
} finally {
|
|
168
|
+
setSendingTestWebhook(false);
|
|
169
|
+
}
|
|
170
|
+
}, [
|
|
171
|
+
bearerTokenValue,
|
|
172
|
+
effectiveAuthMode,
|
|
173
|
+
selectedHookName,
|
|
174
|
+
sendingTestWebhook,
|
|
175
|
+
webhookTestPayloadJson,
|
|
176
|
+
webhookUrl,
|
|
177
|
+
webhookUrlWithQueryToken,
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
const handleDeleteConfirmed = useCallback(async () => {
|
|
181
|
+
if (!selectedHookName || deleting) return;
|
|
182
|
+
setDeleting(true);
|
|
183
|
+
try {
|
|
184
|
+
const data = await deleteWebhook(selectedHookName, {
|
|
185
|
+
deleteTransformDir,
|
|
186
|
+
});
|
|
187
|
+
if (data.restartRequired) onRestartRequired(true);
|
|
188
|
+
onBackToList();
|
|
189
|
+
setShowDeleteConfirm(false);
|
|
190
|
+
setDeleteTransformDir(true);
|
|
191
|
+
showToast("Webhook removed", "success");
|
|
192
|
+
if (data.deletedTransformDir) {
|
|
193
|
+
showToast("Transform directory deleted", "success");
|
|
194
|
+
}
|
|
195
|
+
if (data.syncWarning) {
|
|
196
|
+
showToast(`Deleted, but git-sync failed: ${data.syncWarning}`, "warning");
|
|
197
|
+
}
|
|
198
|
+
refreshDetail();
|
|
199
|
+
} catch (err) {
|
|
200
|
+
showToast(err.message || "Could not delete webhook", "error");
|
|
201
|
+
} finally {
|
|
202
|
+
setDeleting(false);
|
|
203
|
+
}
|
|
204
|
+
}, [
|
|
205
|
+
deleteTransformDir,
|
|
206
|
+
deleting,
|
|
207
|
+
onBackToList,
|
|
208
|
+
onRestartRequired,
|
|
209
|
+
refreshDetail,
|
|
210
|
+
selectedHookName,
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
const handleRotateOauthCallback = useCallback(async () => {
|
|
214
|
+
if (!selectedHookName || rotatingOauthCallback) return;
|
|
215
|
+
setRotatingOauthCallback(true);
|
|
216
|
+
try {
|
|
217
|
+
await rotateWebhookOauthCallback(selectedHookName);
|
|
218
|
+
showToast("OAuth callback rotated", "success");
|
|
219
|
+
setShowRotateOauthConfirm(false);
|
|
220
|
+
refreshDetail();
|
|
221
|
+
} catch (err) {
|
|
222
|
+
showToast(err.message || "Could not rotate OAuth callback", "error");
|
|
223
|
+
} finally {
|
|
224
|
+
setRotatingOauthCallback(false);
|
|
225
|
+
}
|
|
226
|
+
}, [refreshDetail, rotatingOauthCallback, selectedHookName]);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
state: {
|
|
230
|
+
authMode,
|
|
231
|
+
selectedWebhook,
|
|
232
|
+
selectedWebhookManaged,
|
|
233
|
+
selectedDeliveryAgentName,
|
|
234
|
+
selectedDeliveryChannel,
|
|
235
|
+
webhookUrl,
|
|
236
|
+
oauthCallbackUrl,
|
|
237
|
+
hasOauthCallback,
|
|
238
|
+
webhookUrlWithQueryToken,
|
|
239
|
+
authHeaderValue,
|
|
240
|
+
bearerTokenValue,
|
|
241
|
+
effectiveAuthMode,
|
|
242
|
+
activeCurlCommand,
|
|
243
|
+
deleting,
|
|
244
|
+
showDeleteConfirm,
|
|
245
|
+
deleteTransformDir,
|
|
246
|
+
rotatingOauthCallback,
|
|
247
|
+
showRotateOauthConfirm,
|
|
248
|
+
sendingTestWebhook,
|
|
249
|
+
},
|
|
250
|
+
actions: {
|
|
251
|
+
refreshDetail,
|
|
252
|
+
setAuthMode,
|
|
253
|
+
setShowDeleteConfirm,
|
|
254
|
+
setDeleteTransformDir,
|
|
255
|
+
setShowRotateOauthConfirm,
|
|
256
|
+
handleDeleteConfirmed,
|
|
257
|
+
handleRotateOauthCallback,
|
|
258
|
+
handleSendTestWebhook,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
};
|