@zendfi/sdk 0.4.0 → 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/README.md +1161 -0
- package/dist/express.d.mts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/index.d.mts +1490 -3
- package/dist/index.d.ts +1490 -3
- package/dist/index.js +2539 -411
- package/dist/index.mjs +2501 -401
- package/dist/nextjs.d.mts +1 -1
- package/dist/nextjs.d.ts +1 -1
- package/dist/webhook-handler-D8wEoYd7.d.mts +869 -0
- package/dist/webhook-handler-D8wEoYd7.d.ts +869 -0
- package/package.json +21 -1
- package/dist/webhook-handler-B9ZczHQn.d.mts +0 -373
- package/dist/webhook-handler-B9ZczHQn.d.ts +0 -373
package/dist/index.js
CHANGED
|
@@ -30,21 +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
|
+
AgentAPI: () => AgentAPI,
|
|
33
34
|
ApiError: () => ApiError,
|
|
34
35
|
AuthenticationError: () => AuthenticationError2,
|
|
36
|
+
AutonomyAPI: () => AutonomyAPI,
|
|
35
37
|
ConfigLoader: () => ConfigLoader,
|
|
38
|
+
DeviceBoundSessionKey: () => DeviceBoundSessionKey,
|
|
39
|
+
DeviceFingerprintGenerator: () => DeviceFingerprintGenerator,
|
|
36
40
|
ERROR_CODES: () => ERROR_CODES,
|
|
37
41
|
InterceptorManager: () => InterceptorManager,
|
|
42
|
+
LitCryptoSigner: () => LitCryptoSigner,
|
|
38
43
|
NetworkError: () => NetworkError2,
|
|
39
44
|
PaymentError: () => PaymentError,
|
|
45
|
+
PaymentIntentsAPI: () => PaymentIntentsAPI,
|
|
46
|
+
PricingAPI: () => PricingAPI,
|
|
40
47
|
RateLimitError: () => RateLimitError2,
|
|
48
|
+
RateLimiter: () => RateLimiter,
|
|
49
|
+
RecoveryQRGenerator: () => RecoveryQRGenerator,
|
|
50
|
+
SPENDING_LIMIT_ACTION_CID: () => SPENDING_LIMIT_ACTION_CID,
|
|
51
|
+
SessionKeyCrypto: () => SessionKeyCrypto,
|
|
52
|
+
SmartPaymentsAPI: () => SmartPaymentsAPI,
|
|
41
53
|
ValidationError: () => ValidationError2,
|
|
42
54
|
WebhookError: () => WebhookError,
|
|
43
55
|
ZendFiClient: () => ZendFiClient,
|
|
44
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,
|
|
45
68
|
createZendFiError: () => createZendFiError,
|
|
69
|
+
decodeSignatureFromLit: () => decodeSignatureFromLit,
|
|
70
|
+
encodeTransactionForLit: () => encodeTransactionForLit,
|
|
71
|
+
generateIdempotencyKey: () => generateIdempotencyKey,
|
|
46
72
|
isZendFiError: () => isZendFiError,
|
|
47
73
|
processWebhook: () => processWebhook,
|
|
74
|
+
requiresLitSigning: () => requiresLitSigning,
|
|
75
|
+
sleep: () => sleep,
|
|
48
76
|
verifyExpressWebhook: () => verifyExpressWebhook,
|
|
49
77
|
verifyNextWebhook: () => verifyNextWebhook,
|
|
50
78
|
verifyWebhookSignature: () => verifyWebhookSignature,
|
|
@@ -56,6 +84,18 @@ module.exports = __toCommonJS(index_exports);
|
|
|
56
84
|
var import_cross_fetch = __toESM(require("cross-fetch"));
|
|
57
85
|
var import_crypto = require("crypto");
|
|
58
86
|
|
|
87
|
+
// src/types.ts
|
|
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;
|
|
98
|
+
|
|
59
99
|
// src/utils.ts
|
|
60
100
|
var ConfigLoader = class {
|
|
61
101
|
/**
|
|
@@ -194,6 +234,66 @@ function generateIdempotencyKey() {
|
|
|
194
234
|
function sleep(ms) {
|
|
195
235
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
236
|
}
|
|
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;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Check if a request can be made without exceeding rate limit
|
|
247
|
+
*/
|
|
248
|
+
canMakeRequest() {
|
|
249
|
+
this.pruneOldRequests();
|
|
250
|
+
return this.requests.length < this.maxRequests;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Record a request timestamp
|
|
254
|
+
*/
|
|
255
|
+
recordRequest() {
|
|
256
|
+
this.requests.push(Date.now());
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Get remaining requests in current window
|
|
260
|
+
*/
|
|
261
|
+
getRemainingRequests() {
|
|
262
|
+
this.pruneOldRequests();
|
|
263
|
+
return Math.max(0, this.maxRequests - this.requests.length);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get time in ms until the rate limit window resets
|
|
267
|
+
*/
|
|
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());
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get current rate limit status
|
|
276
|
+
*/
|
|
277
|
+
getStatus() {
|
|
278
|
+
this.pruneOldRequests();
|
|
279
|
+
return {
|
|
280
|
+
remaining: this.getRemainingRequests(),
|
|
281
|
+
limit: this.maxRequests,
|
|
282
|
+
resetInMs: this.getTimeUntilReset(),
|
|
283
|
+
isLimited: !this.canMakeRequest()
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Reset the rate limiter (useful for testing)
|
|
288
|
+
*/
|
|
289
|
+
reset() {
|
|
290
|
+
this.requests = [];
|
|
291
|
+
}
|
|
292
|
+
pruneOldRequests() {
|
|
293
|
+
const cutoff = Date.now() - this.windowMs;
|
|
294
|
+
this.requests = this.requests.filter((t) => t > cutoff);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
197
297
|
|
|
198
298
|
// src/errors.ts
|
|
199
299
|
var ZendFiError2 = class _ZendFiError extends Error {
|
|
@@ -435,533 +535,2387 @@ function createInterceptors() {
|
|
|
435
535
|
};
|
|
436
536
|
}
|
|
437
537
|
|
|
438
|
-
// src/
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
constructor(options) {
|
|
443
|
-
this.config = ConfigLoader.load(options);
|
|
444
|
-
ConfigLoader.validateApiKey(this.config.apiKey);
|
|
445
|
-
this.interceptors = createInterceptors();
|
|
446
|
-
if (this.config.environment === "development" || this.config.debug) {
|
|
447
|
-
console.log(
|
|
448
|
-
`\u2713 ZendFi SDK initialized in ${this.config.mode} mode (${this.config.mode === "test" ? "devnet" : "mainnet"})`
|
|
449
|
-
);
|
|
450
|
-
if (this.config.debug) {
|
|
451
|
-
console.log("[ZendFi] Debug mode enabled");
|
|
452
|
-
}
|
|
453
|
-
}
|
|
538
|
+
// src/api/agent.ts
|
|
539
|
+
function normalizeArrayResponse(response, key) {
|
|
540
|
+
if (Array.isArray(response)) {
|
|
541
|
+
return response;
|
|
454
542
|
}
|
|
543
|
+
return response[key] || [];
|
|
544
|
+
}
|
|
545
|
+
var AgentAPI = class {
|
|
546
|
+
constructor(request) {
|
|
547
|
+
this.request = request;
|
|
548
|
+
}
|
|
549
|
+
// ============================================
|
|
550
|
+
// Agent API Keys
|
|
551
|
+
// ============================================
|
|
455
552
|
/**
|
|
456
|
-
* Create a new
|
|
553
|
+
* Create a new agent API key with scoped permissions
|
|
554
|
+
*
|
|
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!)
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```typescript
|
|
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,
|
|
568
|
+
* });
|
|
569
|
+
*
|
|
570
|
+
* // IMPORTANT: Save the full_key now - it won't be shown again!
|
|
571
|
+
* console.log(agentKey.full_key); // => "zai_test_abc123..."
|
|
572
|
+
* ```
|
|
457
573
|
*/
|
|
458
|
-
async
|
|
459
|
-
return this.request("POST", "/api/v1/
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
463
582
|
});
|
|
464
583
|
}
|
|
465
584
|
/**
|
|
466
|
-
*
|
|
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
|
+
* ```
|
|
467
596
|
*/
|
|
468
|
-
async
|
|
469
|
-
|
|
597
|
+
async listKeys() {
|
|
598
|
+
const response = await this.request(
|
|
599
|
+
"GET",
|
|
600
|
+
"/api/v1/agent-keys"
|
|
601
|
+
);
|
|
602
|
+
return normalizeArrayResponse(response, "keys");
|
|
470
603
|
}
|
|
471
604
|
/**
|
|
472
|
-
*
|
|
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
|
+
* ```
|
|
473
617
|
*/
|
|
474
|
-
async
|
|
475
|
-
|
|
476
|
-
if (request?.page) params.append("page", request.page.toString());
|
|
477
|
-
if (request?.limit) params.append("limit", request.limit.toString());
|
|
478
|
-
if (request?.status) params.append("status", request.status);
|
|
479
|
-
if (request?.from_date) params.append("from_date", request.from_date);
|
|
480
|
-
if (request?.to_date) params.append("to_date", request.to_date);
|
|
481
|
-
const query = params.toString() ? `?${params.toString()}` : "";
|
|
482
|
-
return this.request("GET", `/api/v1/payments${query}`);
|
|
618
|
+
async revokeKey(keyId) {
|
|
619
|
+
await this.request("POST", `/api/v1/agent-keys/${keyId}/revoke`);
|
|
483
620
|
}
|
|
621
|
+
// ============================================
|
|
622
|
+
// Agent Sessions
|
|
623
|
+
// ============================================
|
|
484
624
|
/**
|
|
485
|
-
* Create
|
|
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
|
+
* ```
|
|
486
650
|
*/
|
|
487
|
-
async
|
|
488
|
-
return this.request("POST", "/api/v1/
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
|
493
667
|
});
|
|
494
668
|
}
|
|
495
669
|
/**
|
|
496
|
-
*
|
|
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
|
+
* ```
|
|
497
680
|
*/
|
|
498
|
-
async
|
|
499
|
-
|
|
681
|
+
async listSessions() {
|
|
682
|
+
const response = await this.request(
|
|
683
|
+
"GET",
|
|
684
|
+
"/api/v1/ai/sessions"
|
|
685
|
+
);
|
|
686
|
+
return normalizeArrayResponse(response, "sessions");
|
|
500
687
|
}
|
|
501
688
|
/**
|
|
502
|
-
*
|
|
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
|
+
* ```
|
|
503
700
|
*/
|
|
504
|
-
async
|
|
505
|
-
return this.request("
|
|
701
|
+
async getSession(sessionId) {
|
|
702
|
+
return this.request("GET", `/api/v1/ai/sessions/${sessionId}`);
|
|
506
703
|
}
|
|
507
704
|
/**
|
|
508
|
-
*
|
|
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
|
+
* ```
|
|
509
717
|
*/
|
|
510
|
-
async
|
|
511
|
-
|
|
718
|
+
async revokeSession(sessionId) {
|
|
719
|
+
await this.request("POST", `/api/v1/ai/sessions/${sessionId}/revoke`);
|
|
512
720
|
}
|
|
721
|
+
// ============================================
|
|
722
|
+
// Agent Analytics
|
|
723
|
+
// ============================================
|
|
513
724
|
/**
|
|
514
|
-
*
|
|
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
|
+
* ```
|
|
515
736
|
*/
|
|
516
|
-
async
|
|
517
|
-
return this.request(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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;
|
|
521
746
|
}
|
|
522
747
|
/**
|
|
523
|
-
* Create a payment
|
|
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
|
+
* ```
|
|
524
769
|
*/
|
|
525
|
-
async
|
|
526
|
-
|
|
527
|
-
|
|
770
|
+
async create(request) {
|
|
771
|
+
return this.request("POST", "/api/v1/payment-intents", {
|
|
772
|
+
amount: request.amount,
|
|
528
773
|
currency: request.currency || "USD",
|
|
529
|
-
|
|
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
|
|
530
781
|
});
|
|
531
|
-
return {
|
|
532
|
-
...response,
|
|
533
|
-
url: response.hosted_page_url
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Get payment link by link code
|
|
538
|
-
*/
|
|
539
|
-
async getPaymentLink(linkCode) {
|
|
540
|
-
const response = await this.request("GET", `/api/v1/payment-links/${linkCode}`);
|
|
541
|
-
return {
|
|
542
|
-
...response,
|
|
543
|
-
url: response.hosted_page_url
|
|
544
|
-
};
|
|
545
782
|
}
|
|
546
783
|
/**
|
|
547
|
-
*
|
|
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
|
+
* ```
|
|
548
797
|
*/
|
|
549
|
-
async
|
|
550
|
-
|
|
551
|
-
return response.map((link) => ({
|
|
552
|
-
...link,
|
|
553
|
-
url: link.hosted_page_url
|
|
554
|
-
}));
|
|
798
|
+
async get(intentId) {
|
|
799
|
+
return this.request("GET", `/api/v1/payment-intents/${intentId}`);
|
|
555
800
|
}
|
|
556
801
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
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
|
+
* ```
|
|
559
815
|
*/
|
|
560
|
-
async
|
|
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()}` : "";
|
|
561
822
|
const response = await this.request(
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
request
|
|
823
|
+
"GET",
|
|
824
|
+
`/api/v1/payment-intents${query}`
|
|
565
825
|
);
|
|
566
|
-
return
|
|
567
|
-
id: response.plan_id,
|
|
568
|
-
plan_id: response.plan_id,
|
|
569
|
-
status: response.status
|
|
570
|
-
};
|
|
826
|
+
return Array.isArray(response) ? response : response.intents;
|
|
571
827
|
}
|
|
572
828
|
/**
|
|
573
|
-
*
|
|
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
|
+
* ```
|
|
574
850
|
*/
|
|
575
|
-
async
|
|
576
|
-
return this.request("
|
|
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
|
+
});
|
|
577
859
|
}
|
|
578
860
|
/**
|
|
579
|
-
*
|
|
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
|
+
* ```
|
|
580
874
|
*/
|
|
581
|
-
async
|
|
582
|
-
|
|
583
|
-
if (params?.limit) query.append("limit", params.limit.toString());
|
|
584
|
-
if (params?.offset) query.append("offset", params.offset.toString());
|
|
585
|
-
const queryString = query.toString() ? `?${query.toString()}` : "";
|
|
586
|
-
return this.request("GET", `/api/v1/installment-plans${queryString}`);
|
|
875
|
+
async cancel(intentId) {
|
|
876
|
+
return this.request("POST", `/api/v1/payment-intents/${intentId}/cancel`);
|
|
587
877
|
}
|
|
588
878
|
/**
|
|
589
|
-
*
|
|
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
|
+
* ```
|
|
590
894
|
*/
|
|
591
|
-
async
|
|
592
|
-
|
|
895
|
+
async getEvents(intentId) {
|
|
896
|
+
const response = await this.request(
|
|
593
897
|
"GET",
|
|
594
|
-
`/api/v1/
|
|
898
|
+
`/api/v1/payment-intents/${intentId}/events`
|
|
595
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;
|
|
596
908
|
}
|
|
597
909
|
/**
|
|
598
|
-
*
|
|
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
|
+
* ```
|
|
599
934
|
*/
|
|
600
|
-
async
|
|
601
|
-
return this.request(
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
);
|
|
935
|
+
async getPPPFactor(countryCode) {
|
|
936
|
+
return this.request("POST", "/api/v1/ai/pricing/ppp-factor", {
|
|
937
|
+
country_code: countryCode.toUpperCase()
|
|
938
|
+
});
|
|
605
939
|
}
|
|
606
940
|
/**
|
|
607
|
-
*
|
|
608
|
-
*
|
|
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
|
+
* ```
|
|
609
960
|
*/
|
|
610
|
-
async
|
|
611
|
-
|
|
612
|
-
|
|
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,
|
|
613
1004
|
currency: request.currency || "USD",
|
|
614
|
-
|
|
1005
|
+
user_profile: request.user_profile,
|
|
1006
|
+
ppp_config: request.ppp_config
|
|
615
1007
|
});
|
|
616
1008
|
}
|
|
617
1009
|
/**
|
|
618
|
-
*
|
|
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
|
+
* ```
|
|
619
1025
|
*/
|
|
620
|
-
async
|
|
621
|
-
|
|
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;
|
|
622
1045
|
}
|
|
623
1046
|
/**
|
|
624
|
-
*
|
|
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
|
+
* ```
|
|
625
1081
|
*/
|
|
626
|
-
async
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
+
);
|
|
632
1096
|
}
|
|
633
1097
|
/**
|
|
634
|
-
*
|
|
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
|
+
* ```
|
|
635
1111
|
*/
|
|
636
|
-
async
|
|
637
|
-
|
|
1112
|
+
async revoke(sessionKeyId, reason) {
|
|
1113
|
+
const request = { reason };
|
|
1114
|
+
await this.request(
|
|
638
1115
|
"POST",
|
|
639
|
-
`/api/v1/
|
|
1116
|
+
`/api/v1/ai/session-keys/${sessionKeyId}/revoke-autonomy`,
|
|
640
1117
|
request
|
|
641
1118
|
);
|
|
642
1119
|
}
|
|
643
1120
|
/**
|
|
644
|
-
*
|
|
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
|
+
* ```
|
|
645
1140
|
*/
|
|
646
|
-
async
|
|
647
|
-
return this.request(
|
|
1141
|
+
async getStatus(sessionKeyId) {
|
|
1142
|
+
return this.request(
|
|
1143
|
+
"GET",
|
|
1144
|
+
`/api/v1/ai/session-keys/${sessionKeyId}/autonomy-status`
|
|
1145
|
+
);
|
|
648
1146
|
}
|
|
649
1147
|
/**
|
|
650
|
-
*
|
|
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
|
+
* ```
|
|
651
1176
|
*/
|
|
652
|
-
|
|
653
|
-
return
|
|
1177
|
+
createDelegationMessage(sessionKeyId, maxAmountUsd, expiresAt) {
|
|
1178
|
+
return `I authorize autonomous delegate for session ${sessionKeyId} to spend up to $${maxAmountUsd} until ${expiresAt}`;
|
|
654
1179
|
}
|
|
655
1180
|
/**
|
|
656
|
-
*
|
|
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
|
+
* ```
|
|
657
1198
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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;
|
|
663
1220
|
}
|
|
664
1221
|
/**
|
|
665
|
-
*
|
|
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
|
+
* ```
|
|
666
1272
|
*/
|
|
667
|
-
async
|
|
668
|
-
return this.request("
|
|
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
|
+
});
|
|
669
1288
|
}
|
|
670
1289
|
/**
|
|
671
|
-
*
|
|
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
|
+
* ```
|
|
672
1309
|
*/
|
|
673
|
-
async
|
|
674
|
-
return this.request(
|
|
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
|
+
);
|
|
675
1318
|
}
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
// src/client.ts
|
|
1322
|
+
var ZendFiClient = class {
|
|
1323
|
+
config;
|
|
1324
|
+
interceptors;
|
|
1325
|
+
// ============================================
|
|
1326
|
+
// Agentic Intent Protocol APIs
|
|
1327
|
+
// ============================================
|
|
676
1328
|
/**
|
|
677
|
-
*
|
|
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
|
+
* ```
|
|
678
1347
|
*/
|
|
679
|
-
|
|
680
|
-
return this.request("POST", `/api/v1/invoices/${invoiceId}/send`);
|
|
681
|
-
}
|
|
1348
|
+
agent;
|
|
682
1349
|
/**
|
|
683
|
-
*
|
|
1350
|
+
* Payment Intents API - Two-phase payment flow
|
|
684
1351
|
*
|
|
685
|
-
* @
|
|
686
|
-
*
|
|
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
|
|
687
1367
|
*
|
|
688
1368
|
* @example
|
|
689
1369
|
* ```typescript
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
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,
|
|
694
1393
|
* });
|
|
695
1394
|
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
* }
|
|
1395
|
+
* // Check status
|
|
1396
|
+
* const status = await zendfi.autonomy.getStatus(sessionKeyId);
|
|
699
1397
|
* ```
|
|
700
1398
|
*/
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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");
|
|
734
1441
|
}
|
|
735
|
-
return false;
|
|
736
1442
|
}
|
|
737
1443
|
}
|
|
738
1444
|
/**
|
|
739
|
-
*
|
|
740
|
-
|
|
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) {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
let payloadString;
|
|
1744
|
+
let parsedPayload = null;
|
|
1745
|
+
if (typeof request.payload === "string") {
|
|
1746
|
+
payloadString = request.payload;
|
|
1747
|
+
try {
|
|
1748
|
+
parsedPayload = JSON.parse(payloadString);
|
|
1749
|
+
} catch (e) {
|
|
1750
|
+
return false;
|
|
1751
|
+
}
|
|
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") {
|
|
2050
|
+
try {
|
|
2051
|
+
components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2052
|
+
} catch {
|
|
2053
|
+
components.timezone = "unknown";
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
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");
|
|
2067
|
+
}
|
|
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");
|
|
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();
|
|
2472
|
+
return false;
|
|
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;
|
|
2594
|
+
}
|
|
2595
|
+
/**
|
|
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
|
+
* ```
|
|
2613
|
+
*/
|
|
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);
|
|
2627
|
+
}
|
|
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
|
|
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
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
/**
|
|
2655
|
+
* Load an existing session key from backend
|
|
2656
|
+
* Requires PIN to decrypt
|
|
2657
|
+
*/
|
|
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
|
|
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
|
+
);
|
|
2672
|
+
}
|
|
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;
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
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
|
+
* ```
|
|
2722
|
+
*/
|
|
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
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
if (!paymentResponse.unsigned_transaction) {
|
|
2751
|
+
throw new Error("Backend did not return unsigned transaction");
|
|
2752
|
+
}
|
|
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
|
+
};
|
|
2770
|
+
}
|
|
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");
|
|
2811
|
+
}
|
|
2812
|
+
await this.request("POST", "/api/v1/ai/session-keys/revoke", {
|
|
2813
|
+
session_key_id: keyId
|
|
2814
|
+
});
|
|
2815
|
+
if (keyId === this.sessionKeyId) {
|
|
2816
|
+
this.sessionKey = null;
|
|
2817
|
+
this.sessionKeyId = null;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
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");
|
|
2840
|
+
}
|
|
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();
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Check if keypair is cached (auto-signing enabled)
|
|
741
2865
|
*/
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
return (0, import_crypto.createHmac)("sha256", secret).update(payload, "utf8").digest("hex");
|
|
745
|
-
}
|
|
746
|
-
throw new Error(
|
|
747
|
-
"Webhook verification in browser is not supported. Use this method in your backend/server environment."
|
|
748
|
-
);
|
|
2866
|
+
isCached() {
|
|
2867
|
+
return this.sessionKey?.isCached() || false;
|
|
749
2868
|
}
|
|
750
2869
|
/**
|
|
751
|
-
*
|
|
2870
|
+
* Get time remaining until cache expires (in milliseconds)
|
|
752
2871
|
*/
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
return false;
|
|
756
|
-
}
|
|
757
|
-
if (typeof process !== "undefined" && process.versions?.node) {
|
|
758
|
-
try {
|
|
759
|
-
const bufferA = Buffer.from(a, "utf8");
|
|
760
|
-
const bufferB = Buffer.from(b, "utf8");
|
|
761
|
-
return (0, import_crypto.timingSafeEqual)(bufferA, bufferB);
|
|
762
|
-
} catch {
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
let result = 0;
|
|
766
|
-
for (let i = 0; i < a.length; i++) {
|
|
767
|
-
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
768
|
-
}
|
|
769
|
-
return result === 0;
|
|
2872
|
+
getCacheTimeRemaining() {
|
|
2873
|
+
return this.sessionKey?.getCacheTimeRemaining() || 0;
|
|
770
2874
|
}
|
|
771
2875
|
/**
|
|
772
|
-
*
|
|
2876
|
+
* Extend cache expiry time
|
|
2877
|
+
* Useful to keep session active during user activity
|
|
773
2878
|
*/
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const startTime = Date.now();
|
|
778
|
-
try {
|
|
779
|
-
const url = `${this.config.baseURL}${endpoint}`;
|
|
780
|
-
const headers = {
|
|
781
|
-
"Content-Type": "application/json",
|
|
782
|
-
Authorization: `Bearer ${this.config.apiKey}`
|
|
783
|
-
};
|
|
784
|
-
if (idempotencyKey) {
|
|
785
|
-
headers["Idempotency-Key"] = idempotencyKey;
|
|
786
|
-
}
|
|
787
|
-
let requestConfig = {
|
|
788
|
-
method,
|
|
789
|
-
url,
|
|
790
|
-
headers,
|
|
791
|
-
body: data
|
|
792
|
-
};
|
|
793
|
-
if (this.interceptors.request.has()) {
|
|
794
|
-
requestConfig = await this.interceptors.request.execute(requestConfig);
|
|
795
|
-
}
|
|
796
|
-
if (this.config.debug) {
|
|
797
|
-
console.log(`[ZendFi] ${method} ${endpoint}`);
|
|
798
|
-
if (data) {
|
|
799
|
-
console.log("[ZendFi] Request:", JSON.stringify(data, null, 2));
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
const controller = new AbortController();
|
|
803
|
-
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
804
|
-
const response = await (0, import_cross_fetch.default)(requestConfig.url, {
|
|
805
|
-
method: requestConfig.method,
|
|
806
|
-
headers: requestConfig.headers,
|
|
807
|
-
body: requestConfig.body ? JSON.stringify(requestConfig.body) : void 0,
|
|
808
|
-
signal: controller.signal
|
|
809
|
-
});
|
|
810
|
-
clearTimeout(timeoutId);
|
|
811
|
-
let body;
|
|
812
|
-
try {
|
|
813
|
-
body = await response.json();
|
|
814
|
-
} catch {
|
|
815
|
-
body = null;
|
|
816
|
-
}
|
|
817
|
-
const duration = Date.now() - startTime;
|
|
818
|
-
if (!response.ok) {
|
|
819
|
-
const error = createZendFiError(response.status, body);
|
|
820
|
-
if (this.config.debug) {
|
|
821
|
-
console.error(`[ZendFi] \u274C ${response.status} ${response.statusText} (${duration}ms)`);
|
|
822
|
-
console.error(`[ZendFi] Error:`, error.toString());
|
|
823
|
-
}
|
|
824
|
-
if (response.status >= 500 && attempt < this.config.retries) {
|
|
825
|
-
const delay = Math.pow(2, attempt) * 1e3;
|
|
826
|
-
if (this.config.debug) {
|
|
827
|
-
console.log(`[ZendFi] Retrying in ${delay}ms... (attempt ${attempt + 1}/${this.config.retries})`);
|
|
828
|
-
}
|
|
829
|
-
await sleep(delay);
|
|
830
|
-
return this.request(method, endpoint, data, {
|
|
831
|
-
idempotencyKey,
|
|
832
|
-
attempt: attempt + 1
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
if (this.interceptors.error.has()) {
|
|
836
|
-
const interceptedError = await this.interceptors.error.execute(error);
|
|
837
|
-
throw interceptedError;
|
|
838
|
-
}
|
|
839
|
-
throw error;
|
|
840
|
-
}
|
|
841
|
-
if (this.config.debug) {
|
|
842
|
-
console.log(`[ZendFi] \u2713 ${response.status} ${response.statusText} (${duration}ms)`);
|
|
843
|
-
if (body) {
|
|
844
|
-
console.log("[ZendFi] Response:", JSON.stringify(body, null, 2));
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
const headersObj = {};
|
|
848
|
-
response.headers.forEach((value, key) => {
|
|
849
|
-
headersObj[key] = value;
|
|
850
|
-
});
|
|
851
|
-
let responseData = {
|
|
852
|
-
status: response.status,
|
|
853
|
-
statusText: response.statusText,
|
|
854
|
-
headers: headersObj,
|
|
855
|
-
data: body,
|
|
856
|
-
config: requestConfig
|
|
857
|
-
};
|
|
858
|
-
if (this.interceptors.response.has()) {
|
|
859
|
-
responseData = await this.interceptors.response.execute(responseData);
|
|
860
|
-
}
|
|
861
|
-
return responseData.data;
|
|
862
|
-
} catch (error) {
|
|
863
|
-
if (error.name === "AbortError") {
|
|
864
|
-
const timeoutError = createZendFiError(0, {}, `Request timeout after ${this.config.timeout}ms`);
|
|
865
|
-
if (this.config.debug) {
|
|
866
|
-
console.error(`[ZendFi] \u274C Timeout (${this.config.timeout}ms)`);
|
|
867
|
-
}
|
|
868
|
-
throw timeoutError;
|
|
869
|
-
}
|
|
870
|
-
if (attempt < this.config.retries && (error.message?.includes("fetch") || error.message?.includes("network"))) {
|
|
871
|
-
const delay = Math.pow(2, attempt) * 1e3;
|
|
872
|
-
if (this.config.debug) {
|
|
873
|
-
console.log(`[ZendFi] Network error, retrying in ${delay}ms... (attempt ${attempt + 1}/${this.config.retries})`);
|
|
874
|
-
}
|
|
875
|
-
await sleep(delay);
|
|
876
|
-
return this.request(method, endpoint, data, {
|
|
877
|
-
idempotencyKey,
|
|
878
|
-
attempt: attempt + 1
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
if (isZendFiError(error)) {
|
|
882
|
-
throw error;
|
|
883
|
-
}
|
|
884
|
-
const wrappedError = createZendFiError(0, {}, error.message || "An unknown error occurred");
|
|
885
|
-
if (this.config.debug) {
|
|
886
|
-
console.error(`[ZendFi] \u274C Unexpected error:`, error);
|
|
887
|
-
}
|
|
888
|
-
throw wrappedError;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
};
|
|
892
|
-
var zendfi = (() => {
|
|
893
|
-
try {
|
|
894
|
-
return new ZendFiClient();
|
|
895
|
-
} catch (error) {
|
|
896
|
-
if (process.env.NODE_ENV === "test" || !process.env.ZENDFI_API_KEY) {
|
|
897
|
-
return new Proxy({}, {
|
|
898
|
-
get() {
|
|
899
|
-
throw new Error(
|
|
900
|
-
'ZendFi singleton not initialized. Set ZENDFI_API_KEY environment variable or create a custom instance: new ZendFiClient({ apiKey: "..." })'
|
|
901
|
-
);
|
|
902
|
-
}
|
|
903
|
-
});
|
|
2879
|
+
extendCache(additionalTTL) {
|
|
2880
|
+
if (!this.sessionKey) {
|
|
2881
|
+
throw new Error("No session key loaded");
|
|
904
2882
|
}
|
|
905
|
-
|
|
2883
|
+
this.sessionKey.extendCache(additionalTTL);
|
|
906
2884
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
async
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
if (!signature) {
|
|
915
|
-
return null;
|
|
916
|
-
}
|
|
917
|
-
const webhookSecret = secret || process.env.ZENDFI_WEBHOOK_SECRET;
|
|
918
|
-
if (!webhookSecret) {
|
|
919
|
-
throw new Error("ZENDFI_WEBHOOK_SECRET not configured");
|
|
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");
|
|
920
2892
|
}
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
signature,
|
|
924
|
-
secret: webhookSecret
|
|
2893
|
+
return await this.request("POST", "/api/v1/ai/session-keys/status", {
|
|
2894
|
+
session_key_id: keyId
|
|
925
2895
|
});
|
|
926
|
-
if (!isValid) {
|
|
927
|
-
return null;
|
|
928
|
-
}
|
|
929
|
-
return JSON.parse(payload);
|
|
930
|
-
} catch {
|
|
931
|
-
return null;
|
|
932
2896
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
signature,
|
|
948
|
-
secret: webhookSecret
|
|
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
|
|
949
2911
|
});
|
|
950
|
-
if (!
|
|
951
|
-
|
|
2912
|
+
if (!response.ok) {
|
|
2913
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
2914
|
+
throw new Error(`API Error: ${error.error || response.statusText}`);
|
|
952
2915
|
}
|
|
953
|
-
return
|
|
954
|
-
} catch {
|
|
955
|
-
return null;
|
|
2916
|
+
return await response.json();
|
|
956
2917
|
}
|
|
957
|
-
}
|
|
958
|
-
function verifyWebhookSignature(payload, signature, secret) {
|
|
959
|
-
return zendfi.verifyWebhook({
|
|
960
|
-
payload,
|
|
961
|
-
signature,
|
|
962
|
-
secret
|
|
963
|
-
});
|
|
964
|
-
}
|
|
2918
|
+
};
|
|
965
2919
|
|
|
966
2920
|
// src/webhook-handler.ts
|
|
967
2921
|
var import_crypto2 = require("crypto");
|
|
@@ -1103,23 +3057,197 @@ async function processWebhook(a, b, c) {
|
|
|
1103
3057
|
};
|
|
1104
3058
|
}
|
|
1105
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
|
+
}
|
|
1106
3206
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1107
3207
|
0 && (module.exports = {
|
|
3208
|
+
AgentAPI,
|
|
1108
3209
|
ApiError,
|
|
1109
3210
|
AuthenticationError,
|
|
3211
|
+
AutonomyAPI,
|
|
1110
3212
|
ConfigLoader,
|
|
3213
|
+
DeviceBoundSessionKey,
|
|
3214
|
+
DeviceFingerprintGenerator,
|
|
1111
3215
|
ERROR_CODES,
|
|
1112
3216
|
InterceptorManager,
|
|
3217
|
+
LitCryptoSigner,
|
|
1113
3218
|
NetworkError,
|
|
1114
3219
|
PaymentError,
|
|
3220
|
+
PaymentIntentsAPI,
|
|
3221
|
+
PricingAPI,
|
|
1115
3222
|
RateLimitError,
|
|
3223
|
+
RateLimiter,
|
|
3224
|
+
RecoveryQRGenerator,
|
|
3225
|
+
SPENDING_LIMIT_ACTION_CID,
|
|
3226
|
+
SessionKeyCrypto,
|
|
3227
|
+
SmartPaymentsAPI,
|
|
1116
3228
|
ValidationError,
|
|
1117
3229
|
WebhookError,
|
|
1118
3230
|
ZendFiClient,
|
|
1119
3231
|
ZendFiError,
|
|
3232
|
+
ZendFiSessionKeyManager,
|
|
3233
|
+
asAgentKeyId,
|
|
3234
|
+
asEscrowId,
|
|
3235
|
+
asInstallmentPlanId,
|
|
3236
|
+
asIntentId,
|
|
3237
|
+
asInvoiceId,
|
|
3238
|
+
asMerchantId,
|
|
3239
|
+
asPaymentId,
|
|
3240
|
+
asPaymentLinkCode,
|
|
3241
|
+
asSessionId,
|
|
3242
|
+
asSubscriptionId,
|
|
1120
3243
|
createZendFiError,
|
|
3244
|
+
decodeSignatureFromLit,
|
|
3245
|
+
encodeTransactionForLit,
|
|
3246
|
+
generateIdempotencyKey,
|
|
1121
3247
|
isZendFiError,
|
|
1122
3248
|
processWebhook,
|
|
3249
|
+
requiresLitSigning,
|
|
3250
|
+
sleep,
|
|
1123
3251
|
verifyExpressWebhook,
|
|
1124
3252
|
verifyNextWebhook,
|
|
1125
3253
|
verifyWebhookSignature,
|