@andreprado/agentkit 0.1.0-alpha.11 → 0.1.0-alpha.12
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/channel-security.md +1 -1
- package/docs/guides/channels-production-handoff.md +1 -1
- package/docs/guides/connect-whatsapp-zapster.md +1 -0
- package/package.json +1 -1
- package/src/runtime/channels/whatsapp-zapster.ts +198 -22
- package/src/templates/skills/agentkit-channels/references/whatsapp-zapster.md +2 -0
|
@@ -27,7 +27,7 @@ AGENTKIT_RUN_ZAPSTER_CHANNEL_TESTS=1 bun test
|
|
|
27
27
|
|
|
28
28
|
Telegram smoke also requires `TELEGRAM_BOT_TOKEN`, `TELEGRAM_WEBHOOK_SECRET`, and `AGENTKIT_TELEGRAM_WEBHOOK_URL`.
|
|
29
29
|
|
|
30
|
-
Zapster smoke also requires `ZAPSTER_API_KEY`, `
|
|
30
|
+
Zapster smoke also requires `ZAPSTER_API_KEY`, `ZAPSTER_INSTANCE_ID`, and `AGENTKIT_ZAPSTER_TO`. `AGENTKIT_ZAPSTER_SEND_URL` is optional and defaults to `https://api.zapsterapi.com/v1/wa/messages`.
|
|
31
31
|
|
|
32
32
|
## Files Created Or Edited
|
|
33
33
|
|
|
@@ -74,7 +74,7 @@ AGENTKIT_RUN_ZAPSTER_CHANNEL_TESTS=1 bun test
|
|
|
74
74
|
|
|
75
75
|
Telegram smoke requires `TELEGRAM_BOT_TOKEN`, `TELEGRAM_WEBHOOK_SECRET`, and `AGENTKIT_TELEGRAM_WEBHOOK_URL`.
|
|
76
76
|
|
|
77
|
-
Zapster smoke requires `ZAPSTER_API_KEY`, `
|
|
77
|
+
Zapster smoke requires `ZAPSTER_API_KEY`, `ZAPSTER_INSTANCE_ID`, and `AGENTKIT_ZAPSTER_TO`. `AGENTKIT_ZAPSTER_SEND_URL` is optional and defaults to `https://api.zapsterapi.com/v1/wa/messages`.
|
|
78
78
|
|
|
79
79
|
## Smoke Checks
|
|
80
80
|
|
|
@@ -98,6 +98,7 @@ https://<deploy-host>/channels/chn_<id>/whatsapp/zapster/webhook?token=<ZAPSTER_
|
|
|
98
98
|
- Use optional `ZAPSTER_WEBHOOK_TOKEN` in the webhook URL when the endpoint should require an extra secret known only to AgentKit and Zapster.
|
|
99
99
|
- Unsupported media should be logged as skipped/unsupported without creating an agent run.
|
|
100
100
|
- AgentKit handles Zapster `message.received` envelopes with event id at `id`, message text at `data.content.text`, and contact identity at `data.sender.id`.
|
|
101
|
+
- Outbound replies call `POST https://api.zapsterapi.com/v1/wa/messages` with bearer auth and a JSON body containing `recipient`, `text`, and `instance_id`. Only set `AGENTKIT_CHANNEL_SEND_DRY_RUN=1` in tests when Zapster should not receive a real message.
|
|
101
102
|
|
|
102
103
|
## Verification
|
|
103
104
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ChannelAdapter,
|
|
3
3
|
ChannelSendInput,
|
|
4
|
+
ChannelSendResult,
|
|
4
5
|
RawWebhookEvent,
|
|
5
6
|
WebhookVerificationInput,
|
|
6
7
|
WebhookVerificationResult,
|
|
@@ -140,17 +141,7 @@ export const zapsterWhatsappChannelAdapter: ChannelAdapter = {
|
|
|
140
141
|
];
|
|
141
142
|
},
|
|
142
143
|
async sendMessage(input) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
ok: true,
|
|
147
|
-
status: "sent",
|
|
148
|
-
providerRequestId: request.idempotencyKey,
|
|
149
|
-
providerMetadata: {
|
|
150
|
-
method: request.method,
|
|
151
|
-
url: request.url,
|
|
152
|
-
},
|
|
153
|
-
};
|
|
144
|
+
return sendZapsterMessage(input);
|
|
154
145
|
},
|
|
155
146
|
getStatus(input) {
|
|
156
147
|
const missingSecrets = ["ZAPSTER_API_KEY", "ZAPSTER_INSTANCE_ID", "ZAPSTER_WEBHOOK_ID"].filter(
|
|
@@ -248,16 +239,136 @@ export type ZapsterSendMessageRequest = {
|
|
|
248
239
|
method: "POST";
|
|
249
240
|
url: string;
|
|
250
241
|
body: {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
body: string;
|
|
255
|
-
};
|
|
242
|
+
recipient: string;
|
|
243
|
+
text: string;
|
|
244
|
+
instance_id: string;
|
|
256
245
|
};
|
|
257
246
|
idempotencyKey: string;
|
|
258
247
|
};
|
|
259
248
|
|
|
249
|
+
type ZapsterFetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
250
|
+
|
|
251
|
+
const ZAPSTER_SEND_MESSAGE_URL = "https://api.zapsterapi.com/v1/wa/messages";
|
|
252
|
+
|
|
260
253
|
export function buildZapsterSendMessageRequest(input: ChannelSendInput): ZapsterSendMessageRequest {
|
|
254
|
+
return buildZapsterSendMessageRequestInternal(input, "<redacted>");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function sendZapsterMessage(
|
|
258
|
+
input: ChannelSendInput,
|
|
259
|
+
fetcher: ZapsterFetch = fetch,
|
|
260
|
+
): Promise<ChannelSendResult> {
|
|
261
|
+
if (input.secrets.AGENTKIT_CHANNEL_SEND_DRY_RUN === "1") {
|
|
262
|
+
const request = buildZapsterSendMessageRequest(input);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
status: "sent",
|
|
267
|
+
providerRequestId: request.idempotencyKey,
|
|
268
|
+
providerMetadata: {
|
|
269
|
+
method: request.method,
|
|
270
|
+
url: request.url,
|
|
271
|
+
body: request.body,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const apiKey = input.secrets.ZAPSTER_API_KEY;
|
|
277
|
+
const instanceId = input.secrets.ZAPSTER_INSTANCE_ID;
|
|
278
|
+
|
|
279
|
+
if (!apiKey || !instanceId) {
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
retryable: false,
|
|
283
|
+
code: "channel_secret_missing",
|
|
284
|
+
message: "Secret ZAPSTER_API_KEY or ZAPSTER_INSTANCE_ID is missing.",
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let request: ZapsterSendMessageRequest;
|
|
289
|
+
let actualRequest: ZapsterSendMessageRequest;
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
request = buildZapsterSendMessageRequest(input);
|
|
293
|
+
actualRequest = buildZapsterSendMessageRequestInternal(input, instanceId);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
return {
|
|
296
|
+
ok: false,
|
|
297
|
+
retryable: false,
|
|
298
|
+
code: "channel_send_failed",
|
|
299
|
+
message: error instanceof Error ? error.message : String(error),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let response: Response;
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
response = await fetcher(actualRequest.url, {
|
|
307
|
+
method: actualRequest.method,
|
|
308
|
+
headers: {
|
|
309
|
+
Authorization: `Bearer ${apiKey}`,
|
|
310
|
+
"Content-Type": "application/json",
|
|
311
|
+
Accept: "application/json",
|
|
312
|
+
},
|
|
313
|
+
body: JSON.stringify(actualRequest.body),
|
|
314
|
+
});
|
|
315
|
+
} catch (error) {
|
|
316
|
+
const errorMessage = redactString(error instanceof Error ? error.message : String(error), [apiKey, instanceId]);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
ok: false,
|
|
320
|
+
retryable: true,
|
|
321
|
+
code: "channel_provider_unavailable",
|
|
322
|
+
message: `Zapster sendMessage request failed before a response: ${errorMessage}`,
|
|
323
|
+
providerRequestId: request.idempotencyKey,
|
|
324
|
+
providerMetadata: {
|
|
325
|
+
method: request.method,
|
|
326
|
+
url: request.url,
|
|
327
|
+
body: request.body,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const payload = await readZapsterResponseBody(response);
|
|
333
|
+
const redactedPayload = redactValue(payload, [apiKey, instanceId]);
|
|
334
|
+
const messageId = isRecord(payload)
|
|
335
|
+
? readString(payload, "message_id") ?? readString(payload, "id")
|
|
336
|
+
: undefined;
|
|
337
|
+
|
|
338
|
+
if (!response.ok) {
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
342
|
+
code: "channel_send_failed",
|
|
343
|
+
message: redactString(readZapsterErrorMessage(payload, response.status), [apiKey, instanceId]),
|
|
344
|
+
providerRequestId: request.idempotencyKey,
|
|
345
|
+
providerMetadata: {
|
|
346
|
+
method: request.method,
|
|
347
|
+
url: request.url,
|
|
348
|
+
status: response.status,
|
|
349
|
+
body: redactedPayload,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
ok: true,
|
|
356
|
+
status: "sent",
|
|
357
|
+
providerRequestId: messageId ?? request.idempotencyKey,
|
|
358
|
+
...(messageId ? { providerMessageId: messageId } : {}),
|
|
359
|
+
providerMetadata: {
|
|
360
|
+
method: request.method,
|
|
361
|
+
url: request.url,
|
|
362
|
+
status: response.status,
|
|
363
|
+
body: redactedPayload,
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function buildZapsterSendMessageRequestInternal(
|
|
369
|
+
input: ChannelSendInput,
|
|
370
|
+
instanceId: string,
|
|
371
|
+
): ZapsterSendMessageRequest {
|
|
261
372
|
const to = input.externalIdentity.key.match(/contact:([^:]+)$/)?.[1];
|
|
262
373
|
|
|
263
374
|
if (!to) {
|
|
@@ -266,18 +377,83 @@ export function buildZapsterSendMessageRequest(input: ChannelSendInput): Zapster
|
|
|
266
377
|
|
|
267
378
|
return {
|
|
268
379
|
method: "POST",
|
|
269
|
-
url:
|
|
380
|
+
url: ZAPSTER_SEND_MESSAGE_URL,
|
|
270
381
|
body: {
|
|
271
|
-
to,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
body: input.message.content,
|
|
275
|
-
},
|
|
382
|
+
recipient: to,
|
|
383
|
+
text: input.message.content,
|
|
384
|
+
instance_id: instanceId,
|
|
276
385
|
},
|
|
277
386
|
idempotencyKey: `whatsapp:zapster:${input.channelId}:${input.conversationId}:${redactPhone(to)}`,
|
|
278
387
|
};
|
|
279
388
|
}
|
|
280
389
|
|
|
390
|
+
async function readZapsterResponseBody(response: Response): Promise<unknown> {
|
|
391
|
+
const text = await response.text().catch(() => "");
|
|
392
|
+
|
|
393
|
+
if (!text) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
return JSON.parse(text) as unknown;
|
|
399
|
+
} catch {
|
|
400
|
+
return text;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function readZapsterErrorMessage(payload: unknown, status: number): string {
|
|
405
|
+
if (typeof payload === "string" && payload.length > 0) {
|
|
406
|
+
return payload;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (isRecord(payload)) {
|
|
410
|
+
const message = readString(payload, "message") ?? readString(payload, "error") ?? readString(payload, "detail");
|
|
411
|
+
|
|
412
|
+
if (message) {
|
|
413
|
+
return message;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const error = readRecord(payload, "error");
|
|
417
|
+
const nestedMessage = readString(error, "message") ?? readString(error, "detail");
|
|
418
|
+
|
|
419
|
+
if (nestedMessage) {
|
|
420
|
+
return nestedMessage;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return `Zapster sendMessage returned HTTP ${status}.`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function redactValue(value: unknown, secrets: string[]): unknown {
|
|
428
|
+
if (typeof value === "string") {
|
|
429
|
+
return redactString(value, secrets);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (!isRecord(value) && !Array.isArray(value)) {
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const serialized = redactString(JSON.stringify(value), secrets);
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
return JSON.parse(serialized) as unknown;
|
|
440
|
+
} catch {
|
|
441
|
+
return serialized;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function redactString(value: string, secrets: string[]): string {
|
|
446
|
+
let redacted = value;
|
|
447
|
+
|
|
448
|
+
for (const secret of secrets) {
|
|
449
|
+
if (secret) {
|
|
450
|
+
redacted = redacted.split(secret).join("<redacted>");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return redacted;
|
|
455
|
+
}
|
|
456
|
+
|
|
281
457
|
function parseJsonObject(input: RawWebhookEvent): Record<string, unknown> {
|
|
282
458
|
try {
|
|
283
459
|
const parsed = JSON.parse(input.rawBody) as unknown;
|
|
@@ -29,6 +29,8 @@ Paste the stable AgentKit webhook URL into Zapster settings. If the channel decl
|
|
|
29
29
|
|
|
30
30
|
AgentKit handles Zapster `message.received` envelopes with event id at `id`, message text at `data.content.text`, and contact identity at `data.sender.id`. Unsupported media should be logged as skipped/unsupported without creating an agent run.
|
|
31
31
|
|
|
32
|
+
Outbound replies call `POST https://api.zapsterapi.com/v1/wa/messages` with bearer auth and a JSON body containing `recipient`, `text`, and `instance_id`. Only set `AGENTKIT_CHANNEL_SEND_DRY_RUN=1` in tests when Zapster should not receive a real message.
|
|
33
|
+
|
|
32
34
|
Buffer rapid WhatsApp messages:
|
|
33
35
|
|
|
34
36
|
```ts
|