@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.
@@ -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`, `AGENTKIT_ZAPSTER_SEND_URL`, and `AGENTKIT_ZAPSTER_TO`.
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`, `AGENTKIT_ZAPSTER_SEND_URL`, and `AGENTKIT_ZAPSTER_TO`.
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,6 @@
1
1
  {
2
2
  "name": "@andreprado/agentkit",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "repository": {
@@ -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
- const request = buildZapsterSendMessageRequest(input);
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
- to: string;
252
- type: "text";
253
- text: {
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: "https://api.zapster.example/messages",
380
+ url: ZAPSTER_SEND_MESSAGE_URL,
270
381
  body: {
271
- to,
272
- type: "text",
273
- text: {
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