@zendfi/sdk 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,14 +30,49 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- AuthenticationError: () => AuthenticationError,
33
+ AgentAPI: () => AgentAPI,
34
+ ApiError: () => ApiError,
35
+ AuthenticationError: () => AuthenticationError2,
36
+ AutonomyAPI: () => AutonomyAPI,
34
37
  ConfigLoader: () => ConfigLoader,
35
- NetworkError: () => NetworkError,
36
- RateLimitError: () => RateLimitError,
37
- ValidationError: () => ValidationError,
38
+ DeviceBoundSessionKey: () => DeviceBoundSessionKey,
39
+ DeviceFingerprintGenerator: () => DeviceFingerprintGenerator,
40
+ ERROR_CODES: () => ERROR_CODES,
41
+ InterceptorManager: () => InterceptorManager,
42
+ LitCryptoSigner: () => LitCryptoSigner,
43
+ NetworkError: () => NetworkError2,
44
+ PaymentError: () => PaymentError,
45
+ PaymentIntentsAPI: () => PaymentIntentsAPI,
46
+ PricingAPI: () => PricingAPI,
47
+ RateLimitError: () => RateLimitError2,
48
+ RateLimiter: () => RateLimiter,
49
+ RecoveryQRGenerator: () => RecoveryQRGenerator,
50
+ SPENDING_LIMIT_ACTION_CID: () => SPENDING_LIMIT_ACTION_CID,
51
+ SessionKeyCrypto: () => SessionKeyCrypto,
52
+ SmartPaymentsAPI: () => SmartPaymentsAPI,
53
+ ValidationError: () => ValidationError2,
54
+ WebhookError: () => WebhookError,
38
55
  ZendFiClient: () => ZendFiClient,
39
- ZendFiError: () => ZendFiError,
56
+ ZendFiError: () => ZendFiError2,
57
+ ZendFiSessionKeyManager: () => ZendFiSessionKeyManager,
58
+ asAgentKeyId: () => asAgentKeyId,
59
+ asEscrowId: () => asEscrowId,
60
+ asInstallmentPlanId: () => asInstallmentPlanId,
61
+ asIntentId: () => asIntentId,
62
+ asInvoiceId: () => asInvoiceId,
63
+ asMerchantId: () => asMerchantId,
64
+ asPaymentId: () => asPaymentId,
65
+ asPaymentLinkCode: () => asPaymentLinkCode,
66
+ asSessionId: () => asSessionId,
67
+ asSubscriptionId: () => asSubscriptionId,
68
+ createZendFiError: () => createZendFiError,
69
+ decodeSignatureFromLit: () => decodeSignatureFromLit,
70
+ encodeTransactionForLit: () => encodeTransactionForLit,
71
+ generateIdempotencyKey: () => generateIdempotencyKey,
72
+ isZendFiError: () => isZendFiError,
40
73
  processWebhook: () => processWebhook,
74
+ requiresLitSigning: () => requiresLitSigning,
75
+ sleep: () => sleep,
41
76
  verifyExpressWebhook: () => verifyExpressWebhook,
42
77
  verifyNextWebhook: () => verifyNextWebhook,
43
78
  verifyWebhookSignature: () => verifyWebhookSignature,
@@ -50,39 +85,16 @@ var import_cross_fetch = __toESM(require("cross-fetch"));
50
85
  var import_crypto = require("crypto");
51
86
 
52
87
  // src/types.ts
53
- var ZendFiError = class extends Error {
54
- constructor(message, statusCode, code, details) {
55
- super(message);
56
- this.statusCode = statusCode;
57
- this.code = code;
58
- this.details = details;
59
- this.name = "ZendFiError";
60
- }
61
- };
62
- var AuthenticationError = class extends ZendFiError {
63
- constructor(message = "Authentication failed") {
64
- super(message, 401, "AUTHENTICATION_ERROR");
65
- this.name = "AuthenticationError";
66
- }
67
- };
68
- var ValidationError = class extends ZendFiError {
69
- constructor(message, details) {
70
- super(message, 400, "VALIDATION_ERROR", details);
71
- this.name = "ValidationError";
72
- }
73
- };
74
- var NetworkError = class extends ZendFiError {
75
- constructor(message) {
76
- super(message, 0, "NETWORK_ERROR");
77
- this.name = "NetworkError";
78
- }
79
- };
80
- var RateLimitError = class extends ZendFiError {
81
- constructor(message = "Rate limit exceeded") {
82
- super(message, 429, "RATE_LIMIT_ERROR");
83
- this.name = "RateLimitError";
84
- }
85
- };
88
+ var asPaymentId = (id) => id;
89
+ var asSessionId = (id) => id;
90
+ var asAgentKeyId = (id) => id;
91
+ var asMerchantId = (id) => id;
92
+ var asInvoiceId = (id) => id;
93
+ var asSubscriptionId = (id) => id;
94
+ var asEscrowId = (id) => id;
95
+ var asInstallmentPlanId = (id) => id;
96
+ var asPaymentLinkCode = (id) => id;
97
+ var asIntentId = (id) => id;
86
98
 
87
99
  // src/utils.ts
88
100
  var ConfigLoader = class {
@@ -101,7 +113,8 @@ var ConfigLoader = class {
101
113
  mode,
102
114
  timeout: options?.timeout ?? 3e4,
103
115
  retries: options?.retries ?? 3,
104
- idempotencyEnabled: options?.idempotencyEnabled ?? true
116
+ idempotencyEnabled: options?.idempotencyEnabled ?? true,
117
+ debug: options?.debug ?? false
105
118
  };
106
119
  }
107
120
  /**
@@ -213,28 +226,6 @@ var ConfigLoader = class {
213
226
  }
214
227
  }
215
228
  };
216
- function parseError(response, body) {
217
- const statusCode = response.status;
218
- const errorMessage = body?.error || body?.message || response.statusText || "Unknown error";
219
- const errorCode = body?.code;
220
- const details = body?.details;
221
- switch (statusCode) {
222
- case 401:
223
- case 403:
224
- return new AuthenticationError(errorMessage);
225
- case 400:
226
- return new ValidationError(errorMessage, details);
227
- case 429:
228
- return new RateLimitError(errorMessage);
229
- case 500:
230
- case 502:
231
- case 503:
232
- case 504:
233
- return new NetworkError(errorMessage);
234
- default:
235
- return new ZendFiError(errorMessage, statusCode, errorCode, details);
236
- }
237
- }
238
229
  function generateIdempotencyKey() {
239
230
  const timestamp = Date.now();
240
231
  const random = Math.random().toString(36).substring(2, 15);
@@ -243,268 +234,1510 @@ function generateIdempotencyKey() {
243
234
  function sleep(ms) {
244
235
  return new Promise((resolve) => setTimeout(resolve, ms));
245
236
  }
246
-
247
- // src/client.ts
248
- var ZendFiClient = class {
249
- config;
250
- constructor(options) {
251
- this.config = ConfigLoader.load(options);
252
- ConfigLoader.validateApiKey(this.config.apiKey);
253
- if (this.config.environment === "development") {
254
- console.log(
255
- `\u2713 ZendFi SDK initialized in ${this.config.mode} mode (${this.config.mode === "test" ? "devnet" : "mainnet"})`
256
- );
257
- }
237
+ var RateLimiter = class {
238
+ requests = [];
239
+ maxRequests;
240
+ windowMs;
241
+ constructor(options = {}) {
242
+ this.maxRequests = options.maxRequests ?? 100;
243
+ this.windowMs = options.windowMs ?? 6e4;
258
244
  }
259
245
  /**
260
- * Create a new payment
246
+ * Check if a request can be made without exceeding rate limit
261
247
  */
262
- async createPayment(request) {
263
- return this.request("POST", "/api/v1/payments", {
264
- ...request,
265
- currency: request.currency || "USD",
266
- token: request.token || "USDC"
267
- });
248
+ canMakeRequest() {
249
+ this.pruneOldRequests();
250
+ return this.requests.length < this.maxRequests;
268
251
  }
269
252
  /**
270
- * Get payment by ID
253
+ * Record a request timestamp
271
254
  */
272
- async getPayment(paymentId) {
273
- return this.request("GET", `/api/v1/payments/${paymentId}`);
255
+ recordRequest() {
256
+ this.requests.push(Date.now());
274
257
  }
275
258
  /**
276
- * List all payments with pagination
259
+ * Get remaining requests in current window
277
260
  */
278
- async listPayments(request) {
279
- const params = new URLSearchParams();
280
- if (request?.page) params.append("page", request.page.toString());
281
- if (request?.limit) params.append("limit", request.limit.toString());
282
- if (request?.status) params.append("status", request.status);
283
- if (request?.from_date) params.append("from_date", request.from_date);
284
- if (request?.to_date) params.append("to_date", request.to_date);
285
- const query = params.toString() ? `?${params.toString()}` : "";
286
- return this.request("GET", `/api/v1/payments${query}`);
261
+ getRemainingRequests() {
262
+ this.pruneOldRequests();
263
+ return Math.max(0, this.maxRequests - this.requests.length);
287
264
  }
288
265
  /**
289
- * Create a subscription plan
266
+ * Get time in ms until the rate limit window resets
290
267
  */
291
- async createSubscriptionPlan(request) {
292
- return this.request("POST", "/api/v1/subscriptions/plans", {
293
- ...request,
294
- currency: request.currency || "USD",
295
- interval_count: request.interval_count || 1,
296
- trial_days: request.trial_days || 0
297
- });
268
+ getTimeUntilReset() {
269
+ if (this.requests.length === 0) return 0;
270
+ const oldestRequest = Math.min(...this.requests);
271
+ const resetTime = oldestRequest + this.windowMs;
272
+ return Math.max(0, resetTime - Date.now());
298
273
  }
299
274
  /**
300
- * Get subscription plan by ID
275
+ * Get current rate limit status
301
276
  */
302
- async getSubscriptionPlan(planId) {
303
- return this.request("GET", `/api/v1/subscriptions/plans/${planId}`);
277
+ getStatus() {
278
+ this.pruneOldRequests();
279
+ return {
280
+ remaining: this.getRemainingRequests(),
281
+ limit: this.maxRequests,
282
+ resetInMs: this.getTimeUntilReset(),
283
+ isLimited: !this.canMakeRequest()
284
+ };
304
285
  }
305
286
  /**
306
- * Create a subscription
287
+ * Reset the rate limiter (useful for testing)
307
288
  */
308
- async createSubscription(request) {
309
- return this.request("POST", "/api/v1/subscriptions", request);
289
+ reset() {
290
+ this.requests = [];
310
291
  }
311
- /**
312
- * Get subscription by ID
313
- */
314
- async getSubscription(subscriptionId) {
315
- return this.request("GET", `/api/v1/subscriptions/${subscriptionId}`);
292
+ pruneOldRequests() {
293
+ const cutoff = Date.now() - this.windowMs;
294
+ this.requests = this.requests.filter((t) => t > cutoff);
316
295
  }
317
- /**
318
- * Cancel a subscription
319
- */
320
- async cancelSubscription(subscriptionId) {
321
- return this.request(
322
- "POST",
323
- `/api/v1/subscriptions/${subscriptionId}/cancel`
324
- );
296
+ };
297
+
298
+ // src/errors.ts
299
+ var ZendFiError2 = class _ZendFiError extends Error {
300
+ code;
301
+ type;
302
+ suggestion;
303
+ docs_url;
304
+ statusCode;
305
+ response;
306
+ constructor(data) {
307
+ super(data.message);
308
+ this.name = "ZendFiError";
309
+ this.code = data.code;
310
+ this.type = data.type;
311
+ this.suggestion = data.suggestion;
312
+ this.statusCode = data.statusCode;
313
+ this.response = data.response;
314
+ this.docs_url = `https://docs.zendfi.com/errors/${data.code}`;
315
+ if (Error.captureStackTrace) {
316
+ Error.captureStackTrace(this, _ZendFiError);
317
+ }
325
318
  }
326
319
  /**
327
- * Create a payment link (shareable checkout URL)
320
+ * Format error for display
328
321
  */
329
- async createPaymentLink(request) {
330
- const response = await this.request("POST", "/api/v1/payment-links", {
331
- ...request,
332
- currency: request.currency || "USD",
333
- token: request.token || "USDC"
334
- });
335
- return {
336
- ...response,
337
- url: response.hosted_page_url
338
- };
322
+ toString() {
323
+ let message = `[${this.code}] ${this.message}`;
324
+ if (this.suggestion) {
325
+ message += `
326
+ \u{1F4A1} Suggestion: ${this.suggestion}`;
327
+ }
328
+ message += `
329
+ \u{1F4DA} Docs: ${this.docs_url}`;
330
+ return message;
339
331
  }
340
332
  /**
341
- * Get payment link by link code
333
+ * Convert error to JSON
342
334
  */
343
- async getPaymentLink(linkCode) {
344
- const response = await this.request("GET", `/api/v1/payment-links/${linkCode}`);
335
+ toJSON() {
345
336
  return {
346
- ...response,
347
- url: response.hosted_page_url
337
+ name: this.name,
338
+ code: this.code,
339
+ type: this.type,
340
+ message: this.message,
341
+ suggestion: this.suggestion,
342
+ docs_url: this.docs_url,
343
+ statusCode: this.statusCode
348
344
  };
349
345
  }
350
- /**
351
- * List all payment links for the authenticated merchant
352
- */
353
- async listPaymentLinks() {
354
- const response = await this.request("GET", "/api/v1/payment-links");
355
- return response.map((link) => ({
356
- ...link,
357
- url: link.hosted_page_url
358
- }));
346
+ };
347
+ var AuthenticationError2 = class extends ZendFiError2 {
348
+ constructor(message, code = "authentication_failed", suggestion) {
349
+ super({
350
+ code,
351
+ message,
352
+ type: "authentication_error",
353
+ suggestion: suggestion || "Check your API key in the dashboard at https://app.zendfi.com/settings/api-keys",
354
+ statusCode: 401
355
+ });
356
+ this.name = "AuthenticationError";
359
357
  }
360
- /**
361
- * Create an installment plan
362
- * Split a purchase into multiple scheduled payments
363
- */
364
- async createInstallmentPlan(request) {
365
- const response = await this.request(
366
- "POST",
367
- "/api/v1/installment-plans",
368
- request
369
- );
370
- return {
371
- id: response.plan_id,
372
- plan_id: response.plan_id,
373
- status: response.status
374
- };
358
+ };
359
+ var PaymentError = class extends ZendFiError2 {
360
+ constructor(message, code = "payment_failed", suggestion) {
361
+ super({
362
+ code,
363
+ message,
364
+ type: "payment_error",
365
+ suggestion,
366
+ statusCode: 400
367
+ });
368
+ this.name = "PaymentError";
375
369
  }
376
- /**
377
- * Get installment plan by ID
378
- */
379
- async getInstallmentPlan(planId) {
380
- return this.request("GET", `/api/v1/installment-plans/${planId}`);
370
+ };
371
+ var ValidationError2 = class extends ZendFiError2 {
372
+ constructor(message, code = "validation_failed", suggestion) {
373
+ super({
374
+ code,
375
+ message,
376
+ type: "validation_error",
377
+ suggestion,
378
+ statusCode: 400
379
+ });
380
+ this.name = "ValidationError";
381
381
  }
382
- /**
383
- * List all installment plans for merchant
384
- */
385
- async listInstallmentPlans(params) {
386
- const query = new URLSearchParams();
387
- if (params?.limit) query.append("limit", params.limit.toString());
388
- if (params?.offset) query.append("offset", params.offset.toString());
389
- const queryString = query.toString() ? `?${query.toString()}` : "";
390
- return this.request("GET", `/api/v1/installment-plans${queryString}`);
382
+ };
383
+ var NetworkError2 = class extends ZendFiError2 {
384
+ constructor(message, code = "network_error", suggestion) {
385
+ super({
386
+ code,
387
+ message,
388
+ type: "network_error",
389
+ suggestion: suggestion || "Check your internet connection and try again",
390
+ statusCode: 0
391
+ });
392
+ this.name = "NetworkError";
391
393
  }
392
- /**
393
- * List installment plans for a specific customer
394
- */
395
- async listCustomerInstallmentPlans(customerWallet) {
396
- return this.request(
397
- "GET",
398
- `/api/v1/customers/${customerWallet}/installment-plans`
399
- );
394
+ };
395
+ var RateLimitError2 = class extends ZendFiError2 {
396
+ constructor(message, retryAfter) {
397
+ super({
398
+ code: "rate_limit_exceeded",
399
+ message,
400
+ type: "rate_limit_error",
401
+ suggestion: retryAfter ? `Wait ${retryAfter} seconds before retrying` : "You are making too many requests. Please slow down.",
402
+ statusCode: 429
403
+ });
404
+ this.name = "RateLimitError";
400
405
  }
401
- /**
402
- * Cancel an installment plan
403
- */
404
- async cancelInstallmentPlan(planId) {
405
- return this.request(
406
- "POST",
407
- `/api/v1/installment-plans/${planId}/cancel`
408
- );
406
+ };
407
+ var ApiError = class extends ZendFiError2 {
408
+ constructor(message, code, statusCode, response) {
409
+ super({
410
+ code,
411
+ message,
412
+ type: "api_error",
413
+ statusCode,
414
+ response
415
+ });
416
+ this.name = "ApiError";
409
417
  }
410
- /**
411
- * Create an escrow transaction
412
- * Hold funds until conditions are met
413
- */
414
- async createEscrow(request) {
415
- return this.request("POST", "/api/v1/escrows", {
416
- ...request,
417
- currency: request.currency || "USD",
418
- token: request.token || "USDC"
418
+ };
419
+ var WebhookError = class extends ZendFiError2 {
420
+ constructor(message, code = "webhook_verification_failed", suggestion) {
421
+ super({
422
+ code,
423
+ message,
424
+ type: "webhook_error",
425
+ suggestion: suggestion || "Check your webhook secret matches the one in your dashboard",
426
+ statusCode: 400
419
427
  });
428
+ this.name = "WebhookError";
420
429
  }
421
- /**
422
- * Get escrow by ID
423
- */
424
- async getEscrow(escrowId) {
425
- return this.request("GET", `/api/v1/escrows/${escrowId}`);
430
+ };
431
+ function createZendFiError(statusCode, responseBody, message) {
432
+ const errorMessage = message || responseBody?.error?.message || responseBody?.message || "An error occurred";
433
+ const errorCode = responseBody?.error?.code || responseBody?.code || "unknown_error";
434
+ if (statusCode === 401) {
435
+ return new AuthenticationError2(
436
+ errorMessage,
437
+ errorCode,
438
+ "Verify your API key is correct and not expired"
439
+ );
426
440
  }
427
- /**
428
- * List all escrows for merchant
429
- */
430
- async listEscrows(params) {
431
- const query = new URLSearchParams();
432
- if (params?.limit) query.append("limit", params.limit.toString());
433
- if (params?.offset) query.append("offset", params.offset.toString());
434
- const queryString = query.toString() ? `?${query.toString()}` : "";
435
- return this.request("GET", `/api/v1/escrows${queryString}`);
441
+ if (statusCode === 429) {
442
+ const retryAfter = responseBody?.retry_after;
443
+ return new RateLimitError2(errorMessage, retryAfter);
436
444
  }
437
- /**
438
- * Approve escrow release to seller
439
- */
440
- async approveEscrow(escrowId, request) {
441
- return this.request(
442
- "POST",
443
- `/api/v1/escrows/${escrowId}/approve`,
444
- request
445
+ if (statusCode === 400 || statusCode === 422) {
446
+ return new ValidationError2(errorMessage, errorCode);
447
+ }
448
+ if (statusCode === 402) {
449
+ return new PaymentError(errorMessage, errorCode);
450
+ }
451
+ if (statusCode === 0 || statusCode >= 500) {
452
+ return new NetworkError2(
453
+ errorMessage,
454
+ errorCode,
455
+ statusCode >= 500 ? "The ZendFi API is experiencing issues. Please try again later." : void 0
445
456
  );
446
457
  }
458
+ return new ApiError(errorMessage, errorCode, statusCode, responseBody);
459
+ }
460
+ var ERROR_CODES = {
461
+ // Authentication
462
+ INVALID_API_KEY: "invalid_api_key",
463
+ API_KEY_EXPIRED: "api_key_expired",
464
+ API_KEY_REVOKED: "api_key_revoked",
465
+ // Payment
466
+ INSUFFICIENT_BALANCE: "insufficient_balance",
467
+ PAYMENT_DECLINED: "payment_declined",
468
+ PAYMENT_EXPIRED: "payment_expired",
469
+ INVALID_AMOUNT: "invalid_amount",
470
+ INVALID_CURRENCY: "invalid_currency",
471
+ // Validation
472
+ MISSING_REQUIRED_FIELD: "missing_required_field",
473
+ INVALID_PARAMETER: "invalid_parameter",
474
+ // Network
475
+ NETWORK_ERROR: "network_error",
476
+ TIMEOUT: "timeout",
477
+ // Rate limiting
478
+ RATE_LIMIT_EXCEEDED: "rate_limit_exceeded",
479
+ // Webhook
480
+ WEBHOOK_SIGNATURE_INVALID: "webhook_signature_invalid",
481
+ WEBHOOK_TIMESTAMP_TOO_OLD: "webhook_timestamp_too_old"
482
+ };
483
+ function isZendFiError(error) {
484
+ return error instanceof ZendFiError2;
485
+ }
486
+
487
+ // src/interceptors.ts
488
+ var InterceptorManager = class {
489
+ handlers = [];
447
490
  /**
448
- * Refund escrow to buyer
491
+ * Add an interceptor
449
492
  */
450
- async refundEscrow(escrowId, request) {
451
- return this.request("POST", `/api/v1/escrows/${escrowId}/refund`, request);
493
+ use(handler) {
494
+ this.handlers.push(handler);
495
+ return this.handlers.length - 1;
452
496
  }
453
497
  /**
454
- * Raise a dispute for an escrow
498
+ * Remove an interceptor
455
499
  */
456
- async disputeEscrow(escrowId, request) {
457
- return this.request("POST", `/api/v1/escrows/${escrowId}/dispute`, request);
500
+ eject(id) {
501
+ if (this.handlers[id]) {
502
+ this.handlers[id] = null;
503
+ }
458
504
  }
459
505
  /**
460
- * Create an invoice
506
+ * Execute all interceptors in sequence
461
507
  */
462
- async createInvoice(request) {
463
- return this.request("POST", "/api/v1/invoices", {
464
- ...request,
465
- token: request.token || "USDC"
466
- });
508
+ async execute(initialValue) {
509
+ let result = initialValue;
510
+ for (const handler of this.handlers) {
511
+ if (handler !== null) {
512
+ result = await handler(result);
513
+ }
514
+ }
515
+ return result;
467
516
  }
468
517
  /**
469
- * Get invoice by ID
518
+ * Check if any interceptors are registered
470
519
  */
471
- async getInvoice(invoiceId) {
472
- return this.request("GET", `/api/v1/invoices/${invoiceId}`);
520
+ has() {
521
+ return this.handlers.some((h) => h !== null);
473
522
  }
474
523
  /**
475
- * List all invoices for merchant
524
+ * Clear all interceptors
476
525
  */
477
- async listInvoices() {
478
- return this.request("GET", "/api/v1/invoices");
526
+ clear() {
527
+ this.handlers = [];
479
528
  }
480
- /**
481
- * Send invoice to customer via email
482
- */
483
- async sendInvoice(invoiceId) {
484
- return this.request("POST", `/api/v1/invoices/${invoiceId}/send`);
529
+ };
530
+ function createInterceptors() {
531
+ return {
532
+ request: new InterceptorManager(),
533
+ response: new InterceptorManager(),
534
+ error: new InterceptorManager()
535
+ };
536
+ }
537
+
538
+ // src/api/agent.ts
539
+ function normalizeArrayResponse(response, key) {
540
+ if (Array.isArray(response)) {
541
+ return response;
485
542
  }
543
+ return response[key] || [];
544
+ }
545
+ var AgentAPI = class {
546
+ constructor(request) {
547
+ this.request = request;
548
+ }
549
+ // ============================================
550
+ // Agent API Keys
551
+ // ============================================
486
552
  /**
487
- * Verify webhook signature using HMAC-SHA256
553
+ * Create a new agent API key with scoped permissions
488
554
  *
489
- * @param request - Webhook verification request containing payload, signature, and secret
490
- * @returns true if signature is valid, false otherwise
555
+ * Agent keys (prefixed with `zai_`) have limited permissions compared to
556
+ * merchant keys. This enables safe delegation to AI agents.
557
+ *
558
+ * @param request - Agent key configuration
559
+ * @returns The created agent key (full_key only returned on creation!)
491
560
  *
492
561
  * @example
493
562
  * ```typescript
494
- * const isValid = zendfi.verifyWebhook({
495
- * payload: req.body,
496
- * signature: req.headers['x-zendfi-signature'],
497
- * secret: process.env.ZENDFI_WEBHOOK_SECRET
563
+ * const agentKey = await zendfi.agent.createKey({
564
+ * name: 'Shopping Assistant',
565
+ * agent_id: 'shopping-assistant-v1',
566
+ * scopes: ['create_payments'],
567
+ * rate_limit_per_hour: 500,
498
568
  * });
499
569
  *
500
- * if (!isValid) {
501
- * return res.status(401).json({ error: 'Invalid signature' });
502
- * }
570
+ * // IMPORTANT: Save the full_key now - it won't be shown again!
571
+ * console.log(agentKey.full_key); // => "zai_test_abc123..."
503
572
  * ```
504
573
  */
505
- verifyWebhook(request) {
506
- try {
507
- if (!request.payload || !request.signature || !request.secret) {
574
+ async createKey(request) {
575
+ return this.request("POST", "/api/v1/agent-keys", {
576
+ name: request.name,
577
+ agent_id: request.agent_id,
578
+ agent_name: request.agent_name,
579
+ scopes: request.scopes || ["create_payments"],
580
+ rate_limit_per_hour: request.rate_limit_per_hour || 1e3,
581
+ metadata: request.metadata
582
+ });
583
+ }
584
+ /**
585
+ * List all agent API keys for the merchant
586
+ *
587
+ * @returns Array of agent API keys (without full_key for security)
588
+ *
589
+ * @example
590
+ * ```typescript
591
+ * const keys = await zendfi.agent.listKeys();
592
+ * keys.forEach(key => {
593
+ * console.log(`${key.name}: ${key.key_prefix}*** (${key.scopes.join(', ')})`);
594
+ * });
595
+ * ```
596
+ */
597
+ async listKeys() {
598
+ const response = await this.request(
599
+ "GET",
600
+ "/api/v1/agent-keys"
601
+ );
602
+ return normalizeArrayResponse(response, "keys");
603
+ }
604
+ /**
605
+ * Revoke an agent API key
606
+ *
607
+ * Once revoked, the key cannot be used for any API calls.
608
+ * This action is irreversible.
609
+ *
610
+ * @param keyId - UUID of the agent key to revoke
611
+ *
612
+ * @example
613
+ * ```typescript
614
+ * await zendfi.agent.revokeKey('ak_123...');
615
+ * console.log('Agent key revoked');
616
+ * ```
617
+ */
618
+ async revokeKey(keyId) {
619
+ await this.request("POST", `/api/v1/agent-keys/${keyId}/revoke`);
620
+ }
621
+ // ============================================
622
+ // Agent Sessions
623
+ // ============================================
624
+ /**
625
+ * Create an agent session with spending limits
626
+ *
627
+ * Sessions provide time-bounded authorization for agents to make payments
628
+ * on behalf of users, with configurable spending limits.
629
+ *
630
+ * @param request - Session configuration
631
+ * @returns The created session with token
632
+ *
633
+ * @example
634
+ * ```typescript
635
+ * const session = await zendfi.agent.createSession({
636
+ * agent_id: 'shopping-assistant-v1',
637
+ * agent_name: 'Shopping Assistant',
638
+ * user_wallet: 'Hx7B...abc',
639
+ * limits: {
640
+ * max_per_transaction: 100,
641
+ * max_per_day: 500,
642
+ * require_approval_above: 50,
643
+ * },
644
+ * duration_hours: 24,
645
+ * });
646
+ *
647
+ * // Use session_token for subsequent API calls
648
+ * console.log(session.session_token); // => "zai_session_..."
649
+ * ```
650
+ */
651
+ async createSession(request) {
652
+ return this.request("POST", "/api/v1/ai/sessions", {
653
+ agent_id: request.agent_id,
654
+ agent_name: request.agent_name,
655
+ user_wallet: request.user_wallet,
656
+ limits: request.limits || {
657
+ max_per_transaction: 1e3,
658
+ max_per_day: 5e3,
659
+ max_per_week: 2e4,
660
+ max_per_month: 5e4,
661
+ require_approval_above: 500
662
+ },
663
+ allowed_merchants: request.allowed_merchants,
664
+ duration_hours: request.duration_hours || 24,
665
+ mint_pkp: request.mint_pkp,
666
+ metadata: request.metadata
667
+ });
668
+ }
669
+ /**
670
+ * List all agent sessions
671
+ *
672
+ * @returns Array of agent sessions (both active and expired)
673
+ *
674
+ * @example
675
+ * ```typescript
676
+ * const sessions = await zendfi.agent.listSessions();
677
+ * const activeSessions = sessions.filter(s => s.is_active);
678
+ * console.log(`${activeSessions.length} active sessions`);
679
+ * ```
680
+ */
681
+ async listSessions() {
682
+ const response = await this.request(
683
+ "GET",
684
+ "/api/v1/ai/sessions"
685
+ );
686
+ return normalizeArrayResponse(response, "sessions");
687
+ }
688
+ /**
689
+ * Get a specific agent session by ID
690
+ *
691
+ * @param sessionId - UUID of the session
692
+ * @returns The session details with remaining limits
693
+ *
694
+ * @example
695
+ * ```typescript
696
+ * const session = await zendfi.agent.getSession('sess_123...');
697
+ * console.log(`Remaining today: $${session.remaining_today}`);
698
+ * console.log(`Expires: ${session.expires_at}`);
699
+ * ```
700
+ */
701
+ async getSession(sessionId) {
702
+ return this.request("GET", `/api/v1/ai/sessions/${sessionId}`);
703
+ }
704
+ /**
705
+ * Revoke an agent session
706
+ *
707
+ * Immediately invalidates the session, preventing any further payments.
708
+ * This action is irreversible.
709
+ *
710
+ * @param sessionId - UUID of the session to revoke
711
+ *
712
+ * @example
713
+ * ```typescript
714
+ * await zendfi.agent.revokeSession('sess_123...');
715
+ * console.log('Session revoked - agent can no longer make payments');
716
+ * ```
717
+ */
718
+ async revokeSession(sessionId) {
719
+ await this.request("POST", `/api/v1/ai/sessions/${sessionId}/revoke`);
720
+ }
721
+ // ============================================
722
+ // Agent Analytics
723
+ // ============================================
724
+ /**
725
+ * Get analytics for all agent activity
726
+ *
727
+ * @returns Comprehensive analytics including payments, success rate, and PPP savings
728
+ *
729
+ * @example
730
+ * ```typescript
731
+ * const analytics = await zendfi.agent.getAnalytics();
732
+ * console.log(`Total volume: $${analytics.total_volume_usd}`);
733
+ * console.log(`Success rate: ${(analytics.success_rate * 100).toFixed(1)}%`);
734
+ * console.log(`PPP savings: $${analytics.ppp_savings_usd}`);
735
+ * ```
736
+ */
737
+ async getAnalytics() {
738
+ return this.request("GET", "/api/v1/analytics/agents");
739
+ }
740
+ };
741
+
742
+ // src/api/intents.ts
743
+ var PaymentIntentsAPI = class {
744
+ constructor(request) {
745
+ this.request = request;
746
+ }
747
+ /**
748
+ * Create a payment intent
749
+ *
750
+ * This is step 1 of the two-phase payment flow. The intent reserves
751
+ * the payment amount and provides a client_secret for confirmation.
752
+ *
753
+ * @param request - Payment intent configuration
754
+ * @returns The created payment intent with client_secret
755
+ *
756
+ * @example
757
+ * ```typescript
758
+ * const intent = await zendfi.intents.create({
759
+ * amount: 49.99,
760
+ * description: 'Pro Plan - Monthly',
761
+ * capture_method: 'automatic', // or 'manual' for auth-only
762
+ * expires_in_seconds: 3600, // 1 hour
763
+ * });
764
+ *
765
+ * // Store intent.id and pass intent.client_secret to frontend
766
+ * console.log(`Intent created: ${intent.id}`);
767
+ * console.log(`Status: ${intent.status}`); // "requires_payment"
768
+ * ```
769
+ */
770
+ async create(request) {
771
+ return this.request("POST", "/api/v1/payment-intents", {
772
+ amount: request.amount,
773
+ currency: request.currency || "USD",
774
+ description: request.description,
775
+ capture_method: request.capture_method || "automatic",
776
+ agent_id: request.agent_id,
777
+ agent_name: request.agent_name,
778
+ metadata: request.metadata,
779
+ expires_in_seconds: request.expires_in_seconds || 86400
780
+ // 24h default
781
+ });
782
+ }
783
+ /**
784
+ * Get a payment intent by ID
785
+ *
786
+ * @param intentId - UUID of the payment intent
787
+ * @returns The payment intent details
788
+ *
789
+ * @example
790
+ * ```typescript
791
+ * const intent = await zendfi.intents.get('pi_123...');
792
+ * console.log(`Status: ${intent.status}`);
793
+ * if (intent.payment_id) {
794
+ * console.log(`Payment: ${intent.payment_id}`);
795
+ * }
796
+ * ```
797
+ */
798
+ async get(intentId) {
799
+ return this.request("GET", `/api/v1/payment-intents/${intentId}`);
800
+ }
801
+ /**
802
+ * List payment intents
803
+ *
804
+ * @param options - Filter and pagination options
805
+ * @returns Array of payment intents
806
+ *
807
+ * @example
808
+ * ```typescript
809
+ * // Get recent pending intents
810
+ * const intents = await zendfi.intents.list({
811
+ * status: 'requires_payment',
812
+ * limit: 20,
813
+ * });
814
+ * ```
815
+ */
816
+ async list(options) {
817
+ const params = new URLSearchParams();
818
+ if (options?.status) params.append("status", options.status);
819
+ if (options?.limit) params.append("limit", options.limit.toString());
820
+ if (options?.offset) params.append("offset", options.offset.toString());
821
+ const query = params.toString() ? `?${params.toString()}` : "";
822
+ const response = await this.request(
823
+ "GET",
824
+ `/api/v1/payment-intents${query}`
825
+ );
826
+ return Array.isArray(response) ? response : response.intents;
827
+ }
828
+ /**
829
+ * Confirm a payment intent
830
+ *
831
+ * This is step 2 of the two-phase payment flow. Confirmation triggers
832
+ * the actual payment using the customer's wallet.
833
+ *
834
+ * @param intentId - UUID of the payment intent
835
+ * @param request - Confirmation details including customer wallet
836
+ * @returns The confirmed payment intent with payment_id
837
+ *
838
+ * @example
839
+ * ```typescript
840
+ * const confirmed = await zendfi.intents.confirm('pi_123...', {
841
+ * client_secret: 'pi_secret_abc...',
842
+ * customer_wallet: 'Hx7B...abc',
843
+ * auto_gasless: true,
844
+ * });
845
+ *
846
+ * if (confirmed.status === 'succeeded') {
847
+ * console.log(`Payment complete: ${confirmed.payment_id}`);
848
+ * }
849
+ * ```
850
+ */
851
+ async confirm(intentId, request) {
852
+ return this.request("POST", `/api/v1/payment-intents/${intentId}/confirm`, {
853
+ client_secret: request.client_secret,
854
+ customer_wallet: request.customer_wallet,
855
+ payment_type: request.payment_type,
856
+ auto_gasless: request.auto_gasless,
857
+ metadata: request.metadata
858
+ });
859
+ }
860
+ /**
861
+ * Cancel a payment intent
862
+ *
863
+ * Canceling releases any hold on the payment amount. Cannot cancel
864
+ * intents that are already processing or succeeded.
865
+ *
866
+ * @param intentId - UUID of the payment intent
867
+ * @returns The canceled payment intent
868
+ *
869
+ * @example
870
+ * ```typescript
871
+ * const canceled = await zendfi.intents.cancel('pi_123...');
872
+ * console.log(`Status: ${canceled.status}`); // "canceled"
873
+ * ```
874
+ */
875
+ async cancel(intentId) {
876
+ return this.request("POST", `/api/v1/payment-intents/${intentId}/cancel`);
877
+ }
878
+ /**
879
+ * Get events for a payment intent
880
+ *
881
+ * Events track the full lifecycle of the intent, including creation,
882
+ * confirmation attempts, and status changes.
883
+ *
884
+ * @param intentId - UUID of the payment intent
885
+ * @returns Array of events in chronological order
886
+ *
887
+ * @example
888
+ * ```typescript
889
+ * const events = await zendfi.intents.getEvents('pi_123...');
890
+ * events.forEach(event => {
891
+ * console.log(`${event.created_at}: ${event.event_type}`);
892
+ * });
893
+ * ```
894
+ */
895
+ async getEvents(intentId) {
896
+ const response = await this.request(
897
+ "GET",
898
+ `/api/v1/payment-intents/${intentId}/events`
899
+ );
900
+ return Array.isArray(response) ? response : response.events;
901
+ }
902
+ };
903
+
904
+ // src/api/pricing.ts
905
+ var PricingAPI = class {
906
+ constructor(request) {
907
+ this.request = request;
908
+ }
909
+ /**
910
+ * Get PPP factor for a specific country
911
+ *
912
+ * Returns the purchasing power parity adjustment factor for the given
913
+ * country code. Use this to calculate localized pricing.
914
+ *
915
+ * @param countryCode - ISO 3166-1 alpha-2 country code (e.g., "BR", "IN", "NG")
916
+ * @returns PPP factor and suggested adjustment
917
+ *
918
+ * @example
919
+ * ```typescript
920
+ * const factor = await zendfi.pricing.getPPPFactor('BR');
921
+ * // {
922
+ * // country_code: 'BR',
923
+ * // country_name: 'Brazil',
924
+ * // ppp_factor: 0.35,
925
+ * // currency_code: 'BRL',
926
+ * // adjustment_percentage: 35.0
927
+ * // }
928
+ *
929
+ * // Calculate localized price
930
+ * const usdPrice = 100;
931
+ * const localPrice = usdPrice * (1 - factor.adjustment_percentage / 100);
932
+ * console.log(`$${localPrice} for Brazilian customers`);
933
+ * ```
934
+ */
935
+ async getPPPFactor(countryCode) {
936
+ return this.request("POST", "/api/v1/ai/pricing/ppp-factor", {
937
+ country_code: countryCode.toUpperCase()
938
+ });
939
+ }
940
+ /**
941
+ * List all available PPP factors
942
+ *
943
+ * Returns PPP factors for all supported countries. Useful for building
944
+ * pricing tables or pre-computing regional prices.
945
+ *
946
+ * @returns Array of PPP factors for all supported countries
947
+ *
948
+ * @example
949
+ * ```typescript
950
+ * const factors = await zendfi.pricing.listFactors();
951
+ *
952
+ * // Create pricing tiers
953
+ * const tiers = factors.map(f => ({
954
+ * country: f.country_name,
955
+ * price: (100 * f.ppp_factor).toFixed(2),
956
+ * }));
957
+ *
958
+ * console.table(tiers);
959
+ * ```
960
+ */
961
+ async listFactors() {
962
+ const response = await this.request(
963
+ "GET",
964
+ "/api/v1/ai/pricing/ppp-factors"
965
+ );
966
+ return Array.isArray(response) ? response : response.factors;
967
+ }
968
+ /**
969
+ * Get AI-powered pricing suggestion
970
+ *
971
+ * Returns an intelligent pricing recommendation based on the user's
972
+ * location, wallet history, and your pricing configuration.
973
+ *
974
+ * @param request - Pricing suggestion request with user context
975
+ * @returns AI-generated pricing suggestion with reasoning
976
+ *
977
+ * @example
978
+ * ```typescript
979
+ * const suggestion = await zendfi.pricing.getSuggestion({
980
+ * agent_id: 'shopping-assistant',
981
+ * base_price: 99.99,
982
+ * user_profile: {
983
+ * location_country: 'BR',
984
+ * context: 'first-time',
985
+ * },
986
+ * ppp_config: {
987
+ * enabled: true,
988
+ * max_discount_percent: 50,
989
+ * floor_price: 29.99,
990
+ * },
991
+ * });
992
+ *
993
+ * console.log(`Suggested: $${suggestion.suggested_amount}`);
994
+ * console.log(`Reason: ${suggestion.reasoning}`);
995
+ * // => "Price adjusted for Brazilian purchasing power (35% PPP discount)
996
+ * // plus 10% first-time customer discount"
997
+ * ```
998
+ */
999
+ async getSuggestion(request) {
1000
+ return this.request("POST", "/api/v1/ai/pricing/suggest", {
1001
+ agent_id: request.agent_id,
1002
+ product_id: request.product_id,
1003
+ base_price: request.base_price,
1004
+ currency: request.currency || "USD",
1005
+ user_profile: request.user_profile,
1006
+ ppp_config: request.ppp_config
1007
+ });
1008
+ }
1009
+ /**
1010
+ * Calculate localized price for a given base price and country
1011
+ *
1012
+ * Convenience method that combines getPPPFactor with price calculation.
1013
+ *
1014
+ * @param basePrice - Original price in USD
1015
+ * @param countryCode - ISO 3166-1 alpha-2 country code
1016
+ * @returns Object with original and adjusted prices
1017
+ *
1018
+ * @example
1019
+ * ```typescript
1020
+ * const result = await zendfi.pricing.calculateLocalPrice(100, 'IN');
1021
+ * console.log(`Original: $${result.original}`);
1022
+ * console.log(`Local: $${result.adjusted}`);
1023
+ * console.log(`Savings: $${result.savings} (${result.discount_percentage}%)`);
1024
+ * ```
1025
+ */
1026
+ async calculateLocalPrice(basePrice, countryCode) {
1027
+ const factor = await this.getPPPFactor(countryCode);
1028
+ const adjusted = Number((basePrice * factor.ppp_factor).toFixed(2));
1029
+ const savings = Number((basePrice - adjusted).toFixed(2));
1030
+ return {
1031
+ original: basePrice,
1032
+ adjusted,
1033
+ savings,
1034
+ discount_percentage: factor.adjustment_percentage,
1035
+ country: factor.country_name,
1036
+ ppp_factor: factor.ppp_factor
1037
+ };
1038
+ }
1039
+ };
1040
+
1041
+ // src/api/autonomy.ts
1042
+ var AutonomyAPI = class {
1043
+ constructor(request) {
1044
+ this.request = request;
1045
+ }
1046
+ /**
1047
+ * Enable autonomous signing for a session key
1048
+ *
1049
+ * This grants an AI agent the ability to sign transactions on behalf of
1050
+ * the user, up to the specified spending limit and duration.
1051
+ *
1052
+ * **Prerequisites:**
1053
+ * 1. Create a device-bound session key first
1054
+ * 2. Generate a delegation signature (see `createDelegationMessage`)
1055
+ * 3. Optionally encrypt keypair with Lit Protocol for true autonomy
1056
+ *
1057
+ * @param sessionKeyId - UUID of the session key
1058
+ * @param request - Autonomy configuration including delegation signature
1059
+ * @returns The created autonomous delegate
1060
+ *
1061
+ * @example
1062
+ * ```typescript
1063
+ * // The user must sign this exact message format
1064
+ * const message = zendfi.autonomy.createDelegationMessage(
1065
+ * sessionKeyId, 100, '2024-12-10T00:00:00Z'
1066
+ * );
1067
+ *
1068
+ * // Have user sign with their session key
1069
+ * const signature = await signWithSessionKey(message, pin);
1070
+ *
1071
+ * // Enable autonomous mode
1072
+ * const delegate = await zendfi.autonomy.enable(sessionKeyId, {
1073
+ * max_amount_usd: 100,
1074
+ * duration_hours: 24,
1075
+ * delegation_signature: signature,
1076
+ * });
1077
+ *
1078
+ * console.log(`Delegate ID: ${delegate.delegate_id}`);
1079
+ * console.log(`Expires: ${delegate.expires_at}`);
1080
+ * ```
1081
+ */
1082
+ async enable(sessionKeyId, request) {
1083
+ return this.request(
1084
+ "POST",
1085
+ `/api/v1/ai/session-keys/${sessionKeyId}/enable-autonomy`,
1086
+ {
1087
+ max_amount_usd: request.max_amount_usd,
1088
+ duration_hours: request.duration_hours,
1089
+ delegation_signature: request.delegation_signature,
1090
+ expires_at: request.expires_at,
1091
+ lit_encrypted_keypair: request.lit_encrypted_keypair,
1092
+ lit_data_hash: request.lit_data_hash,
1093
+ metadata: request.metadata
1094
+ }
1095
+ );
1096
+ }
1097
+ /**
1098
+ * Revoke autonomous mode for a session key
1099
+ *
1100
+ * Immediately invalidates the autonomous delegate, preventing any further
1101
+ * automatic payments. The session key itself remains valid for manual use.
1102
+ *
1103
+ * @param sessionKeyId - UUID of the session key
1104
+ * @param reason - Optional reason for revocation (logged for audit)
1105
+ *
1106
+ * @example
1107
+ * ```typescript
1108
+ * await zendfi.autonomy.revoke('sk_123...', 'User requested revocation');
1109
+ * console.log('Autonomous mode disabled');
1110
+ * ```
1111
+ */
1112
+ async revoke(sessionKeyId, reason) {
1113
+ const request = { reason };
1114
+ await this.request(
1115
+ "POST",
1116
+ `/api/v1/ai/session-keys/${sessionKeyId}/revoke-autonomy`,
1117
+ request
1118
+ );
1119
+ }
1120
+ /**
1121
+ * Get autonomy status for a session key
1122
+ *
1123
+ * Returns whether autonomous mode is enabled and details about the
1124
+ * active delegate including remaining spending allowance.
1125
+ *
1126
+ * @param sessionKeyId - UUID of the session key
1127
+ * @returns Autonomy status with delegate details
1128
+ *
1129
+ * @example
1130
+ * ```typescript
1131
+ * const status = await zendfi.autonomy.getStatus('sk_123...');
1132
+ *
1133
+ * if (status.autonomous_mode_enabled && status.delegate) {
1134
+ * console.log(`Remaining: $${status.delegate.remaining_usd}`);
1135
+ * console.log(`Expires: ${status.delegate.expires_at}`);
1136
+ * } else {
1137
+ * console.log('Autonomous mode not enabled');
1138
+ * }
1139
+ * ```
1140
+ */
1141
+ async getStatus(sessionKeyId) {
1142
+ return this.request(
1143
+ "GET",
1144
+ `/api/v1/ai/session-keys/${sessionKeyId}/autonomy-status`
1145
+ );
1146
+ }
1147
+ /**
1148
+ * Create the delegation message that needs to be signed
1149
+ *
1150
+ * This generates the exact message format required for the delegation
1151
+ * signature. The user must sign this message with their session key.
1152
+ *
1153
+ * **Message format:**
1154
+ * ```
1155
+ * I authorize autonomous delegate for session {id} to spend up to ${amount} until {expiry}
1156
+ * ```
1157
+ *
1158
+ * @param sessionKeyId - UUID of the session key
1159
+ * @param maxAmountUsd - Maximum spending amount in USD
1160
+ * @param expiresAt - ISO 8601 expiration timestamp
1161
+ * @returns The message to be signed
1162
+ *
1163
+ * @example
1164
+ * ```typescript
1165
+ * const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
1166
+ * const message = zendfi.autonomy.createDelegationMessage(
1167
+ * 'sk_123...',
1168
+ * 100,
1169
+ * expiresAt
1170
+ * );
1171
+ * // => "I authorize autonomous delegate for session sk_123... to spend up to $100 until 2024-12-06T..."
1172
+ *
1173
+ * // Sign with nacl.sign.detached() or similar
1174
+ * const signature = signMessage(message, keypair);
1175
+ * ```
1176
+ */
1177
+ createDelegationMessage(sessionKeyId, maxAmountUsd, expiresAt) {
1178
+ return `I authorize autonomous delegate for session ${sessionKeyId} to spend up to $${maxAmountUsd} until ${expiresAt}`;
1179
+ }
1180
+ /**
1181
+ * Validate delegation signature parameters
1182
+ *
1183
+ * Helper method to check if autonomy parameters are valid before
1184
+ * making the API call.
1185
+ *
1186
+ * @param request - The enable autonomy request to validate
1187
+ * @throws Error if validation fails
1188
+ *
1189
+ * @example
1190
+ * ```typescript
1191
+ * try {
1192
+ * zendfi.autonomy.validateRequest(request);
1193
+ * const delegate = await zendfi.autonomy.enable(sessionKeyId, request);
1194
+ * } catch (error) {
1195
+ * console.error('Invalid request:', error.message);
1196
+ * }
1197
+ * ```
1198
+ */
1199
+ validateRequest(request) {
1200
+ if (request.max_amount_usd <= 0) {
1201
+ throw new Error("max_amount_usd must be positive");
1202
+ }
1203
+ if (request.duration_hours < 1 || request.duration_hours > 168) {
1204
+ throw new Error("duration_hours must be between 1 and 168 (7 days)");
1205
+ }
1206
+ if (!request.delegation_signature || request.delegation_signature.length === 0) {
1207
+ throw new Error("delegation_signature is required");
1208
+ }
1209
+ const base64Regex = /^[A-Za-z0-9+/]+=*$/;
1210
+ if (!base64Regex.test(request.delegation_signature)) {
1211
+ throw new Error("delegation_signature must be base64 encoded");
1212
+ }
1213
+ }
1214
+ };
1215
+
1216
+ // src/api/smart-payments.ts
1217
+ var SmartPaymentsAPI = class {
1218
+ constructor(request) {
1219
+ this.request = request;
1220
+ }
1221
+ /**
1222
+ * Execute an AI-powered smart payment
1223
+ *
1224
+ * Smart payments analyze the context and automatically apply optimizations:
1225
+ * - **PPP Pricing**: Auto-adjusts based on customer location
1226
+ * - **Gasless**: Detects when user needs gas subsidization
1227
+ * - **Instant Settlement**: Optional immediate merchant payout
1228
+ * - **Escrow**: Optional fund holding for service delivery
1229
+ *
1230
+ * @param request - Smart payment request configuration
1231
+ * @returns Payment result with status and receipt
1232
+ *
1233
+ * @example
1234
+ * ```typescript
1235
+ * // Basic smart payment
1236
+ * const result = await zendfi.payments.smart({
1237
+ * agent_id: 'my-agent',
1238
+ * user_wallet: 'Hx7B...abc',
1239
+ * amount_usd: 99.99,
1240
+ * description: 'Annual Pro Plan',
1241
+ * });
1242
+ *
1243
+ * // With all options
1244
+ * const result = await zendfi.payments.smart({
1245
+ * agent_id: 'my-agent',
1246
+ * session_token: 'zai_session_...', // For limit enforcement
1247
+ * user_wallet: 'Hx7B...abc',
1248
+ * amount_usd: 99.99,
1249
+ * token: 'USDC',
1250
+ * auto_detect_gasless: true,
1251
+ * instant_settlement: true,
1252
+ * enable_escrow: false,
1253
+ * description: 'Annual Pro Plan',
1254
+ * product_details: {
1255
+ * name: 'Pro Plan',
1256
+ * sku: 'PRO-ANNUAL',
1257
+ * },
1258
+ * metadata: {
1259
+ * user_id: 'usr_123',
1260
+ * },
1261
+ * });
1262
+ *
1263
+ * if (result.requires_signature) {
1264
+ * // Device-bound flow: need user to sign
1265
+ * console.log('Please sign:', result.unsigned_transaction);
1266
+ * console.log('Submit to:', result.submit_url);
1267
+ * } else {
1268
+ * // Auto-signed (custodial or autonomous delegate)
1269
+ * console.log('Payment complete:', result.transaction_signature);
1270
+ * }
1271
+ * ```
1272
+ */
1273
+ async execute(request) {
1274
+ return this.request("POST", "/api/v1/ai/smart-payment", {
1275
+ session_token: request.session_token,
1276
+ agent_id: request.agent_id,
1277
+ user_wallet: request.user_wallet,
1278
+ amount_usd: request.amount_usd,
1279
+ merchant_id: request.merchant_id,
1280
+ token: request.token || "USDC",
1281
+ auto_detect_gasless: request.auto_detect_gasless,
1282
+ instant_settlement: request.instant_settlement,
1283
+ enable_escrow: request.enable_escrow,
1284
+ description: request.description,
1285
+ product_details: request.product_details,
1286
+ metadata: request.metadata
1287
+ });
1288
+ }
1289
+ /**
1290
+ * Submit a signed transaction from device-bound flow
1291
+ *
1292
+ * When a smart payment returns `requires_signature: true`, the client
1293
+ * must sign the transaction and submit it here.
1294
+ *
1295
+ * @param paymentId - UUID of the payment
1296
+ * @param signedTransaction - Base64 encoded signed transaction
1297
+ * @returns Updated payment response
1298
+ *
1299
+ * @example
1300
+ * ```typescript
1301
+ * // After user signs the transaction
1302
+ * const result = await zendfi.payments.submitSigned(
1303
+ * payment.payment_id,
1304
+ * signedTransaction
1305
+ * );
1306
+ *
1307
+ * console.log(`Confirmed in ${result.confirmed_in_ms}ms`);
1308
+ * ```
1309
+ */
1310
+ async submitSigned(paymentId, signedTransaction) {
1311
+ return this.request(
1312
+ "POST",
1313
+ `/api/v1/ai/payments/${paymentId}/submit-signed`,
1314
+ {
1315
+ signed_transaction: signedTransaction
1316
+ }
1317
+ );
1318
+ }
1319
+ };
1320
+
1321
+ // src/client.ts
1322
+ var ZendFiClient = class {
1323
+ config;
1324
+ interceptors;
1325
+ // ============================================
1326
+ // Agentic Intent Protocol APIs
1327
+ // ============================================
1328
+ /**
1329
+ * Agent API - Manage agent API keys and sessions
1330
+ *
1331
+ * @example
1332
+ * ```typescript
1333
+ * // Create an agent API key
1334
+ * const agentKey = await zendfi.agent.createKey({
1335
+ * name: 'Shopping Assistant',
1336
+ * agent_id: 'shopping-assistant-v1',
1337
+ * scopes: ['create_payments'],
1338
+ * });
1339
+ *
1340
+ * // Create an agent session
1341
+ * const session = await zendfi.agent.createSession({
1342
+ * agent_id: 'shopping-assistant-v1',
1343
+ * user_wallet: 'Hx7B...abc',
1344
+ * limits: { max_per_day: 500 },
1345
+ * });
1346
+ * ```
1347
+ */
1348
+ agent;
1349
+ /**
1350
+ * Payment Intents API - Two-phase payment flow
1351
+ *
1352
+ * @example
1353
+ * ```typescript
1354
+ * // Create intent
1355
+ * const intent = await zendfi.intents.create({ amount: 99.99 });
1356
+ *
1357
+ * // Confirm when ready
1358
+ * await zendfi.intents.confirm(intent.id, {
1359
+ * client_secret: intent.client_secret,
1360
+ * customer_wallet: 'Hx7B...abc',
1361
+ * });
1362
+ * ```
1363
+ */
1364
+ intents;
1365
+ /**
1366
+ * Pricing API - PPP and AI-powered pricing
1367
+ *
1368
+ * @example
1369
+ * ```typescript
1370
+ * // Get PPP factor for Brazil
1371
+ * const factor = await zendfi.pricing.getPPPFactor('BR');
1372
+ * const localPrice = 100 * factor.ppp_factor; // $35 for Brazil
1373
+ *
1374
+ * // Get AI pricing suggestion
1375
+ * const suggestion = await zendfi.pricing.getSuggestion({
1376
+ * agent_id: 'my-agent',
1377
+ * base_price: 100,
1378
+ * user_profile: { location_country: 'BR' },
1379
+ * });
1380
+ * ```
1381
+ */
1382
+ pricing;
1383
+ /**
1384
+ * Autonomy API - Enable autonomous agent signing
1385
+ *
1386
+ * @example
1387
+ * ```typescript
1388
+ * // Enable autonomous mode for a session key
1389
+ * const delegate = await zendfi.autonomy.enable(sessionKeyId, {
1390
+ * max_amount_usd: 100,
1391
+ * duration_hours: 24,
1392
+ * delegation_signature: signature,
1393
+ * });
1394
+ *
1395
+ * // Check status
1396
+ * const status = await zendfi.autonomy.getStatus(sessionKeyId);
1397
+ * ```
1398
+ */
1399
+ autonomy;
1400
+ /**
1401
+ * Smart Payments API - AI-powered payment routing
1402
+ *
1403
+ * Create intelligent payments that automatically:
1404
+ * - Apply PPP discounts based on user location
1405
+ * - Use agent sessions when available
1406
+ * - Route to optimal payment paths
1407
+ *
1408
+ * @example
1409
+ * ```typescript
1410
+ * const payment = await zendfi.smart.create({
1411
+ * amount_usd: 99.99,
1412
+ * wallet_address: 'Hx7B...abc',
1413
+ * merchant_id: 'merch_123',
1414
+ * country_code: 'BR', // Apply PPP
1415
+ * enable_ppp: true,
1416
+ * });
1417
+ *
1418
+ * console.log(`Original: $${payment.original_amount_usd}`);
1419
+ * console.log(`Final: $${payment.final_amount_usd}`);
1420
+ * // Original: $99.99
1421
+ * // Final: $64.99 (35% PPP discount applied)
1422
+ * ```
1423
+ */
1424
+ smart;
1425
+ constructor(options) {
1426
+ this.config = ConfigLoader.load(options);
1427
+ ConfigLoader.validateApiKey(this.config.apiKey);
1428
+ this.interceptors = createInterceptors();
1429
+ const boundRequest = this.request.bind(this);
1430
+ this.agent = new AgentAPI(boundRequest);
1431
+ this.intents = new PaymentIntentsAPI(boundRequest);
1432
+ this.pricing = new PricingAPI(boundRequest);
1433
+ this.autonomy = new AutonomyAPI(boundRequest);
1434
+ this.smart = new SmartPaymentsAPI(boundRequest);
1435
+ if (this.config.environment === "development" || this.config.debug) {
1436
+ console.log(
1437
+ `\u2713 ZendFi SDK initialized in ${this.config.mode} mode (${this.config.mode === "test" ? "devnet" : "mainnet"})`
1438
+ );
1439
+ if (this.config.debug) {
1440
+ console.log("[ZendFi] Debug mode enabled");
1441
+ }
1442
+ }
1443
+ }
1444
+ /**
1445
+ * Create a new payment
1446
+ */
1447
+ async createPayment(request) {
1448
+ return this.request("POST", "/api/v1/payments", {
1449
+ ...request,
1450
+ currency: request.currency || "USD",
1451
+ token: request.token || "USDC"
1452
+ });
1453
+ }
1454
+ /**
1455
+ * Get payment by ID
1456
+ */
1457
+ async getPayment(paymentId) {
1458
+ return this.request("GET", `/api/v1/payments/${paymentId}`);
1459
+ }
1460
+ /**
1461
+ * List all payments with pagination
1462
+ */
1463
+ async listPayments(request) {
1464
+ const params = new URLSearchParams();
1465
+ if (request?.page) params.append("page", request.page.toString());
1466
+ if (request?.limit) params.append("limit", request.limit.toString());
1467
+ if (request?.status) params.append("status", request.status);
1468
+ if (request?.from_date) params.append("from_date", request.from_date);
1469
+ if (request?.to_date) params.append("to_date", request.to_date);
1470
+ const query = params.toString() ? `?${params.toString()}` : "";
1471
+ return this.request("GET", `/api/v1/payments${query}`);
1472
+ }
1473
+ /**
1474
+ * Create a subscription plan
1475
+ */
1476
+ async createSubscriptionPlan(request) {
1477
+ return this.request("POST", "/api/v1/subscriptions/plans", {
1478
+ ...request,
1479
+ currency: request.currency || "USD",
1480
+ interval_count: request.interval_count || 1,
1481
+ trial_days: request.trial_days || 0
1482
+ });
1483
+ }
1484
+ /**
1485
+ * Get subscription plan by ID
1486
+ */
1487
+ async getSubscriptionPlan(planId) {
1488
+ return this.request("GET", `/api/v1/subscriptions/plans/${planId}`);
1489
+ }
1490
+ /**
1491
+ * Create a subscription
1492
+ */
1493
+ async createSubscription(request) {
1494
+ return this.request("POST", "/api/v1/subscriptions", request);
1495
+ }
1496
+ /**
1497
+ * Get subscription by ID
1498
+ */
1499
+ async getSubscription(subscriptionId) {
1500
+ return this.request("GET", `/api/v1/subscriptions/${subscriptionId}`);
1501
+ }
1502
+ /**
1503
+ * Cancel a subscription
1504
+ */
1505
+ async cancelSubscription(subscriptionId) {
1506
+ return this.request(
1507
+ "POST",
1508
+ `/api/v1/subscriptions/${subscriptionId}/cancel`
1509
+ );
1510
+ }
1511
+ /**
1512
+ * Create a payment link (shareable checkout URL)
1513
+ */
1514
+ async createPaymentLink(request) {
1515
+ const response = await this.request("POST", "/api/v1/payment-links", {
1516
+ ...request,
1517
+ currency: request.currency || "USD",
1518
+ token: request.token || "USDC"
1519
+ });
1520
+ return {
1521
+ ...response,
1522
+ url: response.hosted_page_url
1523
+ };
1524
+ }
1525
+ /**
1526
+ * Get payment link by link code
1527
+ */
1528
+ async getPaymentLink(linkCode) {
1529
+ const response = await this.request("GET", `/api/v1/payment-links/${linkCode}`);
1530
+ return {
1531
+ ...response,
1532
+ url: response.hosted_page_url
1533
+ };
1534
+ }
1535
+ /**
1536
+ * List all payment links for the authenticated merchant
1537
+ */
1538
+ async listPaymentLinks() {
1539
+ const response = await this.request("GET", "/api/v1/payment-links");
1540
+ return response.map((link) => ({
1541
+ ...link,
1542
+ url: link.hosted_page_url
1543
+ }));
1544
+ }
1545
+ /**
1546
+ * Create an installment plan
1547
+ * Split a purchase into multiple scheduled payments
1548
+ */
1549
+ async createInstallmentPlan(request) {
1550
+ const response = await this.request(
1551
+ "POST",
1552
+ "/api/v1/installment-plans",
1553
+ request
1554
+ );
1555
+ return {
1556
+ id: response.plan_id,
1557
+ plan_id: response.plan_id,
1558
+ status: response.status
1559
+ };
1560
+ }
1561
+ /**
1562
+ * Get installment plan by ID
1563
+ */
1564
+ async getInstallmentPlan(planId) {
1565
+ return this.request("GET", `/api/v1/installment-plans/${planId}`);
1566
+ }
1567
+ /**
1568
+ * List all installment plans for merchant
1569
+ */
1570
+ async listInstallmentPlans(params) {
1571
+ const query = new URLSearchParams();
1572
+ if (params?.limit) query.append("limit", params.limit.toString());
1573
+ if (params?.offset) query.append("offset", params.offset.toString());
1574
+ const queryString = query.toString() ? `?${query.toString()}` : "";
1575
+ return this.request("GET", `/api/v1/installment-plans${queryString}`);
1576
+ }
1577
+ /**
1578
+ * List installment plans for a specific customer
1579
+ */
1580
+ async listCustomerInstallmentPlans(customerWallet) {
1581
+ return this.request(
1582
+ "GET",
1583
+ `/api/v1/customers/${customerWallet}/installment-plans`
1584
+ );
1585
+ }
1586
+ /**
1587
+ * Cancel an installment plan
1588
+ */
1589
+ async cancelInstallmentPlan(planId) {
1590
+ return this.request(
1591
+ "POST",
1592
+ `/api/v1/installment-plans/${planId}/cancel`
1593
+ );
1594
+ }
1595
+ /**
1596
+ * Create an escrow transaction
1597
+ * Hold funds until conditions are met
1598
+ */
1599
+ async createEscrow(request) {
1600
+ return this.request("POST", "/api/v1/escrows", {
1601
+ ...request,
1602
+ currency: request.currency || "USD",
1603
+ token: request.token || "USDC"
1604
+ });
1605
+ }
1606
+ /**
1607
+ * Get escrow by ID
1608
+ */
1609
+ async getEscrow(escrowId) {
1610
+ return this.request("GET", `/api/v1/escrows/${escrowId}`);
1611
+ }
1612
+ /**
1613
+ * List all escrows for merchant
1614
+ */
1615
+ async listEscrows(params) {
1616
+ const query = new URLSearchParams();
1617
+ if (params?.limit) query.append("limit", params.limit.toString());
1618
+ if (params?.offset) query.append("offset", params.offset.toString());
1619
+ const queryString = query.toString() ? `?${query.toString()}` : "";
1620
+ return this.request("GET", `/api/v1/escrows${queryString}`);
1621
+ }
1622
+ /**
1623
+ * Approve escrow release to seller
1624
+ */
1625
+ async approveEscrow(escrowId, request) {
1626
+ return this.request(
1627
+ "POST",
1628
+ `/api/v1/escrows/${escrowId}/approve`,
1629
+ request
1630
+ );
1631
+ }
1632
+ /**
1633
+ * Refund escrow to buyer
1634
+ */
1635
+ async refundEscrow(escrowId, request) {
1636
+ return this.request("POST", `/api/v1/escrows/${escrowId}/refund`, request);
1637
+ }
1638
+ /**
1639
+ * Raise a dispute for an escrow
1640
+ */
1641
+ async disputeEscrow(escrowId, request) {
1642
+ return this.request("POST", `/api/v1/escrows/${escrowId}/dispute`, request);
1643
+ }
1644
+ /**
1645
+ * Create an invoice
1646
+ */
1647
+ async createInvoice(request) {
1648
+ return this.request("POST", "/api/v1/invoices", {
1649
+ ...request,
1650
+ token: request.token || "USDC"
1651
+ });
1652
+ }
1653
+ /**
1654
+ * Get invoice by ID
1655
+ */
1656
+ async getInvoice(invoiceId) {
1657
+ return this.request("GET", `/api/v1/invoices/${invoiceId}`);
1658
+ }
1659
+ /**
1660
+ * List all invoices for merchant
1661
+ */
1662
+ async listInvoices() {
1663
+ return this.request("GET", "/api/v1/invoices");
1664
+ }
1665
+ /**
1666
+ * Send invoice to customer via email
1667
+ */
1668
+ async sendInvoice(invoiceId) {
1669
+ return this.request("POST", `/api/v1/invoices/${invoiceId}/send`);
1670
+ }
1671
+ // ============================================
1672
+ // Agentic Intent Protocol - Smart Payments
1673
+ // ============================================
1674
+ /**
1675
+ * Execute an AI-powered smart payment
1676
+ *
1677
+ * Smart payments combine multiple features:
1678
+ * - Automatic PPP pricing adjustments
1679
+ * - Gasless transaction detection
1680
+ * - Instant settlement options
1681
+ * - Escrow integration
1682
+ * - Receipt generation
1683
+ *
1684
+ * @param request - Smart payment configuration
1685
+ * @returns Payment result with status and receipt
1686
+ *
1687
+ * @example
1688
+ * ```typescript
1689
+ * const result = await zendfi.smartPayment({
1690
+ * agent_id: 'shopping-assistant',
1691
+ * user_wallet: 'Hx7B...abc',
1692
+ * amount_usd: 50,
1693
+ * auto_detect_gasless: true,
1694
+ * description: 'Premium subscription',
1695
+ * });
1696
+ *
1697
+ * if (result.requires_signature) {
1698
+ * // Device-bound flow: user needs to sign
1699
+ * console.log('Sign and submit:', result.submit_url);
1700
+ * } else {
1701
+ * // Auto-signed
1702
+ * console.log('Payment complete:', result.transaction_signature);
1703
+ * }
1704
+ * ```
1705
+ */
1706
+ async smartPayment(request) {
1707
+ return this.smart.execute(request);
1708
+ }
1709
+ /**
1710
+ * Submit a signed transaction for device-bound smart payment
1711
+ *
1712
+ * @param paymentId - UUID of the payment
1713
+ * @param signedTransaction - Base64 encoded signed transaction
1714
+ * @returns Updated payment response
1715
+ */
1716
+ async submitSignedPayment(paymentId, signedTransaction) {
1717
+ return this.smart.submitSigned(paymentId, signedTransaction);
1718
+ }
1719
+ /**
1720
+ * Verify webhook signature using HMAC-SHA256
1721
+ *
1722
+ * @param request - Webhook verification request containing payload, signature, and secret
1723
+ * @returns true if signature is valid, false otherwise
1724
+ *
1725
+ * @example
1726
+ * ```typescript
1727
+ * const isValid = zendfi.verifyWebhook({
1728
+ * payload: req.body,
1729
+ * signature: req.headers['x-zendfi-signature'],
1730
+ * secret: process.env.ZENDFI_WEBHOOK_SECRET
1731
+ * });
1732
+ *
1733
+ * if (!isValid) {
1734
+ * return res.status(401).json({ error: 'Invalid signature' });
1735
+ * }
1736
+ * ```
1737
+ */
1738
+ verifyWebhook(request) {
1739
+ try {
1740
+ if (!request.payload || !request.signature || !request.secret) {
508
1741
  return false;
509
1742
  }
510
1743
  let payloadString;
@@ -516,194 +1749,1173 @@ var ZendFiClient = class {
516
1749
  } catch (e) {
517
1750
  return false;
518
1751
  }
519
- } else if (typeof request.payload === "object") {
520
- parsedPayload = request.payload;
1752
+ } else if (typeof request.payload === "object") {
1753
+ parsedPayload = request.payload;
1754
+ try {
1755
+ payloadString = JSON.stringify(request.payload);
1756
+ } catch (e) {
1757
+ return false;
1758
+ }
1759
+ } else {
1760
+ return false;
1761
+ }
1762
+ if (!parsedPayload || !parsedPayload.event || !parsedPayload.merchant_id || !parsedPayload.timestamp) {
1763
+ return false;
1764
+ }
1765
+ const computedSignature = this.computeHmacSignature(payloadString, request.secret);
1766
+ return this.timingSafeEqual(request.signature, computedSignature);
1767
+ } catch (err) {
1768
+ const error = err;
1769
+ if (this.config.environment === "development") {
1770
+ console.error("Webhook verification error:", error?.message || String(error));
1771
+ }
1772
+ return false;
1773
+ }
1774
+ }
1775
+ /**
1776
+ * Compute HMAC-SHA256 signature
1777
+ * Works in both Node.js and browser environments
1778
+ */
1779
+ computeHmacSignature(payload, secret) {
1780
+ if (typeof process !== "undefined" && process.versions?.node) {
1781
+ return (0, import_crypto.createHmac)("sha256", secret).update(payload, "utf8").digest("hex");
1782
+ }
1783
+ throw new Error(
1784
+ "Webhook verification in browser is not supported. Use this method in your backend/server environment."
1785
+ );
1786
+ }
1787
+ /**
1788
+ * Timing-safe string comparison to prevent timing attacks
1789
+ */
1790
+ timingSafeEqual(a, b) {
1791
+ if (a.length !== b.length) {
1792
+ return false;
1793
+ }
1794
+ if (typeof process !== "undefined" && process.versions?.node) {
1795
+ try {
1796
+ const bufferA = Buffer.from(a, "utf8");
1797
+ const bufferB = Buffer.from(b, "utf8");
1798
+ return (0, import_crypto.timingSafeEqual)(bufferA, bufferB);
1799
+ } catch {
1800
+ }
1801
+ }
1802
+ let result = 0;
1803
+ for (let i = 0; i < a.length; i++) {
1804
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
1805
+ }
1806
+ return result === 0;
1807
+ }
1808
+ /**
1809
+ * Make an HTTP request with retry logic, interceptors, and debug logging
1810
+ */
1811
+ async request(method, endpoint, data, options = {}) {
1812
+ const attempt = options.attempt || 1;
1813
+ const idempotencyKey = options.idempotencyKey || (this.config.idempotencyEnabled && method !== "GET" ? generateIdempotencyKey() : void 0);
1814
+ const startTime = Date.now();
1815
+ try {
1816
+ const url = `${this.config.baseURL}${endpoint}`;
1817
+ const headers = {
1818
+ "Content-Type": "application/json",
1819
+ Authorization: `Bearer ${this.config.apiKey}`
1820
+ };
1821
+ if (idempotencyKey) {
1822
+ headers["Idempotency-Key"] = idempotencyKey;
1823
+ }
1824
+ let requestConfig = {
1825
+ method,
1826
+ url,
1827
+ headers,
1828
+ body: data
1829
+ };
1830
+ if (this.interceptors.request.has()) {
1831
+ requestConfig = await this.interceptors.request.execute(requestConfig);
1832
+ }
1833
+ if (this.config.debug) {
1834
+ console.log(`[ZendFi] ${method} ${endpoint}`);
1835
+ if (data) {
1836
+ console.log("[ZendFi] Request:", JSON.stringify(data, null, 2));
1837
+ }
1838
+ }
1839
+ const controller = new AbortController();
1840
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
1841
+ const response = await (0, import_cross_fetch.default)(requestConfig.url, {
1842
+ method: requestConfig.method,
1843
+ headers: requestConfig.headers,
1844
+ body: requestConfig.body ? JSON.stringify(requestConfig.body) : void 0,
1845
+ signal: controller.signal
1846
+ });
1847
+ clearTimeout(timeoutId);
1848
+ let body;
1849
+ try {
1850
+ body = await response.json();
1851
+ } catch {
1852
+ body = null;
1853
+ }
1854
+ const duration = Date.now() - startTime;
1855
+ if (!response.ok) {
1856
+ const error = createZendFiError(response.status, body);
1857
+ if (this.config.debug) {
1858
+ console.error(`[ZendFi] \u274C ${response.status} ${response.statusText} (${duration}ms)`);
1859
+ console.error(`[ZendFi] Error:`, error.toString());
1860
+ }
1861
+ if (response.status >= 500 && attempt < this.config.retries) {
1862
+ const delay = Math.pow(2, attempt) * 1e3;
1863
+ if (this.config.debug) {
1864
+ console.log(`[ZendFi] Retrying in ${delay}ms... (attempt ${attempt + 1}/${this.config.retries})`);
1865
+ }
1866
+ await sleep(delay);
1867
+ return this.request(method, endpoint, data, {
1868
+ idempotencyKey,
1869
+ attempt: attempt + 1
1870
+ });
1871
+ }
1872
+ if (this.interceptors.error.has()) {
1873
+ const interceptedError = await this.interceptors.error.execute(error);
1874
+ throw interceptedError;
1875
+ }
1876
+ throw error;
1877
+ }
1878
+ if (this.config.debug) {
1879
+ console.log(`[ZendFi] \u2713 ${response.status} ${response.statusText} (${duration}ms)`);
1880
+ if (body) {
1881
+ console.log("[ZendFi] Response:", JSON.stringify(body, null, 2));
1882
+ }
1883
+ }
1884
+ const headersObj = {};
1885
+ response.headers.forEach((value, key) => {
1886
+ headersObj[key] = value;
1887
+ });
1888
+ let responseData = {
1889
+ status: response.status,
1890
+ statusText: response.statusText,
1891
+ headers: headersObj,
1892
+ data: body,
1893
+ config: requestConfig
1894
+ };
1895
+ if (this.interceptors.response.has()) {
1896
+ responseData = await this.interceptors.response.execute(responseData);
1897
+ }
1898
+ return responseData.data;
1899
+ } catch (error) {
1900
+ if (error.name === "AbortError") {
1901
+ const timeoutError = createZendFiError(0, {}, `Request timeout after ${this.config.timeout}ms`);
1902
+ if (this.config.debug) {
1903
+ console.error(`[ZendFi] \u274C Timeout (${this.config.timeout}ms)`);
1904
+ }
1905
+ throw timeoutError;
1906
+ }
1907
+ if (attempt < this.config.retries && (error.message?.includes("fetch") || error.message?.includes("network"))) {
1908
+ const delay = Math.pow(2, attempt) * 1e3;
1909
+ if (this.config.debug) {
1910
+ console.log(`[ZendFi] Network error, retrying in ${delay}ms... (attempt ${attempt + 1}/${this.config.retries})`);
1911
+ }
1912
+ await sleep(delay);
1913
+ return this.request(method, endpoint, data, {
1914
+ idempotencyKey,
1915
+ attempt: attempt + 1
1916
+ });
1917
+ }
1918
+ if (isZendFiError(error)) {
1919
+ throw error;
1920
+ }
1921
+ const wrappedError = createZendFiError(0, {}, error.message || "An unknown error occurred");
1922
+ if (this.config.debug) {
1923
+ console.error(`[ZendFi] \u274C Unexpected error:`, error);
1924
+ }
1925
+ throw wrappedError;
1926
+ }
1927
+ }
1928
+ };
1929
+ var zendfi = (() => {
1930
+ try {
1931
+ return new ZendFiClient();
1932
+ } catch (error) {
1933
+ if (process.env.NODE_ENV === "test" || !process.env.ZENDFI_API_KEY) {
1934
+ return new Proxy({}, {
1935
+ get() {
1936
+ throw new Error(
1937
+ 'ZendFi singleton not initialized. Set ZENDFI_API_KEY environment variable or create a custom instance: new ZendFiClient({ apiKey: "..." })'
1938
+ );
1939
+ }
1940
+ });
1941
+ }
1942
+ throw error;
1943
+ }
1944
+ })();
1945
+
1946
+ // src/webhooks.ts
1947
+ async function verifyNextWebhook(request, secret) {
1948
+ try {
1949
+ const payload = await request.text();
1950
+ const signature = request.headers.get("x-zendfi-signature");
1951
+ if (!signature) {
1952
+ return null;
1953
+ }
1954
+ const webhookSecret = secret || process.env.ZENDFI_WEBHOOK_SECRET;
1955
+ if (!webhookSecret) {
1956
+ throw new Error("ZENDFI_WEBHOOK_SECRET not configured");
1957
+ }
1958
+ const isValid = zendfi.verifyWebhook({
1959
+ payload,
1960
+ signature,
1961
+ secret: webhookSecret
1962
+ });
1963
+ if (!isValid) {
1964
+ return null;
1965
+ }
1966
+ return JSON.parse(payload);
1967
+ } catch {
1968
+ return null;
1969
+ }
1970
+ }
1971
+ async function verifyExpressWebhook(request, secret) {
1972
+ try {
1973
+ const payload = request.rawBody || JSON.stringify(request.body);
1974
+ const signature = request.headers["x-zendfi-signature"];
1975
+ if (!signature) {
1976
+ return null;
1977
+ }
1978
+ const webhookSecret = secret || process.env.ZENDFI_WEBHOOK_SECRET;
1979
+ if (!webhookSecret) {
1980
+ throw new Error("ZENDFI_WEBHOOK_SECRET not configured");
1981
+ }
1982
+ const isValid = zendfi.verifyWebhook({
1983
+ payload,
1984
+ signature,
1985
+ secret: webhookSecret
1986
+ });
1987
+ if (!isValid) {
1988
+ return null;
1989
+ }
1990
+ return JSON.parse(payload);
1991
+ } catch {
1992
+ return null;
1993
+ }
1994
+ }
1995
+ function verifyWebhookSignature(payload, signature, secret) {
1996
+ return zendfi.verifyWebhook({
1997
+ payload,
1998
+ signature,
1999
+ secret
2000
+ });
2001
+ }
2002
+
2003
+ // src/device-bound-crypto.ts
2004
+ var import_web3 = require("@solana/web3.js");
2005
+ var crypto = __toESM(require("crypto"));
2006
+ var DeviceFingerprintGenerator = class {
2007
+ /**
2008
+ * Generate a unique device fingerprint
2009
+ * Combines multiple browser attributes for uniqueness
2010
+ */
2011
+ static async generate() {
2012
+ const components = {};
2013
+ try {
2014
+ components.canvas = await this.getCanvasFingerprint();
2015
+ components.webgl = await this.getWebGLFingerprint();
2016
+ components.audio = await this.getAudioFingerprint();
2017
+ components.screen = `${screen.width}x${screen.height}x${screen.colorDepth}`;
2018
+ components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
2019
+ components.languages = navigator.languages?.join(",") || navigator.language;
2020
+ components.platform = navigator.platform;
2021
+ components.hardwareConcurrency = navigator.hardwareConcurrency?.toString() || "unknown";
2022
+ const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|");
2023
+ const fingerprint = await this.sha256(combined);
2024
+ return {
2025
+ fingerprint,
2026
+ generatedAt: Date.now(),
2027
+ components
2028
+ };
2029
+ } catch (error) {
2030
+ console.warn("Device fingerprinting failed, using fallback", error);
2031
+ return this.generateFallbackFingerprint();
2032
+ }
2033
+ }
2034
+ /**
2035
+ * Graceful fallback fingerprint generation
2036
+ * Works in headless browsers, SSR, and restricted environments
2037
+ */
2038
+ static async generateFallbackFingerprint() {
2039
+ const components = {};
2040
+ try {
2041
+ if (typeof navigator !== "undefined") {
2042
+ components.platform = navigator.platform || "unknown";
2043
+ components.languages = navigator.languages?.join(",") || navigator.language || "unknown";
2044
+ components.hardwareConcurrency = navigator.hardwareConcurrency?.toString() || "unknown";
2045
+ }
2046
+ if (typeof screen !== "undefined") {
2047
+ components.screen = `${screen.width || 0}x${screen.height || 0}x${screen.colorDepth || 0}`;
2048
+ }
2049
+ if (typeof Intl !== "undefined") {
521
2050
  try {
522
- payloadString = JSON.stringify(request.payload);
523
- } catch (e) {
524
- return false;
2051
+ components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
2052
+ } catch {
2053
+ components.timezone = "unknown";
525
2054
  }
526
- } else {
527
- return false;
528
2055
  }
529
- if (!parsedPayload || !parsedPayload.event || !parsedPayload.merchant_id || !parsedPayload.timestamp) {
530
- return false;
2056
+ } catch {
2057
+ components.platform = "fallback";
2058
+ }
2059
+ let randomEntropy = "";
2060
+ try {
2061
+ if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
2062
+ const arr = new Uint8Array(16);
2063
+ window.crypto.getRandomValues(arr);
2064
+ randomEntropy = Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("");
2065
+ } else if (typeof crypto !== "undefined" && crypto.randomBytes) {
2066
+ randomEntropy = crypto.randomBytes(16).toString("hex");
531
2067
  }
532
- const computedSignature = this.computeHmacSignature(payloadString, request.secret);
533
- return this.timingSafeEqual(request.signature, computedSignature);
534
- } catch (err) {
535
- const error = err;
536
- if (this.config.environment === "development") {
537
- console.error("Webhook verification error:", error?.message || String(error));
2068
+ } catch {
2069
+ randomEntropy = Date.now().toString(36) + Math.random().toString(36);
2070
+ }
2071
+ const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|") + "|entropy:" + randomEntropy;
2072
+ const fingerprint = await this.sha256(combined);
2073
+ return {
2074
+ fingerprint,
2075
+ generatedAt: Date.now(),
2076
+ components
2077
+ };
2078
+ }
2079
+ static async getCanvasFingerprint() {
2080
+ const canvas = document.createElement("canvas");
2081
+ const ctx = canvas.getContext("2d");
2082
+ if (!ctx) return "no-canvas";
2083
+ canvas.width = 200;
2084
+ canvas.height = 50;
2085
+ ctx.textBaseline = "top";
2086
+ ctx.font = '14px "Arial"';
2087
+ ctx.fillStyle = "#f60";
2088
+ ctx.fillRect(0, 0, 100, 50);
2089
+ ctx.fillStyle = "#069";
2090
+ ctx.fillText("ZendFi \u{1F510}", 2, 2);
2091
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
2092
+ ctx.fillText("Device-Bound", 4, 17);
2093
+ return canvas.toDataURL();
2094
+ }
2095
+ static async getWebGLFingerprint() {
2096
+ const canvas = document.createElement("canvas");
2097
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
2098
+ if (!gl) return "no-webgl";
2099
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
2100
+ if (!debugInfo) return "no-debug-info";
2101
+ const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
2102
+ const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
2103
+ return `${vendor}|${renderer}`;
2104
+ }
2105
+ static async getAudioFingerprint() {
2106
+ try {
2107
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
2108
+ if (!AudioContext) return "no-audio";
2109
+ const context = new AudioContext();
2110
+ const oscillator = context.createOscillator();
2111
+ const analyser = context.createAnalyser();
2112
+ const gainNode = context.createGain();
2113
+ const scriptProcessor = context.createScriptProcessor(4096, 1, 1);
2114
+ gainNode.gain.value = 0;
2115
+ oscillator.connect(analyser);
2116
+ analyser.connect(scriptProcessor);
2117
+ scriptProcessor.connect(gainNode);
2118
+ gainNode.connect(context.destination);
2119
+ oscillator.start(0);
2120
+ return new Promise((resolve) => {
2121
+ scriptProcessor.onaudioprocess = (event) => {
2122
+ const output = event.inputBuffer.getChannelData(0);
2123
+ const hash = Array.from(output.slice(0, 30)).reduce((acc, val) => acc + Math.abs(val), 0);
2124
+ oscillator.stop();
2125
+ scriptProcessor.disconnect();
2126
+ context.close();
2127
+ resolve(hash.toString());
2128
+ };
2129
+ });
2130
+ } catch (error) {
2131
+ return "audio-error";
2132
+ }
2133
+ }
2134
+ static async sha256(data) {
2135
+ if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
2136
+ const encoder = new TextEncoder();
2137
+ const dataBuffer = encoder.encode(data);
2138
+ const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);
2139
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
2140
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
2141
+ } else {
2142
+ return crypto.createHash("sha256").update(data).digest("hex");
2143
+ }
2144
+ }
2145
+ };
2146
+ var SessionKeyCrypto = class {
2147
+ /**
2148
+ * Encrypt a Solana keypair with PIN + device fingerprint
2149
+ * Uses Argon2id for key derivation and AES-256-GCM for encryption
2150
+ */
2151
+ static async encrypt(keypair, pin, deviceFingerprint) {
2152
+ if (!/^\d{6}$/.test(pin)) {
2153
+ throw new Error("PIN must be exactly 6 numeric digits");
2154
+ }
2155
+ const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
2156
+ const nonce = this.generateNonce();
2157
+ const secretKey = keypair.secretKey;
2158
+ const encryptedData = await this.aesEncrypt(secretKey, encryptionKey, nonce);
2159
+ return {
2160
+ encryptedData: Buffer.from(encryptedData).toString("base64"),
2161
+ nonce: Buffer.from(nonce).toString("base64"),
2162
+ publicKey: keypair.publicKey.toBase58(),
2163
+ deviceFingerprint,
2164
+ version: "argon2id-aes256gcm-v1"
2165
+ };
2166
+ }
2167
+ /**
2168
+ * Decrypt an encrypted session key with PIN + device fingerprint
2169
+ */
2170
+ static async decrypt(encrypted, pin, deviceFingerprint) {
2171
+ if (!/^\d{6}$/.test(pin)) {
2172
+ throw new Error("PIN must be exactly 6 numeric digits");
2173
+ }
2174
+ if (encrypted.deviceFingerprint !== deviceFingerprint) {
2175
+ throw new Error("Device fingerprint mismatch - wrong device or security threat");
2176
+ }
2177
+ const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
2178
+ const encryptedData = Buffer.from(encrypted.encryptedData, "base64");
2179
+ const nonce = Buffer.from(encrypted.nonce, "base64");
2180
+ try {
2181
+ const secretKey = await this.aesDecrypt(encryptedData, encryptionKey, nonce);
2182
+ return import_web3.Keypair.fromSecretKey(secretKey);
2183
+ } catch (error) {
2184
+ throw new Error("Decryption failed - wrong PIN or corrupted data");
2185
+ }
2186
+ }
2187
+ /**
2188
+ * Derive encryption key from PIN + device fingerprint using Argon2id
2189
+ *
2190
+ * Argon2id parameters (OWASP recommended):
2191
+ * - Memory: 64MB (65536 KB)
2192
+ * - Iterations: 3
2193
+ * - Parallelism: 4
2194
+ * - Salt: device fingerprint
2195
+ */
2196
+ static async deriveKey(pin, deviceFingerprint) {
2197
+ if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
2198
+ const encoder = new TextEncoder();
2199
+ const keyMaterial = await window.crypto.subtle.importKey(
2200
+ "raw",
2201
+ encoder.encode(pin),
2202
+ { name: "PBKDF2" },
2203
+ false,
2204
+ ["deriveBits"]
2205
+ );
2206
+ const derivedBits = await window.crypto.subtle.deriveBits(
2207
+ {
2208
+ name: "PBKDF2",
2209
+ salt: encoder.encode(deviceFingerprint),
2210
+ iterations: 1e5,
2211
+ // High iteration count for security
2212
+ hash: "SHA-256"
2213
+ },
2214
+ keyMaterial,
2215
+ 256
2216
+ // 256 bits = 32 bytes for AES-256
2217
+ );
2218
+ return new Uint8Array(derivedBits);
2219
+ } else {
2220
+ const salt = crypto.createHash("sha256").update(deviceFingerprint).digest();
2221
+ return crypto.pbkdf2Sync(pin, salt, 1e5, 32, "sha256");
2222
+ }
2223
+ }
2224
+ /**
2225
+ * Generate random nonce for AES-GCM (12 bytes)
2226
+ */
2227
+ static generateNonce() {
2228
+ if (typeof window !== "undefined" && window.crypto) {
2229
+ return window.crypto.getRandomValues(new Uint8Array(12));
2230
+ } else {
2231
+ return crypto.randomBytes(12);
2232
+ }
2233
+ }
2234
+ /**
2235
+ * Encrypt with AES-256-GCM
2236
+ */
2237
+ static async aesEncrypt(plaintext, key, nonce) {
2238
+ if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
2239
+ const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
2240
+ const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
2241
+ const plaintextBuffer = plaintext.buffer.slice(plaintext.byteOffset, plaintext.byteOffset + plaintext.byteLength);
2242
+ const cryptoKey = await window.crypto.subtle.importKey(
2243
+ "raw",
2244
+ keyBuffer,
2245
+ { name: "AES-GCM" },
2246
+ false,
2247
+ ["encrypt"]
2248
+ );
2249
+ const encrypted = await window.crypto.subtle.encrypt(
2250
+ {
2251
+ name: "AES-GCM",
2252
+ iv: nonceBuffer
2253
+ },
2254
+ cryptoKey,
2255
+ plaintextBuffer
2256
+ );
2257
+ return new Uint8Array(encrypted);
2258
+ } else {
2259
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
2260
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
2261
+ const authTag = cipher.getAuthTag();
2262
+ return new Uint8Array(Buffer.concat([encrypted, authTag]));
2263
+ }
2264
+ }
2265
+ /**
2266
+ * Decrypt with AES-256-GCM
2267
+ */
2268
+ static async aesDecrypt(ciphertext, key, nonce) {
2269
+ if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
2270
+ const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
2271
+ const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
2272
+ const ciphertextBuffer = ciphertext.buffer.slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength);
2273
+ const cryptoKey = await window.crypto.subtle.importKey(
2274
+ "raw",
2275
+ keyBuffer,
2276
+ { name: "AES-GCM" },
2277
+ false,
2278
+ ["decrypt"]
2279
+ );
2280
+ const decrypted = await window.crypto.subtle.decrypt(
2281
+ {
2282
+ name: "AES-GCM",
2283
+ iv: nonceBuffer
2284
+ },
2285
+ cryptoKey,
2286
+ ciphertextBuffer
2287
+ );
2288
+ return new Uint8Array(decrypted);
2289
+ } else {
2290
+ const authTag = ciphertext.slice(-16);
2291
+ const encrypted = ciphertext.slice(0, -16);
2292
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
2293
+ decipher.setAuthTag(authTag);
2294
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
2295
+ return new Uint8Array(decrypted);
2296
+ }
2297
+ }
2298
+ };
2299
+ var RecoveryQRGenerator = class {
2300
+ /**
2301
+ * Generate recovery QR data
2302
+ * This allows users to recover their session key on a new device
2303
+ */
2304
+ static generate(encrypted) {
2305
+ return {
2306
+ encryptedSessionKey: encrypted.encryptedData,
2307
+ nonce: encrypted.nonce,
2308
+ publicKey: encrypted.publicKey,
2309
+ version: "v1",
2310
+ createdAt: Date.now()
2311
+ };
2312
+ }
2313
+ /**
2314
+ * Encode recovery QR as JSON string
2315
+ */
2316
+ static encode(recoveryQR) {
2317
+ return JSON.stringify(recoveryQR);
2318
+ }
2319
+ /**
2320
+ * Decode recovery QR from JSON string
2321
+ */
2322
+ static decode(qrData) {
2323
+ try {
2324
+ const parsed = JSON.parse(qrData);
2325
+ if (!parsed.encryptedSessionKey || !parsed.nonce || !parsed.publicKey) {
2326
+ throw new Error("Invalid recovery QR data");
2327
+ }
2328
+ return parsed;
2329
+ } catch (error) {
2330
+ throw new Error("Failed to decode recovery QR");
2331
+ }
2332
+ }
2333
+ /**
2334
+ * Re-encrypt session key for new device
2335
+ */
2336
+ static async reEncryptForNewDevice(recoveryQR, oldPin, oldDeviceFingerprint, newPin, newDeviceFingerprint) {
2337
+ const oldEncrypted = {
2338
+ encryptedData: recoveryQR.encryptedSessionKey,
2339
+ nonce: recoveryQR.nonce,
2340
+ publicKey: recoveryQR.publicKey,
2341
+ deviceFingerprint: oldDeviceFingerprint,
2342
+ version: "argon2id-aes256gcm-v1"
2343
+ };
2344
+ const keypair = await SessionKeyCrypto.decrypt(oldEncrypted, oldPin, oldDeviceFingerprint);
2345
+ return await SessionKeyCrypto.encrypt(keypair, newPin, newDeviceFingerprint);
2346
+ }
2347
+ };
2348
+ var DeviceBoundSessionKey = class _DeviceBoundSessionKey {
2349
+ encrypted = null;
2350
+ deviceFingerprint = null;
2351
+ sessionKeyId = null;
2352
+ recoveryQR = null;
2353
+ // Auto-signing cache: decrypted keypair stored in memory
2354
+ // Enables instant signing without re-entering PIN for subsequent payments
2355
+ cachedKeypair = null;
2356
+ cacheExpiry = null;
2357
+ // Timestamp when cache expires
2358
+ DEFAULT_CACHE_TTL_MS = 30 * 60 * 1e3;
2359
+ // 30 minutes
2360
+ /**
2361
+ * Create a new device-bound session key
2362
+ */
2363
+ static async create(options) {
2364
+ const deviceFingerprint = await DeviceFingerprintGenerator.generate();
2365
+ const keypair = import_web3.Keypair.generate();
2366
+ const encrypted = await SessionKeyCrypto.encrypt(
2367
+ keypair,
2368
+ options.pin,
2369
+ deviceFingerprint.fingerprint
2370
+ );
2371
+ const instance = new _DeviceBoundSessionKey();
2372
+ instance.encrypted = encrypted;
2373
+ instance.deviceFingerprint = deviceFingerprint;
2374
+ if (options.generateRecoveryQR) {
2375
+ instance.recoveryQR = RecoveryQRGenerator.generate(encrypted);
2376
+ }
2377
+ return instance;
2378
+ }
2379
+ /**
2380
+ * Get encrypted data for backend storage
2381
+ */
2382
+ getEncryptedData() {
2383
+ if (!this.encrypted) {
2384
+ throw new Error("Session key not created yet");
2385
+ }
2386
+ return this.encrypted;
2387
+ }
2388
+ /**
2389
+ * Get device fingerprint
2390
+ */
2391
+ getDeviceFingerprint() {
2392
+ if (!this.deviceFingerprint) {
2393
+ throw new Error("Device fingerprint not generated yet");
2394
+ }
2395
+ return this.deviceFingerprint.fingerprint;
2396
+ }
2397
+ /**
2398
+ * Get public key
2399
+ */
2400
+ getPublicKey() {
2401
+ if (!this.encrypted) {
2402
+ throw new Error("Session key not created yet");
2403
+ }
2404
+ return this.encrypted.publicKey;
2405
+ }
2406
+ /**
2407
+ * Get recovery QR data (if generated)
2408
+ */
2409
+ getRecoveryQR() {
2410
+ return this.recoveryQR;
2411
+ }
2412
+ /**
2413
+ * Decrypt and sign a transaction
2414
+ *
2415
+ * @param transaction - The transaction to sign
2416
+ * @param pin - User's PIN (required only if keypair not cached)
2417
+ * @param cacheKeypair - Whether to cache the decrypted keypair for future use (default: true)
2418
+ * @param cacheTTL - Cache time-to-live in milliseconds (default: 30 minutes)
2419
+ *
2420
+ * @example
2421
+ * ```typescript
2422
+ * // First payment: requires PIN, caches keypair
2423
+ * await sessionKey.signTransaction(tx1, '123456', true);
2424
+ *
2425
+ * // Subsequent payments: uses cached keypair, no PIN needed!
2426
+ * await sessionKey.signTransaction(tx2, '', false); // PIN ignored if cached
2427
+ *
2428
+ * // Clear cache when done
2429
+ * sessionKey.clearCache();
2430
+ * ```
2431
+ */
2432
+ async signTransaction(transaction, pin = "", cacheKeypair = true, cacheTTL) {
2433
+ if (!this.encrypted || !this.deviceFingerprint) {
2434
+ throw new Error("Session key not initialized");
2435
+ }
2436
+ let keypair;
2437
+ if (this.isCached()) {
2438
+ keypair = this.cachedKeypair;
2439
+ if (typeof console !== "undefined") {
2440
+ console.log("\u{1F680} Using cached keypair - instant signing (no PIN required)");
2441
+ }
2442
+ } else {
2443
+ if (!pin) {
2444
+ throw new Error("PIN required: no cached keypair available");
538
2445
  }
2446
+ keypair = await SessionKeyCrypto.decrypt(
2447
+ this.encrypted,
2448
+ pin,
2449
+ this.deviceFingerprint.fingerprint
2450
+ );
2451
+ if (cacheKeypair) {
2452
+ const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
2453
+ this.cacheKeypair(keypair, ttl);
2454
+ if (typeof console !== "undefined") {
2455
+ console.log(`\u2705 Keypair decrypted and cached for ${ttl / 1e3 / 60} minutes`);
2456
+ }
2457
+ }
2458
+ }
2459
+ transaction.sign(keypair);
2460
+ return transaction;
2461
+ }
2462
+ /**
2463
+ * Check if keypair is cached and valid
2464
+ */
2465
+ isCached() {
2466
+ if (!this.cachedKeypair || !this.cacheExpiry) {
2467
+ return false;
2468
+ }
2469
+ const now = Date.now();
2470
+ if (now > this.cacheExpiry) {
2471
+ this.clearCache();
539
2472
  return false;
540
2473
  }
2474
+ return true;
2475
+ }
2476
+ /**
2477
+ * Manually cache a keypair
2478
+ * Called internally after PIN decryption
2479
+ */
2480
+ cacheKeypair(keypair, ttl) {
2481
+ this.cachedKeypair = keypair;
2482
+ this.cacheExpiry = Date.now() + ttl;
2483
+ }
2484
+ /**
2485
+ * Clear cached keypair
2486
+ * Should be called when user logs out or session ends
2487
+ *
2488
+ * @example
2489
+ * ```typescript
2490
+ * // Clear cache on logout
2491
+ * sessionKey.clearCache();
2492
+ *
2493
+ * // Or clear automatically on tab close
2494
+ * window.addEventListener('beforeunload', () => {
2495
+ * sessionKey.clearCache();
2496
+ * });
2497
+ * ```
2498
+ */
2499
+ clearCache() {
2500
+ this.cachedKeypair = null;
2501
+ this.cacheExpiry = null;
2502
+ if (typeof console !== "undefined") {
2503
+ console.log("\u{1F9F9} Keypair cache cleared");
2504
+ }
2505
+ }
2506
+ /**
2507
+ * Decrypt and cache keypair without signing a transaction
2508
+ * Useful for pre-warming the cache before user makes payments
2509
+ *
2510
+ * @example
2511
+ * ```typescript
2512
+ * // After session key creation, decrypt and cache
2513
+ * await sessionKey.unlockWithPin('123456');
2514
+ *
2515
+ * // Now all subsequent payments are instant (no PIN)
2516
+ * await sessionKey.signTransaction(tx1, '', false); // Instant!
2517
+ * await sessionKey.signTransaction(tx2, '', false); // Instant!
2518
+ * ```
2519
+ */
2520
+ async unlockWithPin(pin, cacheTTL) {
2521
+ if (!this.encrypted || !this.deviceFingerprint) {
2522
+ throw new Error("Session key not initialized");
2523
+ }
2524
+ const keypair = await SessionKeyCrypto.decrypt(
2525
+ this.encrypted,
2526
+ pin,
2527
+ this.deviceFingerprint.fingerprint
2528
+ );
2529
+ const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
2530
+ this.cacheKeypair(keypair, ttl);
2531
+ if (typeof console !== "undefined") {
2532
+ console.log(`\u{1F513} Session key unlocked and cached for ${ttl / 1e3 / 60} minutes`);
2533
+ }
2534
+ }
2535
+ /**
2536
+ * Get time remaining until cache expires (in milliseconds)
2537
+ * Returns 0 if not cached
2538
+ */
2539
+ getCacheTimeRemaining() {
2540
+ if (!this.isCached() || !this.cacheExpiry) {
2541
+ return 0;
2542
+ }
2543
+ const remaining = this.cacheExpiry - Date.now();
2544
+ return Math.max(0, remaining);
2545
+ }
2546
+ /**
2547
+ * Extend cache expiry time
2548
+ * Useful to keep session active during user activity
2549
+ *
2550
+ * @example
2551
+ * ```typescript
2552
+ * // Extend cache by 15 minutes on each payment
2553
+ * await sessionKey.signTransaction(tx, '');
2554
+ * sessionKey.extendCache(15 * 60 * 1000);
2555
+ * ```
2556
+ */
2557
+ extendCache(additionalTTL) {
2558
+ if (!this.isCached()) {
2559
+ throw new Error("Cannot extend cache: no cached keypair");
2560
+ }
2561
+ this.cacheExpiry += additionalTTL;
2562
+ if (typeof console !== "undefined") {
2563
+ const remainingMinutes = this.getCacheTimeRemaining() / 1e3 / 60;
2564
+ console.log(`\u23F0 Cache extended - ${remainingMinutes.toFixed(1)} minutes remaining`);
2565
+ }
2566
+ }
2567
+ /**
2568
+ * Set session key ID after backend creation
2569
+ */
2570
+ setSessionKeyId(id) {
2571
+ this.sessionKeyId = id;
2572
+ }
2573
+ /**
2574
+ * Get session key ID
2575
+ */
2576
+ getSessionKeyId() {
2577
+ if (!this.sessionKeyId) {
2578
+ throw new Error("Session key not registered with backend");
2579
+ }
2580
+ return this.sessionKeyId;
2581
+ }
2582
+ };
2583
+
2584
+ // src/device-bound-session-keys.ts
2585
+ var import_web32 = require("@solana/web3.js");
2586
+ var ZendFiSessionKeyManager = class {
2587
+ baseURL;
2588
+ apiKey;
2589
+ sessionKey = null;
2590
+ sessionKeyId = null;
2591
+ constructor(apiKey, baseURL = "https://api.zendfi.com") {
2592
+ this.apiKey = apiKey;
2593
+ this.baseURL = baseURL;
541
2594
  }
542
2595
  /**
543
- * Compute HMAC-SHA256 signature
544
- * Works in both Node.js and browser environments
2596
+ * Create a new device-bound session key
2597
+ *
2598
+ * @example
2599
+ * ```typescript
2600
+ * const manager = new ZendFiSessionKeyManager('your-api-key');
2601
+ *
2602
+ * const sessionKey = await manager.createSessionKey({
2603
+ * userWallet: '7xKNH....',
2604
+ * limitUSDC: 100,
2605
+ * durationDays: 7,
2606
+ * pin: '123456',
2607
+ * generateRecoveryQR: true,
2608
+ * });
2609
+ *
2610
+ * console.log('Session key created:', sessionKey.sessionKeyId);
2611
+ * console.log('Recovery QR:', sessionKey.recoveryQR);
2612
+ * ```
545
2613
  */
546
- computeHmacSignature(payload, secret) {
547
- if (typeof process !== "undefined" && process.versions?.node) {
548
- return (0, import_crypto.createHmac)("sha256", secret).update(payload, "utf8").digest("hex");
2614
+ async createSessionKey(options) {
2615
+ const sessionKey = await DeviceBoundSessionKey.create({
2616
+ pin: options.pin,
2617
+ limitUSDC: options.limitUSDC,
2618
+ durationDays: options.durationDays,
2619
+ userWallet: options.userWallet,
2620
+ generateRecoveryQR: options.generateRecoveryQR
2621
+ });
2622
+ const encrypted = sessionKey.getEncryptedData();
2623
+ let recoveryQR;
2624
+ if (options.generateRecoveryQR) {
2625
+ const qr = RecoveryQRGenerator.generate(encrypted);
2626
+ recoveryQR = RecoveryQRGenerator.encode(qr);
549
2627
  }
550
- throw new Error(
551
- "Webhook verification in browser is not supported. Use this method in your backend/server environment."
2628
+ const request = {
2629
+ userWallet: options.userWallet,
2630
+ limitUsdc: options.limitUSDC,
2631
+ durationDays: options.durationDays,
2632
+ encryptedSessionKey: encrypted.encryptedData,
2633
+ nonce: encrypted.nonce,
2634
+ sessionPublicKey: encrypted.publicKey,
2635
+ deviceFingerprint: sessionKey.getDeviceFingerprint(),
2636
+ recoveryQrData: recoveryQR
2637
+ };
2638
+ const response = await this.request(
2639
+ "POST",
2640
+ "/api/v1/ai/session-keys/device-bound/create",
2641
+ request
552
2642
  );
2643
+ this.sessionKey = sessionKey;
2644
+ this.sessionKeyId = response.sessionKeyId;
2645
+ sessionKey.setSessionKeyId(response.sessionKeyId);
2646
+ return {
2647
+ sessionKeyId: response.sessionKeyId,
2648
+ sessionWallet: response.sessionWallet,
2649
+ expiresAt: response.expiresAt,
2650
+ recoveryQR,
2651
+ limitUsdc: response.limitUsdc
2652
+ };
553
2653
  }
554
2654
  /**
555
- * Timing-safe string comparison to prevent timing attacks
2655
+ * Load an existing session key from backend
2656
+ * Requires PIN to decrypt
556
2657
  */
557
- timingSafeEqual(a, b) {
558
- if (a.length !== b.length) {
559
- return false;
560
- }
561
- if (typeof process !== "undefined" && process.versions?.node) {
562
- try {
563
- const bufferA = Buffer.from(a, "utf8");
564
- const bufferB = Buffer.from(b, "utf8");
565
- return (0, import_crypto.timingSafeEqual)(bufferA, bufferB);
566
- } catch {
2658
+ async loadSessionKey(sessionKeyId, pin) {
2659
+ const deviceInfo = await DeviceFingerprintGenerator.generate();
2660
+ const response = await this.request(
2661
+ "POST",
2662
+ "/api/v1/ai/session-keys/device-bound/get-encrypted",
2663
+ {
2664
+ sessionKeyId,
2665
+ deviceFingerprint: deviceInfo.fingerprint
567
2666
  }
2667
+ );
2668
+ if (!response.deviceFingerprintValid) {
2669
+ throw new Error(
2670
+ "Device fingerprint mismatch - this session key was created on a different device. Use recovery QR to migrate."
2671
+ );
568
2672
  }
569
- let result = 0;
570
- for (let i = 0; i < a.length; i++) {
571
- result |= a.charCodeAt(i) ^ b.charCodeAt(i);
572
- }
573
- return result === 0;
2673
+ const encrypted = {
2674
+ encryptedData: response.encryptedSessionKey,
2675
+ nonce: response.nonce,
2676
+ publicKey: "",
2677
+ // Will be populated after decryption
2678
+ deviceFingerprint: deviceInfo.fingerprint,
2679
+ version: "argon2id-aes256gcm-v1"
2680
+ };
2681
+ const keypair = await SessionKeyCrypto.decrypt(encrypted, pin, deviceInfo.fingerprint);
2682
+ encrypted.publicKey = keypair.publicKey.toBase58();
2683
+ this.sessionKey = new DeviceBoundSessionKey();
2684
+ this.sessionKey.encrypted = encrypted;
2685
+ this.sessionKey.deviceFingerprint = deviceInfo;
2686
+ this.sessionKey.setSessionKeyId(sessionKeyId);
2687
+ this.sessionKeyId = sessionKeyId;
574
2688
  }
575
2689
  /**
576
- * Make an HTTP request with retry logic
2690
+ * Make a payment using the session key
2691
+ *
2692
+ * First payment: Requires PIN to decrypt session key
2693
+ * Subsequent payments: Uses cached keypair (no PIN needed!) ✨
2694
+ *
2695
+ * @example
2696
+ * ```typescript
2697
+ * // First payment: requires PIN
2698
+ * const result1 = await manager.makePayment({
2699
+ * amount: 5.0,
2700
+ * recipient: '7xKNH....',
2701
+ * pin: '123456',
2702
+ * description: 'Coffee purchase',
2703
+ * });
2704
+ *
2705
+ * // Second payment: NO PIN NEEDED! Instant signing!
2706
+ * const result2 = await manager.makePayment({
2707
+ * amount: 3.0,
2708
+ * recipient: '7xKNH....',
2709
+ * description: 'Donut purchase',
2710
+ * }); // <- No PIN! Uses cached keypair
2711
+ *
2712
+ * console.log('Payment signature:', result2.signature);
2713
+ *
2714
+ * // Disable auto-signing for single payment
2715
+ * const result3 = await manager.makePayment({
2716
+ * amount: 100.0,
2717
+ * recipient: '7xKNH....',
2718
+ * pin: '123456',
2719
+ * enableAutoSign: false, // Will require PIN every time
2720
+ * });
2721
+ * ```
577
2722
  */
578
- async request(method, endpoint, data, options = {}) {
579
- const attempt = options.attempt || 1;
580
- const idempotencyKey = options.idempotencyKey || (this.config.idempotencyEnabled && method !== "GET" ? generateIdempotencyKey() : void 0);
581
- try {
582
- const url = `${this.config.baseURL}${endpoint}`;
583
- const headers = {
584
- "Content-Type": "application/json",
585
- Authorization: `Bearer ${this.config.apiKey}`
2723
+ async makePayment(options) {
2724
+ if (!this.sessionKey || !this.sessionKeyId) {
2725
+ throw new Error("No session key loaded. Call createSessionKey() or loadSessionKey() first.");
2726
+ }
2727
+ const enableAutoSign = options.enableAutoSign !== false;
2728
+ const needsPin = !this.sessionKey.isCached();
2729
+ if (needsPin && !options.pin) {
2730
+ throw new Error(
2731
+ "PIN required: no cached keypair available. Please provide PIN or call unlockSessionKey() first."
2732
+ );
2733
+ }
2734
+ const paymentResponse = await this.request("POST", "/api/v1/ai/smart-payment", {
2735
+ amount_usd: options.amount,
2736
+ user_wallet: options.recipient,
2737
+ token: options.token || "USDC",
2738
+ description: options.description
2739
+ }, {
2740
+ "X-Session-Key-ID": this.sessionKeyId
2741
+ });
2742
+ if (!paymentResponse.requires_signature && paymentResponse.status === "confirmed") {
2743
+ return {
2744
+ paymentId: paymentResponse.paymentId,
2745
+ signature: "",
2746
+ // Backend signed
2747
+ status: paymentResponse.status
586
2748
  };
587
- if (idempotencyKey) {
588
- headers["Idempotency-Key"] = idempotencyKey;
589
- }
590
- const controller = new AbortController();
591
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
592
- const response = await (0, import_cross_fetch.default)(url, {
593
- method,
594
- headers,
595
- body: data ? JSON.stringify(data) : void 0,
596
- signal: controller.signal
597
- });
598
- clearTimeout(timeoutId);
599
- let body;
600
- try {
601
- body = await response.json();
602
- } catch {
603
- body = null;
604
- }
605
- if (!response.ok) {
606
- const error = parseError(response, body);
607
- if (response.status >= 500 && attempt < this.config.retries) {
608
- const delay = Math.pow(2, attempt) * 1e3;
609
- await sleep(delay);
610
- return this.request(method, endpoint, data, {
611
- idempotencyKey,
612
- attempt: attempt + 1
613
- });
614
- }
615
- throw error;
616
- }
617
- return body;
618
- } catch (error) {
619
- if (error.name === "AbortError") {
620
- throw new Error(`Request timeout after ${this.config.timeout}ms`);
621
- }
622
- if (attempt < this.config.retries && error.message?.includes("fetch")) {
623
- const delay = Math.pow(2, attempt) * 1e3;
624
- await sleep(delay);
625
- return this.request(method, endpoint, data, {
626
- idempotencyKey,
627
- attempt: attempt + 1
628
- });
629
- }
630
- throw error;
631
2749
  }
632
- }
633
- };
634
- var zendfi = (() => {
635
- try {
636
- return new ZendFiClient();
637
- } catch (error) {
638
- if (process.env.NODE_ENV === "test" || !process.env.ZENDFI_API_KEY) {
639
- return new Proxy({}, {
640
- get() {
641
- throw new Error(
642
- 'ZendFi singleton not initialized. Set ZENDFI_API_KEY environment variable or create a custom instance: new ZendFiClient({ apiKey: "..." })'
643
- );
644
- }
645
- });
2750
+ if (!paymentResponse.unsigned_transaction) {
2751
+ throw new Error("Backend did not return unsigned transaction");
646
2752
  }
647
- throw error;
2753
+ const transactionBuffer = Buffer.from(paymentResponse.unsigned_transaction, "base64");
2754
+ const transaction = import_web32.Transaction.from(transactionBuffer);
2755
+ const signedTransaction = await this.sessionKey.signTransaction(
2756
+ transaction,
2757
+ options.pin || "",
2758
+ // PIN only needed if not cached
2759
+ enableAutoSign
2760
+ // Cache for future payments
2761
+ );
2762
+ const submitResponse = await this.request("POST", `/api/v1/ai/payments/${paymentResponse.paymentId}/submit-signed`, {
2763
+ signed_transaction: signedTransaction.serialize().toString("base64")
2764
+ });
2765
+ return {
2766
+ paymentId: paymentResponse.paymentId,
2767
+ signature: submitResponse.signature,
2768
+ status: submitResponse.status
2769
+ };
648
2770
  }
649
- })();
650
-
651
- // src/webhooks.ts
652
- async function verifyNextWebhook(request, secret) {
653
- try {
654
- const payload = await request.text();
655
- const signature = request.headers.get("x-zendfi-signature");
656
- if (!signature) {
657
- return null;
658
- }
659
- const webhookSecret = secret || process.env.ZENDFI_WEBHOOK_SECRET;
660
- if (!webhookSecret) {
661
- throw new Error("ZENDFI_WEBHOOK_SECRET not configured");
2771
+ /**
2772
+ * Recover session key on new device
2773
+ * Requires recovery QR and PIN from original device
2774
+ *
2775
+ * @example
2776
+ * ```typescript
2777
+ * const recovered = await manager.recoverSessionKey({
2778
+ * sessionKeyId: 'uuid...',
2779
+ * recoveryQR: '{"encryptedSessionKey":"..."}',
2780
+ * oldPin: '123456',
2781
+ * newPin: '654321',
2782
+ * });
2783
+ * ```
2784
+ */
2785
+ async recoverSessionKey(options) {
2786
+ const recoveryData = RecoveryQRGenerator.decode(options.recoveryQR);
2787
+ const oldDeviceFingerprint = "recovery-mode";
2788
+ const newDeviceInfo = await DeviceFingerprintGenerator.generate();
2789
+ const newEncrypted = await RecoveryQRGenerator.reEncryptForNewDevice(
2790
+ recoveryData,
2791
+ options.oldPin,
2792
+ oldDeviceFingerprint,
2793
+ options.newPin,
2794
+ newDeviceInfo.fingerprint
2795
+ );
2796
+ await this.request("POST", `/api/v1/ai/session-keys/device-bound/${options.sessionKeyId}/recover`, {
2797
+ recoveryQrData: options.recoveryQR,
2798
+ newDeviceFingerprint: newDeviceInfo.fingerprint,
2799
+ newEncryptedSessionKey: newEncrypted.encryptedData,
2800
+ newNonce: newEncrypted.nonce
2801
+ });
2802
+ await this.loadSessionKey(options.sessionKeyId, options.newPin);
2803
+ }
2804
+ /**
2805
+ * Revoke session key
2806
+ */
2807
+ async revokeSessionKey(sessionKeyId) {
2808
+ const keyId = sessionKeyId || this.sessionKeyId;
2809
+ if (!keyId) {
2810
+ throw new Error("No session key ID provided");
662
2811
  }
663
- const isValid = zendfi.verifyWebhook({
664
- payload,
665
- signature,
666
- secret: webhookSecret
2812
+ await this.request("POST", "/api/v1/ai/session-keys/revoke", {
2813
+ session_key_id: keyId
667
2814
  });
668
- if (!isValid) {
669
- return null;
2815
+ if (keyId === this.sessionKeyId) {
2816
+ this.sessionKey = null;
2817
+ this.sessionKeyId = null;
670
2818
  }
671
- return JSON.parse(payload);
672
- } catch {
673
- return null;
674
2819
  }
675
- }
676
- async function verifyExpressWebhook(request, secret) {
677
- try {
678
- const payload = request.rawBody || JSON.stringify(request.body);
679
- const signature = request.headers["x-zendfi-signature"];
680
- if (!signature) {
681
- return null;
2820
+ /**
2821
+ * Unlock session key with PIN and cache for auto-signing
2822
+ * Call this after creating/loading session key to enable instant payments
2823
+ *
2824
+ * @example
2825
+ * ```typescript
2826
+ * // Create session key
2827
+ * await manager.createSessionKey({...});
2828
+ *
2829
+ * // Unlock with PIN (one-time)
2830
+ * await manager.unlockSessionKey('123456');
2831
+ *
2832
+ * // Now all payments are instant (no PIN!)
2833
+ * await manager.makePayment({amount: 5, ...}); // Instant!
2834
+ * await manager.makePayment({amount: 3, ...}); // Instant!
2835
+ * ```
2836
+ */
2837
+ async unlockSessionKey(pin, cacheTTL) {
2838
+ if (!this.sessionKey) {
2839
+ throw new Error("No session key loaded");
682
2840
  }
683
- const webhookSecret = secret || process.env.ZENDFI_WEBHOOK_SECRET;
684
- if (!webhookSecret) {
685
- throw new Error("ZENDFI_WEBHOOK_SECRET not configured");
2841
+ await this.sessionKey.unlockWithPin(pin, cacheTTL);
2842
+ }
2843
+ /**
2844
+ * Clear cached keypair
2845
+ * Should be called on logout or when session ends
2846
+ *
2847
+ * @example
2848
+ * ```typescript
2849
+ * // Clear on logout
2850
+ * manager.clearCache();
2851
+ *
2852
+ * // Or auto-clear on tab close
2853
+ * window.addEventListener('beforeunload', () => {
2854
+ * manager.clearCache();
2855
+ * });
2856
+ * ```
2857
+ */
2858
+ clearCache() {
2859
+ if (this.sessionKey) {
2860
+ this.sessionKey.clearCache();
686
2861
  }
687
- const isValid = zendfi.verifyWebhook({
688
- payload,
689
- signature,
690
- secret: webhookSecret
2862
+ }
2863
+ /**
2864
+ * Check if keypair is cached (auto-signing enabled)
2865
+ */
2866
+ isCached() {
2867
+ return this.sessionKey?.isCached() || false;
2868
+ }
2869
+ /**
2870
+ * Get time remaining until cache expires (in milliseconds)
2871
+ */
2872
+ getCacheTimeRemaining() {
2873
+ return this.sessionKey?.getCacheTimeRemaining() || 0;
2874
+ }
2875
+ /**
2876
+ * Extend cache expiry time
2877
+ * Useful to keep session active during user activity
2878
+ */
2879
+ extendCache(additionalTTL) {
2880
+ if (!this.sessionKey) {
2881
+ throw new Error("No session key loaded");
2882
+ }
2883
+ this.sessionKey.extendCache(additionalTTL);
2884
+ }
2885
+ /**
2886
+ * Get session key status
2887
+ */
2888
+ async getStatus(sessionKeyId) {
2889
+ const keyId = sessionKeyId || this.sessionKeyId;
2890
+ if (!keyId) {
2891
+ throw new Error("No session key ID provided");
2892
+ }
2893
+ return await this.request("POST", "/api/v1/ai/session-keys/status", {
2894
+ session_key_id: keyId
691
2895
  });
692
- if (!isValid) {
693
- return null;
2896
+ }
2897
+ // ============================================
2898
+ // Private HTTP Helper
2899
+ // ============================================
2900
+ async request(method, path, body, additionalHeaders) {
2901
+ const url = `${this.baseURL}${path}`;
2902
+ const headers = {
2903
+ "Content-Type": "application/json",
2904
+ "Authorization": `Bearer ${this.apiKey}`,
2905
+ ...additionalHeaders
2906
+ };
2907
+ const response = await fetch(url, {
2908
+ method,
2909
+ headers,
2910
+ body: body ? JSON.stringify(body) : void 0
2911
+ });
2912
+ if (!response.ok) {
2913
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
2914
+ throw new Error(`API Error: ${error.error || response.statusText}`);
694
2915
  }
695
- return JSON.parse(payload);
696
- } catch {
697
- return null;
2916
+ return await response.json();
698
2917
  }
699
- }
700
- function verifyWebhookSignature(payload, signature, secret) {
701
- return zendfi.verifyWebhook({
702
- payload,
703
- signature,
704
- secret
705
- });
706
- }
2918
+ };
707
2919
 
708
2920
  // src/webhook-handler.ts
709
2921
  var import_crypto2 = require("crypto");
@@ -845,16 +3057,197 @@ async function processWebhook(a, b, c) {
845
3057
  };
846
3058
  }
847
3059
  }
3060
+
3061
+ // src/lit-crypto-signer.ts
3062
+ var SPENDING_LIMIT_ACTION_CID = "QmXXunoMeNhXhnr4onzBuvnMzDqH8rf1qdM94RKXayypX3";
3063
+ var LitCryptoSigner = class {
3064
+ config;
3065
+ litNodeClient = null;
3066
+ connected = false;
3067
+ constructor(config = {}) {
3068
+ this.config = {
3069
+ network: config.network || "datil-dev",
3070
+ apiEndpoint: config.apiEndpoint || "https://api.zendfi.tech",
3071
+ apiKey: config.apiKey || "",
3072
+ debug: config.debug || false
3073
+ };
3074
+ }
3075
+ async connect() {
3076
+ if (this.connected && this.litNodeClient) {
3077
+ return;
3078
+ }
3079
+ this.log("Connecting to Lit Protocol network:", this.config.network);
3080
+ const { LitNodeClient } = await import("@lit-protocol/lit-node-client");
3081
+ this.litNodeClient = new LitNodeClient({
3082
+ litNetwork: this.config.network,
3083
+ debug: this.config.debug
3084
+ });
3085
+ await this.litNodeClient.connect();
3086
+ this.connected = true;
3087
+ this.log("Connected to Lit Protocol");
3088
+ }
3089
+ async disconnect() {
3090
+ if (this.litNodeClient) {
3091
+ await this.litNodeClient.disconnect();
3092
+ this.litNodeClient = null;
3093
+ this.connected = false;
3094
+ }
3095
+ }
3096
+ async signPayment(params) {
3097
+ if (!this.connected || !this.litNodeClient) {
3098
+ throw new Error("Not connected to Lit Protocol. Call connect() first.");
3099
+ }
3100
+ this.log("Signing payment with Lit Protocol");
3101
+ this.log(" Session:", params.sessionId);
3102
+ this.log(" Amount: $" + params.amountUsd);
3103
+ try {
3104
+ let sessionSigs = params.sessionSigs;
3105
+ if (!sessionSigs) {
3106
+ sessionSigs = await this.getSessionSigs(params.pkpPublicKey);
3107
+ }
3108
+ const result = await this.litNodeClient.executeJs({
3109
+ ipfsId: SPENDING_LIMIT_ACTION_CID,
3110
+ sessionSigs,
3111
+ jsParams: {
3112
+ sessionId: params.sessionId,
3113
+ requestedAmountUsd: params.amountUsd,
3114
+ merchantId: params.merchantId,
3115
+ transactionToSign: params.transactionToSign,
3116
+ apiEndpoint: this.config.apiEndpoint,
3117
+ apiKey: this.config.apiKey,
3118
+ pkpPublicKey: params.pkpPublicKey
3119
+ }
3120
+ });
3121
+ this.log("Lit Action result:", result);
3122
+ const response = JSON.parse(result.response);
3123
+ return {
3124
+ success: response.success,
3125
+ signature: response.signature,
3126
+ publicKey: response.publicKey,
3127
+ recid: response.recid,
3128
+ sessionId: response.session_id,
3129
+ amountUsd: response.amount_usd,
3130
+ remainingBudget: response.remaining_budget,
3131
+ cryptoEnforced: response.crypto_enforced ?? true,
3132
+ error: response.error,
3133
+ code: response.code,
3134
+ currentSpent: response.current_spent,
3135
+ limit: response.limit,
3136
+ remaining: response.remaining
3137
+ };
3138
+ } catch (error) {
3139
+ this.log("Lit signing error:", error);
3140
+ return {
3141
+ success: false,
3142
+ cryptoEnforced: true,
3143
+ error: error instanceof Error ? error.message : "Unknown error",
3144
+ code: "LIT_ERROR"
3145
+ };
3146
+ }
3147
+ }
3148
+ async getSessionSigs(pkpPublicKey) {
3149
+ this.log("Generating Lit session signatures");
3150
+ if (typeof window !== "undefined" && window.ethereum) {
3151
+ const { ethers } = await import("ethers");
3152
+ const provider = new ethers.BrowserProvider(window.ethereum);
3153
+ const signer = await provider.getSigner();
3154
+ const { LitAbility, LitPKPResource } = await import("@lit-protocol/auth-helpers");
3155
+ const sessionSigs = await this.litNodeClient.getSessionSigs({
3156
+ pkpPublicKey,
3157
+ chain: "ethereum",
3158
+ expiration: new Date(Date.now() + 1e3 * 60 * 10).toISOString(),
3159
+ resourceAbilityRequests: [
3160
+ {
3161
+ resource: new LitPKPResource("*"),
3162
+ ability: LitAbility.PKPSigning
3163
+ }
3164
+ ],
3165
+ authNeededCallback: async (params) => {
3166
+ const message = params.message;
3167
+ const signature = await signer.signMessage(message);
3168
+ return {
3169
+ sig: signature,
3170
+ derivedVia: "web3.eth.personal.sign",
3171
+ signedMessage: message,
3172
+ address: await signer.getAddress()
3173
+ };
3174
+ }
3175
+ });
3176
+ return sessionSigs;
3177
+ }
3178
+ throw new Error(
3179
+ "No wallet available. In browser, ensure MetaMask or similar is connected. In Node.js, pass pre-generated sessionSigs to signPayment()."
3180
+ );
3181
+ }
3182
+ log(...args) {
3183
+ if (this.config.debug) {
3184
+ console.log("[LitCryptoSigner]", ...args);
3185
+ }
3186
+ }
3187
+ };
3188
+ function requiresLitSigning(session) {
3189
+ return session.crypto_enforced === true || session.mint_pkp === true;
3190
+ }
3191
+ function encodeTransactionForLit(transaction) {
3192
+ const bytes = transaction instanceof Uint8Array ? transaction : new Uint8Array(transaction);
3193
+ return btoa(String.fromCharCode(...bytes));
3194
+ }
3195
+ function decodeSignatureFromLit(result) {
3196
+ if (!result.success || !result.signature) {
3197
+ return null;
3198
+ }
3199
+ const hex = result.signature.startsWith("0x") ? result.signature.slice(2) : result.signature;
3200
+ const bytes = new Uint8Array(hex.length / 2);
3201
+ for (let i = 0; i < hex.length; i += 2) {
3202
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
3203
+ }
3204
+ return bytes;
3205
+ }
848
3206
  // Annotate the CommonJS export names for ESM import in node:
849
3207
  0 && (module.exports = {
3208
+ AgentAPI,
3209
+ ApiError,
850
3210
  AuthenticationError,
3211
+ AutonomyAPI,
851
3212
  ConfigLoader,
3213
+ DeviceBoundSessionKey,
3214
+ DeviceFingerprintGenerator,
3215
+ ERROR_CODES,
3216
+ InterceptorManager,
3217
+ LitCryptoSigner,
852
3218
  NetworkError,
3219
+ PaymentError,
3220
+ PaymentIntentsAPI,
3221
+ PricingAPI,
853
3222
  RateLimitError,
3223
+ RateLimiter,
3224
+ RecoveryQRGenerator,
3225
+ SPENDING_LIMIT_ACTION_CID,
3226
+ SessionKeyCrypto,
3227
+ SmartPaymentsAPI,
854
3228
  ValidationError,
3229
+ WebhookError,
855
3230
  ZendFiClient,
856
3231
  ZendFiError,
3232
+ ZendFiSessionKeyManager,
3233
+ asAgentKeyId,
3234
+ asEscrowId,
3235
+ asInstallmentPlanId,
3236
+ asIntentId,
3237
+ asInvoiceId,
3238
+ asMerchantId,
3239
+ asPaymentId,
3240
+ asPaymentLinkCode,
3241
+ asSessionId,
3242
+ asSubscriptionId,
3243
+ createZendFiError,
3244
+ decodeSignatureFromLit,
3245
+ encodeTransactionForLit,
3246
+ generateIdempotencyKey,
3247
+ isZendFiError,
857
3248
  processWebhook,
3249
+ requiresLitSigning,
3250
+ sleep,
858
3251
  verifyExpressWebhook,
859
3252
  verifyNextWebhook,
860
3253
  verifyWebhookSignature,