@naturalpay/sdk 0.1.3 → 0.2.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/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var async_hooks = require('async_hooks');
4
+ var crypto = require('crypto');
4
5
 
5
6
  // src/errors.ts
6
7
  var NaturalError = class extends Error {
@@ -60,9 +61,15 @@ var ServerError = class extends NaturalError {
60
61
  this.name = "ServerError";
61
62
  }
62
63
  };
64
+ var WebhookVerificationError = class extends NaturalError {
65
+ constructor(message) {
66
+ super(message, { code: "webhook_verification_failed" });
67
+ this.name = "WebhookVerificationError";
68
+ }
69
+ };
63
70
 
64
71
  // src/version.ts
65
- var VERSION = "0.1.3";
72
+ var VERSION = "0.1.4";
66
73
 
67
74
  // src/logging.ts
68
75
  var LOG_LEVEL_VALUES = {
@@ -253,7 +260,7 @@ function getLogger(name) {
253
260
  }
254
261
  return new Logger(name);
255
262
  }
256
- function logError(logger2, message, options) {
263
+ function logError(logger3, message, options) {
257
264
  const extra = { ...options };
258
265
  if (options?.error) {
259
266
  extra["errorType"] = options.error.name;
@@ -263,9 +270,9 @@ function logError(logger2, message, options) {
263
270
  }
264
271
  delete extra["error"];
265
272
  }
266
- logger2.error(message, extra);
273
+ logger3.error(message, extra);
267
274
  }
268
- function logApiCall(logger2, method, path, options) {
275
+ function logApiCall(logger3, method, path, options) {
269
276
  const extra = {
270
277
  method,
271
278
  path,
@@ -278,14 +285,14 @@ function logApiCall(logger2, method, path, options) {
278
285
  extra["durationMs"] = Math.round(options.durationMs * 100) / 100;
279
286
  }
280
287
  if (options?.error) {
281
- logError(logger2, `API call failed: ${method} ${path}`, extra);
288
+ logError(logger3, `API call failed: ${method} ${path}`, extra);
282
289
  } else if (options?.statusCode && options.statusCode >= 400) {
283
- logger2.warning(`API call error: ${method} ${path} -> ${options.statusCode}`, extra);
290
+ logger3.warning(`API call error: ${method} ${path} -> ${options.statusCode}`, extra);
284
291
  } else {
285
- logger2.info(`API call: ${method} ${path} -> ${options?.statusCode}`, extra);
292
+ logger3.info(`API call: ${method} ${path} -> ${options?.statusCode}`, extra);
286
293
  }
287
294
  }
288
- function logToolCall(logger2, toolName, options) {
295
+ function logToolCall(logger3, toolName, options) {
289
296
  const extra = {
290
297
  toolName,
291
298
  ...options
@@ -294,22 +301,79 @@ function logToolCall(logger2, toolName, options) {
294
301
  extra["durationMs"] = Math.round(options.durationMs * 100) / 100;
295
302
  }
296
303
  if (options?.error) {
297
- logError(logger2, `Tool call failed: ${toolName}`, extra);
304
+ logError(logger3, `Tool call failed: ${toolName}`, extra);
298
305
  } else if (options?.success === false) {
299
- logger2.warning(`Tool call returned error: ${toolName}`, extra);
306
+ logger3.warning(`Tool call returned error: ${toolName}`, extra);
300
307
  } else {
301
- logger2.info(`Tool call: ${toolName}`, extra);
308
+ logger3.info(`Tool call: ${toolName}`, extra);
309
+ }
310
+ }
311
+ function configHash(cfg) {
312
+ const content = JSON.stringify({
313
+ system_prompt: cfg.systemPrompt,
314
+ tools_available: [...cfg.toolsAvailable].sort()
315
+ });
316
+ return crypto.createHash("sha256").update(content).digest("hex");
317
+ }
318
+ function agentConfigToDict(cfg) {
319
+ return {
320
+ system_prompt: cfg.systemPrompt,
321
+ tools_available: [...cfg.toolsAvailable].sort()
322
+ };
323
+ }
324
+ function modelUsageToDict(usage) {
325
+ const dict = {};
326
+ if (usage.model) {
327
+ dict.model = usage.model;
328
+ }
329
+ if (usage.provider) {
330
+ dict.provider = usage.provider;
331
+ }
332
+ if (usage.inputTokens != null) {
333
+ dict.input_tokens = usage.inputTokens;
334
+ }
335
+ if (usage.outputTokens != null) {
336
+ dict.output_tokens = usage.outputTokens;
337
+ }
338
+ if (usage.toolsCalled != null && usage.toolsCalled.length > 0) {
339
+ dict.tools_called = usage.toolsCalled;
302
340
  }
341
+ return dict;
303
342
  }
343
+ var logger = getLogger("tool-call-context");
304
344
  var toolCallStorage = new async_hooks.AsyncLocalStorage();
345
+ function generateToolCallId() {
346
+ return `tc_${crypto.randomUUID()}`;
347
+ }
305
348
  function getToolCallHeader() {
306
349
  const data = toolCallStorage.getStore();
307
350
  if (!data) return void 0;
308
- return btoa(JSON.stringify(data));
351
+ const full = Buffer.from(JSON.stringify(data)).toString("base64");
352
+ if (full.length <= 16 * 1024) return full;
353
+ logger.warning("Tool call header exceeds 16KB, omitting arguments", {
354
+ toolCallId: data.tool_call_id,
355
+ toolName: data.tool_name,
356
+ fullSizeBytes: full.length
357
+ });
358
+ const slim = {
359
+ tool_call_id: data.tool_call_id,
360
+ tool_name: data.tool_name
361
+ };
362
+ return Buffer.from(JSON.stringify(slim)).toString("base64");
363
+ }
364
+ function runWithToolCall(toolCallId, name, args, fn) {
365
+ return toolCallStorage.run(
366
+ {
367
+ tool_call_id: toolCallId,
368
+ tool_name: name,
369
+ tool_call_arguments: JSON.stringify(args)
370
+ },
371
+ fn
372
+ );
309
373
  }
310
374
 
311
375
  // src/http.ts
312
- var logger = getLogger("http");
376
+ var logger2 = getLogger("http");
313
377
  var DEFAULT_BASE_URL = "https://api.natural.co";
314
378
  var DEFAULT_TIMEOUT = 3e4;
315
379
  var API_KEY_PREFIX_REGEX = /^sk_ntl_(dev|sandbox|prod)_/;
@@ -354,11 +418,46 @@ function hashString(str) {
354
418
  }
355
419
  return Math.abs(hash).toString(16).slice(0, 16);
356
420
  }
357
- var HTTPClient = class {
421
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
422
+ function hasIdempotencyKey(headers) {
423
+ return Object.entries(headers).some(
424
+ ([k, v]) => k.toLowerCase() === "idempotency-key" && v !== ""
425
+ );
426
+ }
427
+ function shouldRetry(method, headers, error, attempt, maxRetries) {
428
+ if (attempt >= maxRetries) return false;
429
+ if (error instanceof AuthenticationError) return false;
430
+ if (error instanceof InvalidRequestError) return false;
431
+ if (!(error instanceof RateLimitError) && error instanceof NaturalError && error.statusCode && error.statusCode >= 400 && error.statusCode < 500) {
432
+ return false;
433
+ }
434
+ const isRetryable = error instanceof ServerError || error instanceof RateLimitError || error instanceof NaturalError && !(error instanceof AuthenticationError);
435
+ if (!isRetryable) return false;
436
+ if (SAFE_METHODS.has(method.toUpperCase())) return true;
437
+ return hasIdempotencyKey(headers);
438
+ }
439
+ var MAX_RETRY_AFTER_MS = 6e4;
440
+ function calculateRetryDelay(attempt, retryAfterSeconds) {
441
+ if (retryAfterSeconds != null && Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0) {
442
+ return Math.min(retryAfterSeconds * 1e3, MAX_RETRY_AFTER_MS);
443
+ }
444
+ const jitter = 1 + Math.random() * 0.25;
445
+ return Math.min(500 * Math.pow(2, attempt) * jitter, 5e3);
446
+ }
447
+ var HTTPClient = class _HTTPClient {
358
448
  apiKey;
359
449
  baseUrl;
360
450
  timeout;
451
+ maxRetries;
361
452
  jwtCache = /* @__PURE__ */ new Map();
453
+ agentConfig;
454
+ _defaultModelUsage;
455
+ _configResolved = false;
456
+ _configAttempted = false;
457
+ _registerConfigPromise = null;
458
+ _nextRetryAt = 0;
459
+ _retryBackoffMs = 1e3;
460
+ static MAX_RETRY_BACKOFF_MS = 5 * 60 * 1e3;
362
461
  constructor(options = {}) {
363
462
  this.apiKey = options.apiKey ?? process.env["NATURAL_API_KEY"] ?? "";
364
463
  this.baseUrl = (options.baseUrl ?? process.env["NATURAL_SERVER_URL"] ?? DEFAULT_BASE_URL).replace(/\/$/, "");
@@ -367,9 +466,25 @@ var HTTPClient = class {
367
466
  parseApiKeyEnv(this.apiKey);
368
467
  }
369
468
  this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
469
+ this.agentConfig = options.agentConfig ? {
470
+ systemPrompt: options.agentConfig.systemPrompt,
471
+ toolsAvailable: [...options.agentConfig.toolsAvailable]
472
+ } : void 0;
473
+ if (options.defaultModelUsage) {
474
+ this._defaultModelUsage = {
475
+ model: options.defaultModelUsage.model,
476
+ provider: options.defaultModelUsage.provider
477
+ };
478
+ }
479
+ const maxRetries = options.maxRetries ?? 2;
480
+ if (!Number.isInteger(maxRetries) || maxRetries < 0) {
481
+ throw new InvalidRequestError("maxRetries must be a non-negative integer");
482
+ }
483
+ this.maxRetries = maxRetries;
370
484
  }
371
485
  /**
372
486
  * Get a cached JWT or exchange API key for a new one.
487
+ * Retries on transient failures (5xx, network) but not on 401.
373
488
  */
374
489
  async getJwt() {
375
490
  if (!this.apiKey) {
@@ -380,57 +495,90 @@ var HTTPClient = class {
380
495
  if (cached && Date.now() < cached.expiresAt) {
381
496
  return cached.token;
382
497
  }
383
- const controller = new AbortController();
384
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
385
- logger.debug("Exchanging API key for JWT", { path: "/auth/api/token" });
386
- try {
387
- const response = await fetch(`${this.baseUrl}/auth/api/token`, {
388
- method: "POST",
389
- headers: {
390
- Authorization: `Bearer ${this.apiKey}`,
391
- "Content-Type": "application/json"
392
- },
393
- signal: controller.signal
394
- });
395
- clearTimeout(timeoutId);
396
- if (!response.ok) {
397
- const authError = new AuthenticationError(
398
- `Authentication failed (status=${response.status})`
399
- );
400
- logError(logger, "JWT exchange failed", {
401
- error: authError,
402
- statusCode: response.status,
403
- path: "/auth/api/token"
498
+ logger2.debug("Exchanging API key for JWT", { path: "/auth/api/token" });
499
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
500
+ const controller = new AbortController();
501
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
502
+ try {
503
+ const response = await fetch(`${this.baseUrl}/auth/api/token`, {
504
+ method: "POST",
505
+ headers: {
506
+ Authorization: `Bearer ${this.apiKey}`,
507
+ "Content-Type": "application/json"
508
+ },
509
+ signal: controller.signal
404
510
  });
405
- throw authError;
406
- }
407
- const data = await response.json();
408
- const expiresIn = data.expiresIn ?? 900;
409
- const expiresAt = Date.now() + (expiresIn - 30) * 1e3;
410
- this.jwtCache.set(cacheKey, { token: data.accessToken, expiresAt });
411
- return data.accessToken;
412
- } catch (error) {
413
- clearTimeout(timeoutId);
414
- if (error instanceof AuthenticationError) {
415
- throw error;
416
- }
417
- if (error instanceof Error && error.name === "AbortError") {
418
- const networkError2 = new NaturalError("Request timed out during authentication");
419
- logError(logger, "JWT exchange network error", {
420
- error: networkError2,
511
+ clearTimeout(timeoutId);
512
+ if (response.status === 429) {
513
+ const retryAfter = response.headers.get("Retry-After");
514
+ const rateError = new RateLimitError(
515
+ "JWT exchange rate limited",
516
+ retryAfter ? parseInt(retryAfter, 10) : void 0
517
+ );
518
+ throw rateError;
519
+ }
520
+ if (response.status >= 400 && response.status < 500) {
521
+ const authError = new AuthenticationError(
522
+ `Authentication failed (status=${response.status})`
523
+ );
524
+ logError(logger2, "JWT exchange failed", {
525
+ error: authError,
526
+ statusCode: response.status,
527
+ path: "/auth/api/token"
528
+ });
529
+ throw authError;
530
+ }
531
+ if (response.status >= 500) {
532
+ const serverError = new ServerError(`JWT exchange failed (status=${response.status})`);
533
+ logError(logger2, "JWT exchange server error", {
534
+ error: serverError,
535
+ statusCode: response.status,
536
+ path: "/auth/api/token"
537
+ });
538
+ throw serverError;
539
+ }
540
+ const data = await response.json();
541
+ const expiresIn = data.expiresIn ?? 900;
542
+ const expiresAt = Date.now() + (expiresIn - 30) * 1e3;
543
+ this.jwtCache.set(cacheKey, { token: data.accessToken, expiresAt });
544
+ return data.accessToken;
545
+ } catch (error) {
546
+ clearTimeout(timeoutId);
547
+ if (error instanceof AuthenticationError) {
548
+ throw error;
549
+ }
550
+ let sdkError;
551
+ if (error instanceof NaturalError) {
552
+ sdkError = error;
553
+ } else if (error instanceof Error && error.name === "AbortError") {
554
+ sdkError = new NaturalError("Request timed out during authentication");
555
+ } else {
556
+ sdkError = new NaturalError(
557
+ `Network error during authentication: ${error instanceof Error ? error.message : "Unknown error"}`
558
+ );
559
+ }
560
+ if (attempt < this.maxRetries) {
561
+ const retryAfter = sdkError instanceof RateLimitError ? sdkError.retryAfter : void 0;
562
+ const delay = calculateRetryDelay(attempt, retryAfter);
563
+ const reason = sdkError instanceof RateLimitError ? "429 rate limited" : sdkError instanceof ServerError ? `status ${sdkError.statusCode}` : "network error";
564
+ logger2.warning(`Retrying JWT exchange (attempt ${attempt + 1}/${this.maxRetries})`, {
565
+ path: "/auth/api/token",
566
+ attempt: attempt + 1,
567
+ maxRetries: this.maxRetries,
568
+ delayMs: Math.round(delay),
569
+ reason
570
+ });
571
+ await new Promise((resolve) => setTimeout(resolve, delay));
572
+ continue;
573
+ }
574
+ logError(logger2, "JWT exchange failed after retries", {
575
+ error: sdkError,
421
576
  path: "/auth/api/token"
422
577
  });
423
- throw networkError2;
578
+ throw sdkError;
424
579
  }
425
- const networkError = new NaturalError(
426
- `Network error during authentication: ${error instanceof Error ? error.message : "Unknown error"}`
427
- );
428
- logError(logger, "JWT exchange network error", {
429
- error: networkError,
430
- path: "/auth/api/token"
431
- });
432
- throw networkError;
433
580
  }
581
+ throw new NaturalError("Unexpected retry exhaustion during JWT exchange");
434
582
  }
435
583
  /**
436
584
  * Build URL with query parameters.
@@ -452,7 +600,7 @@ var HTTPClient = class {
452
600
  async handleResponse(response, method, path, durationMs) {
453
601
  if (response.status === 401) {
454
602
  const authError = new AuthenticationError();
455
- logApiCall(logger, method, path, {
603
+ logApiCall(logger2, method, path, {
456
604
  statusCode: response.status,
457
605
  durationMs,
458
606
  error: authError
@@ -465,7 +613,7 @@ var HTTPClient = class {
465
613
  "Rate limit exceeded",
466
614
  retryAfter ? parseInt(retryAfter, 10) : void 0
467
615
  );
468
- logger.warning(`Rate limited: ${method} ${path}`, {
616
+ logger2.warning(`Rate limited: ${method} ${path}`, {
469
617
  method,
470
618
  path,
471
619
  statusCode: response.status,
@@ -476,7 +624,7 @@ var HTTPClient = class {
476
624
  }
477
625
  if (response.status >= 500) {
478
626
  const serverError = new ServerError(`Server error: ${response.status}`);
479
- logApiCall(logger, method, path, {
627
+ logApiCall(logger2, method, path, {
480
628
  statusCode: response.status,
481
629
  durationMs,
482
630
  error: serverError
@@ -489,17 +637,18 @@ var HTTPClient = class {
489
637
  data = text ? JSON.parse(text) : {};
490
638
  } catch {
491
639
  if (response.status >= 400) {
492
- const parseError = new NaturalError(`Request failed: ${response.status}`, {
493
- statusCode: response.status
494
- });
495
- logApiCall(logger, method, path, {
640
+ const parseError = new NaturalError(
641
+ `Request failed: ${response.status} (non-JSON response)`,
642
+ { statusCode: response.status, code: "parse_error" }
643
+ );
644
+ logApiCall(logger2, method, path, {
496
645
  statusCode: response.status,
497
646
  durationMs,
498
647
  error: parseError
499
648
  });
500
649
  throw parseError;
501
650
  }
502
- logApiCall(logger, method, path, { statusCode: response.status, durationMs });
651
+ logApiCall(logger2, method, path, { statusCode: response.status, durationMs });
503
652
  return {};
504
653
  }
505
654
  if (response.status >= 400) {
@@ -512,69 +661,185 @@ var HTTPClient = class {
512
661
  `${errorMessage} (status=${response.status})`,
513
662
  errorCode
514
663
  );
515
- logApiCall(logger, method, path, {
664
+ logApiCall(logger2, method, path, {
516
665
  statusCode: response.status,
517
666
  durationMs,
518
667
  error: requestError
519
668
  });
520
669
  throw requestError;
521
670
  }
522
- logApiCall(logger, method, path, { statusCode: response.status, durationMs });
671
+ logApiCall(logger2, method, path, { statusCode: response.status, durationMs });
523
672
  return data;
524
673
  }
525
674
  /**
526
- * Make an authenticated request.
675
+ * Register agent config with the observability service.
676
+ *
677
+ * Called lazily before the first request. Errors are caught and logged,
678
+ * never thrown.
527
679
  */
528
- async request(method, path, options) {
529
- const jwt = await this.getJwt();
530
- const url = this.buildUrl(path, options?.params);
531
- const controller = new AbortController();
532
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
533
- logger.debug(`API request: ${method} ${path}`, {
534
- method,
535
- path,
536
- hasBody: !!options?.body
537
- });
538
- const startTime = Date.now();
680
+ async registerConfig() {
681
+ if (!this.agentConfig) return;
539
682
  try {
540
- const headers = {
541
- Authorization: `Bearer ${jwt}`,
542
- "Content-Type": "application/json",
543
- "User-Agent": `naturalpay-ts/${VERSION}`
683
+ const jwt = await this.getJwt();
684
+ const hash = configHash(this.agentConfig);
685
+ const body = {
686
+ config_hash: hash,
687
+ ...agentConfigToDict(this.agentConfig)
544
688
  };
545
- const toolCallHeader = getToolCallHeader();
546
- if (toolCallHeader) {
547
- headers["X-Tool-Call"] = toolCallHeader;
548
- }
549
- if (options?.headers) {
550
- Object.assign(headers, options.headers);
689
+ const controller = new AbortController();
690
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
691
+ try {
692
+ const response = await fetch(`${this.baseUrl}/agents/config`, {
693
+ method: "POST",
694
+ headers: {
695
+ Authorization: `Bearer ${jwt}`,
696
+ "Content-Type": "application/json",
697
+ "User-Agent": `naturalpay-ts/${VERSION}`
698
+ },
699
+ body: JSON.stringify(body),
700
+ signal: controller.signal
701
+ });
702
+ if (response.ok) {
703
+ this._configResolved = true;
704
+ } else if (response.status === 429 || response.status === 408 || response.status >= 500) {
705
+ this._nextRetryAt = Date.now() + this._retryBackoffMs;
706
+ this._retryBackoffMs = Math.min(
707
+ this._retryBackoffMs * 2,
708
+ _HTTPClient.MAX_RETRY_BACKOFF_MS
709
+ );
710
+ logger2.warning("Agent config registration failed (transient)", {
711
+ statusCode: response.status
712
+ });
713
+ } else {
714
+ this._configResolved = true;
715
+ logger2.warning("Agent config registration failed (permanent)", {
716
+ statusCode: response.status
717
+ });
718
+ }
719
+ } finally {
720
+ clearTimeout(timeoutId);
551
721
  }
552
- const response = await fetch(url, {
553
- method,
554
- headers,
555
- body: options?.body ? JSON.stringify(options.body) : void 0,
556
- signal: controller.signal
557
- });
558
- clearTimeout(timeoutId);
559
- const durationMs = Date.now() - startTime;
560
- return this.handleResponse(response, method, path, durationMs);
561
722
  } catch (error) {
562
- clearTimeout(timeoutId);
563
- const durationMs = Date.now() - startTime;
564
- if (error instanceof NaturalError || error instanceof AuthenticationError || error instanceof InvalidRequestError || error instanceof RateLimitError || error instanceof ServerError) {
565
- throw error;
723
+ this._nextRetryAt = Date.now() + this._retryBackoffMs;
724
+ this._retryBackoffMs = Math.min(this._retryBackoffMs * 2, _HTTPClient.MAX_RETRY_BACKOFF_MS);
725
+ logger2.warning("Failed to register agent config", {
726
+ error: error instanceof Error ? error.message : "Unknown error"
727
+ });
728
+ } finally {
729
+ this._configAttempted = true;
730
+ }
731
+ }
732
+ /**
733
+ * Add agent config hash and model usage headers if configured.
734
+ */
735
+ injectModelContextHeaders(headers, modelUsage) {
736
+ if (this.agentConfig) {
737
+ headers["X-Model-Config-Hash"] = configHash(this.agentConfig);
738
+ }
739
+ const effectiveUsage = (() => {
740
+ if (!modelUsage && !this._defaultModelUsage) return void 0;
741
+ return {
742
+ model: modelUsage?.model ?? this._defaultModelUsage?.model,
743
+ provider: modelUsage?.provider ?? this._defaultModelUsage?.provider,
744
+ inputTokens: modelUsage?.inputTokens,
745
+ outputTokens: modelUsage?.outputTokens,
746
+ toolsCalled: modelUsage?.toolsCalled
747
+ };
748
+ })();
749
+ if (effectiveUsage) {
750
+ const usageDict = modelUsageToDict(effectiveUsage);
751
+ if (Object.keys(usageDict).length > 0) {
752
+ headers["X-Model-Usage"] = Buffer.from(JSON.stringify(usageDict)).toString("base64");
566
753
  }
567
- if (error instanceof Error && error.name === "AbortError") {
568
- const networkError2 = new NaturalError("Request timed out");
569
- logApiCall(logger, method, path, { durationMs, error: networkError2 });
570
- throw networkError2;
754
+ }
755
+ }
756
+ /**
757
+ * Make an authenticated request with automatic retries.
758
+ */
759
+ async request(method, path, options) {
760
+ if (this.agentConfig && !this._configResolved) {
761
+ if (!this._registerConfigPromise) {
762
+ if (!this._configAttempted || Date.now() >= this._nextRetryAt) {
763
+ this._registerConfigPromise = this.registerConfig().catch(() => {
764
+ }).finally(() => {
765
+ this._registerConfigPromise = null;
766
+ });
767
+ }
768
+ }
769
+ if (!this._configAttempted) {
770
+ await this._registerConfigPromise;
571
771
  }
572
- const networkError = new NaturalError(
573
- `Network error: ${error instanceof Error ? error.message : "Unknown error"}`
574
- );
575
- logApiCall(logger, method, path, { durationMs, error: networkError });
576
- throw networkError;
577
772
  }
773
+ const jwt = await this.getJwt();
774
+ const url = this.buildUrl(path, options?.params);
775
+ const mergedHeaders = {
776
+ Authorization: `Bearer ${jwt}`,
777
+ "Content-Type": "application/json",
778
+ "User-Agent": `naturalpay-ts/${VERSION}`
779
+ };
780
+ const toolCallHeader = getToolCallHeader();
781
+ if (toolCallHeader) {
782
+ mergedHeaders["X-Tool-Call"] = toolCallHeader;
783
+ }
784
+ this.injectModelContextHeaders(mergedHeaders, options?.modelUsage);
785
+ if (options?.headers) {
786
+ Object.assign(mergedHeaders, options.headers);
787
+ }
788
+ const bodyStr = options?.body ? JSON.stringify(options.body) : void 0;
789
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
790
+ const controller = new AbortController();
791
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
792
+ const startTime = Date.now();
793
+ if (attempt === 0) {
794
+ logger2.debug(`API request: ${method} ${path}`, {
795
+ method,
796
+ path,
797
+ hasBody: !!options?.body
798
+ });
799
+ }
800
+ try {
801
+ const response = await fetch(url, {
802
+ method,
803
+ headers: mergedHeaders,
804
+ body: bodyStr,
805
+ signal: controller.signal
806
+ });
807
+ clearTimeout(timeoutId);
808
+ const durationMs = Date.now() - startTime;
809
+ return await this.handleResponse(response, method, path, durationMs);
810
+ } catch (error) {
811
+ clearTimeout(timeoutId);
812
+ const durationMs = Date.now() - startTime;
813
+ let sdkError;
814
+ if (error instanceof NaturalError) {
815
+ sdkError = error;
816
+ } else if (error instanceof Error && error.name === "AbortError") {
817
+ sdkError = new NaturalError("Request timed out");
818
+ logApiCall(logger2, method, path, { durationMs, error: sdkError });
819
+ } else {
820
+ sdkError = new NaturalError(
821
+ `Network error: ${error instanceof Error ? error.message : "Unknown error"}`
822
+ );
823
+ logApiCall(logger2, method, path, { durationMs, error: sdkError });
824
+ }
825
+ if (shouldRetry(method, mergedHeaders, sdkError, attempt, this.maxRetries)) {
826
+ const retryAfter = sdkError instanceof RateLimitError ? sdkError.retryAfter : void 0;
827
+ const delay = calculateRetryDelay(attempt, retryAfter);
828
+ logger2.warning(`Retrying ${method} ${path} (attempt ${attempt + 1}/${this.maxRetries})`, {
829
+ method,
830
+ path,
831
+ attempt: attempt + 1,
832
+ maxRetries: this.maxRetries,
833
+ delayMs: Math.round(delay),
834
+ reason: sdkError instanceof ServerError ? `status ${sdkError.statusCode}` : sdkError instanceof RateLimitError ? "429 rate limited" : "network error"
835
+ });
836
+ await new Promise((resolve) => setTimeout(resolve, delay));
837
+ continue;
838
+ }
839
+ throw sdkError;
840
+ }
841
+ }
842
+ throw new NaturalError("Unexpected retry exhaustion");
578
843
  }
579
844
  async get(path, options) {
580
845
  return this.request("GET", path, options);
@@ -597,6 +862,9 @@ var BaseResource = class {
597
862
  this.http = http;
598
863
  }
599
864
  };
865
+ function sanitizeHeaderValue(value) {
866
+ return value.replace(/[\x00-\x1f\x7f]/g, "");
867
+ }
600
868
 
601
869
  // src/resources/transactions.ts
602
870
  function unwrapTransactionResource(resource) {
@@ -611,8 +879,9 @@ function unwrapTransactionResource(resource) {
611
879
  amount: Number(attributes.amount),
612
880
  currency: String(attributes.currency),
613
881
  status: String(attributes.status),
882
+ escalationId: attributes.escalationId != null ? String(attributes.escalationId) : void 0,
883
+ disputeId: attributes.disputeId != null ? String(attributes.disputeId) : void 0,
614
884
  description: attributes.description != null ? String(attributes.description) : void 0,
615
- memo: attributes.memo != null ? String(attributes.memo) : void 0,
616
885
  createdAt: String(attributes.createdAt),
617
886
  updatedAt: attributes.updatedAt != null ? String(attributes.updatedAt) : void 0,
618
887
  isDelegated: Boolean(attributes.isDelegated),
@@ -620,6 +889,8 @@ function unwrapTransactionResource(resource) {
620
889
  customerAgentId: attributes.customerAgentId != null ? String(attributes.customerAgentId) : void 0,
621
890
  senderName: attributes.senderName != null ? String(attributes.senderName) : void 0,
622
891
  recipientName: attributes.recipientName != null ? String(attributes.recipientName) : void 0,
892
+ initiatorName: attributes.initiatorName != null ? String(attributes.initiatorName) : void 0,
893
+ initiatorIsAgent: attributes.initiatorIsAgent != null ? Boolean(attributes.initiatorIsAgent) : void 0,
623
894
  transactionType: String(attributes.transactionType),
624
895
  category: String(attributes.category),
625
896
  direction: String(attributes.direction),
@@ -641,9 +912,15 @@ var TransactionsResource = class extends BaseResource {
641
912
  */
642
913
  async get(transactionId, params) {
643
914
  const headers = {};
915
+ if (params?.agentId) {
916
+ headers["X-Agent-ID"] = params.agentId;
917
+ }
644
918
  if (params?.instanceId) {
645
919
  headers["X-Instance-ID"] = params.instanceId;
646
920
  }
921
+ if (params?.traceId) {
922
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
923
+ }
647
924
  const queryParams = {};
648
925
  if (params?.customerPartyId) {
649
926
  queryParams["partyId"] = params.customerPartyId;
@@ -674,6 +951,9 @@ var TransactionsResource = class extends BaseResource {
674
951
  if (params?.instanceId) {
675
952
  headers["X-Instance-ID"] = params.instanceId;
676
953
  }
954
+ if (params?.traceId) {
955
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
956
+ }
677
957
  const queryParams = {
678
958
  limit: params?.limit ?? 50,
679
959
  cursor: params?.cursor,
@@ -725,8 +1005,6 @@ function unwrapPaymentResponse(response) {
725
1005
  transactionType: String(attributes.transactionType ?? "payment"),
726
1006
  category: String(attributes.category ?? "sent"),
727
1007
  direction: String(attributes.direction ?? "OUTBOUND"),
728
- // Mirror description into memo for payment convenience.
729
- memo: base.description,
730
1008
  // Payments use customerParty/counterparty relationship keys,
731
1009
  // falling back to the generic sourceParty/destinationParty.
732
1010
  sourcePartyId: relationships?.customerParty?.data?.id ?? base.sourcePartyId,
@@ -750,9 +1028,9 @@ var PaymentsResource = class extends BaseResource {
750
1028
  amount: params.amount,
751
1029
  currency: params.currency ?? "USD",
752
1030
  counterparty: recipient,
753
- customerPartyId: params.customerPartyId
1031
+ description: params.description,
1032
+ ...params.customerPartyId != null ? { customerPartyId: params.customerPartyId } : {}
754
1033
  };
755
- attributes["description"] = params.memo;
756
1034
  const body = { data: { attributes } };
757
1035
  const headers = {
758
1036
  "Idempotency-Key": params.idempotencyKey
@@ -766,6 +1044,9 @@ var PaymentsResource = class extends BaseResource {
766
1044
  if (params.instanceId) {
767
1045
  headers["X-Instance-ID"] = params.instanceId;
768
1046
  }
1047
+ if (params.traceId) {
1048
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1049
+ }
769
1050
  const response = await this.http.post("/payments", {
770
1051
  body,
771
1052
  headers
@@ -798,9 +1079,9 @@ function unwrapBalance(response) {
798
1079
  const breakdown = {
799
1080
  operatingFunded: toAmountInfo(rawBreakdown?.operatingFunded),
800
1081
  operatingAdvanced: toAmountInfo(rawBreakdown?.operatingAdvanced),
801
- escrowFundedSettled: toAmountInfo(rawBreakdown?.escrowFundedSettled),
802
- escrowAdvanced: toAmountInfo(rawBreakdown?.escrowAdvanced),
803
- holdsOutbound: toAmountInfo(rawBreakdown?.holdsOutbound)
1082
+ pendingIn: toAmountInfo(rawBreakdown?.pendingIn),
1083
+ pendingOut: toAmountInfo(rawBreakdown?.pendingOut),
1084
+ held: toAmountInfo(rawBreakdown?.held)
804
1085
  };
805
1086
  return {
806
1087
  walletId: id,
@@ -848,9 +1129,15 @@ var WalletResource = class extends BaseResource {
848
1129
  */
849
1130
  async balance(options) {
850
1131
  const headers = {};
1132
+ if (options?.agentId) {
1133
+ headers["X-Agent-ID"] = options.agentId;
1134
+ }
851
1135
  if (options?.instanceId) {
852
1136
  headers["X-Instance-ID"] = options.instanceId;
853
1137
  }
1138
+ if (options?.traceId) {
1139
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1140
+ }
854
1141
  const params = {};
855
1142
  if (options?.customerPartyId) {
856
1143
  params["partyId"] = options.customerPartyId;
@@ -878,9 +1165,21 @@ var WalletResource = class extends BaseResource {
878
1165
  };
879
1166
  if (params.description) attributes["description"] = params.description;
880
1167
  const body = { data: { attributes } };
1168
+ const headers = {
1169
+ "Idempotency-Key": params.idempotencyKey
1170
+ };
1171
+ if (params.agentId) {
1172
+ headers["X-Agent-ID"] = params.agentId;
1173
+ }
1174
+ if (params.instanceId) {
1175
+ headers["X-Instance-ID"] = params.instanceId;
1176
+ }
1177
+ if (params.traceId) {
1178
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1179
+ }
881
1180
  const response = await this.http.post("/wallet/withdraw", {
882
1181
  body,
883
- headers: { "Idempotency-Key": params.idempotencyKey }
1182
+ headers
884
1183
  });
885
1184
  return unwrapWithdrawal(response);
886
1185
  }
@@ -935,9 +1234,15 @@ var AgentsResource = class extends BaseResource {
935
1234
  */
936
1235
  async list(params) {
937
1236
  const headers = {};
1237
+ if (params?.agentId) {
1238
+ headers["X-Agent-ID"] = params.agentId;
1239
+ }
938
1240
  if (params?.instanceId) {
939
1241
  headers["X-Instance-ID"] = params.instanceId;
940
1242
  }
1243
+ if (params?.traceId) {
1244
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1245
+ }
941
1246
  const queryParams = {
942
1247
  status: params?.status,
943
1248
  limit: params?.limit ?? 50,
@@ -957,9 +1262,15 @@ var AgentsResource = class extends BaseResource {
957
1262
  */
958
1263
  async get(agentId, options) {
959
1264
  const headers = {};
1265
+ if (options?.agentId) {
1266
+ headers["X-Agent-ID"] = options.agentId;
1267
+ }
960
1268
  if (options?.instanceId) {
961
1269
  headers["X-Instance-ID"] = options.instanceId;
962
1270
  }
1271
+ if (options?.traceId) {
1272
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1273
+ }
963
1274
  const response = await this.http.get(`/agents/${agentId}`, {
964
1275
  headers: Object.keys(headers).length > 0 ? headers : void 0
965
1276
  });
@@ -986,9 +1297,15 @@ var AgentsResource = class extends BaseResource {
986
1297
  if (params.idempotencyKey) {
987
1298
  headers["Idempotency-Key"] = params.idempotencyKey;
988
1299
  }
1300
+ if (params.agentId) {
1301
+ headers["X-Agent-ID"] = params.agentId;
1302
+ }
989
1303
  if (params.instanceId) {
990
1304
  headers["X-Instance-ID"] = params.instanceId;
991
1305
  }
1306
+ if (params.traceId) {
1307
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1308
+ }
992
1309
  const response = await this.http.post("/agents", {
993
1310
  body,
994
1311
  headers: Object.keys(headers).length > 0 ? headers : void 0
@@ -1012,9 +1329,15 @@ var AgentsResource = class extends BaseResource {
1012
1329
  if (params.idempotencyKey) {
1013
1330
  headers["Idempotency-Key"] = params.idempotencyKey;
1014
1331
  }
1332
+ if (params.agentId) {
1333
+ headers["X-Agent-ID"] = params.agentId;
1334
+ }
1015
1335
  if (params.instanceId) {
1016
1336
  headers["X-Instance-ID"] = params.instanceId;
1017
1337
  }
1338
+ if (params.traceId) {
1339
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1340
+ }
1018
1341
  const response = await this.http.put(`/agents/${agentId}`, {
1019
1342
  body,
1020
1343
  headers: Object.keys(headers).length > 0 ? headers : void 0
@@ -1029,9 +1352,15 @@ var AgentsResource = class extends BaseResource {
1029
1352
  */
1030
1353
  async delete(agentId, options) {
1031
1354
  const headers = {};
1355
+ if (options?.agentId) {
1356
+ headers["X-Agent-ID"] = options.agentId;
1357
+ }
1032
1358
  if (options?.instanceId) {
1033
1359
  headers["X-Instance-ID"] = options.instanceId;
1034
1360
  }
1361
+ if (options?.traceId) {
1362
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1363
+ }
1035
1364
  await this.http.delete(`/agents/${agentId}`, {
1036
1365
  headers: Object.keys(headers).length > 0 ? headers : void 0
1037
1366
  });
@@ -1097,6 +1426,9 @@ var DelegationsResource = class extends BaseResource {
1097
1426
  if (params?.instanceId) {
1098
1427
  headers["X-Instance-ID"] = params.instanceId;
1099
1428
  }
1429
+ if (params?.traceId) {
1430
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1431
+ }
1100
1432
  const queryParams = {
1101
1433
  delegationId: params?.delegationId,
1102
1434
  agentId: params?.agentId,
@@ -1127,18 +1459,14 @@ var DelegationsResource = class extends BaseResource {
1127
1459
  };
1128
1460
 
1129
1461
  // src/resources/customers.ts
1130
- var VALID_CUSTOMER_TYPES = /* @__PURE__ */ new Set(["party", "delegationInvitation"]);
1131
1462
  var VALID_WALLET_ACCESS = /* @__PURE__ */ new Set(["granted", "denied", "noWallet"]);
1132
- function isCustomerType(value) {
1133
- return VALID_CUSTOMER_TYPES.has(value);
1134
- }
1135
1463
  function isWalletAccess(value) {
1136
1464
  return typeof value === "string" && VALID_WALLET_ACCESS.has(value);
1137
1465
  }
1138
1466
  function unwrapCustomerResource(resource) {
1139
- if (!isCustomerType(resource.type) || !resource.attributes) {
1467
+ if (resource.type !== "party" || !resource.attributes) {
1140
1468
  throw new NaturalError(
1141
- `Unexpected resource format: expected type "party" or "delegationInvitation", got "${resource.type}"`
1469
+ `Unexpected resource format: expected type "party", got "${resource.type}"`
1142
1470
  );
1143
1471
  }
1144
1472
  const { id, attributes } = resource;
@@ -1149,9 +1477,8 @@ function unwrapCustomerResource(resource) {
1149
1477
  } : void 0;
1150
1478
  return {
1151
1479
  id,
1152
- type: resource.type,
1480
+ type: "party",
1153
1481
  party,
1154
- email: typeof attributes.email === "string" ? attributes.email : void 0,
1155
1482
  status: typeof attributes.status === "string" ? attributes.status : "",
1156
1483
  permissions: Array.isArray(attributes.permissions) ? attributes.permissions : [],
1157
1484
  createdAt: typeof attributes.createdAt === "string" ? attributes.createdAt : "",
@@ -1160,6 +1487,22 @@ function unwrapCustomerResource(resource) {
1160
1487
  walletAccess: isWalletAccess(attributes.walletAccess) ? attributes.walletAccess : "denied"
1161
1488
  };
1162
1489
  }
1490
+ function unwrapPendingInvitationResource(resource) {
1491
+ if (resource.type !== "delegationInvitation" || !resource.attributes) {
1492
+ throw new NaturalError(
1493
+ `Unexpected resource format: expected type "delegationInvitation", got "${resource.type}"`
1494
+ );
1495
+ }
1496
+ const { id, attributes } = resource;
1497
+ return {
1498
+ id,
1499
+ type: "delegationInvitation",
1500
+ email: typeof attributes.email === "string" ? attributes.email : void 0,
1501
+ status: typeof attributes.status === "string" ? attributes.status : "",
1502
+ permissions: Array.isArray(attributes.permissions) ? attributes.permissions : [],
1503
+ createdAt: typeof attributes.createdAt === "string" ? attributes.createdAt : ""
1504
+ };
1505
+ }
1163
1506
  function unwrapCustomerList(response) {
1164
1507
  if (!response?.data || !Array.isArray(response.data)) {
1165
1508
  throw new NaturalError(
@@ -1173,28 +1516,59 @@ function unwrapCustomerList(response) {
1173
1516
  nextCursor: pagination.nextCursor ?? null
1174
1517
  };
1175
1518
  }
1519
+ function unwrapPendingInvitationList(response) {
1520
+ if (!response?.data || !Array.isArray(response.data)) {
1521
+ throw new NaturalError(
1522
+ 'Unexpected response format: missing "data" array in pending invitation list response'
1523
+ );
1524
+ }
1525
+ const pagination = response.meta?.pagination ?? {};
1526
+ return {
1527
+ items: response.data.map(unwrapPendingInvitationResource),
1528
+ hasMore: pagination.hasMore ?? false,
1529
+ nextCursor: pagination.nextCursor ?? null
1530
+ };
1531
+ }
1532
+ function buildRequest(params) {
1533
+ const headers = {};
1534
+ if (params?.agentId) {
1535
+ headers["X-Agent-ID"] = params.agentId;
1536
+ }
1537
+ if (params?.instanceId) {
1538
+ headers["X-Instance-ID"] = params.instanceId;
1539
+ }
1540
+ if (params?.traceId) {
1541
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1542
+ }
1543
+ const queryParams = {
1544
+ limit: params?.limit,
1545
+ cursor: params?.cursor
1546
+ };
1547
+ return {
1548
+ params: queryParams,
1549
+ headers: Object.keys(headers).length > 0 ? headers : void 0
1550
+ };
1551
+ }
1176
1552
  var CustomersResource = class extends BaseResource {
1177
1553
  /**
1178
- * List customers who have delegated access to the partner.
1554
+ * List active customers who have delegated access to the partner.
1179
1555
  *
1180
- * @param params - Pagination parameters
1181
- * @returns CustomerListResponse with items, hasMore, nextCursor
1556
+ * Returns only accepted delegations. Use {@link listInvitations} for pending
1557
+ * invitations that have not yet been accepted.
1182
1558
  */
1183
1559
  async list(params) {
1184
- const headers = {};
1185
- if (params?.instanceId) {
1186
- headers["X-Instance-ID"] = params.instanceId;
1187
- }
1188
- const queryParams = {
1189
- limit: params?.limit,
1190
- cursor: params?.cursor
1191
- };
1192
- const response = await this.http.get("/customers", {
1193
- params: queryParams,
1194
- headers: Object.keys(headers).length > 0 ? headers : void 0
1195
- });
1560
+ const request = buildRequest(params);
1561
+ const response = await this.http.get("/customers", request);
1196
1562
  return unwrapCustomerList(response);
1197
1563
  }
1564
+ /**
1565
+ * List pending customer invitations you've sent that have not yet been accepted.
1566
+ */
1567
+ async listInvitations(params) {
1568
+ const request = buildRequest(params);
1569
+ const response = await this.http.get("/customers/invitations", request);
1570
+ return unwrapPendingInvitationList(response);
1571
+ }
1198
1572
  };
1199
1573
 
1200
1574
  // src/client.ts
@@ -1231,6 +1605,95 @@ var NaturalClient = class {
1231
1605
  }
1232
1606
  };
1233
1607
 
1608
+ // src/tool-names.ts
1609
+ var NATURAL_TOOL_NAMES = {
1610
+ CREATE_PAYMENT: "create_payment",
1611
+ GET_PAYMENT_STATUS: "get_payment_status",
1612
+ GET_ACCOUNT_BALANCE: "get_account_balance",
1613
+ LIST_TRANSACTIONS: "list_transactions",
1614
+ LIST_AGENTS: "list_agents",
1615
+ LIST_CUSTOMERS: "list_customers",
1616
+ LIST_CUSTOMER_INVITATIONS: "list_customer_invitations"
1617
+ };
1618
+ var WHSEC_PREFIX = "whsec_";
1619
+ var DEFAULT_TOLERANCE_SECONDS = 300;
1620
+ function getHeader(headers, name) {
1621
+ if (typeof headers.get === "function") {
1622
+ return headers.get(name) ?? void 0;
1623
+ }
1624
+ const lower = name.toLowerCase();
1625
+ for (const key of Object.keys(headers)) {
1626
+ if (key.toLowerCase() === lower) {
1627
+ return headers[key];
1628
+ }
1629
+ }
1630
+ return void 0;
1631
+ }
1632
+ function verifyWebhookSignature(body, headers, secret, options) {
1633
+ const tolerance = options?.toleranceInSeconds ?? DEFAULT_TOLERANCE_SECONDS;
1634
+ if (!Number.isInteger(tolerance) || tolerance <= 0) {
1635
+ throw new WebhookVerificationError("toleranceInSeconds must be a positive integer");
1636
+ }
1637
+ const webhookId = getHeader(headers, "webhook-id");
1638
+ if (!webhookId) {
1639
+ throw new WebhookVerificationError("webhook-id header is missing");
1640
+ }
1641
+ const timestampStr = getHeader(headers, "webhook-timestamp");
1642
+ if (!timestampStr) {
1643
+ throw new WebhookVerificationError("webhook-timestamp header is missing");
1644
+ }
1645
+ const signatureHeader = getHeader(headers, "webhook-signature");
1646
+ if (!signatureHeader) {
1647
+ throw new WebhookVerificationError("webhook-signature header is missing");
1648
+ }
1649
+ if (!/^\d+$/.test(timestampStr)) {
1650
+ throw new WebhookVerificationError(
1651
+ `Invalid webhook-timestamp: '${timestampStr}' is not a valid integer`
1652
+ );
1653
+ }
1654
+ const timestamp = parseInt(timestampStr, 10);
1655
+ const now = Math.floor(Date.now() / 1e3);
1656
+ if (Math.abs(now - timestamp) > tolerance) {
1657
+ throw new WebhookVerificationError(
1658
+ timestamp > now ? "Webhook timestamp is too far in the future" : "Webhook timestamp is too old"
1659
+ );
1660
+ }
1661
+ let keyBytes;
1662
+ const base64Part = secret.startsWith(WHSEC_PREFIX) ? secret.slice(WHSEC_PREFIX.length) : secret;
1663
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Part) || base64Part.length === 0) {
1664
+ throw new WebhookVerificationError("Invalid signing secret: could not base64-decode");
1665
+ }
1666
+ try {
1667
+ keyBytes = Buffer.from(base64Part, "base64");
1668
+ if (keyBytes.length === 0) {
1669
+ throw new Error("empty key");
1670
+ }
1671
+ } catch {
1672
+ throw new WebhookVerificationError("Invalid signing secret: could not base64-decode");
1673
+ }
1674
+ const bodyStr = typeof body === "string" ? body : Buffer.from(body).toString("utf-8");
1675
+ const signedContent = `${webhookId}.${timestampStr}.${bodyStr}`;
1676
+ const expectedSig = crypto.createHmac("sha256", keyBytes).update(signedContent).digest("base64");
1677
+ const candidates = signatureHeader.split(" ");
1678
+ for (const candidate of candidates) {
1679
+ if (!candidate.startsWith("v1,")) continue;
1680
+ const candidateSig = candidate.slice(3);
1681
+ const expectedBuf = Buffer.from(expectedSig, "utf-8");
1682
+ const candidateBuf = Buffer.from(candidateSig, "utf-8");
1683
+ if (expectedBuf.length !== candidateBuf.length) continue;
1684
+ if (crypto.timingSafeEqual(expectedBuf, candidateBuf)) {
1685
+ try {
1686
+ return JSON.parse(bodyStr);
1687
+ } catch {
1688
+ throw new WebhookVerificationError("Webhook signature is valid but body is not valid JSON");
1689
+ }
1690
+ }
1691
+ }
1692
+ throw new WebhookVerificationError(
1693
+ "Webhook signature does not match \u2014 ensure you are using the raw request body"
1694
+ );
1695
+ }
1696
+
1234
1697
  // src/types/transactions.ts
1235
1698
  var TransactionTypeFilter = /* @__PURE__ */ ((TransactionTypeFilter2) => {
1236
1699
  TransactionTypeFilter2["PAYMENT"] = "payment";
@@ -1242,6 +1705,7 @@ var TransactionTypeFilter = /* @__PURE__ */ ((TransactionTypeFilter2) => {
1242
1705
  exports.AuthenticationError = AuthenticationError;
1243
1706
  exports.InsufficientFundsError = InsufficientFundsError;
1244
1707
  exports.InvalidRequestError = InvalidRequestError;
1708
+ exports.NATURAL_TOOL_NAMES = NATURAL_TOOL_NAMES;
1245
1709
  exports.NaturalClient = NaturalClient;
1246
1710
  exports.NaturalError = NaturalError;
1247
1711
  exports.PaymentError = PaymentError;
@@ -1250,16 +1714,24 @@ exports.RecipientNotFoundError = RecipientNotFoundError;
1250
1714
  exports.ServerError = ServerError;
1251
1715
  exports.TransactionTypeFilter = TransactionTypeFilter;
1252
1716
  exports.VERSION = VERSION;
1717
+ exports.WebhookVerificationError = WebhookVerificationError;
1718
+ exports.agentConfigToDict = agentConfigToDict;
1253
1719
  exports.bindContext = bindContext;
1254
1720
  exports.clearContext = clearContext;
1721
+ exports.configHash = configHash;
1255
1722
  exports.configureLogging = configureLogging;
1723
+ exports.generateToolCallId = generateToolCallId;
1256
1724
  exports.getContext = getContext;
1257
1725
  exports.getLogger = getLogger;
1726
+ exports.getToolCallHeader = getToolCallHeader;
1258
1727
  exports.logApiCall = logApiCall;
1259
1728
  exports.logError = logError;
1260
1729
  exports.logToolCall = logToolCall;
1730
+ exports.modelUsageToDict = modelUsageToDict;
1261
1731
  exports.parseApiKeyEnv = parseApiKeyEnv;
1262
1732
  exports.runWithContext = runWithContext;
1733
+ exports.runWithToolCall = runWithToolCall;
1263
1734
  exports.validateBaseUrl = validateBaseUrl;
1735
+ exports.verifyWebhookSignature = verifyWebhookSignature;
1264
1736
  //# sourceMappingURL=index.cjs.map
1265
1737
  //# sourceMappingURL=index.cjs.map