@naturalpay/sdk 0.1.2 → 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.1";
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,24 +299,114 @@ 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;
377
+ var API_KEY_PREFIX_REGEX = /^sk_ntl_(dev|sandbox|prod)_/;
378
+ function parseApiKeyEnv(key) {
379
+ const match = API_KEY_PREFIX_REGEX.exec(key);
380
+ if (match) {
381
+ return match[1];
382
+ }
383
+ const preview = key.length > 16 ? `${key.slice(0, 16)}...` : key;
384
+ throw new InvalidRequestError(
385
+ `Invalid API key prefix. Expected a key starting with 'sk_ntl_dev_', 'sk_ntl_sandbox_', or 'sk_ntl_prod_'. Got: '${preview}'`
386
+ );
387
+ }
388
+ function validateBaseUrl(baseUrl) {
389
+ let url;
390
+ try {
391
+ url = new URL(baseUrl);
392
+ } catch {
393
+ throw new InvalidRequestError(`Invalid baseUrl: '${baseUrl}'. Must be a valid absolute URL.`);
394
+ }
395
+ if (url.protocol === "https:") {
396
+ return;
397
+ }
398
+ const host = url.hostname;
399
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]") {
400
+ return;
401
+ }
402
+ const allowHttp = process.env["NATURAL_ALLOW_HTTP"];
403
+ if (allowHttp && allowHttp !== "0" && allowHttp.toLowerCase() !== "false") {
404
+ return;
405
+ }
406
+ throw new InvalidRequestError(
407
+ `baseUrl must use HTTPS (got '${baseUrl}'). To allow plaintext HTTP for development, set NATURAL_ALLOW_HTTP=1 or use a localhost host.`
408
+ );
409
+ }
313
410
  function hashString(str) {
314
411
  let hash = 0;
315
412
  for (let i = 0; i < str.length; i++) {
@@ -319,82 +416,167 @@ function hashString(str) {
319
416
  }
320
417
  return Math.abs(hash).toString(16).slice(0, 16);
321
418
  }
322
- 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 {
323
446
  apiKey;
324
447
  baseUrl;
325
448
  timeout;
449
+ maxRetries;
326
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;
327
459
  constructor(options = {}) {
328
460
  this.apiKey = options.apiKey ?? process.env["NATURAL_API_KEY"] ?? "";
329
461
  this.baseUrl = (options.baseUrl ?? process.env["NATURAL_SERVER_URL"] ?? DEFAULT_BASE_URL).replace(/\/$/, "");
462
+ validateBaseUrl(this.baseUrl);
463
+ if (this.apiKey) {
464
+ parseApiKeyEnv(this.apiKey);
465
+ }
330
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;
331
482
  }
332
483
  /**
333
484
  * Get a cached JWT or exchange API key for a new one.
485
+ * Retries on transient failures (5xx, network) but not on 401.
334
486
  */
335
487
  async getJwt() {
336
488
  if (!this.apiKey) {
337
489
  throw new AuthenticationError();
338
490
  }
339
- if (!this.apiKey.startsWith("sk_ntl_")) {
340
- return this.apiKey;
341
- }
342
491
  const cacheKey = hashString(this.apiKey);
343
492
  const cached = this.jwtCache.get(cacheKey);
344
493
  if (cached && Date.now() < cached.expiresAt) {
345
494
  return cached.token;
346
495
  }
347
- const controller = new AbortController();
348
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
349
- logger.debug("Exchanging API key for JWT", { path: "/auth/api/token" });
350
- try {
351
- const response = await fetch(`${this.baseUrl}/auth/api/token`, {
352
- method: "POST",
353
- headers: {
354
- Authorization: `Bearer ${this.apiKey}`,
355
- "Content-Type": "application/json"
356
- },
357
- signal: controller.signal
358
- });
359
- clearTimeout(timeoutId);
360
- if (!response.ok) {
361
- const authError = new AuthenticationError(
362
- `Authentication failed (status=${response.status})`
363
- );
364
- logError(logger, "JWT exchange failed", {
365
- error: authError,
366
- statusCode: response.status,
367
- 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
368
508
  });
369
- throw authError;
370
- }
371
- const data = await response.json();
372
- const expiresIn = data.expiresIn ?? 900;
373
- const expiresAt = Date.now() + (expiresIn - 30) * 1e3;
374
- this.jwtCache.set(cacheKey, { token: data.accessToken, expiresAt });
375
- return data.accessToken;
376
- } catch (error) {
377
- clearTimeout(timeoutId);
378
- if (error instanceof AuthenticationError) {
379
- throw error;
380
- }
381
- if (error instanceof Error && error.name === "AbortError") {
382
- const networkError2 = new NaturalError("Request timed out during authentication");
383
- logError(logger, "JWT exchange network error", {
384
- 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,
385
574
  path: "/auth/api/token"
386
575
  });
387
- throw networkError2;
576
+ throw sdkError;
388
577
  }
389
- const networkError = new NaturalError(
390
- `Network error during authentication: ${error instanceof Error ? error.message : "Unknown error"}`
391
- );
392
- logError(logger, "JWT exchange network error", {
393
- error: networkError,
394
- path: "/auth/api/token"
395
- });
396
- throw networkError;
397
578
  }
579
+ throw new NaturalError("Unexpected retry exhaustion during JWT exchange");
398
580
  }
399
581
  /**
400
582
  * Build URL with query parameters.
@@ -416,7 +598,7 @@ var HTTPClient = class {
416
598
  async handleResponse(response, method, path, durationMs) {
417
599
  if (response.status === 401) {
418
600
  const authError = new AuthenticationError();
419
- logApiCall(logger, method, path, {
601
+ logApiCall(logger2, method, path, {
420
602
  statusCode: response.status,
421
603
  durationMs,
422
604
  error: authError
@@ -429,7 +611,7 @@ var HTTPClient = class {
429
611
  "Rate limit exceeded",
430
612
  retryAfter ? parseInt(retryAfter, 10) : void 0
431
613
  );
432
- logger.warning(`Rate limited: ${method} ${path}`, {
614
+ logger2.warning(`Rate limited: ${method} ${path}`, {
433
615
  method,
434
616
  path,
435
617
  statusCode: response.status,
@@ -440,7 +622,7 @@ var HTTPClient = class {
440
622
  }
441
623
  if (response.status >= 500) {
442
624
  const serverError = new ServerError(`Server error: ${response.status}`);
443
- logApiCall(logger, method, path, {
625
+ logApiCall(logger2, method, path, {
444
626
  statusCode: response.status,
445
627
  durationMs,
446
628
  error: serverError
@@ -453,17 +635,18 @@ var HTTPClient = class {
453
635
  data = text ? JSON.parse(text) : {};
454
636
  } catch {
455
637
  if (response.status >= 400) {
456
- const parseError = new NaturalError(`Request failed: ${response.status}`, {
457
- statusCode: response.status
458
- });
459
- 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, {
460
643
  statusCode: response.status,
461
644
  durationMs,
462
645
  error: parseError
463
646
  });
464
647
  throw parseError;
465
648
  }
466
- logApiCall(logger, method, path, { statusCode: response.status, durationMs });
649
+ logApiCall(logger2, method, path, { statusCode: response.status, durationMs });
467
650
  return {};
468
651
  }
469
652
  if (response.status >= 400) {
@@ -476,69 +659,185 @@ var HTTPClient = class {
476
659
  `${errorMessage} (status=${response.status})`,
477
660
  errorCode
478
661
  );
479
- logApiCall(logger, method, path, {
662
+ logApiCall(logger2, method, path, {
480
663
  statusCode: response.status,
481
664
  durationMs,
482
665
  error: requestError
483
666
  });
484
667
  throw requestError;
485
668
  }
486
- logApiCall(logger, method, path, { statusCode: response.status, durationMs });
669
+ logApiCall(logger2, method, path, { statusCode: response.status, durationMs });
487
670
  return data;
488
671
  }
489
672
  /**
490
- * 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.
491
677
  */
492
- async request(method, path, options) {
493
- const jwt = await this.getJwt();
494
- const url = this.buildUrl(path, options?.params);
495
- const controller = new AbortController();
496
- const timeoutId = setTimeout(() => controller.abort(), this.timeout);
497
- logger.debug(`API request: ${method} ${path}`, {
498
- method,
499
- path,
500
- hasBody: !!options?.body
501
- });
502
- const startTime = Date.now();
678
+ async registerConfig() {
679
+ if (!this.agentConfig) return;
503
680
  try {
504
- const headers = {
505
- Authorization: `Bearer ${jwt}`,
506
- "Content-Type": "application/json",
507
- "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)
508
686
  };
509
- const toolCallHeader = getToolCallHeader();
510
- if (toolCallHeader) {
511
- headers["X-Tool-Call"] = toolCallHeader;
512
- }
513
- if (options?.headers) {
514
- 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);
515
719
  }
516
- const response = await fetch(url, {
517
- method,
518
- headers,
519
- body: options?.body ? JSON.stringify(options.body) : void 0,
520
- signal: controller.signal
521
- });
522
- clearTimeout(timeoutId);
523
- const durationMs = Date.now() - startTime;
524
- return this.handleResponse(response, method, path, durationMs);
525
720
  } catch (error) {
526
- clearTimeout(timeoutId);
527
- const durationMs = Date.now() - startTime;
528
- if (error instanceof NaturalError || error instanceof AuthenticationError || error instanceof InvalidRequestError || error instanceof RateLimitError || error instanceof ServerError) {
529
- 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");
751
+ }
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
+ }
530
766
  }
531
- if (error instanceof Error && error.name === "AbortError") {
532
- const networkError2 = new NaturalError("Request timed out");
533
- logApiCall(logger, method, path, { durationMs, error: networkError2 });
534
- throw networkError2;
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;
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;
535
838
  }
536
- const networkError = new NaturalError(
537
- `Network error: ${error instanceof Error ? error.message : "Unknown error"}`
538
- );
539
- logApiCall(logger, method, path, { durationMs, error: networkError });
540
- throw networkError;
541
839
  }
840
+ throw new NaturalError("Unexpected retry exhaustion");
542
841
  }
543
842
  async get(path, options) {
544
843
  return this.request("GET", path, options);
@@ -561,6 +860,9 @@ var BaseResource = class {
561
860
  this.http = http;
562
861
  }
563
862
  };
863
+ function sanitizeHeaderValue(value) {
864
+ return value.replace(/[\x00-\x1f\x7f]/g, "");
865
+ }
564
866
 
565
867
  // src/resources/transactions.ts
566
868
  function unwrapTransactionResource(resource) {
@@ -605,9 +907,15 @@ var TransactionsResource = class extends BaseResource {
605
907
  */
606
908
  async get(transactionId, params) {
607
909
  const headers = {};
910
+ if (params?.agentId) {
911
+ headers["X-Agent-ID"] = params.agentId;
912
+ }
608
913
  if (params?.instanceId) {
609
914
  headers["X-Instance-ID"] = params.instanceId;
610
915
  }
916
+ if (params?.traceId) {
917
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
918
+ }
611
919
  const queryParams = {};
612
920
  if (params?.customerPartyId) {
613
921
  queryParams["partyId"] = params.customerPartyId;
@@ -638,6 +946,9 @@ var TransactionsResource = class extends BaseResource {
638
946
  if (params?.instanceId) {
639
947
  headers["X-Instance-ID"] = params.instanceId;
640
948
  }
949
+ if (params?.traceId) {
950
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
951
+ }
641
952
  const queryParams = {
642
953
  limit: params?.limit ?? 50,
643
954
  cursor: params?.cursor,
@@ -730,6 +1041,9 @@ var PaymentsResource = class extends BaseResource {
730
1041
  if (params.instanceId) {
731
1042
  headers["X-Instance-ID"] = params.instanceId;
732
1043
  }
1044
+ if (params.traceId) {
1045
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1046
+ }
733
1047
  const response = await this.http.post("/payments", {
734
1048
  body,
735
1049
  headers
@@ -812,9 +1126,15 @@ var WalletResource = class extends BaseResource {
812
1126
  */
813
1127
  async balance(options) {
814
1128
  const headers = {};
1129
+ if (options?.agentId) {
1130
+ headers["X-Agent-ID"] = options.agentId;
1131
+ }
815
1132
  if (options?.instanceId) {
816
1133
  headers["X-Instance-ID"] = options.instanceId;
817
1134
  }
1135
+ if (options?.traceId) {
1136
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1137
+ }
818
1138
  const params = {};
819
1139
  if (options?.customerPartyId) {
820
1140
  params["partyId"] = options.customerPartyId;
@@ -842,9 +1162,21 @@ var WalletResource = class extends BaseResource {
842
1162
  };
843
1163
  if (params.description) attributes["description"] = params.description;
844
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
+ }
845
1177
  const response = await this.http.post("/wallet/withdraw", {
846
1178
  body,
847
- headers: { "Idempotency-Key": params.idempotencyKey }
1179
+ headers
848
1180
  });
849
1181
  return unwrapWithdrawal(response);
850
1182
  }
@@ -899,9 +1231,15 @@ var AgentsResource = class extends BaseResource {
899
1231
  */
900
1232
  async list(params) {
901
1233
  const headers = {};
1234
+ if (params?.agentId) {
1235
+ headers["X-Agent-ID"] = params.agentId;
1236
+ }
902
1237
  if (params?.instanceId) {
903
1238
  headers["X-Instance-ID"] = params.instanceId;
904
1239
  }
1240
+ if (params?.traceId) {
1241
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1242
+ }
905
1243
  const queryParams = {
906
1244
  status: params?.status,
907
1245
  limit: params?.limit ?? 50,
@@ -921,9 +1259,15 @@ var AgentsResource = class extends BaseResource {
921
1259
  */
922
1260
  async get(agentId, options) {
923
1261
  const headers = {};
1262
+ if (options?.agentId) {
1263
+ headers["X-Agent-ID"] = options.agentId;
1264
+ }
924
1265
  if (options?.instanceId) {
925
1266
  headers["X-Instance-ID"] = options.instanceId;
926
1267
  }
1268
+ if (options?.traceId) {
1269
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1270
+ }
927
1271
  const response = await this.http.get(`/agents/${agentId}`, {
928
1272
  headers: Object.keys(headers).length > 0 ? headers : void 0
929
1273
  });
@@ -950,9 +1294,15 @@ var AgentsResource = class extends BaseResource {
950
1294
  if (params.idempotencyKey) {
951
1295
  headers["Idempotency-Key"] = params.idempotencyKey;
952
1296
  }
1297
+ if (params.agentId) {
1298
+ headers["X-Agent-ID"] = params.agentId;
1299
+ }
953
1300
  if (params.instanceId) {
954
1301
  headers["X-Instance-ID"] = params.instanceId;
955
1302
  }
1303
+ if (params.traceId) {
1304
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1305
+ }
956
1306
  const response = await this.http.post("/agents", {
957
1307
  body,
958
1308
  headers: Object.keys(headers).length > 0 ? headers : void 0
@@ -976,9 +1326,15 @@ var AgentsResource = class extends BaseResource {
976
1326
  if (params.idempotencyKey) {
977
1327
  headers["Idempotency-Key"] = params.idempotencyKey;
978
1328
  }
1329
+ if (params.agentId) {
1330
+ headers["X-Agent-ID"] = params.agentId;
1331
+ }
979
1332
  if (params.instanceId) {
980
1333
  headers["X-Instance-ID"] = params.instanceId;
981
1334
  }
1335
+ if (params.traceId) {
1336
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1337
+ }
982
1338
  const response = await this.http.put(`/agents/${agentId}`, {
983
1339
  body,
984
1340
  headers: Object.keys(headers).length > 0 ? headers : void 0
@@ -993,9 +1349,15 @@ var AgentsResource = class extends BaseResource {
993
1349
  */
994
1350
  async delete(agentId, options) {
995
1351
  const headers = {};
1352
+ if (options?.agentId) {
1353
+ headers["X-Agent-ID"] = options.agentId;
1354
+ }
996
1355
  if (options?.instanceId) {
997
1356
  headers["X-Instance-ID"] = options.instanceId;
998
1357
  }
1358
+ if (options?.traceId) {
1359
+ headers["X-Trace-ID"] = sanitizeHeaderValue(options.traceId);
1360
+ }
999
1361
  await this.http.delete(`/agents/${agentId}`, {
1000
1362
  headers: Object.keys(headers).length > 0 ? headers : void 0
1001
1363
  });
@@ -1061,6 +1423,9 @@ var DelegationsResource = class extends BaseResource {
1061
1423
  if (params?.instanceId) {
1062
1424
  headers["X-Instance-ID"] = params.instanceId;
1063
1425
  }
1426
+ if (params?.traceId) {
1427
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1428
+ }
1064
1429
  const queryParams = {
1065
1430
  delegationId: params?.delegationId,
1066
1431
  agentId: params?.agentId,
@@ -1146,9 +1511,15 @@ var CustomersResource = class extends BaseResource {
1146
1511
  */
1147
1512
  async list(params) {
1148
1513
  const headers = {};
1514
+ if (params?.agentId) {
1515
+ headers["X-Agent-ID"] = params.agentId;
1516
+ }
1149
1517
  if (params?.instanceId) {
1150
1518
  headers["X-Instance-ID"] = params.instanceId;
1151
1519
  }
1520
+ if (params?.traceId) {
1521
+ headers["X-Trace-ID"] = sanitizeHeaderValue(params.traceId);
1522
+ }
1152
1523
  const queryParams = {
1153
1524
  limit: params?.limit,
1154
1525
  cursor: params?.cursor
@@ -1194,6 +1565,84 @@ var NaturalClient = class {
1194
1565
  this.customers = new CustomersResource(this.http);
1195
1566
  }
1196
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
+ }
1197
1646
 
1198
1647
  // src/types/transactions.ts
1199
1648
  var TransactionTypeFilter = /* @__PURE__ */ ((TransactionTypeFilter2) => {
@@ -1203,6 +1652,6 @@ var TransactionTypeFilter = /* @__PURE__ */ ((TransactionTypeFilter2) => {
1203
1652
  return TransactionTypeFilter2;
1204
1653
  })(TransactionTypeFilter || {});
1205
1654
 
1206
- export { AuthenticationError, InsufficientFundsError, InvalidRequestError, NaturalClient, NaturalError, PaymentError, RateLimitError, RecipientNotFoundError, ServerError, TransactionTypeFilter, VERSION, bindContext, clearContext, configureLogging, getContext, getLogger, logApiCall, logError, logToolCall, runWithContext };
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 };
1207
1656
  //# sourceMappingURL=index.js.map
1208
1657
  //# sourceMappingURL=index.js.map