@andreprado/agentkit 0.1.0-alpha.20 → 0.1.0-alpha.22
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 +1 -0
- package/docs/guides/add-channel.md +75 -4
- package/docs/guides/add-managed-composio.md +3 -1
- package/docs/guides/add-tool.md +6 -3
- package/docs/guides/channel-security.md +32 -1
- package/docs/guides/connect-whatsapp-evolution.md +121 -0
- package/docs/guides/connect-whatsapp-uazapi.md +126 -0
- package/docs/guides/create-agent.md +6 -4
- package/docs/guides/debug-channel.md +3 -3
- package/docs/guides/prepare-deploy.md +1 -1
- package/docs/guides/run-evals.md +5 -1
- package/docs/guides/use-provider.md +26 -2
- package/docs/llms-full.txt +82 -9
- package/docs/llms.txt +9 -2
- package/package.json +1 -1
- package/src/cli/commands/channels.ts +285 -14
- package/src/cli/deploy-chat-ui.ts +86 -15
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +107 -8
- package/src/index.ts +42 -5
- package/src/providers/pi.ts +12 -2
- package/src/runtime/channel-test-harness.ts +14 -1
- package/src/runtime/channels/generic-webhook.ts +225 -0
- package/src/runtime/channels/whatsapp-evolution.ts +1357 -0
- package/src/runtime/channels/whatsapp-uazapi.ts +1327 -0
- package/src/runtime/channels.ts +1 -1
- package/src/runtime/chat.ts +7 -1
- package/src/runtime/config.ts +40 -5
- package/src/runtime/core/targets.ts +5 -5
- package/src/runtime/dev-server.ts +21 -5
- package/src/runtime/evals.ts +39 -24
- package/src/runtime/targets/cloudflare/build.ts +23 -3
- package/src/templates/blank.ts +29 -6
- package/src/templates/dentista.ts +22 -4
- package/src/templates/skills/agentkit-build-agent/SKILL.md +30 -2
- package/src/templates/skills/agentkit-capsule/SKILL.md +22 -1
- package/src/templates/skills/agentkit-capsule/references/docs-router.md +1 -1
- package/src/templates/skills/agentkit-channels/SKILL.md +25 -2
- package/src/templates/skills/agentkit-channels/references/channel-debugging.md +1 -1
- package/src/templates/skills/agentkit-channels/references/whatsapp-evolution.md +57 -0
- package/src/templates/skills/agentkit-channels/references/whatsapp-uazapi.md +61 -0
- package/src/templates/skills/agentkit-database/SKILL.md +11 -0
- package/src/templates/skills/agentkit-deploy/SKILL.md +2 -0
- package/src/templates/skills/agentkit-evals/SKILL.md +15 -0
- package/src/templates/skills/agentkit-improve/SKILL.md +14 -4
- package/src/templates/skills/agentkit-integrations/SKILL.md +22 -0
- package/src/templates/skills/agentkit-provider/SKILL.md +20 -2
- package/src/templates/skills/agentkit-tools/SKILL.md +2 -0
- package/src/templates/support.ts +30 -7
package/src/cli/index.ts
CHANGED
|
@@ -109,6 +109,8 @@ async function main() {
|
|
|
109
109
|
throw new Error("Usage: agentkit help [commands]");
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
warnIfUnsupportedNode();
|
|
113
|
+
|
|
112
114
|
if (args.command === "new") {
|
|
113
115
|
const [name] = args.positional;
|
|
114
116
|
const template = String(args.flags.template ?? "blank");
|
|
@@ -136,8 +138,8 @@ async function main() {
|
|
|
136
138
|
console.log(" npm install");
|
|
137
139
|
}
|
|
138
140
|
console.log(" open this folder in Codex or Claude Code");
|
|
139
|
-
console.log(
|
|
140
|
-
console.log(
|
|
141
|
+
console.log(' ask Codex: "Develop an appointment and intake agent for an ophthalmology office"');
|
|
142
|
+
console.log(" Codex starts at skills/agentkit-capsule/SKILL.md and treats your request as the brief");
|
|
141
143
|
console.log(' npm run chat -- --message "hello"');
|
|
142
144
|
console.log(" npm run dev");
|
|
143
145
|
console.log(" npm run agentkit -- deploy");
|
|
@@ -187,16 +189,25 @@ async function main() {
|
|
|
187
189
|
throw new Error("Usage: agentkit chat-ui --deploy [--port <number>] [--token-file <path>]");
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
const
|
|
192
|
+
const requestedPort = parsePort(args.flags.port, "agentkit chat-ui --deploy --port <number>");
|
|
193
|
+
const port = requestedPort ?? 4124;
|
|
191
194
|
const tokenFile = parseOptionalStringFlag(
|
|
192
195
|
args.flags["token-file"],
|
|
193
196
|
"token-file",
|
|
194
197
|
"agentkit chat-ui --deploy --token-file <path>",
|
|
195
198
|
);
|
|
196
|
-
const server = await startDeployChatUiServer(process.cwd(), {
|
|
199
|
+
const server = await startDeployChatUiServer(process.cwd(), {
|
|
200
|
+
port,
|
|
201
|
+
tokenFile,
|
|
202
|
+
autoPort: requestedPort === undefined,
|
|
203
|
+
});
|
|
197
204
|
|
|
198
205
|
console.log("AgentKit hosted chat UI running");
|
|
199
206
|
console.log("");
|
|
207
|
+
if (server.portConflict) {
|
|
208
|
+
console.log(`Port ${server.portConflict.port} is already used. Started hosted chat UI at ${server.url}.`);
|
|
209
|
+
console.log("");
|
|
210
|
+
}
|
|
200
211
|
console.log(`Chat: ${server.url}`);
|
|
201
212
|
console.log(`Production: ${server.deployUrl}`);
|
|
202
213
|
console.log(`Token: ${server.tokenSource}`);
|
|
@@ -936,14 +947,16 @@ async function main() {
|
|
|
936
947
|
return;
|
|
937
948
|
}
|
|
938
949
|
|
|
950
|
+
let readinessReport: Awaited<ReturnType<typeof checkDeployReadiness>> | null = null;
|
|
951
|
+
|
|
939
952
|
if (!args.flags["dry-run"] && !args.flags["local-wrangler"]) {
|
|
940
|
-
|
|
953
|
+
readinessReport = await checkDeployReadiness({
|
|
941
954
|
apiUrl: await resolveCloudApiUrl(args.flags.api),
|
|
942
955
|
requireLogin: !anonymous,
|
|
943
956
|
});
|
|
944
957
|
|
|
945
|
-
if (!
|
|
946
|
-
printDeployReadinessReport(
|
|
958
|
+
if (!readinessReport.ok) {
|
|
959
|
+
printDeployReadinessReport(readinessReport);
|
|
947
960
|
throw new AgentKitError(
|
|
948
961
|
"deploy_readiness_failed",
|
|
949
962
|
"Fix deploy readiness failures before running agentkit deploy.",
|
|
@@ -1016,6 +1029,14 @@ async function main() {
|
|
|
1016
1029
|
: undefined,
|
|
1017
1030
|
});
|
|
1018
1031
|
}
|
|
1032
|
+
printDeployProductionHandoff({
|
|
1033
|
+
deployUrl: deploy.deploy.url,
|
|
1034
|
+
statePath,
|
|
1035
|
+
chatAccess,
|
|
1036
|
+
databaseSchemaPath: result.tursoSchemaPath,
|
|
1037
|
+
readinessReport,
|
|
1038
|
+
smokeMessage,
|
|
1039
|
+
});
|
|
1019
1040
|
return;
|
|
1020
1041
|
}
|
|
1021
1042
|
|
|
@@ -1362,7 +1383,7 @@ Rules:
|
|
|
1362
1383
|
- Edit the agent contract in agentkit.config.ts.
|
|
1363
1384
|
- Add TypeScript tools under tools/.
|
|
1364
1385
|
- Keep the first useful version runnable with test/fake unless the owner has already chosen a real provider.
|
|
1365
|
-
- Before testing real conversation behavior, ask the owner to choose OpenRouter, OpenAI, Anthropic, or another supported provider. Do not choose for them.
|
|
1386
|
+
- Before testing real conversation behavior, ask the owner to choose OpenRouter, OpenAI, Anthropic, OpenCode Zen, OpenCode Go, or another supported provider. Do not choose for them.
|
|
1366
1387
|
- Keep local secrets in .env only.
|
|
1367
1388
|
- Do not commit or print secret values.
|
|
1368
1389
|
- Do not edit .agentkit/ directly.
|
|
@@ -1402,6 +1423,84 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
1402
1423
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1403
1424
|
}
|
|
1404
1425
|
|
|
1426
|
+
function warnIfUnsupportedNode(): void {
|
|
1427
|
+
const major = majorNodeVersion(process.versions.node);
|
|
1428
|
+
|
|
1429
|
+
if (major !== null && major < 24) {
|
|
1430
|
+
console.warn(
|
|
1431
|
+
`Warning: @andreprado/agentkit requires Node >=24; current Node is ${process.versions.node}. Use Node 24+ before relying on local runtime or deploy commands.`,
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
function majorNodeVersion(version: string): number | null {
|
|
1437
|
+
const [major] = version.split(".");
|
|
1438
|
+
const parsed = Number(major);
|
|
1439
|
+
return Number.isInteger(parsed) ? parsed : null;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function printDeployProductionHandoff(input: {
|
|
1443
|
+
deployUrl: string;
|
|
1444
|
+
statePath: string;
|
|
1445
|
+
chatAccess: Awaited<ReturnType<typeof ensureDeployChatAccessToken>>;
|
|
1446
|
+
databaseSchemaPath: string | null;
|
|
1447
|
+
readinessReport: Awaited<ReturnType<typeof checkDeployReadiness>> | null;
|
|
1448
|
+
smokeMessage?: string;
|
|
1449
|
+
}): void {
|
|
1450
|
+
const context = input.readinessReport?.context;
|
|
1451
|
+
const userSecrets = context?.userManagedSecrets ?? [];
|
|
1452
|
+
const managedSecrets = context?.agentKitManagedSecrets ?? [];
|
|
1453
|
+
const composio = context?.integrations.managedComposio;
|
|
1454
|
+
|
|
1455
|
+
console.log("");
|
|
1456
|
+
console.log("Production handoff");
|
|
1457
|
+
console.log(`URL: ${input.deployUrl}`);
|
|
1458
|
+
console.log(`UI: npm run agentkit -- chat-ui --deploy`);
|
|
1459
|
+
console.log(`State: ${input.statePath}`);
|
|
1460
|
+
console.log(
|
|
1461
|
+
`Secrets: ${
|
|
1462
|
+
userSecrets.length > 0
|
|
1463
|
+
? `user-managed set (${userSecrets.join(", ")})`
|
|
1464
|
+
: "no user-managed hosted secrets required"
|
|
1465
|
+
}${managedSecrets.length > 0 ? `; AgentKit-managed: ${managedSecrets.join(", ")}` : ""}`,
|
|
1466
|
+
);
|
|
1467
|
+
console.log(
|
|
1468
|
+
`Database: ${
|
|
1469
|
+
input.databaseSchemaPath
|
|
1470
|
+
? `AgentKit-managed; schema artifact ${input.databaseSchemaPath}`
|
|
1471
|
+
: "AgentKit-managed; no application schema artifact"
|
|
1472
|
+
}`,
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
if (composio?.enabled) {
|
|
1476
|
+
console.log(`Integrations: managed Composio configured for ${composio.toolkits.join(", ")}`);
|
|
1477
|
+
for (const toolkit of composio.toolkits) {
|
|
1478
|
+
console.log(`Connect: npm run agentkit -- integrations connect composio --toolkit ${toolkit}`);
|
|
1479
|
+
}
|
|
1480
|
+
} else {
|
|
1481
|
+
console.log("Integrations: none configured");
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
if (input.smokeMessage) {
|
|
1485
|
+
console.log(`Smoke: verified with ${JSON.stringify(input.smokeMessage)}`);
|
|
1486
|
+
} else {
|
|
1487
|
+
console.log('Smoke: not run; next check: npm run agentkit -- deploy smoke --message "hello"');
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if (input.chatAccess.status === "ready") {
|
|
1491
|
+
console.log(`Chat token: ${relativePath(process.cwd(), input.chatAccess.path)}`);
|
|
1492
|
+
} else if (input.chatAccess.status === "skipped") {
|
|
1493
|
+
console.log("Chat token: not created for anonymous deploy");
|
|
1494
|
+
} else {
|
|
1495
|
+
console.log(`Chat token: failed; run npm run agentkit -- access token create ${defaultDeployChatAccessTokenName} --out ${defaultDeployChatAccessTokenPath}`);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const nextCommand = composio?.enabled && composio.toolkits.length > 0
|
|
1499
|
+
? `npm run agentkit -- integrations connect composio --toolkit ${composio.toolkits[0]}`
|
|
1500
|
+
: "npm run agentkit -- chat-ui --deploy";
|
|
1501
|
+
console.log(`Next: ${nextCommand}`);
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1405
1504
|
async function ensureDeployChatAccessToken(options: {
|
|
1406
1505
|
apiUrl: string;
|
|
1407
1506
|
root: string;
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export type AgentRuntime = "local" | "edge";
|
|
2
|
-
export type AgentProviderName = "test" | "openai" | "anthropic" | "openrouter" | "custom";
|
|
2
|
+
export type AgentProviderName = "test" | "openai" | "anthropic" | "openrouter" | "opencode" | "opencode-go" | "custom";
|
|
3
3
|
export type AccessMode = "private" | "token" | "public";
|
|
4
4
|
export type ToolVisibility = "user" | "internal";
|
|
5
|
-
export type ChannelType = "website" | "telegram" | "whatsapp" | "discord" | "slack";
|
|
6
|
-
export type ChannelProvider = "agentkit" | "telegram" | "zapster" | "meta" | "discord" | "slack";
|
|
7
|
-
export type WhatsappChannelProvider = "zapster" | "meta";
|
|
5
|
+
export type ChannelType = "website" | "telegram" | "whatsapp" | "discord" | "slack" | "webhook";
|
|
6
|
+
export type ChannelProvider = "agentkit" | "telegram" | "zapster" | "meta" | "uazapi" | "evolution" | "discord" | "slack" | "generic";
|
|
7
|
+
export type WhatsappChannelProvider = "zapster" | "meta" | "uazapi" | "evolution";
|
|
8
8
|
export type DiscordChannelMode = "interactions" | "bot";
|
|
9
9
|
export type TelegramAllowedUpdate = "message";
|
|
10
10
|
export type ChannelAudioMode = "off" | "reject" | "transcribe";
|
|
@@ -180,12 +180,30 @@ export type SlackChannelConfig = {
|
|
|
180
180
|
audio?: never;
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
export type GenericWebhookChannelInput = {
|
|
184
|
+
name: string;
|
|
185
|
+
secrets?: string[];
|
|
186
|
+
limits?: ChannelLimits;
|
|
187
|
+
buffer?: ChannelBufferInput;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type GenericWebhookChannelConfig = {
|
|
191
|
+
type: "webhook";
|
|
192
|
+
provider: "generic";
|
|
193
|
+
name: string;
|
|
194
|
+
secrets: string[];
|
|
195
|
+
limits?: ChannelLimits;
|
|
196
|
+
buffer?: ChannelBufferConfig;
|
|
197
|
+
audio?: never;
|
|
198
|
+
};
|
|
199
|
+
|
|
183
200
|
export type AgentChannel =
|
|
184
201
|
| WebsiteChannelConfig
|
|
185
202
|
| TelegramChannelConfig
|
|
186
203
|
| WhatsappChannelConfig
|
|
187
204
|
| DiscordChannelConfig
|
|
188
|
-
| SlackChannelConfig
|
|
205
|
+
| SlackChannelConfig
|
|
206
|
+
| GenericWebhookChannelConfig;
|
|
189
207
|
|
|
190
208
|
export type IntegrationTagsConfig = {
|
|
191
209
|
enable?: string[];
|
|
@@ -495,6 +513,17 @@ export function slackChannel(input: SlackChannelInput): SlackChannelConfig {
|
|
|
495
513
|
};
|
|
496
514
|
}
|
|
497
515
|
|
|
516
|
+
export function webhookChannel(input: GenericWebhookChannelInput): GenericWebhookChannelConfig {
|
|
517
|
+
return {
|
|
518
|
+
type: "webhook",
|
|
519
|
+
provider: "generic",
|
|
520
|
+
name: input.name,
|
|
521
|
+
secrets: normalizeChannelSecrets(input.secrets ?? ["AGENTKIT_WEBHOOK_SECRET"], "generic webhook channel"),
|
|
522
|
+
...(input.limits ? { limits: input.limits } : {}),
|
|
523
|
+
...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "generic webhook channel") } : {}),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
498
527
|
function defaultDiscordChannelSecrets(mode: DiscordChannelMode): string[] {
|
|
499
528
|
return mode === "bot" ? ["DISCORD_BOT_TOKEN"] : ["DISCORD_PUBLIC_KEY"];
|
|
500
529
|
}
|
|
@@ -504,6 +533,14 @@ function defaultWhatsappChannelSecrets(provider: WhatsappChannelProvider): strin
|
|
|
504
533
|
return ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"];
|
|
505
534
|
}
|
|
506
535
|
|
|
536
|
+
if (provider === "uazapi") {
|
|
537
|
+
return ["UAZAPI_BASE_URL", "UAZAPI_TOKEN"];
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (provider === "evolution") {
|
|
541
|
+
return ["EVOLUTION_API_BASE_URL", "EVOLUTION_API_KEY", "EVOLUTION_INSTANCE_NAME", "EVOLUTION_WEBHOOK_TOKEN"];
|
|
542
|
+
}
|
|
543
|
+
|
|
507
544
|
return ["META_WHATSAPP_ACCESS_TOKEN", "META_WHATSAPP_APP_SECRET", "META_WHATSAPP_VERIFY_TOKEN"];
|
|
508
545
|
}
|
|
509
546
|
|
package/src/providers/pi.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { AgentProviderName, AgentTool } from "../index";
|
|
|
17
17
|
import { AgentKitError } from "../runtime/errors";
|
|
18
18
|
import type { ProviderAdapter, ProviderRunInput, ProviderRunResult } from "./types";
|
|
19
19
|
|
|
20
|
-
const PI_PROVIDER_NAMES = ["openai", "anthropic", "openrouter"] as const satisfies AgentProviderName[];
|
|
20
|
+
const PI_PROVIDER_NAMES = ["openai", "anthropic", "openrouter", "opencode", "opencode-go"] as const satisfies AgentProviderName[];
|
|
21
21
|
const MAX_TOOL_TURNS = 8;
|
|
22
22
|
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
23
23
|
const UNKNOWN_OPENROUTER_CONTEXT_WINDOW = 128000;
|
|
@@ -159,7 +159,13 @@ async function runPiTurn(
|
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
function toPiProvider(provider: string): Provider {
|
|
162
|
-
if (
|
|
162
|
+
if (
|
|
163
|
+
provider === "openai" ||
|
|
164
|
+
provider === "anthropic" ||
|
|
165
|
+
provider === "openrouter" ||
|
|
166
|
+
provider === "opencode" ||
|
|
167
|
+
provider === "opencode-go"
|
|
168
|
+
) {
|
|
163
169
|
return provider;
|
|
164
170
|
}
|
|
165
171
|
|
|
@@ -181,6 +187,10 @@ function requireProviderApiKey(provider: Provider, env: Record<string, string |
|
|
|
181
187
|
}
|
|
182
188
|
|
|
183
189
|
function defaultProviderSecretNames(provider: Provider): string[] {
|
|
190
|
+
if (provider === "opencode" || provider === "opencode-go") {
|
|
191
|
+
return ["OPENCODE_API_KEY"];
|
|
192
|
+
}
|
|
193
|
+
|
|
184
194
|
return [`${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`];
|
|
185
195
|
}
|
|
186
196
|
|
|
@@ -15,11 +15,24 @@ export type ChannelFixtureName =
|
|
|
15
15
|
| "zapster-audio-message"
|
|
16
16
|
| "zapster-duplicate-message"
|
|
17
17
|
| "zapster-unsupported-media"
|
|
18
|
+
| "uazapi-message"
|
|
19
|
+
| "uazapi-audio-message"
|
|
20
|
+
| "uazapi-duplicate-message"
|
|
21
|
+
| "uazapi-unsupported-media"
|
|
22
|
+
| "uazapi-was-sent-by-api"
|
|
23
|
+
| "evolution-message"
|
|
24
|
+
| "evolution-audio-message"
|
|
25
|
+
| "evolution-captioned-media"
|
|
26
|
+
| "evolution-duplicate-message"
|
|
27
|
+
| "evolution-from-me"
|
|
28
|
+
| "evolution-unsupported-media"
|
|
18
29
|
| "discord-command"
|
|
19
30
|
| "discord-message-create"
|
|
20
31
|
| "discord-ping"
|
|
21
32
|
| "slack-app-mention"
|
|
22
|
-
| "slack-direct-message"
|
|
33
|
+
| "slack-direct-message"
|
|
34
|
+
| "generic-webhook-message"
|
|
35
|
+
| "generic-webhook-duplicate";
|
|
23
36
|
|
|
24
37
|
export type ChannelFixture = {
|
|
25
38
|
name: ChannelFixtureName;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { ChannelAdapter, ChannelSendInput, RawWebhookEvent } from "../channels";
|
|
4
|
+
import { AgentKitError } from "../errors";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SECRET_NAME = "AGENTKIT_WEBHOOK_SECRET";
|
|
7
|
+
|
|
8
|
+
export const genericWebhookChannelAdapter: ChannelAdapter = {
|
|
9
|
+
type: "webhook",
|
|
10
|
+
provider: "generic",
|
|
11
|
+
requiredSecrets: [DEFAULT_SECRET_NAME],
|
|
12
|
+
verifyWebhook(input) {
|
|
13
|
+
const expected = input.secrets[DEFAULT_SECRET_NAME];
|
|
14
|
+
|
|
15
|
+
if (!expected) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
code: "channel_secret_missing",
|
|
19
|
+
message: `Secret ${DEFAULT_SECRET_NAME} is not set for this channel.`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const signature = input.headers.get("x-agentkit-webhook-signature") ?? input.headers.get("x-agentkit-signature");
|
|
24
|
+
|
|
25
|
+
if (signature) {
|
|
26
|
+
const expectedSignature = `sha256=${createHmac("sha256", expected).update(input.rawBody).digest("hex")}`;
|
|
27
|
+
|
|
28
|
+
if (constantTimeEqual(signature.trim(), expectedSignature)) {
|
|
29
|
+
return { ok: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
code: "channel_signature_invalid",
|
|
35
|
+
message: "Generic webhook HMAC signature did not match.",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const bearer = readBearerToken(input.headers.get("authorization"));
|
|
40
|
+
const headerToken =
|
|
41
|
+
input.headers.get("x-agentkit-webhook-secret") ??
|
|
42
|
+
input.headers.get("x-agentkit-channel-token") ??
|
|
43
|
+
input.headers.get("x-agentkit-webhook-token");
|
|
44
|
+
|
|
45
|
+
if (constantTimeEqual(bearer, expected) || constantTimeEqual(headerToken ?? "", expected)) {
|
|
46
|
+
return { ok: true };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
code: "channel_signature_invalid",
|
|
52
|
+
message: "Generic webhook token did not match.",
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
normalizeInbound(input) {
|
|
56
|
+
const body = parseJsonObject(input);
|
|
57
|
+
const eventId = readFirstScalar(body, ["event_id", "eventId", "id"]) ?? readNestedMessageId(body);
|
|
58
|
+
|
|
59
|
+
if (!eventId) {
|
|
60
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload is missing event_id.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const text = readMessageText(body);
|
|
64
|
+
const identitySource =
|
|
65
|
+
readFirstScalar(body, [
|
|
66
|
+
"external_id",
|
|
67
|
+
"externalId",
|
|
68
|
+
"user_id",
|
|
69
|
+
"userId",
|
|
70
|
+
"contact_id",
|
|
71
|
+
"contactId",
|
|
72
|
+
"conversation_id",
|
|
73
|
+
"conversationId",
|
|
74
|
+
"client_id",
|
|
75
|
+
"clientId",
|
|
76
|
+
"visitor_id",
|
|
77
|
+
"visitorId",
|
|
78
|
+
"source_id",
|
|
79
|
+
"sourceId",
|
|
80
|
+
]) ?? `event:${eventId}`;
|
|
81
|
+
const identityKey = `webhook:generic:identity:${sha256(identitySource).slice(0, 32)}`;
|
|
82
|
+
const base = {
|
|
83
|
+
type: "webhook" as const,
|
|
84
|
+
provider: "generic" as const,
|
|
85
|
+
providerEventId: eventId,
|
|
86
|
+
dedupeKey: `webhook:generic:${input.channelId}:${eventId}`,
|
|
87
|
+
externalIdentity: {
|
|
88
|
+
key: identityKey,
|
|
89
|
+
redactedDisplay: identityKey,
|
|
90
|
+
},
|
|
91
|
+
receivedAt: input.receivedAt,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (!text) {
|
|
95
|
+
return [
|
|
96
|
+
{
|
|
97
|
+
...base,
|
|
98
|
+
kind: "unsupported" as const,
|
|
99
|
+
unsupportedReason: "unsupported_message_type:missing_text",
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
...base,
|
|
107
|
+
kind: "message" as const,
|
|
108
|
+
message: {
|
|
109
|
+
role: "user" as const,
|
|
110
|
+
contentType: "text" as const,
|
|
111
|
+
content: text,
|
|
112
|
+
providerMessageId: readNestedMessageId(body) ?? eventId,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
},
|
|
117
|
+
async sendMessage(input) {
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
status: "stubbed",
|
|
121
|
+
providerRequestId: buildGenericWebhookProviderRequestId(input),
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
getStatus() {
|
|
125
|
+
return {
|
|
126
|
+
status: "connected",
|
|
127
|
+
checkedAt: new Date().toISOString(),
|
|
128
|
+
providerMetadata: {
|
|
129
|
+
outbound: "stubbed",
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function buildGenericWebhookProviderRequestId(input: ChannelSendInput): string {
|
|
136
|
+
return `webhook:generic:${input.channelId}:${input.conversationId}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseJsonObject(input: RawWebhookEvent): Record<string, unknown> {
|
|
140
|
+
const contentType = input.headers.get("content-type") ?? "";
|
|
141
|
+
|
|
142
|
+
if (contentType && !contentType.toLowerCase().includes("application/json")) {
|
|
143
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must use content-type application/json.");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
const parsed = JSON.parse(input.rawBody) as unknown;
|
|
148
|
+
|
|
149
|
+
if (isRecord(parsed)) {
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must be valid JSON.", {
|
|
154
|
+
cause: error,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
throw new AgentKitError("channel_payload_invalid", "Generic webhook payload must be a JSON object.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readMessageText(body: Record<string, unknown>): string | undefined {
|
|
162
|
+
const message = body.message;
|
|
163
|
+
|
|
164
|
+
if (typeof message === "string") {
|
|
165
|
+
return normalizeNonEmpty(message);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (isRecord(message)) {
|
|
169
|
+
return readFirstScalar(message, ["text", "content", "body"]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return readFirstScalar(body, ["text", "content", "prompt", "input"]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function readNestedMessageId(body: Record<string, unknown>): string | undefined {
|
|
176
|
+
const message = body.message;
|
|
177
|
+
return isRecord(message) ? readFirstScalar(message, ["id", "message_id", "messageId"]) : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function readFirstScalar(value: Record<string, unknown>, keys: string[]): string | undefined {
|
|
181
|
+
for (const key of keys) {
|
|
182
|
+
const field = value[key];
|
|
183
|
+
|
|
184
|
+
if (typeof field === "string") {
|
|
185
|
+
const normalized = normalizeNonEmpty(field);
|
|
186
|
+
|
|
187
|
+
if (normalized) {
|
|
188
|
+
return normalized;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (typeof field === "number" && Number.isFinite(field)) {
|
|
193
|
+
return String(field);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeNonEmpty(value: string): string | undefined {
|
|
201
|
+
const trimmed = value.trim();
|
|
202
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readBearerToken(value: string | null): string {
|
|
206
|
+
if (!value?.startsWith("Bearer ")) {
|
|
207
|
+
return "";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return value.slice("Bearer ".length).trim();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function constantTimeEqual(left: string, right: string): boolean {
|
|
214
|
+
const leftDigest = createHash("sha256").update(left).digest();
|
|
215
|
+
const rightDigest = createHash("sha256").update(right).digest();
|
|
216
|
+
return timingSafeEqual(leftDigest, rightDigest) && left === right;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function sha256(value: string): string {
|
|
220
|
+
return createHash("sha256").update(value).digest("hex");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
224
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
225
|
+
}
|