@andreprado/agentkit 0.1.0-alpha.22 → 0.1.0-alpha.24
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/docs/guides/add-channel.md +92 -5
- package/docs/guides/channel-security.md +35 -3
- package/docs/guides/debug-channel.md +16 -4
- package/docs/llms-full.txt +54 -1
- package/docs/llms.txt +2 -0
- package/package.json +1 -1
- package/src/cli/commands/channels.ts +35 -1
- package/src/cli/deploy-readiness.ts +80 -0
- package/src/cli/index.ts +22 -0
- package/src/index.ts +191 -5
- package/src/runtime/channels/generic-webhook.ts +752 -3
- package/src/runtime/channels.ts +3 -0
- package/src/runtime/config.ts +144 -0
- package/src/runtime/dev-server.ts +131 -1
- package/src/runtime/inspect.ts +18 -0
- package/src/runtime/targets/cloudflare/build.ts +112 -4
package/src/runtime/channels.ts
CHANGED
|
@@ -122,6 +122,9 @@ export type ChannelWebhookResponse = {
|
|
|
122
122
|
|
|
123
123
|
export type ChannelSendInput = ChannelAdapterId & {
|
|
124
124
|
channelId: string;
|
|
125
|
+
channel?: AgentChannel;
|
|
126
|
+
jobId?: string;
|
|
127
|
+
deliveryId?: string;
|
|
125
128
|
conversationId: string;
|
|
126
129
|
externalIdentity: ChannelExternalIdentity;
|
|
127
130
|
replyTarget?: ChannelReplyTarget;
|
package/src/runtime/config.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
AgentIntegration,
|
|
21
21
|
ChannelAudioMode,
|
|
22
22
|
TranscriptionProviderName,
|
|
23
|
+
ChannelReplyRouteConfig,
|
|
23
24
|
} from "../index";
|
|
24
25
|
import { MANAGED_COMPOSIO_TOOL_NAME } from "./integrations/composio";
|
|
25
26
|
import { AgentKitError } from "./errors";
|
|
@@ -39,10 +40,12 @@ const TELEGRAM_ALLOWED_UPDATES = new Set<TelegramAllowedUpdate>(["message"]);
|
|
|
39
40
|
const KNOWLEDGE_EMBEDDING_PROVIDERS = new Set<AgentKnowledgeEmbeddingProvider>(["none", "test", "openai"]);
|
|
40
41
|
const TRANSCRIPTION_PROVIDERS = new Set<TranscriptionProviderName>(["test", "openai", "groq"]);
|
|
41
42
|
const CHANNEL_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
43
|
+
const CHANNEL_REPLY_PATH_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
42
44
|
const INTEGRATION_NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
43
45
|
const COMPOSIO_TOOLKIT_PATTERN = /^[a-z][a-z0-9_-]*$/;
|
|
44
46
|
const COMPOSIO_TOOL_SLUG_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
45
47
|
const SECRET_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
48
|
+
const HTTP_HEADER_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]*$/;
|
|
46
49
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
47
50
|
let configImportNonce = 0;
|
|
48
51
|
|
|
@@ -589,6 +592,10 @@ function validateChannels(value: unknown, runtime: AgentRuntime, hasTranscriptio
|
|
|
589
592
|
|
|
590
593
|
names.add(channel.name);
|
|
591
594
|
}
|
|
595
|
+
|
|
596
|
+
for (const [index, channel] of value.entries()) {
|
|
597
|
+
validateChannelReplyRoute(channel, index, names);
|
|
598
|
+
}
|
|
592
599
|
}
|
|
593
600
|
|
|
594
601
|
function validateChannel(value: unknown, index: number, hasTranscriptionConfig: boolean): asserts value is AgentChannel {
|
|
@@ -614,6 +621,7 @@ function validateChannel(value: unknown, index: number, hasTranscriptionConfig:
|
|
|
614
621
|
|
|
615
622
|
expectStringArray(value.secrets, `channels[${index}].secrets`);
|
|
616
623
|
validateSecretNames(value.secrets, `channels[${index}].secrets`);
|
|
624
|
+
validateChannelReplyTo(value.replyTo, index);
|
|
617
625
|
validateChannelLimits(value.limits, index);
|
|
618
626
|
validateChannelBuffer(value.buffer, index);
|
|
619
627
|
|
|
@@ -787,9 +795,145 @@ function validateGenericWebhookChannel(value: Record<string, unknown>, index: nu
|
|
|
787
795
|
throw invalidConfig(`channels[${index}].audio is not supported when type is "webhook".`);
|
|
788
796
|
}
|
|
789
797
|
|
|
798
|
+
if (value.direction !== undefined && value.direction !== "inbound" && value.direction !== "outbound") {
|
|
799
|
+
throw invalidConfig(`channels[${index}].direction must be "inbound" or "outbound" when type is "webhook".`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
if (value.direction === "outbound") {
|
|
803
|
+
if (value.buffer !== undefined) {
|
|
804
|
+
throw invalidConfig(`channels[${index}].buffer is not supported for outbound webhook channels.`);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (value.replyTo !== undefined) {
|
|
808
|
+
throw invalidConfig(`channels[${index}].replyTo is not supported for outbound webhook channels.`);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
expectString(value.urlSecret, `channels[${index}].urlSecret`);
|
|
812
|
+
validateSecretNames([value.urlSecret], `channels[${index}].urlSecret`);
|
|
813
|
+
validateGenericWebhookOutputAuth(value.auth, index);
|
|
814
|
+
expectRequiredSecrets(
|
|
815
|
+
value.secrets,
|
|
816
|
+
[value.urlSecret, ...genericWebhookOutputAuthSecrets(value.auth)],
|
|
817
|
+
`channels[${index}]`,
|
|
818
|
+
);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (value.urlSecret !== undefined || value.auth !== undefined) {
|
|
823
|
+
throw invalidConfig(`channels[${index}].urlSecret and auth are only supported for outbound webhook channels.`);
|
|
824
|
+
}
|
|
825
|
+
|
|
790
826
|
expectRequiredSecrets(value.secrets, ["AGENTKIT_WEBHOOK_SECRET"], `channels[${index}]`);
|
|
791
827
|
}
|
|
792
828
|
|
|
829
|
+
function validateChannelReplyTo(value: unknown, index: number): void {
|
|
830
|
+
if (value === undefined) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (!isRecord(value)) {
|
|
835
|
+
throw invalidConfig(`channels[${index}].replyTo must be an object when provided.`);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
expectString(value.channel, `channels[${index}].replyTo.channel`);
|
|
839
|
+
|
|
840
|
+
if (!CHANNEL_NAME_PATTERN.test(value.channel)) {
|
|
841
|
+
throw invalidConfig(`channels[${index}].replyTo.channel must be a valid channel name.`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (value.recipientFrom !== undefined) {
|
|
845
|
+
expectString(value.recipientFrom, `channels[${index}].replyTo.recipientFrom`);
|
|
846
|
+
|
|
847
|
+
if (!CHANNEL_REPLY_PATH_PATTERN.test(String(value.recipientFrom).replace(/^payload\./, ""))) {
|
|
848
|
+
throw invalidConfig(`channels[${index}].replyTo.recipientFrom must be a dotted JSON field path.`);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function validateChannelReplyRoute(
|
|
854
|
+
channel: unknown,
|
|
855
|
+
index: number,
|
|
856
|
+
names: Set<string>,
|
|
857
|
+
): void {
|
|
858
|
+
if (!isRecord(channel) || !isRecord(channel.replyTo)) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const replyTo = channel.replyTo as ChannelReplyRouteConfig;
|
|
863
|
+
|
|
864
|
+
if (!names.has(replyTo.channel)) {
|
|
865
|
+
throw invalidConfig(`channels[${index}].replyTo.channel must name another channel in this Agent Capsule.`);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (replyTo.channel === channel.name) {
|
|
869
|
+
throw invalidConfig(`channels[${index}].replyTo.channel must not point back to the same channel.`);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function validateGenericWebhookOutputAuth(value: unknown, index: number): void {
|
|
874
|
+
if (!isRecord(value)) {
|
|
875
|
+
throw invalidConfig(`channels[${index}].auth must be an object for outbound webhook channels.`);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (value.type === "none") {
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (value.type === "bearer") {
|
|
883
|
+
expectString(value.tokenSecret, `channels[${index}].auth.tokenSecret`);
|
|
884
|
+
validateSecretNames([value.tokenSecret], `channels[${index}].auth.tokenSecret`);
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
if (value.type === "header") {
|
|
889
|
+
expectString(value.headerName, `channels[${index}].auth.headerName`);
|
|
890
|
+
|
|
891
|
+
if (!HTTP_HEADER_NAME_PATTERN.test(value.headerName)) {
|
|
892
|
+
throw invalidConfig(`channels[${index}].auth.headerName must be an HTTP header name.`);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
expectString(value.valueSecret, `channels[${index}].auth.valueSecret`);
|
|
896
|
+
validateSecretNames([value.valueSecret], `channels[${index}].auth.valueSecret`);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (value.type === "hmac") {
|
|
901
|
+
if (value.headerName !== undefined) {
|
|
902
|
+
expectString(value.headerName, `channels[${index}].auth.headerName`);
|
|
903
|
+
|
|
904
|
+
if (!HTTP_HEADER_NAME_PATTERN.test(value.headerName)) {
|
|
905
|
+
throw invalidConfig(`channels[${index}].auth.headerName must be an HTTP header name.`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
expectString(value.secret, `channels[${index}].auth.secret`);
|
|
910
|
+
validateSecretNames([value.secret], `channels[${index}].auth.secret`);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
throw invalidConfig(`channels[${index}].auth.type must be "none", "bearer", "header", or "hmac".`);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function genericWebhookOutputAuthSecrets(value: unknown): string[] {
|
|
918
|
+
if (!isRecord(value)) {
|
|
919
|
+
return [];
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (value.type === "bearer" && typeof value.tokenSecret === "string") {
|
|
923
|
+
return [value.tokenSecret];
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (value.type === "header" && typeof value.valueSecret === "string") {
|
|
927
|
+
return [value.valueSecret];
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (value.type === "hmac" && typeof value.secret === "string") {
|
|
931
|
+
return [value.secret];
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return [];
|
|
935
|
+
}
|
|
936
|
+
|
|
793
937
|
function validateChannelLimits(value: unknown, index: number): void {
|
|
794
938
|
if (value === undefined) {
|
|
795
939
|
return;
|
|
@@ -564,11 +564,23 @@ async function handlePortableChannel(
|
|
|
564
564
|
conversationId,
|
|
565
565
|
signal: request.signal,
|
|
566
566
|
});
|
|
567
|
+
const routedReply = channel.replyTo
|
|
568
|
+
? await sendDevChannelReply({
|
|
569
|
+
capsule,
|
|
570
|
+
sourceChannel: channel,
|
|
571
|
+
sourceEvent: event,
|
|
572
|
+
rawBody,
|
|
573
|
+
conversationId: result.conversationId,
|
|
574
|
+
message: result.message.content,
|
|
575
|
+
})
|
|
576
|
+
: null;
|
|
577
|
+
|
|
567
578
|
deliveries.push({
|
|
568
579
|
state: "sent",
|
|
569
580
|
dedupeKey: event.dedupeKey,
|
|
570
581
|
conversationId: result.conversationId,
|
|
571
582
|
message: result.message,
|
|
583
|
+
...(routedReply ? { outbound: routedReply } : {}),
|
|
572
584
|
});
|
|
573
585
|
}
|
|
574
586
|
|
|
@@ -579,6 +591,118 @@ async function handlePortableChannel(
|
|
|
579
591
|
return jsonResponse({ deliveries });
|
|
580
592
|
}
|
|
581
593
|
|
|
594
|
+
async function sendDevChannelReply(input: {
|
|
595
|
+
capsule: LoadedAgentCapsule;
|
|
596
|
+
sourceChannel: AgentChannel;
|
|
597
|
+
sourceEvent: Awaited<ReturnType<ChannelAdapter["normalizeInbound"]>>[number];
|
|
598
|
+
rawBody: string;
|
|
599
|
+
conversationId: string;
|
|
600
|
+
message: string;
|
|
601
|
+
}) {
|
|
602
|
+
const replyTo = input.sourceChannel.replyTo;
|
|
603
|
+
|
|
604
|
+
if (!replyTo) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const target = (input.capsule.config.channels ?? []).find((channel) => channel.name === replyTo.channel);
|
|
609
|
+
|
|
610
|
+
if (!target) {
|
|
611
|
+
throw new AgentKitError("channel_not_found", `Reply target channel "${replyTo.channel}" was not found.`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (target.type === "webhook" && target.direction !== "outbound") {
|
|
615
|
+
throw new AgentKitError("channel_payload_invalid", `Reply target channel "${replyTo.channel}" is inbound-only.`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const recipient = replyTo.recipientFrom ? readDevJsonPathScalar(input.rawBody, replyTo.recipientFrom) : null;
|
|
619
|
+
|
|
620
|
+
if (replyTo.recipientFrom && !recipient) {
|
|
621
|
+
throw new AgentKitError("channel_payload_invalid", `Reply route recipientFrom "${replyTo.recipientFrom}" did not resolve to a non-empty value.`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const externalIdentity = recipient
|
|
625
|
+
? recipientIdentityForDevChannel(target, recipient)
|
|
626
|
+
: input.sourceEvent.externalIdentity.key;
|
|
627
|
+
|
|
628
|
+
const targetAdapter = adapterForChannel(target.type, target.provider);
|
|
629
|
+
const secrets = await resolveChannelSecrets(input.capsule.root, target);
|
|
630
|
+
|
|
631
|
+
return targetAdapter.sendMessage({
|
|
632
|
+
type: target.type,
|
|
633
|
+
provider: target.provider,
|
|
634
|
+
channelId: target.name,
|
|
635
|
+
channel: target,
|
|
636
|
+
conversationId: input.conversationId,
|
|
637
|
+
externalIdentity: {
|
|
638
|
+
key: externalIdentity,
|
|
639
|
+
},
|
|
640
|
+
message: {
|
|
641
|
+
role: "assistant",
|
|
642
|
+
contentType: "text",
|
|
643
|
+
content: input.message,
|
|
644
|
+
},
|
|
645
|
+
secrets,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function recipientIdentityForDevChannel(channel: AgentChannel, recipient: string): string {
|
|
650
|
+
if (channel.type === "whatsapp") {
|
|
651
|
+
const prefix = `whatsapp:${channel.provider}:contact:`;
|
|
652
|
+
return recipient.startsWith(prefix) ? recipient : `${prefix}${recipient}`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (channel.type === "telegram") {
|
|
656
|
+
return recipient.includes(":chat:") ? recipient : `telegram:reply:chat:${recipient}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (channel.type === "webhook" && channel.direction === "outbound") {
|
|
660
|
+
return recipient.startsWith("webhook:generic:") ? recipient : `webhook:generic:recipient:${recipient}`;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (channel.type === "website") {
|
|
664
|
+
return recipient.startsWith("website:") ? recipient : `website:reply:${createHash("sha256").update(recipient).digest("hex").slice(0, 32)}`;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
throw new AgentKitError(
|
|
668
|
+
"channel_payload_invalid",
|
|
669
|
+
`Reply target ${channel.type}/${channel.provider} requires a provider-native reply target and cannot use recipientFrom.`,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function readDevJsonPathScalar(rawBody: string, path: string): string | undefined {
|
|
674
|
+
let value: unknown;
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
value = JSON.parse(rawBody) as unknown;
|
|
678
|
+
} catch {
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
for (const segment of path.replace(/^payload\./, "").split(".")) {
|
|
683
|
+
if (!isRecord(value)) {
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
value = value[segment];
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (typeof value === "string") {
|
|
691
|
+
const trimmed = value.trim();
|
|
692
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
696
|
+
return String(value);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (typeof value === "boolean") {
|
|
700
|
+
return String(value);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
582
706
|
async function processDevChannelMessage(
|
|
583
707
|
capsule: LoadedAgentCapsule,
|
|
584
708
|
adapter: ChannelAdapter,
|
|
@@ -1218,7 +1342,13 @@ function discordLocalInteractionResponse(
|
|
|
1218
1342
|
|
|
1219
1343
|
async function resolveChannelSecrets(root: string, channel: AgentChannel): Promise<Record<string, string>> {
|
|
1220
1344
|
const env = await loadCapsuleEnv(root);
|
|
1221
|
-
|
|
1345
|
+
const secrets = Object.fromEntries(channel.secrets.map((name) => [name, env[name] ?? ""]));
|
|
1346
|
+
|
|
1347
|
+
if (env.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
|
|
1348
|
+
secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN = "1";
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
return secrets;
|
|
1222
1352
|
}
|
|
1223
1353
|
|
|
1224
1354
|
function relativePath(root: string, path: string): string {
|
package/src/runtime/inspect.ts
CHANGED
|
@@ -101,9 +101,16 @@ export type AgentInspectChannel = {
|
|
|
101
101
|
name: string;
|
|
102
102
|
type: AgentChannel["type"];
|
|
103
103
|
provider: AgentChannel["provider"];
|
|
104
|
+
direction?: Extract<AgentChannel, { type: "webhook" }>["direction"];
|
|
104
105
|
secrets: string[];
|
|
105
106
|
buffer?: AgentChannel["buffer"];
|
|
106
107
|
audio?: AgentChannel["audio"];
|
|
108
|
+
replyTo?: Extract<AgentChannel, { replyTo?: unknown }>["replyTo"];
|
|
109
|
+
urlSecret?: string;
|
|
110
|
+
auth?: {
|
|
111
|
+
type: string;
|
|
112
|
+
headerName?: string;
|
|
113
|
+
};
|
|
107
114
|
};
|
|
108
115
|
|
|
109
116
|
export type AgentInspectSkills = {
|
|
@@ -209,9 +216,20 @@ export function buildInspectState(
|
|
|
209
216
|
name: channel.name,
|
|
210
217
|
type: channel.type,
|
|
211
218
|
provider: channel.provider,
|
|
219
|
+
...("direction" in channel && channel.direction ? { direction: channel.direction } : {}),
|
|
212
220
|
secrets: [...channel.secrets],
|
|
213
221
|
...(channel.buffer ? { buffer: channel.buffer } : {}),
|
|
214
222
|
...(channel.audio ? { audio: channel.audio } : {}),
|
|
223
|
+
...(channel.replyTo ? { replyTo: channel.replyTo } : {}),
|
|
224
|
+
...("urlSecret" in channel && channel.urlSecret ? { urlSecret: channel.urlSecret } : {}),
|
|
225
|
+
...("auth" in channel && channel.auth
|
|
226
|
+
? {
|
|
227
|
+
auth: {
|
|
228
|
+
type: channel.auth.type,
|
|
229
|
+
...("headerName" in channel.auth && channel.auth.headerName ? { headerName: channel.auth.headerName } : {}),
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
: {}),
|
|
215
233
|
})),
|
|
216
234
|
transcription: {
|
|
217
235
|
enabled: transcription.enabled,
|
|
@@ -466,10 +466,32 @@ export function webhookChannel(input) {
|
|
|
466
466
|
return {
|
|
467
467
|
type: "webhook",
|
|
468
468
|
provider: "generic",
|
|
469
|
+
direction: "inbound",
|
|
469
470
|
name: input.name,
|
|
470
471
|
secrets: normalizeChannelSecrets(input.secrets ?? ["AGENTKIT_WEBHOOK_SECRET"], "generic webhook channel"),
|
|
471
472
|
...(input.limits ? { limits: input.limits } : {}),
|
|
472
473
|
...(input.buffer ? { buffer: normalizeChannelBuffer(input.buffer, "generic webhook channel") } : {}),
|
|
474
|
+
...(input.replyTo ? { replyTo: normalizeChannelReplyRoute(input.replyTo, "generic webhook channel") } : {}),
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export function webhookOutputChannel(input) {
|
|
479
|
+
const auth = normalizeGenericWebhookOutputAuth(input.auth ?? { type: "none" }, "generic webhook output channel");
|
|
480
|
+
const requiredSecrets = [
|
|
481
|
+
input.urlSecret,
|
|
482
|
+
...genericWebhookOutputAuthSecrets(auth),
|
|
483
|
+
...(input.secrets ?? []),
|
|
484
|
+
];
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
type: "webhook",
|
|
488
|
+
provider: "generic",
|
|
489
|
+
direction: "outbound",
|
|
490
|
+
name: input.name,
|
|
491
|
+
urlSecret: normalizeSecretName(input.urlSecret, "generic webhook output channel urlSecret"),
|
|
492
|
+
auth,
|
|
493
|
+
secrets: normalizeChannelSecrets(requiredSecrets, "generic webhook output channel"),
|
|
494
|
+
...(input.limits ? { limits: input.limits } : {}),
|
|
473
495
|
};
|
|
474
496
|
}
|
|
475
497
|
|
|
@@ -493,14 +515,100 @@ function normalizeChannelSecrets(secrets, context) {
|
|
|
493
515
|
const uniqueSecrets = new Set();
|
|
494
516
|
|
|
495
517
|
for (const secret of secrets) {
|
|
496
|
-
|
|
497
|
-
|
|
518
|
+
uniqueSecrets.add(normalizeSecretName(secret, \`\${context} secrets\`));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return Array.from(uniqueSecrets).sort();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function normalizeSecretName(secret, context) {
|
|
525
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(secret)) {
|
|
526
|
+
if (context.endsWith("secrets")) {
|
|
527
|
+
throw new Error(\`\${context} must be environment variable names, not secret values.\`);
|
|
498
528
|
}
|
|
499
529
|
|
|
500
|
-
|
|
530
|
+
throw new Error(\`\${context} must be an environment variable name, not a secret value.\`);
|
|
501
531
|
}
|
|
502
532
|
|
|
503
|
-
return
|
|
533
|
+
return secret;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function normalizeChannelReplyRoute(input, context) {
|
|
537
|
+
const channel = input.channel.trim();
|
|
538
|
+
|
|
539
|
+
if (!/^[a-z][a-z0-9-]*$/.test(channel)) {
|
|
540
|
+
throw new Error(\`\${context} replyTo.channel must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.\`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
channel,
|
|
545
|
+
...(input.recipientFrom ? { recipientFrom: normalizeJsonPath(input.recipientFrom, \`\${context} replyTo.recipientFrom\`) } : {}),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function normalizeJsonPath(value, context) {
|
|
550
|
+
const path = value.trim().replace(/^payload\\./, "");
|
|
551
|
+
|
|
552
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*(?:\\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(path)) {
|
|
553
|
+
throw new Error(\`\${context} must be a dotted JSON field path such as "phone" or "customer.phone".\`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return path;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function normalizeGenericWebhookOutputAuth(auth, context) {
|
|
560
|
+
if (auth.type === "none") {
|
|
561
|
+
return { type: "none" };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (auth.type === "bearer") {
|
|
565
|
+
return {
|
|
566
|
+
type: "bearer",
|
|
567
|
+
tokenSecret: normalizeSecretName(auth.tokenSecret, \`\${context} auth.tokenSecret\`),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (auth.type === "header") {
|
|
572
|
+
if (!/^[A-Za-z][A-Za-z0-9-]*$/.test(auth.headerName)) {
|
|
573
|
+
throw new Error(\`\${context} auth.headerName must be an HTTP header name.\`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
type: "header",
|
|
578
|
+
headerName: auth.headerName,
|
|
579
|
+
valueSecret: normalizeSecretName(auth.valueSecret, \`\${context} auth.valueSecret\`),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (auth.type === "hmac") {
|
|
584
|
+
if (auth.headerName !== undefined && !/^[A-Za-z][A-Za-z0-9-]*$/.test(auth.headerName)) {
|
|
585
|
+
throw new Error(\`\${context} auth.headerName must be an HTTP header name.\`);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return {
|
|
589
|
+
type: "hmac",
|
|
590
|
+
secret: normalizeSecretName(auth.secret, \`\${context} auth.secret\`),
|
|
591
|
+
...(auth.headerName ? { headerName: auth.headerName } : {}),
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
throw new Error(\`\${context} auth.type must be "none", "bearer", "header", or "hmac".\`);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function genericWebhookOutputAuthSecrets(auth) {
|
|
599
|
+
if (auth.type === "bearer") {
|
|
600
|
+
return [auth.tokenSecret];
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (auth.type === "header") {
|
|
604
|
+
return [auth.valueSecret];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (auth.type === "hmac") {
|
|
608
|
+
return [auth.secret];
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return [];
|
|
504
612
|
}
|
|
505
613
|
|
|
506
614
|
function normalizeIntegrationName(value, context) {
|