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