@andreprado/agentkit 0.1.0-alpha.9 → 0.1.0
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/README.md +18 -1
- package/docs/guides/add-channel.md +251 -7
- package/docs/guides/add-knowledge.md +10 -0
- package/docs/guides/add-managed-composio.md +165 -0
- package/docs/guides/add-tool.md +10 -3
- package/docs/guides/channel-security.md +162 -32
- package/docs/guides/connect-discord.md +178 -0
- package/docs/guides/connect-slack.md +126 -0
- package/docs/guides/connect-telegram.md +61 -1
- package/docs/guides/connect-whatsapp-evolution.md +121 -0
- package/docs/guides/connect-whatsapp-uazapi.md +139 -0
- package/docs/guides/connect-whatsapp-zapster.md +119 -16
- package/docs/guides/create-agent.md +31 -4
- package/docs/guides/debug-channel.md +159 -0
- package/docs/guides/improve-from-production.md +151 -0
- package/docs/guides/prepare-deploy.md +32 -14
- package/docs/guides/replay-production-traces.md +72 -0
- package/docs/guides/run-evals.md +95 -25
- package/docs/guides/security-rules.md +9 -5
- package/docs/guides/send-feedback.md +135 -0
- package/docs/guides/use-jev.md +67 -0
- package/docs/guides/use-provider.md +70 -3
- package/docs/llms-full.txt +295 -25
- package/docs/llms.txt +54 -7
- package/package.json +3 -7
- package/src/cli/args.ts +23 -2
- package/src/cli/cloud-client.ts +121 -9
- package/src/cli/commands/channels.ts +856 -36
- package/src/cli/commands/feedback.ts +438 -0
- package/src/cli/commands/provider.ts +47 -0
- package/src/cli/commands/transcribe.ts +171 -0
- package/src/cli/deploy-chat-ui.ts +232 -18
- package/src/cli/deploy-readiness.ts +227 -14
- package/src/cli/help.ts +67 -9
- package/src/cli/index.ts +740 -35
- package/src/cli/new-command.ts +41 -0
- package/src/cloud/client.ts +4 -3
- package/src/cloud/contracts.ts +1 -1
- package/src/create-project.ts +18 -35
- package/src/index.ts +565 -11
- package/src/providers/codex-auth.ts +111 -0
- package/src/providers/pi.ts +88 -19
- package/src/providers/test.ts +36 -0
- package/src/providers/types.ts +8 -0
- package/src/runtime/channel-test-harness.ts +21 -1
- package/src/runtime/channels/discord.ts +896 -0
- package/src/runtime/channels/generic-webhook.ts +974 -0
- package/src/runtime/channels/slack.ts +646 -0
- package/src/runtime/channels/telegram.ts +343 -12
- package/src/runtime/channels/whatsapp-evolution.ts +1357 -0
- package/src/runtime/channels/whatsapp-meta.ts +9 -0
- package/src/runtime/channels/whatsapp-uazapi.ts +1323 -0
- package/src/runtime/channels/whatsapp-zapster.ts +674 -40
- package/src/runtime/channels.ts +83 -3
- package/src/runtime/chat.ts +70 -44
- package/src/runtime/config.ts +489 -20
- package/src/runtime/core/manifest.ts +75 -5
- package/src/runtime/core/targets.ts +5 -5
- package/src/runtime/deploy-readiness.ts +34 -4
- package/src/runtime/dev-server.ts +639 -39
- package/src/runtime/env.ts +8 -3
- package/src/runtime/evals.ts +445 -74
- package/src/runtime/improve.ts +868 -0
- package/src/runtime/inspect.ts +173 -4
- package/src/runtime/integrations/composio.ts +425 -0
- package/src/runtime/knowledge/retrieve.ts +25 -5
- package/src/runtime/knowledge/schema.ts +45 -1
- package/src/runtime/prompt-context.ts +141 -0
- package/src/runtime/runtime-contract.ts +71 -7
- package/src/runtime/skills.ts +95 -0
- package/src/runtime/targets/cloudflare/build.ts +1010 -208
- package/src/runtime/targets/container/server.ts +1 -1
- package/src/runtime/targets/vps/deploy.ts +26 -9
- package/src/runtime/tool-runner.ts +9 -1
- package/src/runtime/tools.ts +26 -2
- package/src/runtime/transcription.ts +483 -0
- package/src/storage/sqlite.ts +7 -2
- package/src/templates/blank.ts +37 -9
- package/src/templates/dentista.ts +40 -14
- package/src/templates/skills/agentkit-build-agent/SKILL.md +34 -5
- package/src/templates/skills/agentkit-build-agent/templates/appointment-intake.instructions.md +2 -1
- package/src/templates/skills/agentkit-capsule/SKILL.md +32 -3
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +2 -2
- package/src/templates/skills/agentkit-channels/SKILL.md +66 -1
- package/src/templates/skills/agentkit-channels/references/channel-buffering.md +8 -1
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +28 -3
- package/src/templates/skills/agentkit-channels/references/discord.md +93 -0
- package/src/templates/skills/agentkit-channels/references/slack.md +56 -0
- package/src/templates/skills/agentkit-channels/references/telegram.md +34 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +54 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +42 -8
- package/src/templates/skills/agentkit-database/SKILL.md +11 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +9 -1
- package/src/templates/skills/agentkit-evals/SKILL.md +77 -13
- package/src/templates/skills/agentkit-evals/templates/multi-turn.eval.md +13 -6
- package/src/templates/skills/agentkit-evals/templates/no-leak.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/smoke.eval.md +8 -4
- package/src/templates/skills/agentkit-evals/templates/tool-call.eval.md +16 -7
- package/src/templates/skills/agentkit-improve/SKILL.md +96 -0
- package/src/templates/skills/agentkit-improve/references/replay-side-effects.md +18 -0
- package/src/templates/skills/agentkit-improve/references/trace-packets.md +22 -0
- package/src/templates/skills/agentkit-improve/templates/regression.eval.md +18 -0
- package/src/templates/skills/agentkit-integrations/SKILL.md +98 -0
- package/src/templates/skills/agentkit-knowledge/SKILL.md +4 -1
- package/src/templates/skills/agentkit-prompts/SKILL.md +3 -1
- package/src/templates/skills/agentkit-provider/SKILL.md +29 -4
- package/src/templates/skills/agentkit-security/SKILL.md +5 -2
- package/src/templates/skills/agentkit-tools/SKILL.md +8 -1
- package/src/templates/skills/agentkit-tools/examples/eval-safe-external-action.tool.md +8 -8
- package/src/templates/skills/agentkit-tools/examples/jev-service-fit.tool.md +110 -0
- package/src/templates/skills/agentkit-troubleshooting/SKILL.md +25 -1
- package/src/templates/support.ts +42 -12
- package/docs/guides/agentkit-skills-architecture.md +0 -471
- package/docs/guides/channels-implementation-map.md +0 -243
- package/docs/guides/channels-production-handoff.md +0 -101
- package/docs/portable-deploy-release-checklist.md +0 -41
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
3
|
+
import { request as httpsRequest } from "node:https";
|
|
4
|
+
import type { LookupFunction } from "node:net";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
|
|
7
|
+
import type { GenericWebhookOutputChannelConfig } from "../../index";
|
|
8
|
+
import type { ChannelAdapter, ChannelSendInput, ChannelSendResult, RawWebhookEvent } from "../channels";
|
|
9
|
+
import { AgentKitError } from "../errors";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SECRET_NAME = "AGENTKIT_WEBHOOK_SECRET";
|
|
12
|
+
const DEFAULT_OUTPUT_HMAC_HEADER = "X-AgentKit-Webhook-Signature";
|
|
13
|
+
const MAX_OUTPUT_RESPONSE_BYTES = 64_000;
|
|
14
|
+
|
|
15
|
+
export const genericWebhookChannelAdapter: ChannelAdapter = {
|
|
16
|
+
type: "webhook",
|
|
17
|
+
provider: "generic",
|
|
18
|
+
requiredSecrets: [DEFAULT_SECRET_NAME],
|
|
19
|
+
verifyWebhook(input) {
|
|
20
|
+
const expected = input.secrets[DEFAULT_SECRET_NAME];
|
|
21
|
+
|
|
22
|
+
if (!expected) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
code: "channel_secret_missing",
|
|
26
|
+
message: `Secret ${DEFAULT_SECRET_NAME} is not set for this channel.`,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const signature = input.headers.get("x-agentkit-webhook-signature") ?? input.headers.get("x-agentkit-signature");
|
|
31
|
+
|
|
32
|
+
if (signature) {
|
|
33
|
+
const expectedSignature = `sha256=${createHmac("sha256", expected).update(input.rawBody).digest("hex")}`;
|
|
34
|
+
|
|
35
|
+
if (constantTimeEqual(signature.trim(), expectedSignature)) {
|
|
36
|
+
return { ok: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
code: "channel_signature_invalid",
|
|
42
|
+
message: "Generic webhook HMAC signature did not match.",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const bearer = readBearerToken(input.headers.get("authorization"));
|
|
47
|
+
const headerToken =
|
|
48
|
+
input.headers.get("x-agentkit-webhook-secret") ??
|
|
49
|
+
input.headers.get("x-agentkit-channel-token") ??
|
|
50
|
+
input.headers.get("x-agentkit-webhook-token");
|
|
51
|
+
|
|
52
|
+
if (constantTimeEqual(bearer, expected) || constantTimeEqual(headerToken ?? "", expected)) {
|
|
53
|
+
return { ok: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
code: "channel_signature_invalid",
|
|
59
|
+
message: "Generic webhook token did not match.",
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
normalizeInbound(input) {
|
|
63
|
+
const body = parseJsonObject(input);
|
|
64
|
+
const eventId = readFirstScalar(body, ["event_id", "eventId", "id"]) ?? readNestedMessageId(body);
|
|
65
|
+
|
|
66
|
+
if (!eventId) {
|
|
67
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload is missing event_id.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const text = readMessageText(body);
|
|
71
|
+
const identitySource =
|
|
72
|
+
readFirstScalar(body, [
|
|
73
|
+
"external_id",
|
|
74
|
+
"externalId",
|
|
75
|
+
"user_id",
|
|
76
|
+
"userId",
|
|
77
|
+
"contact_id",
|
|
78
|
+
"contactId",
|
|
79
|
+
"conversation_id",
|
|
80
|
+
"conversationId",
|
|
81
|
+
"client_id",
|
|
82
|
+
"clientId",
|
|
83
|
+
"visitor_id",
|
|
84
|
+
"visitorId",
|
|
85
|
+
"source_id",
|
|
86
|
+
"sourceId",
|
|
87
|
+
]) ?? `event:${eventId}`;
|
|
88
|
+
const identityKey = `webhook:generic:identity:${sha256(identitySource).slice(0, 32)}`;
|
|
89
|
+
const base = {
|
|
90
|
+
type: "webhook" as const,
|
|
91
|
+
provider: "generic" as const,
|
|
92
|
+
providerEventId: eventId,
|
|
93
|
+
dedupeKey: `webhook:generic:${input.channelId}:${eventId}`,
|
|
94
|
+
externalIdentity: {
|
|
95
|
+
key: identityKey,
|
|
96
|
+
redactedDisplay: identityKey,
|
|
97
|
+
},
|
|
98
|
+
receivedAt: input.receivedAt,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (!text) {
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
...base,
|
|
105
|
+
kind: "unsupported" as const,
|
|
106
|
+
unsupportedReason: "unsupported_message_type:missing_text",
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return [
|
|
112
|
+
{
|
|
113
|
+
...base,
|
|
114
|
+
kind: "message" as const,
|
|
115
|
+
message: {
|
|
116
|
+
role: "user" as const,
|
|
117
|
+
contentType: "text" as const,
|
|
118
|
+
content: text,
|
|
119
|
+
providerMessageId: readNestedMessageId(body) ?? eventId,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
},
|
|
124
|
+
async sendMessage(input) {
|
|
125
|
+
if (isGenericWebhookOutputChannel(input.channel)) {
|
|
126
|
+
return sendGenericWebhookOutputMessage(input, input.channel);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
status: "stubbed",
|
|
132
|
+
providerRequestId: buildGenericWebhookProviderRequestId(input),
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
getStatus() {
|
|
136
|
+
return {
|
|
137
|
+
status: "connected",
|
|
138
|
+
checkedAt: new Date().toISOString(),
|
|
139
|
+
providerMetadata: {
|
|
140
|
+
outbound: "stubbed",
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type GenericWebhookOutputRequest = {
|
|
147
|
+
method: "POST";
|
|
148
|
+
url: string;
|
|
149
|
+
headers: Record<string, string>;
|
|
150
|
+
body: Record<string, unknown>;
|
|
151
|
+
idempotencyKey: string;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
type GenericWebhookOutputFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
155
|
+
type GenericWebhookOutputResolver = (hostname: string) => Promise<string[]>;
|
|
156
|
+
type GenericWebhookOutputResolvedUrl = {
|
|
157
|
+
href: string;
|
|
158
|
+
address: string;
|
|
159
|
+
family: 4 | 6;
|
|
160
|
+
};
|
|
161
|
+
type GenericWebhookOutputRequestInit = {
|
|
162
|
+
method: "POST";
|
|
163
|
+
headers: Record<string, string>;
|
|
164
|
+
body: string;
|
|
165
|
+
redirect: "manual";
|
|
166
|
+
signal: AbortSignal;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export function buildGenericWebhookOutputRequest(
|
|
170
|
+
input: ChannelSendInput,
|
|
171
|
+
channel: GenericWebhookOutputChannelConfig,
|
|
172
|
+
): GenericWebhookOutputRequest {
|
|
173
|
+
if (input.message.contentType !== "text") {
|
|
174
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output supports only text channel messages.");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const rawUrl = input.secrets[channel.urlSecret];
|
|
178
|
+
|
|
179
|
+
if (!rawUrl) {
|
|
180
|
+
throw new AgentKitError("channel_secret_missing", `Secret ${channel.urlSecret} is missing.`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const idempotencyKey = buildGenericWebhookOutputRequestId(input);
|
|
184
|
+
const body = {
|
|
185
|
+
event_id: idempotencyKey,
|
|
186
|
+
conversation_id: input.conversationId,
|
|
187
|
+
channel_id: input.channelId,
|
|
188
|
+
external_identity: input.externalIdentity.key,
|
|
189
|
+
message: {
|
|
190
|
+
role: "assistant",
|
|
191
|
+
content_type: "text",
|
|
192
|
+
content: input.message.content,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
const headers: Record<string, string> = {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
Accept: "application/json",
|
|
198
|
+
"X-AgentKit-Event-ID": idempotencyKey,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
applyGenericWebhookOutputAuthHeaders({
|
|
202
|
+
auth: channel.auth,
|
|
203
|
+
secrets: input.secrets,
|
|
204
|
+
headers,
|
|
205
|
+
body: JSON.stringify(body),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
method: "POST",
|
|
210
|
+
url: validateGenericWebhookOutputUrl(rawUrl),
|
|
211
|
+
headers,
|
|
212
|
+
body,
|
|
213
|
+
idempotencyKey,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function sendGenericWebhookOutputMessage(
|
|
218
|
+
input: ChannelSendInput,
|
|
219
|
+
channel: GenericWebhookOutputChannelConfig,
|
|
220
|
+
fetcher?: GenericWebhookOutputFetch,
|
|
221
|
+
resolver: GenericWebhookOutputResolver = resolveGenericWebhookOutputAddresses,
|
|
222
|
+
): Promise<ChannelSendResult> {
|
|
223
|
+
let request: GenericWebhookOutputRequest;
|
|
224
|
+
let resolvedUrl: GenericWebhookOutputResolvedUrl;
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
request = buildGenericWebhookOutputRequest(input, channel);
|
|
228
|
+
|
|
229
|
+
if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
|
|
230
|
+
return {
|
|
231
|
+
ok: true,
|
|
232
|
+
status: "stubbed",
|
|
233
|
+
providerRequestId: request.idempotencyKey,
|
|
234
|
+
providerMetadata: redactedGenericWebhookOutputRequest(request),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
resolvedUrl = await validateGenericWebhookOutputResolvedUrl(request.url, resolver);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return genericWebhookOutputFailureFromError(error);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let response: Response;
|
|
244
|
+
const requestInit: GenericWebhookOutputRequestInit = {
|
|
245
|
+
method: request.method,
|
|
246
|
+
headers: request.headers,
|
|
247
|
+
body: JSON.stringify(request.body),
|
|
248
|
+
redirect: "manual",
|
|
249
|
+
signal: AbortSignal.timeout(15_000),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
response = fetcher
|
|
254
|
+
? await fetcher(resolvedUrl.href, requestInit)
|
|
255
|
+
: await fetchGenericWebhookOutputResponse(resolvedUrl, requestInit);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof AgentKitError) {
|
|
258
|
+
return {
|
|
259
|
+
...genericWebhookOutputFailureFromError(error),
|
|
260
|
+
providerRequestId: request.idempotencyKey,
|
|
261
|
+
providerMetadata: redactedGenericWebhookOutputRequest(request),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
retryable: true,
|
|
268
|
+
code: "channel_provider_unavailable",
|
|
269
|
+
message: `Generic webhook output request failed before a response: ${redactGenericWebhookOutputText(
|
|
270
|
+
error instanceof Error ? error.message : String(error),
|
|
271
|
+
input.secrets,
|
|
272
|
+
)}`,
|
|
273
|
+
providerRequestId: request.idempotencyKey,
|
|
274
|
+
providerMetadata: redactedGenericWebhookOutputRequest(request),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const payload = await readGenericWebhookOutputResponseBody(response);
|
|
279
|
+
const providerMessageId = readGenericWebhookOutputProviderMessageId(payload) ?? request.idempotencyKey;
|
|
280
|
+
|
|
281
|
+
if (!response.ok) {
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
285
|
+
code: "channel_send_failed",
|
|
286
|
+
message: redactGenericWebhookOutputText(readGenericWebhookOutputErrorMessage(payload, response.status), input.secrets),
|
|
287
|
+
providerRequestId: request.idempotencyKey,
|
|
288
|
+
providerMetadata: {
|
|
289
|
+
...redactedGenericWebhookOutputRequest(request),
|
|
290
|
+
status: response.status,
|
|
291
|
+
body: redactGenericWebhookOutputValue(payload, input.secrets),
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
ok: true,
|
|
298
|
+
status: "sent",
|
|
299
|
+
providerRequestId: request.idempotencyKey,
|
|
300
|
+
providerMessageId,
|
|
301
|
+
providerMetadata: {
|
|
302
|
+
...redactedGenericWebhookOutputRequest(request),
|
|
303
|
+
status: response.status,
|
|
304
|
+
body: redactGenericWebhookOutputValue(payload, input.secrets),
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function buildGenericWebhookProviderRequestId(input: ChannelSendInput): string {
|
|
310
|
+
return `webhook:generic:${input.channelId}:${input.conversationId}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function buildGenericWebhookOutputRequestId(input: ChannelSendInput): string {
|
|
314
|
+
return `webhook:generic:${input.channelId}:${input.deliveryId ?? input.jobId ?? randomUUID()}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isGenericWebhookOutputChannel(value: unknown): value is GenericWebhookOutputChannelConfig {
|
|
318
|
+
return (
|
|
319
|
+
isRecord(value) &&
|
|
320
|
+
value.type === "webhook" &&
|
|
321
|
+
value.provider === "generic" &&
|
|
322
|
+
value.direction === "outbound" &&
|
|
323
|
+
typeof value.urlSecret === "string" &&
|
|
324
|
+
isRecord(value.auth)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function applyGenericWebhookOutputAuthHeaders(input: {
|
|
329
|
+
auth: GenericWebhookOutputChannelConfig["auth"];
|
|
330
|
+
secrets: Record<string, string>;
|
|
331
|
+
headers: Record<string, string>;
|
|
332
|
+
body: string;
|
|
333
|
+
}): void {
|
|
334
|
+
if (input.auth.type === "none") {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (input.auth.type === "bearer") {
|
|
339
|
+
const token = input.secrets[input.auth.tokenSecret];
|
|
340
|
+
|
|
341
|
+
if (!token) {
|
|
342
|
+
throw new AgentKitError("channel_secret_missing", `Secret ${input.auth.tokenSecret} is missing.`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
input.headers.Authorization = `Bearer ${token}`;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (input.auth.type === "header") {
|
|
350
|
+
const value = input.secrets[input.auth.valueSecret];
|
|
351
|
+
|
|
352
|
+
if (!value) {
|
|
353
|
+
throw new AgentKitError("channel_secret_missing", `Secret ${input.auth.valueSecret} is missing.`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
input.headers[input.auth.headerName] = value;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const secret = input.secrets[input.auth.secret];
|
|
361
|
+
|
|
362
|
+
if (!secret) {
|
|
363
|
+
throw new AgentKitError("channel_secret_missing", `Secret ${input.auth.secret} is missing.`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
input.headers[input.auth.headerName ?? DEFAULT_OUTPUT_HMAC_HEADER] =
|
|
367
|
+
`sha256=${createHmac("sha256", secret).update(input.body).digest("hex")}`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function genericWebhookOutputFailureFromError(error: unknown): ChannelSendResult {
|
|
371
|
+
if (error instanceof AgentKitError) {
|
|
372
|
+
if (error.code === "channel_provider_unavailable") {
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
retryable: true,
|
|
376
|
+
code: "channel_provider_unavailable",
|
|
377
|
+
message: error.message,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
retryable: false,
|
|
384
|
+
code: error.code === "channel_secret_missing" ? "channel_secret_missing" : "channel_send_failed",
|
|
385
|
+
message: error.message,
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
ok: false,
|
|
391
|
+
retryable: false,
|
|
392
|
+
code: "channel_send_failed",
|
|
393
|
+
message: error instanceof Error ? error.message : String(error),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function validateGenericWebhookOutputUrl(rawUrl: string): string {
|
|
398
|
+
let url: URL;
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
url = new URL(rawUrl);
|
|
402
|
+
} catch {
|
|
403
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL must be a valid HTTPS URL.");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (url.protocol !== "https:") {
|
|
407
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL must use HTTPS.");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (url.username || url.password) {
|
|
411
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL must not include credentials.");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (isBlockedGenericWebhookOutputHost(url.hostname)) {
|
|
415
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL host is not allowed.");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return url.href;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function validateGenericWebhookOutputResolvedUrl(
|
|
422
|
+
rawUrl: string,
|
|
423
|
+
resolver: GenericWebhookOutputResolver,
|
|
424
|
+
): Promise<GenericWebhookOutputResolvedUrl> {
|
|
425
|
+
const href = validateGenericWebhookOutputUrl(rawUrl);
|
|
426
|
+
const url = new URL(href);
|
|
427
|
+
let addresses: string[];
|
|
428
|
+
|
|
429
|
+
try {
|
|
430
|
+
addresses = await resolver(url.hostname);
|
|
431
|
+
} catch (error) {
|
|
432
|
+
throw new AgentKitError(
|
|
433
|
+
"channel_provider_unavailable",
|
|
434
|
+
`Generic webhook output URL host could not be resolved: ${error instanceof Error ? error.message : String(error)}`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (addresses.length === 0) {
|
|
439
|
+
throw new AgentKitError("channel_provider_unavailable", "Generic webhook output URL host did not resolve.");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
for (const address of addresses) {
|
|
443
|
+
if (isBlockedGenericWebhookOutputHost(address)) {
|
|
444
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL host resolved to a blocked address.");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const address = normalizeGenericWebhookOutputHost(addresses[0]);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
href,
|
|
452
|
+
address,
|
|
453
|
+
family: parseIpv4Octets(address) ? 4 : 6,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function fetchGenericWebhookOutputResponse(
|
|
458
|
+
resolvedUrl: GenericWebhookOutputResolvedUrl,
|
|
459
|
+
init: GenericWebhookOutputRequestInit,
|
|
460
|
+
): Promise<Response> {
|
|
461
|
+
const url = new URL(resolvedUrl.href);
|
|
462
|
+
const lookup: LookupFunction = (_hostname, _options, callback) => {
|
|
463
|
+
callback(null, resolvedUrl.address, resolvedUrl.family);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
return new Promise((resolve, reject) => {
|
|
467
|
+
const request = httpsRequest(
|
|
468
|
+
url,
|
|
469
|
+
{
|
|
470
|
+
method: init.method,
|
|
471
|
+
headers: init.headers,
|
|
472
|
+
lookup,
|
|
473
|
+
signal: init.signal,
|
|
474
|
+
},
|
|
475
|
+
(response) => {
|
|
476
|
+
try {
|
|
477
|
+
assertAllowedGenericWebhookOutputRemoteAddress(response.socket.remoteAddress);
|
|
478
|
+
} catch (error) {
|
|
479
|
+
response.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
480
|
+
reject(error);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
resolve(
|
|
485
|
+
new Response(Readable.toWeb(response) as unknown as ReadableStream<Uint8Array>, {
|
|
486
|
+
status: response.statusCode ?? 502,
|
|
487
|
+
headers: headersFromIncomingMessage(response.headers),
|
|
488
|
+
}),
|
|
489
|
+
);
|
|
490
|
+
},
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
request.on("socket", (socket) => {
|
|
494
|
+
const validateSocket = () => {
|
|
495
|
+
try {
|
|
496
|
+
assertAllowedGenericWebhookOutputRemoteAddress(socket.remoteAddress);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
request.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
socket.once("connect", validateSocket);
|
|
503
|
+
socket.once("secureConnect", validateSocket);
|
|
504
|
+
});
|
|
505
|
+
request.once("error", reject);
|
|
506
|
+
request.end(init.body);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function assertAllowedGenericWebhookOutputRemoteAddress(address: string | undefined): void {
|
|
511
|
+
if (!address || isBlockedGenericWebhookOutputHost(address)) {
|
|
512
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook output URL connected to a blocked address.");
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function headersFromIncomingMessage(headers: Record<string, string | string[] | number | undefined>): Headers {
|
|
517
|
+
const result = new Headers();
|
|
518
|
+
|
|
519
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
520
|
+
if (Array.isArray(value)) {
|
|
521
|
+
for (const item of value) {
|
|
522
|
+
result.append(key, item);
|
|
523
|
+
}
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (value !== undefined) {
|
|
528
|
+
result.set(key, String(value));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function resolveGenericWebhookOutputAddresses(hostname: string): Promise<string[]> {
|
|
536
|
+
const host = normalizeGenericWebhookOutputHost(hostname);
|
|
537
|
+
|
|
538
|
+
if (parseIpv4Octets(host) || parseIpv6Segments(host)) {
|
|
539
|
+
return [host];
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const records = await dnsLookup(host, {
|
|
543
|
+
all: true,
|
|
544
|
+
verbatim: true,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
return records.map((record) => record.address);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function isBlockedGenericWebhookOutputHost(hostname: string): boolean {
|
|
551
|
+
const host = normalizeGenericWebhookOutputHost(hostname);
|
|
552
|
+
|
|
553
|
+
if (
|
|
554
|
+
host === "localhost" ||
|
|
555
|
+
host.endsWith(".localhost") ||
|
|
556
|
+
host === "metadata.google.internal"
|
|
557
|
+
) {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const ipv4 = parseIpv4Octets(host);
|
|
562
|
+
|
|
563
|
+
if (ipv4) {
|
|
564
|
+
return isBlockedIpv4Octets(ipv4);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const ipv6 = parseIpv6Segments(host);
|
|
568
|
+
|
|
569
|
+
if (ipv6) {
|
|
570
|
+
return isBlockedIpv6Segments(ipv6);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function normalizeGenericWebhookOutputHost(hostname: string): string {
|
|
577
|
+
return hostname.toLowerCase().replace(/^\[/, "").replace(/\]$/, "").replace(/\.$/, "");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseIpv4Octets(host: string): number[] | undefined {
|
|
581
|
+
const parts = host.split(".");
|
|
582
|
+
|
|
583
|
+
if (parts.length !== 4) {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const octets = parts.map((part) => Number(part));
|
|
588
|
+
|
|
589
|
+
if (octets.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return octets;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function isBlockedIpv4Octets(parts: number[]): boolean {
|
|
597
|
+
const [a, b] = parts;
|
|
598
|
+
|
|
599
|
+
return (
|
|
600
|
+
a === 0 ||
|
|
601
|
+
a === 10 ||
|
|
602
|
+
a === 127 ||
|
|
603
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
604
|
+
(a === 169 && b === 254) ||
|
|
605
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
606
|
+
(a === 192 && b === 168) ||
|
|
607
|
+
(a === 198 && (b === 18 || b === 19)) ||
|
|
608
|
+
a >= 224
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function parseIpv6Segments(host: string): number[] | undefined {
|
|
613
|
+
if (!host.includes(":")) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (host === "::") {
|
|
618
|
+
return new Array(8).fill(0);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const [leftRaw, rightRaw, extra] = host.split("::");
|
|
622
|
+
|
|
623
|
+
if (extra !== undefined) {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const left = parseIpv6SegmentSide(leftRaw ?? "");
|
|
628
|
+
const right = parseIpv6SegmentSide(rightRaw ?? "");
|
|
629
|
+
|
|
630
|
+
if (!left || !right) {
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (rightRaw === undefined) {
|
|
635
|
+
return left.length === 8 ? left : undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const missing = 8 - left.length - right.length;
|
|
639
|
+
|
|
640
|
+
if (missing < 1) {
|
|
641
|
+
return undefined;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return [...left, ...new Array(missing).fill(0), ...right];
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function parseIpv6SegmentSide(value: string): number[] | undefined {
|
|
648
|
+
if (!value) {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const segments: number[] = [];
|
|
653
|
+
|
|
654
|
+
const parts = value.split(":");
|
|
655
|
+
|
|
656
|
+
for (const [index, part] of parts.entries()) {
|
|
657
|
+
if (part.includes(".")) {
|
|
658
|
+
if (index !== parts.length - 1) {
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const octets = parseIpv4Octets(part);
|
|
663
|
+
|
|
664
|
+
if (!octets) {
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
segments.push((octets[0] << 8) | octets[1], (octets[2] << 8) | octets[3]);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!/^[0-9a-f]{1,4}$/i.test(part)) {
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
segments.push(parseInt(part, 16));
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return segments;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function isBlockedIpv6Segments(parts: number[]): boolean {
|
|
683
|
+
if (parts.length !== 8) {
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const embeddedIpv4 = ipv4OctetsFromIpv6Segments(parts);
|
|
688
|
+
|
|
689
|
+
if (embeddedIpv4) {
|
|
690
|
+
return isBlockedIpv4Octets(embeddedIpv4);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const isAllZero = parts.every((part) => part === 0);
|
|
694
|
+
|
|
695
|
+
if (isAllZero || (parts.slice(0, 7).every((part) => part === 0) && parts[7] === 1)) {
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const first = parts[0];
|
|
700
|
+
|
|
701
|
+
return (
|
|
702
|
+
(first & 0xfe00) === 0xfc00 ||
|
|
703
|
+
(first & 0xffc0) === 0xfe80 ||
|
|
704
|
+
(first & 0xff00) === 0xff00
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function ipv4OctetsFromIpv6Segments(parts: number[]): number[] | undefined {
|
|
709
|
+
const trailingIpv4 = [
|
|
710
|
+
(parts[6] >> 8) & 0xff,
|
|
711
|
+
parts[6] & 0xff,
|
|
712
|
+
(parts[7] >> 8) & 0xff,
|
|
713
|
+
parts[7] & 0xff,
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
if (parts.slice(0, 5).every((part) => part === 0) && (parts[5] === 0xffff || parts[5] === 0)) {
|
|
717
|
+
return trailingIpv4;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (parts[0] === 0x64 && parts[1] === 0xff9b && parts.slice(2, 6).every((part) => part === 0)) {
|
|
721
|
+
return trailingIpv4;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (parts[0] === 0x2002) {
|
|
725
|
+
return [
|
|
726
|
+
(parts[1] >> 8) & 0xff,
|
|
727
|
+
parts[1] & 0xff,
|
|
728
|
+
(parts[2] >> 8) & 0xff,
|
|
729
|
+
parts[2] & 0xff,
|
|
730
|
+
];
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return undefined;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function redactedGenericWebhookOutputRequest(request: GenericWebhookOutputRequest): Record<string, unknown> {
|
|
737
|
+
return {
|
|
738
|
+
method: request.method,
|
|
739
|
+
url: redactGenericWebhookOutputUrl(request.url),
|
|
740
|
+
body: request.body,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function redactGenericWebhookOutputUrl(rawUrl: string): string {
|
|
745
|
+
try {
|
|
746
|
+
const url = new URL(rawUrl);
|
|
747
|
+
url.search = url.search ? "?<redacted>" : "";
|
|
748
|
+
return url.href;
|
|
749
|
+
} catch {
|
|
750
|
+
return "<redacted>";
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
async function readGenericWebhookOutputResponseBody(response: Response): Promise<unknown> {
|
|
755
|
+
const text = await readGenericWebhookOutputResponseText(response, MAX_OUTPUT_RESPONSE_BYTES);
|
|
756
|
+
|
|
757
|
+
if (!text) {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
return JSON.parse(text) as unknown;
|
|
763
|
+
} catch {
|
|
764
|
+
return text;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
async function readGenericWebhookOutputResponseText(response: Response, maxBytes: number): Promise<string> {
|
|
769
|
+
const reader = response.body?.getReader();
|
|
770
|
+
|
|
771
|
+
if (!reader) {
|
|
772
|
+
return "";
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const chunks: Uint8Array[] = [];
|
|
776
|
+
let totalBytes = 0;
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
while (true) {
|
|
780
|
+
const chunk = await reader.read();
|
|
781
|
+
|
|
782
|
+
if (chunk.done) {
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
totalBytes += chunk.value.byteLength;
|
|
787
|
+
|
|
788
|
+
if (totalBytes > maxBytes) {
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
chunks.push(chunk.value);
|
|
793
|
+
}
|
|
794
|
+
} finally {
|
|
795
|
+
reader.releaseLock();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
return new TextDecoder().decode(concatUint8Arrays(chunks));
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function concatUint8Arrays(chunks: Uint8Array[]): Uint8Array {
|
|
802
|
+
const totalBytes = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
|
|
803
|
+
const output = new Uint8Array(totalBytes);
|
|
804
|
+
let offset = 0;
|
|
805
|
+
|
|
806
|
+
for (const chunk of chunks) {
|
|
807
|
+
output.set(chunk, offset);
|
|
808
|
+
offset += chunk.byteLength;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return output;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function readGenericWebhookOutputProviderMessageId(payload: unknown): string | undefined {
|
|
815
|
+
if (!isRecord(payload)) {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return readFirstScalar(payload, ["message_id", "messageId", "event_id", "eventId", "id"]);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function readGenericWebhookOutputErrorMessage(payload: unknown, status: number): string {
|
|
823
|
+
if (isRecord(payload)) {
|
|
824
|
+
const error = payload.error;
|
|
825
|
+
|
|
826
|
+
if (typeof error === "string") {
|
|
827
|
+
return error;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (isRecord(error)) {
|
|
831
|
+
const message = readFirstScalar(error, ["message", "detail", "code"]);
|
|
832
|
+
|
|
833
|
+
if (message) {
|
|
834
|
+
return message;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
const message = readFirstScalar(payload, ["message", "detail", "code"]);
|
|
839
|
+
|
|
840
|
+
if (message) {
|
|
841
|
+
return message;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
if (typeof payload === "string" && payload.trim()) {
|
|
846
|
+
return payload.trim();
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return `Generic webhook output returned HTTP ${status}.`;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function redactGenericWebhookOutputText(value: string, secrets: Record<string, string>): string {
|
|
853
|
+
let redacted = value;
|
|
854
|
+
|
|
855
|
+
for (const secret of Object.values(secrets)) {
|
|
856
|
+
if (secret) {
|
|
857
|
+
redacted = redacted.split(secret).join("<redacted>");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return redacted;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function redactGenericWebhookOutputValue(value: unknown, secrets: Record<string, string>): unknown {
|
|
865
|
+
if (typeof value === "string") {
|
|
866
|
+
return redactGenericWebhookOutputText(value, secrets);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (Array.isArray(value)) {
|
|
870
|
+
return value.map((item) => redactGenericWebhookOutputValue(item, secrets));
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (isRecord(value)) {
|
|
874
|
+
const redacted: Record<string, unknown> = {};
|
|
875
|
+
|
|
876
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
877
|
+
redacted[key] = /secret|token|authorization|password|api[_-]?key/i.test(key)
|
|
878
|
+
? "<redacted>"
|
|
879
|
+
: redactGenericWebhookOutputValue(entry, secrets);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return redacted;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
return value;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function parseJsonObject(input: RawWebhookEvent): Record<string, unknown> {
|
|
889
|
+
const contentType = input.headers.get("content-type") ?? "";
|
|
890
|
+
|
|
891
|
+
if (contentType && !contentType.toLowerCase().includes("application/json")) {
|
|
892
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must use content-type application/json.");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
try {
|
|
896
|
+
const parsed = JSON.parse(input.rawBody) as unknown;
|
|
897
|
+
|
|
898
|
+
if (isRecord(parsed)) {
|
|
899
|
+
return parsed;
|
|
900
|
+
}
|
|
901
|
+
} catch (error) {
|
|
902
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must be valid JSON.", {
|
|
903
|
+
cause: error,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must be a JSON object.");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function readMessageText(body: Record<string, unknown>): string | undefined {
|
|
911
|
+
const message = body.message;
|
|
912
|
+
|
|
913
|
+
if (typeof message === "string") {
|
|
914
|
+
return normalizeNonEmpty(message);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (isRecord(message)) {
|
|
918
|
+
return readFirstScalar(message, ["text", "content", "body"]);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
return readFirstScalar(body, ["text", "content", "prompt", "input"]);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function readNestedMessageId(body: Record<string, unknown>): string | undefined {
|
|
925
|
+
const message = body.message;
|
|
926
|
+
return isRecord(message) ? readFirstScalar(message, ["id", "message_id", "messageId"]) : undefined;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function readFirstScalar(value: Record<string, unknown>, keys: string[]): string | undefined {
|
|
930
|
+
for (const key of keys) {
|
|
931
|
+
const field = value[key];
|
|
932
|
+
|
|
933
|
+
if (typeof field === "string") {
|
|
934
|
+
const normalized = normalizeNonEmpty(field);
|
|
935
|
+
|
|
936
|
+
if (normalized) {
|
|
937
|
+
return normalized;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (typeof field === "number" && Number.isFinite(field)) {
|
|
942
|
+
return String(field);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return undefined;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function normalizeNonEmpty(value: string): string | undefined {
|
|
950
|
+
const trimmed = value.trim();
|
|
951
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function readBearerToken(value: string | null): string {
|
|
955
|
+
if (!value?.startsWith("Bearer ")) {
|
|
956
|
+
return "";
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return value.slice("Bearer ".length).trim();
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function constantTimeEqual(left: string, right: string): boolean {
|
|
963
|
+
const leftDigest = createHash("sha256").update(left).digest();
|
|
964
|
+
const rightDigest = createHash("sha256").update(right).digest();
|
|
965
|
+
return timingSafeEqual(leftDigest, rightDigest) && left === right;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function sha256(value: string): string {
|
|
969
|
+
return createHash("sha256").update(value).digest("hex");
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
973
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
974
|
+
}
|