@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/README.md +23 -231
- package/dist/index.cjs +577 -118
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +208 -5
- package/dist/index.d.ts +208 -5
- package/dist/index.js +568 -119
- package/dist/index.js.map +1 -1
- package/dist/mcp/cli.cjs +1 -1
- package/dist/mcp/cli.cjs.map +1 -1
- package/dist/mcp/cli.js +1 -1
- package/dist/mcp/cli.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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(
|
|
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
|
-
|
|
271
|
+
logger3.error(message, extra);
|
|
265
272
|
}
|
|
266
|
-
function logApiCall(
|
|
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(
|
|
286
|
+
logError(logger3, `API call failed: ${method} ${path}`, extra);
|
|
280
287
|
} else if (options?.statusCode && options.statusCode >= 400) {
|
|
281
|
-
|
|
288
|
+
logger3.warning(`API call error: ${method} ${path} -> ${options.statusCode}`, extra);
|
|
282
289
|
} else {
|
|
283
|
-
|
|
290
|
+
logger3.info(`API call: ${method} ${path} -> ${options?.statusCode}`, extra);
|
|
284
291
|
}
|
|
285
292
|
}
|
|
286
|
-
function logToolCall(
|
|
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(
|
|
302
|
+
logError(logger3, `Tool call failed: ${toolName}`, extra);
|
|
296
303
|
} else if (options?.success === false) {
|
|
297
|
-
|
|
304
|
+
logger3.warning(`Tool call returned error: ${toolName}`, extra);
|
|
298
305
|
} else {
|
|
299
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
669
|
+
logApiCall(logger2, method, path, { statusCode: response.status, durationMs });
|
|
487
670
|
return data;
|
|
488
671
|
}
|
|
489
672
|
/**
|
|
490
|
-
*
|
|
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
|
|
493
|
-
|
|
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
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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 (
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|
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
|