@naturalpay/sdk 0.1.3 → 0.1.4

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);
302
309
  }
303
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;
340
+ }
341
+ return dict;
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
+ }
571
768
  }
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;
769
+ if (!this._configAttempted) {
770
+ await this._registerConfigPromise;
771
+ }
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;
577
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) {
@@ -641,9 +909,15 @@ var TransactionsResource = class extends BaseResource {
641
909
  */
642
910
  async get(transactionId, params) {
643
911
  const headers = {};
912
+ if (params?.agentId) {
913
+ headers["X-Agent-ID"] = params.agentId;
914
+ }
644
915
  if (params?.instanceId) {
645
916
  headers["X-Instance-ID"] = params.instanceId;
646
917
  }
918
+ if (params?.traceId) {
919
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
920
+ }
647
921
  const queryParams = {};
648
922
  if (params?.customerPartyId) {
649
923
  queryParams["partyId"] = params.customerPartyId;
@@ -674,6 +948,9 @@ var TransactionsResource = class extends BaseResource {
674
948
  if (params?.instanceId) {
675
949
  headers["X-Instance-ID"] = params.instanceId;
676
950
  }
951
+ if (params?.traceId) {
952
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
953
+ }
677
954
  const queryParams = {
678
955
  limit: params?.limit ?? 50,
679
956
  cursor: params?.cursor,
@@ -766,6 +1043,9 @@ var PaymentsResource = class extends BaseResource {
766
1043
  if (params.instanceId) {
767
1044
  headers["X-Instance-ID"] = params.instanceId;
768
1045
  }
1046
+ if (params.traceId) {
1047
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1048
+ }
769
1049
  const response = await this.http.post("/payments", {
770
1050
  body,
771
1051
  headers
@@ -848,9 +1128,15 @@ var WalletResource = class extends BaseResource {
848
1128
  */
849
1129
  async balance(options) {
850
1130
  const headers = {};
1131
+ if (options?.agentId) {
1132
+ headers["X-Agent-ID"] = options.agentId;
1133
+ }
851
1134
  if (options?.instanceId) {
852
1135
  headers["X-Instance-ID"] = options.instanceId;
853
1136
  }
1137
+ if (options?.traceId) {
1138
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1139
+ }
854
1140
  const params = {};
855
1141
  if (options?.customerPartyId) {
856
1142
  params["partyId"] = options.customerPartyId;
@@ -878,9 +1164,21 @@ var WalletResource = class extends BaseResource {
878
1164
  };
879
1165
  if (params.description) attributes["description"] = params.description;
880
1166
  const body = { data: { attributes } };
1167
+ const headers = {
1168
+ "Idempotency-Key": params.idempotencyKey
1169
+ };
1170
+ if (params.agentId) {
1171
+ headers["X-Agent-ID"] = params.agentId;
1172
+ }
1173
+ if (params.instanceId) {
1174
+ headers["X-Instance-ID"] = params.instanceId;
1175
+ }
1176
+ if (params.traceId) {
1177
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1178
+ }
881
1179
  const response = await this.http.post("/wallet/withdraw", {
882
1180
  body,
883
- headers: { "Idempotency-Key": params.idempotencyKey }
1181
+ headers
884
1182
  });
885
1183
  return unwrapWithdrawal(response);
886
1184
  }
@@ -935,9 +1233,15 @@ var AgentsResource = class extends BaseResource {
935
1233
  */
936
1234
  async list(params) {
937
1235
  const headers = {};
1236
+ if (params?.agentId) {
1237
+ headers["X-Agent-ID"] = params.agentId;
1238
+ }
938
1239
  if (params?.instanceId) {
939
1240
  headers["X-Instance-ID"] = params.instanceId;
940
1241
  }
1242
+ if (params?.traceId) {
1243
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1244
+ }
941
1245
  const queryParams = {
942
1246
  status: params?.status,
943
1247
  limit: params?.limit ?? 50,
@@ -957,9 +1261,15 @@ var AgentsResource = class extends BaseResource {
957
1261
  */
958
1262
  async get(agentId, options) {
959
1263
  const headers = {};
1264
+ if (options?.agentId) {
1265
+ headers["X-Agent-ID"] = options.agentId;
1266
+ }
960
1267
  if (options?.instanceId) {
961
1268
  headers["X-Instance-ID"] = options.instanceId;
962
1269
  }
1270
+ if (options?.traceId) {
1271
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1272
+ }
963
1273
  const response = await this.http.get(`/agents/${agentId}`, {
964
1274
  headers: Object.keys(headers).length > 0 ? headers : void 0
965
1275
  });
@@ -986,9 +1296,15 @@ var AgentsResource = class extends BaseResource {
986
1296
  if (params.idempotencyKey) {
987
1297
  headers["Idempotency-Key"] = params.idempotencyKey;
988
1298
  }
1299
+ if (params.agentId) {
1300
+ headers["X-Agent-ID"] = params.agentId;
1301
+ }
989
1302
  if (params.instanceId) {
990
1303
  headers["X-Instance-ID"] = params.instanceId;
991
1304
  }
1305
+ if (params.traceId) {
1306
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1307
+ }
992
1308
  const response = await this.http.post("/agents", {
993
1309
  body,
994
1310
  headers: Object.keys(headers).length > 0 ? headers : void 0
@@ -1012,9 +1328,15 @@ var AgentsResource = class extends BaseResource {
1012
1328
  if (params.idempotencyKey) {
1013
1329
  headers["Idempotency-Key"] = params.idempotencyKey;
1014
1330
  }
1331
+ if (params.agentId) {
1332
+ headers["X-Agent-ID"] = params.agentId;
1333
+ }
1015
1334
  if (params.instanceId) {
1016
1335
  headers["X-Instance-ID"] = params.instanceId;
1017
1336
  }
1337
+ if (params.traceId) {
1338
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1339
+ }
1018
1340
  const response = await this.http.put(`/agents/${agentId}`, {
1019
1341
  body,
1020
1342
  headers: Object.keys(headers).length > 0 ? headers : void 0
@@ -1029,9 +1351,15 @@ var AgentsResource = class extends BaseResource {
1029
1351
  */
1030
1352
  async delete(agentId, options) {
1031
1353
  const headers = {};
1354
+ if (options?.agentId) {
1355
+ headers["X-Agent-ID"] = options.agentId;
1356
+ }
1032
1357
  if (options?.instanceId) {
1033
1358
  headers["X-Instance-ID"] = options.instanceId;
1034
1359
  }
1360
+ if (options?.traceId) {
1361
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1362
+ }
1035
1363
  await this.http.delete(`/agents/${agentId}`, {
1036
1364
  headers: Object.keys(headers).length > 0 ? headers : void 0
1037
1365
  });
@@ -1097,6 +1425,9 @@ var DelegationsResource = class extends BaseResource {
1097
1425
  if (params?.instanceId) {
1098
1426
  headers["X-Instance-ID"] = params.instanceId;
1099
1427
  }
1428
+ if (params?.traceId) {
1429
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1430
+ }
1100
1431
  const queryParams = {
1101
1432
  delegationId: params?.delegationId,
1102
1433
  agentId: params?.agentId,
@@ -1182,9 +1513,15 @@ var CustomersResource = class extends BaseResource {
1182
1513
  */
1183
1514
  async list(params) {
1184
1515
  const headers = {};
1516
+ if (params?.agentId) {
1517
+ headers["X-Agent-ID"] = params.agentId;
1518
+ }
1185
1519
  if (params?.instanceId) {
1186
1520
  headers["X-Instance-ID"] = params.instanceId;
1187
1521
  }
1522
+ if (params?.traceId) {
1523
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1524
+ }
1188
1525
  const queryParams = {
1189
1526
  limit: params?.limit,
1190
1527
  cursor: params?.cursor
@@ -1230,6 +1567,84 @@ var NaturalClient = class {
1230
1567
  this.customers = new CustomersResource(this.http);
1231
1568
  }
1232
1569
  };
1570
+ var WHSEC_PREFIX = "whsec_";
1571
+ var DEFAULT_TOLERANCE_SECONDS = 300;
1572
+ function getHeader(headers, name) {
1573
+ if (typeof headers.get === "function") {
1574
+ return headers.get(name) ?? void 0;
1575
+ }
1576
+ const lower = name.toLowerCase();
1577
+ for (const key of Object.keys(headers)) {
1578
+ if (key.toLowerCase() === lower) {
1579
+ return headers[key];
1580
+ }
1581
+ }
1582
+ return void 0;
1583
+ }
1584
+ function verifyWebhookSignature(body, headers, secret, options) {
1585
+ const tolerance = options?.toleranceInSeconds ?? DEFAULT_TOLERANCE_SECONDS;
1586
+ if (!Number.isInteger(tolerance) || tolerance <= 0) {
1587
+ throw new WebhookVerificationError("toleranceInSeconds must be a positive integer");
1588
+ }
1589
+ const webhookId = getHeader(headers, "webhook-id");
1590
+ if (!webhookId) {
1591
+ throw new WebhookVerificationError("webhook-id header is missing");
1592
+ }
1593
+ const timestampStr = getHeader(headers, "webhook-timestamp");
1594
+ if (!timestampStr) {
1595
+ throw new WebhookVerificationError("webhook-timestamp header is missing");
1596
+ }
1597
+ const signatureHeader = getHeader(headers, "webhook-signature");
1598
+ if (!signatureHeader) {
1599
+ throw new WebhookVerificationError("webhook-signature header is missing");
1600
+ }
1601
+ if (!/^\d+$/.test(timestampStr)) {
1602
+ throw new WebhookVerificationError(
1603
+ `Invalid webhook-timestamp: '${timestampStr}' is not a valid integer`
1604
+ );
1605
+ }
1606
+ const timestamp = parseInt(timestampStr, 10);
1607
+ const now = Math.floor(Date.now() / 1e3);
1608
+ if (Math.abs(now - timestamp) > tolerance) {
1609
+ throw new WebhookVerificationError(
1610
+ timestamp > now ? "Webhook timestamp is too far in the future" : "Webhook timestamp is too old"
1611
+ );
1612
+ }
1613
+ let keyBytes;
1614
+ const base64Part = secret.startsWith(WHSEC_PREFIX) ? secret.slice(WHSEC_PREFIX.length) : secret;
1615
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Part) || base64Part.length === 0) {
1616
+ throw new WebhookVerificationError("Invalid signing secret: could not base64-decode");
1617
+ }
1618
+ try {
1619
+ keyBytes = Buffer.from(base64Part, "base64");
1620
+ if (keyBytes.length === 0) {
1621
+ throw new Error("empty key");
1622
+ }
1623
+ } catch {
1624
+ throw new WebhookVerificationError("Invalid signing secret: could not base64-decode");
1625
+ }
1626
+ const bodyStr = typeof body === "string" ? body : Buffer.from(body).toString("utf-8");
1627
+ const signedContent = `${webhookId}.${timestampStr}.${bodyStr}`;
1628
+ const expectedSig = crypto.createHmac("sha256", keyBytes).update(signedContent).digest("base64");
1629
+ const candidates = signatureHeader.split(" ");
1630
+ for (const candidate of candidates) {
1631
+ if (!candidate.startsWith("v1,")) continue;
1632
+ const candidateSig = candidate.slice(3);
1633
+ const expectedBuf = Buffer.from(expectedSig, "utf-8");
1634
+ const candidateBuf = Buffer.from(candidateSig, "utf-8");
1635
+ if (expectedBuf.length !== candidateBuf.length) continue;
1636
+ if (crypto.timingSafeEqual(expectedBuf, candidateBuf)) {
1637
+ try {
1638
+ return JSON.parse(bodyStr);
1639
+ } catch {
1640
+ throw new WebhookVerificationError("Webhook signature is valid but body is not valid JSON");
1641
+ }
1642
+ }
1643
+ }
1644
+ throw new WebhookVerificationError(
1645
+ "Webhook signature does not match \u2014 ensure you are using the raw request body"
1646
+ );
1647
+ }
1233
1648
 
1234
1649
  // src/types/transactions.ts
1235
1650
  var TransactionTypeFilter = /* @__PURE__ */ ((TransactionTypeFilter2) => {
@@ -1250,16 +1665,24 @@ exports.RecipientNotFoundError = RecipientNotFoundError;
1250
1665
  exports.ServerError = ServerError;
1251
1666
  exports.TransactionTypeFilter = TransactionTypeFilter;
1252
1667
  exports.VERSION = VERSION;
1668
+ exports.WebhookVerificationError = WebhookVerificationError;
1669
+ exports.agentConfigToDict = agentConfigToDict;
1253
1670
  exports.bindContext = bindContext;
1254
1671
  exports.clearContext = clearContext;
1672
+ exports.configHash = configHash;
1255
1673
  exports.configureLogging = configureLogging;
1674
+ exports.generateToolCallId = generateToolCallId;
1256
1675
  exports.getContext = getContext;
1257
1676
  exports.getLogger = getLogger;
1677
+ exports.getToolCallHeader = getToolCallHeader;
1258
1678
  exports.logApiCall = logApiCall;
1259
1679
  exports.logError = logError;
1260
1680
  exports.logToolCall = logToolCall;
1681
+ exports.modelUsageToDict = modelUsageToDict;
1261
1682
  exports.parseApiKeyEnv = parseApiKeyEnv;
1262
1683
  exports.runWithContext = runWithContext;
1684
+ exports.runWithToolCall = runWithToolCall;
1263
1685
  exports.validateBaseUrl = validateBaseUrl;
1686
+ exports.verifyWebhookSignature = verifyWebhookSignature;
1264
1687
  //# sourceMappingURL=index.cjs.map
1265
1688
  //# sourceMappingURL=index.cjs.map