@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.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);
300
307
  }
301
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;
338
+ }
339
+ return dict;
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
+ }
569
766
  }
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;
767
+ if (!this._configAttempted) {
768
+ await this._registerConfigPromise;
769
+ }
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;
575
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) {
@@ -639,9 +907,15 @@ var TransactionsResource = class extends BaseResource {
639
907
  */
640
908
  async get(transactionId, params) {
641
909
  const headers = {};
910
+ if (params?.agentId) {
911
+ headers["X-Agent-ID"] = params.agentId;
912
+ }
642
913
  if (params?.instanceId) {
643
914
  headers["X-Instance-ID"] = params.instanceId;
644
915
  }
916
+ if (params?.traceId) {
917
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
918
+ }
645
919
  const queryParams = {};
646
920
  if (params?.customerPartyId) {
647
921
  queryParams["partyId"] = params.customerPartyId;
@@ -672,6 +946,9 @@ var TransactionsResource = class extends BaseResource {
672
946
  if (params?.instanceId) {
673
947
  headers["X-Instance-ID"] = params.instanceId;
674
948
  }
949
+ if (params?.traceId) {
950
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
951
+ }
675
952
  const queryParams = {
676
953
  limit: params?.limit ?? 50,
677
954
  cursor: params?.cursor,
@@ -764,6 +1041,9 @@ var PaymentsResource = class extends BaseResource {
764
1041
  if (params.instanceId) {
765
1042
  headers["X-Instance-ID"] = params.instanceId;
766
1043
  }
1044
+ if (params.traceId) {
1045
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1046
+ }
767
1047
  const response = await this.http.post("/payments", {
768
1048
  body,
769
1049
  headers
@@ -846,9 +1126,15 @@ var WalletResource = class extends BaseResource {
846
1126
  */
847
1127
  async balance(options) {
848
1128
  const headers = {};
1129
+ if (options?.agentId) {
1130
+ headers["X-Agent-ID"] = options.agentId;
1131
+ }
849
1132
  if (options?.instanceId) {
850
1133
  headers["X-Instance-ID"] = options.instanceId;
851
1134
  }
1135
+ if (options?.traceId) {
1136
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1137
+ }
852
1138
  const params = {};
853
1139
  if (options?.customerPartyId) {
854
1140
  params["partyId"] = options.customerPartyId;
@@ -876,9 +1162,21 @@ var WalletResource = class extends BaseResource {
876
1162
  };
877
1163
  if (params.description) attributes["description"] = params.description;
878
1164
  const body = { data: { attributes } };
1165
+ const headers = {
1166
+ "Idempotency-Key": params.idempotencyKey
1167
+ };
1168
+ if (params.agentId) {
1169
+ headers["X-Agent-ID"] = params.agentId;
1170
+ }
1171
+ if (params.instanceId) {
1172
+ headers["X-Instance-ID"] = params.instanceId;
1173
+ }
1174
+ if (params.traceId) {
1175
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1176
+ }
879
1177
  const response = await this.http.post("/wallet/withdraw", {
880
1178
  body,
881
- headers: { "Idempotency-Key": params.idempotencyKey }
1179
+ headers
882
1180
  });
883
1181
  return unwrapWithdrawal(response);
884
1182
  }
@@ -933,9 +1231,15 @@ var AgentsResource = class extends BaseResource {
933
1231
  */
934
1232
  async list(params) {
935
1233
  const headers = {};
1234
+ if (params?.agentId) {
1235
+ headers["X-Agent-ID"] = params.agentId;
1236
+ }
936
1237
  if (params?.instanceId) {
937
1238
  headers["X-Instance-ID"] = params.instanceId;
938
1239
  }
1240
+ if (params?.traceId) {
1241
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1242
+ }
939
1243
  const queryParams = {
940
1244
  status: params?.status,
941
1245
  limit: params?.limit ?? 50,
@@ -955,9 +1259,15 @@ var AgentsResource = class extends BaseResource {
955
1259
  */
956
1260
  async get(agentId, options) {
957
1261
  const headers = {};
1262
+ if (options?.agentId) {
1263
+ headers["X-Agent-ID"] = options.agentId;
1264
+ }
958
1265
  if (options?.instanceId) {
959
1266
  headers["X-Instance-ID"] = options.instanceId;
960
1267
  }
1268
+ if (options?.traceId) {
1269
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1270
+ }
961
1271
  const response = await this.http.get(`/agents/${agentId}`, {
962
1272
  headers: Object.keys(headers).length > 0 ? headers : void 0
963
1273
  });
@@ -984,9 +1294,15 @@ var AgentsResource = class extends BaseResource {
984
1294
  if (params.idempotencyKey) {
985
1295
  headers["Idempotency-Key"] = params.idempotencyKey;
986
1296
  }
1297
+ if (params.agentId) {
1298
+ headers["X-Agent-ID"] = params.agentId;
1299
+ }
987
1300
  if (params.instanceId) {
988
1301
  headers["X-Instance-ID"] = params.instanceId;
989
1302
  }
1303
+ if (params.traceId) {
1304
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1305
+ }
990
1306
  const response = await this.http.post("/agents", {
991
1307
  body,
992
1308
  headers: Object.keys(headers).length > 0 ? headers : void 0
@@ -1010,9 +1326,15 @@ var AgentsResource = class extends BaseResource {
1010
1326
  if (params.idempotencyKey) {
1011
1327
  headers["Idempotency-Key"] = params.idempotencyKey;
1012
1328
  }
1329
+ if (params.agentId) {
1330
+ headers["X-Agent-ID"] = params.agentId;
1331
+ }
1013
1332
  if (params.instanceId) {
1014
1333
  headers["X-Instance-ID"] = params.instanceId;
1015
1334
  }
1335
+ if (params.traceId) {
1336
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1337
+ }
1016
1338
  const response = await this.http.put(`/agents/${agentId}`, {
1017
1339
  body,
1018
1340
  headers: Object.keys(headers).length > 0 ? headers : void 0
@@ -1027,9 +1349,15 @@ var AgentsResource = class extends BaseResource {
1027
1349
  */
1028
1350
  async delete(agentId, options) {
1029
1351
  const headers = {};
1352
+ if (options?.agentId) {
1353
+ headers["X-Agent-ID"] = options.agentId;
1354
+ }
1030
1355
  if (options?.instanceId) {
1031
1356
  headers["X-Instance-ID"] = options.instanceId;
1032
1357
  }
1358
+ if (options?.traceId) {
1359
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1360
+ }
1033
1361
  await this.http.delete(`/agents/${agentId}`, {
1034
1362
  headers: Object.keys(headers).length > 0 ? headers : void 0
1035
1363
  });
@@ -1095,6 +1423,9 @@ var DelegationsResource = class extends BaseResource {
1095
1423
  if (params?.instanceId) {
1096
1424
  headers["X-Instance-ID"] = params.instanceId;
1097
1425
  }
1426
+ if (params?.traceId) {
1427
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1428
+ }
1098
1429
  const queryParams = {
1099
1430
  delegationId: params?.delegationId,
1100
1431
  agentId: params?.agentId,
@@ -1180,9 +1511,15 @@ var CustomersResource = class extends BaseResource {
1180
1511
  */
1181
1512
  async list(params) {
1182
1513
  const headers = {};
1514
+ if (params?.agentId) {
1515
+ headers["X-Agent-ID"] = params.agentId;
1516
+ }
1183
1517
  if (params?.instanceId) {
1184
1518
  headers["X-Instance-ID"] = params.instanceId;
1185
1519
  }
1520
+ if (params?.traceId) {
1521
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1522
+ }
1186
1523
  const queryParams = {
1187
1524
  limit: params?.limit,
1188
1525
  cursor: params?.cursor
@@ -1228,6 +1565,84 @@ var NaturalClient = class {
1228
1565
  this.customers = new CustomersResource(this.http);
1229
1566
  }
1230
1567
  };
1568
+ var WHSEC_PREFIX = "whsec_";
1569
+ var DEFAULT_TOLERANCE_SECONDS = 300;
1570
+ function getHeader(headers, name) {
1571
+ if (typeof headers.get === "function") {
1572
+ return headers.get(name) ?? void 0;
1573
+ }
1574
+ const lower = name.toLowerCase();
1575
+ for (const key of Object.keys(headers)) {
1576
+ if (key.toLowerCase() === lower) {
1577
+ return headers[key];
1578
+ }
1579
+ }
1580
+ return void 0;
1581
+ }
1582
+ function verifyWebhookSignature(body, headers, secret, options) {
1583
+ const tolerance = options?.toleranceInSeconds ?? DEFAULT_TOLERANCE_SECONDS;
1584
+ if (!Number.isInteger(tolerance) || tolerance <= 0) {
1585
+ throw new WebhookVerificationError("toleranceInSeconds must be a positive integer");
1586
+ }
1587
+ const webhookId = getHeader(headers, "webhook-id");
1588
+ if (!webhookId) {
1589
+ throw new WebhookVerificationError("webhook-id header is missing");
1590
+ }
1591
+ const timestampStr = getHeader(headers, "webhook-timestamp");
1592
+ if (!timestampStr) {
1593
+ throw new WebhookVerificationError("webhook-timestamp header is missing");
1594
+ }
1595
+ const signatureHeader = getHeader(headers, "webhook-signature");
1596
+ if (!signatureHeader) {
1597
+ throw new WebhookVerificationError("webhook-signature header is missing");
1598
+ }
1599
+ if (!/^\d+$/.test(timestampStr)) {
1600
+ throw new WebhookVerificationError(
1601
+ `Invalid webhook-timestamp: '${timestampStr}' is not a valid integer`
1602
+ );
1603
+ }
1604
+ const timestamp = parseInt(timestampStr, 10);
1605
+ const now = Math.floor(Date.now() / 1e3);
1606
+ if (Math.abs(now - timestamp) > tolerance) {
1607
+ throw new WebhookVerificationError(
1608
+ timestamp > now ? "Webhook timestamp is too far in the future" : "Webhook timestamp is too old"
1609
+ );
1610
+ }
1611
+ let keyBytes;
1612
+ const base64Part = secret.startsWith(WHSEC_PREFIX) ? secret.slice(WHSEC_PREFIX.length) : secret;
1613
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64Part) || base64Part.length === 0) {
1614
+ throw new WebhookVerificationError("Invalid signing secret: could not base64-decode");
1615
+ }
1616
+ try {
1617
+ keyBytes = Buffer.from(base64Part, "base64");
1618
+ if (keyBytes.length === 0) {
1619
+ throw new Error("empty key");
1620
+ }
1621
+ } catch {
1622
+ throw new WebhookVerificationError("Invalid signing secret: could not base64-decode");
1623
+ }
1624
+ const bodyStr = typeof body === "string" ? body : Buffer.from(body).toString("utf-8");
1625
+ const signedContent = `${webhookId}.${timestampStr}.${bodyStr}`;
1626
+ const expectedSig = createHmac("sha256", keyBytes).update(signedContent).digest("base64");
1627
+ const candidates = signatureHeader.split(" ");
1628
+ for (const candidate of candidates) {
1629
+ if (!candidate.startsWith("v1,")) continue;
1630
+ const candidateSig = candidate.slice(3);
1631
+ const expectedBuf = Buffer.from(expectedSig, "utf-8");
1632
+ const candidateBuf = Buffer.from(candidateSig, "utf-8");
1633
+ if (expectedBuf.length !== candidateBuf.length) continue;
1634
+ if (timingSafeEqual(expectedBuf, candidateBuf)) {
1635
+ try {
1636
+ return JSON.parse(bodyStr);
1637
+ } catch {
1638
+ throw new WebhookVerificationError("Webhook signature is valid but body is not valid JSON");
1639
+ }
1640
+ }
1641
+ }
1642
+ throw new WebhookVerificationError(
1643
+ "Webhook signature does not match \u2014 ensure you are using the raw request body"
1644
+ );
1645
+ }
1231
1646
 
1232
1647
  // src/types/transactions.ts
1233
1648
  var TransactionTypeFilter = /* @__PURE__ */ ((TransactionTypeFilter2) => {
@@ -1237,6 +1652,6 @@ var TransactionTypeFilter = /* @__PURE__ */ ((TransactionTypeFilter2) => {
1237
1652
  return TransactionTypeFilter2;
1238
1653
  })(TransactionTypeFilter || {});
1239
1654
 
1240
- export { AuthenticationError, InsufficientFundsError, InvalidRequestError, NaturalClient, NaturalError, PaymentError, RateLimitError, RecipientNotFoundError, ServerError, TransactionTypeFilter, VERSION, bindContext, clearContext, configureLogging, getContext, getLogger, logApiCall, logError, logToolCall, parseApiKeyEnv, runWithContext, validateBaseUrl };
1655
+ export { AuthenticationError, InsufficientFundsError, InvalidRequestError, 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
1656
  //# sourceMappingURL=index.js.map
1242
1657
  //# sourceMappingURL=index.js.map