@zendfi/sdk 0.5.8 → 0.7.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 +220 -12
- package/dist/cache-T5YPC7OK.mjs +9 -0
- package/dist/{chunk-YFOBPGQE.mjs → chunk-3ACJUM6V.mjs} +0 -8
- package/dist/chunk-5O5NAX65.mjs +366 -0
- package/dist/chunk-XERHBDUK.mjs +587 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/device-bound-crypto-VX7SFVHT.mjs +13 -0
- package/dist/express.d.mts +1 -1
- package/dist/express.d.ts +1 -1
- package/dist/express.mjs +2 -1
- package/dist/index.d.mts +1267 -3
- package/dist/index.d.ts +1267 -3
- package/dist/index.js +3765 -516
- package/dist/index.mjs +2783 -516
- package/dist/nextjs.d.mts +1 -1
- package/dist/nextjs.d.ts +1 -1
- package/dist/nextjs.mjs +2 -1
- package/dist/{webhook-handler-D5CigE9G.d.mts → webhook-handler-D5INiR-l.d.mts} +21 -1
- package/dist/{webhook-handler-D5CigE9G.d.ts → webhook-handler-D5INiR-l.d.ts} +21 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -26,11 +26,11 @@ console.log(payment.payment_url); // Send customer here
|
|
|
26
26
|
|
|
27
27
|
| Feature | Stripe | PayPal | **ZendFi** |
|
|
28
28
|
|---------|--------|--------|------------|
|
|
29
|
-
| **Fees** | 2.9% + $0.30 | 3.5% + $0.49 | **0.6% flat**
|
|
30
|
-
| **Settlement** | 7 days | 3-5 days | **Instant**
|
|
31
|
-
| **Crypto Native** | Via 3rd party | Via 3rd party | **Built-in**
|
|
32
|
-
| **AI Agent Ready** |
|
|
33
|
-
| **Setup Time** | 30 min | 30 min | **5 min**
|
|
29
|
+
| **Fees** | 2.9% + $0.30 | 3.5% + $0.49 | **0.6% flat** |
|
|
30
|
+
| **Settlement** | 7 days | 3-5 days | **Instant** |
|
|
31
|
+
| **Crypto Native** | Via 3rd party | Via 3rd party | **Built-in** |
|
|
32
|
+
| **AI Agent Ready** | ACP | NO | **Native** |
|
|
33
|
+
| **Setup Time** | 30 min | 30 min | **5 min** |
|
|
34
34
|
|
|
35
35
|
**Save 81% on fees.** Get paid instantly. Scale to AI when ready.
|
|
36
36
|
|
|
@@ -38,20 +38,21 @@ console.log(payment.payment_url); // Send customer here
|
|
|
38
38
|
|
|
39
39
|
## Features
|
|
40
40
|
|
|
41
|
-
###
|
|
41
|
+
### **Core Payments** (Start Here)
|
|
42
|
+
- **Embedded Checkout** — Drop-in checkout component for your website/app
|
|
42
43
|
- **Simple Payments** — QR codes, payment links, instant settlements
|
|
43
44
|
- **Payment Links** — Reusable checkout pages for social/email
|
|
44
45
|
- **Webhooks** — Real-time notifications with auto-verification
|
|
45
46
|
- **Test Mode** — Free devnet testing with no real money
|
|
46
47
|
- **Type-Safe** — Full TypeScript support with auto-completion
|
|
47
48
|
|
|
48
|
-
###
|
|
49
|
+
### **Scale Up** (When You Grow)
|
|
49
50
|
- **Subscriptions** — Recurring billing with trials
|
|
50
51
|
- **Installments** — Buy now, pay later flows
|
|
51
52
|
- **Invoices** — Professional invoicing with email
|
|
52
53
|
- **Payment Splits** — Revenue sharing for marketplaces
|
|
53
54
|
|
|
54
|
-
###
|
|
55
|
+
### **AI-Ready** (Optional Advanced)
|
|
55
56
|
- **Agent Keys** — Scoped API keys for AI with spending limits
|
|
56
57
|
- **Session Keys** — Pre-funded wallets for autonomous payments
|
|
57
58
|
- **Payment Intents** — Two-phase commit for reliable checkout
|
|
@@ -125,6 +126,66 @@ console.log(payment.payment_url);
|
|
|
125
126
|
|
|
126
127
|
---
|
|
127
128
|
|
|
129
|
+
## Embedded Checkout
|
|
130
|
+
|
|
131
|
+
Skip redirects entirely—embed the checkout directly into your website or app. Perfect for seamless user experiences.
|
|
132
|
+
|
|
133
|
+
### Quick Example
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { ZendFiEmbeddedCheckout } from '@zendfi/sdk';
|
|
137
|
+
|
|
138
|
+
const checkout = new ZendFiEmbeddedCheckout({
|
|
139
|
+
linkCode: 'your-payment-link-code',
|
|
140
|
+
containerId: 'checkout-container',
|
|
141
|
+
mode: 'test',
|
|
142
|
+
|
|
143
|
+
onSuccess: (payment) => {
|
|
144
|
+
console.log('Payment successful!', payment.transactionSignature);
|
|
145
|
+
// Redirect to success page or show confirmation
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
onError: (error) => {
|
|
149
|
+
console.error('Payment failed:', error.message);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Mount the checkout
|
|
154
|
+
await checkout.mount();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### HTML Setup
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<div id="checkout-container"></div>
|
|
161
|
+
<script type="module">
|
|
162
|
+
import { ZendFiEmbeddedCheckout } from '@zendfi/sdk';
|
|
163
|
+
// ... (setup code above)
|
|
164
|
+
</script>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Features
|
|
168
|
+
|
|
169
|
+
- **Drop-in Integration** — Works with React, Vue, Next.js, or vanilla JS
|
|
170
|
+
- **QR Code Generation** — Automatic mobile wallet support
|
|
171
|
+
- **Wallet Connect** — Phantom, Solflare, Backpack support
|
|
172
|
+
- **Real-time Updates** — Live payment confirmation polling
|
|
173
|
+
- **Gasless Transactions** — Optional backend-signed payments
|
|
174
|
+
- **Customizable Theme** — Match your brand colors & styles
|
|
175
|
+
- **TypeScript First** — Full type safety and autocomplete
|
|
176
|
+
|
|
177
|
+
### Complete Documentation
|
|
178
|
+
|
|
179
|
+
For comprehensive guides, React examples, theming, and advanced usage:
|
|
180
|
+
|
|
181
|
+
- **Quick Start:** [`EMBEDDED_CHECKOUT_QUICKSTART.md`](./EMBEDDED_CHECKOUT_QUICKSTART.md)
|
|
182
|
+
- **Full Guide:** [`EMBEDDED_CHECKOUT.md`](./EMBEDDED_CHECKOUT.md)
|
|
183
|
+
- **Implementation Details:** [`EMBEDDED_CHECKOUT_IMPLEMENTATION.md`](./EMBEDDED_CHECKOUT_IMPLEMENTATION.md)
|
|
184
|
+
- **React Example:** [`examples/embedded-checkout-react.tsx`](./examples/embedded-checkout-react.tsx)
|
|
185
|
+
- **Vanilla JS Example:** [`examples/embedded-checkout-vanilla.html`](./examples/embedded-checkout-vanilla.html)
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
128
189
|
## API Key Modes
|
|
129
190
|
|
|
130
191
|
ZendFi uses **smart API keys** that automatically route to the correct network:
|
|
@@ -172,7 +233,7 @@ This covers:
|
|
|
172
233
|
|
|
173
234
|
---
|
|
174
235
|
|
|
175
|
-
##
|
|
236
|
+
## AI-Ready Features (Optional Advanced)
|
|
176
237
|
|
|
177
238
|
Building AI agents? ZendFi has native support for autonomous payments with cryptographic security and spending limits.
|
|
178
239
|
|
|
@@ -226,12 +287,15 @@ const payment = await zendfi.agent.pay({
|
|
|
226
287
|
|
|
227
288
|
---
|
|
228
289
|
|
|
229
|
-
##
|
|
290
|
+
## Core API Reference
|
|
230
291
|
|
|
231
292
|
### Namespaced APIs
|
|
232
293
|
|
|
233
294
|
```typescript
|
|
234
|
-
import { zendfi } from '@zendfi/sdk';
|
|
295
|
+
import { zendfi, ZendFiEmbeddedCheckout } from '@zendfi/sdk';
|
|
296
|
+
|
|
297
|
+
// Embedded Checkout (New!)
|
|
298
|
+
const checkout = new ZendFiEmbeddedCheckout({...});
|
|
235
299
|
|
|
236
300
|
// Traditional Payments (Most Common)
|
|
237
301
|
zendfi.createPayment(...)
|
|
@@ -399,6 +463,111 @@ const suggestion = await zendfi.pricing.getSuggestion({
|
|
|
399
463
|
**Supported Countries (27+):**
|
|
400
464
|
Argentina, Australia, Brazil, Canada, China, Colombia, Egypt, France, Germany, Ghana, Hong Kong, Hungary, India, Indonesia, Israel, Japan, Kenya, Mexico, Nigeria, Philippines, Poland, South Africa, Thailand, Turkey, Ukraine, United Kingdom, Vietnam, and more.
|
|
401
465
|
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
## Optional Helper Utilities
|
|
469
|
+
|
|
470
|
+
Production-ready utilities to simplify common integration patterns. All helpers are **optional**, **tree-shakeable**, and **zero-config**.
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
import {
|
|
474
|
+
SessionKeyCache,
|
|
475
|
+
WalletConnector,
|
|
476
|
+
TransactionPoller,
|
|
477
|
+
DevTools
|
|
478
|
+
} from '@zendfi/sdk/helpers';
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Why Use Helpers?
|
|
482
|
+
|
|
483
|
+
- **Optional**: Import only what you need
|
|
484
|
+
- **Tree-shakeable**: Unused code eliminated by bundlers
|
|
485
|
+
- **Zero config**: Sensible defaults, works out of the box
|
|
486
|
+
- **Pluggable**: Bring your own storage/AI/PIN providers
|
|
487
|
+
- **Production-ready**: Full TypeScript types, error handling
|
|
488
|
+
|
|
489
|
+
### Available Helpers
|
|
490
|
+
|
|
491
|
+
| Helper | Purpose | Use Case |
|
|
492
|
+
|--------|---------|----------|
|
|
493
|
+
| `SessionKeyCache` | Cache encrypted session keys | Avoid re-prompting for PIN |
|
|
494
|
+
| `WalletConnector` | Detect & connect Solana wallets | Phantom, Solflare, Backpack |
|
|
495
|
+
| `PaymentIntentParser` | Parse natural language to payments | AI chat interfaces |
|
|
496
|
+
| `PINValidator` | Validate PIN strength | Device-bound security |
|
|
497
|
+
| `TransactionPoller` | Poll for confirmations | Wait for on-chain finality |
|
|
498
|
+
| `RetryStrategy` | Exponential backoff retries | Handle network failures |
|
|
499
|
+
| `SessionKeyLifecycle` | High-level session key manager | One-liner setup |
|
|
500
|
+
| `DevTools` | Debug mode & test utilities | Development & testing |
|
|
501
|
+
|
|
502
|
+
### Session Key Cache
|
|
503
|
+
|
|
504
|
+
Cache encrypted session keys to avoid re-prompting users for their PIN:
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
import { SessionKeyCache, QuickCaches } from '@zendfi/sdk/helpers';
|
|
508
|
+
|
|
509
|
+
// Use presets
|
|
510
|
+
const cache = QuickCaches.persistent(); // 1 hour localStorage
|
|
511
|
+
|
|
512
|
+
// Use with device-bound session keys
|
|
513
|
+
const keypair = await cache.getCached(
|
|
514
|
+
sessionKeyId,
|
|
515
|
+
async () => {
|
|
516
|
+
const pin = await promptUserForPIN();
|
|
517
|
+
return await decryptKeypair(pin);
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### Wallet Connector
|
|
523
|
+
|
|
524
|
+
Auto-detect and connect to Solana wallets:
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
import { WalletConnector } from '@zendfi/sdk/helpers';
|
|
528
|
+
|
|
529
|
+
const wallet = await WalletConnector.detectAndConnect();
|
|
530
|
+
console.log(wallet.address);
|
|
531
|
+
|
|
532
|
+
const signedTx = await wallet.signTransaction(transaction);
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Transaction Polling
|
|
536
|
+
|
|
537
|
+
Wait for confirmations with exponential backoff:
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
import { TransactionPoller } from '@zendfi/sdk/helpers';
|
|
541
|
+
|
|
542
|
+
const poller = new TransactionPoller({ connection: rpcConnection });
|
|
543
|
+
const result = await poller.waitForConfirmation(signature);
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Session Key Lifecycle
|
|
547
|
+
|
|
548
|
+
High-level wrapper for complete session key management:
|
|
549
|
+
|
|
550
|
+
```typescript
|
|
551
|
+
import { SessionKeyLifecycle } from '@zendfi/sdk/helpers';
|
|
552
|
+
|
|
553
|
+
const lifecycle = new SessionKeyLifecycle(zendfi, {
|
|
554
|
+
cache: QuickCaches.persistent(),
|
|
555
|
+
autoCleanup: true,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
await lifecycle.createAndFund({
|
|
559
|
+
userWallet: userAddress,
|
|
560
|
+
agentId: 'my-agent',
|
|
561
|
+
limitUsdc: 100,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
await lifecycle.pay(5.00, 'Coffee');
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Full documentation:** See [Helper Utilities Guide](https://docs.zendfi.tech/helpers) for complete API reference and examples.
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
402
571
|
### Autonomous Delegation
|
|
403
572
|
|
|
404
573
|
Enable agents to make payments without per-transaction approval:
|
|
@@ -709,7 +878,7 @@ await zendfi.cancelInstallmentPlan(plan.id);
|
|
|
709
878
|
|
|
710
879
|
---
|
|
711
880
|
|
|
712
|
-
###
|
|
881
|
+
### Invoices
|
|
713
882
|
|
|
714
883
|
Professional invoices with crypto payment options.
|
|
715
884
|
|
|
@@ -1097,6 +1266,45 @@ console.log('Status:', updated.status); // "Confirmed"
|
|
|
1097
1266
|
|
|
1098
1267
|
## Examples
|
|
1099
1268
|
|
|
1269
|
+
### Embedded Checkout Integration
|
|
1270
|
+
|
|
1271
|
+
```typescript
|
|
1272
|
+
import { ZendFiEmbeddedCheckout } from '@zendfi/sdk';
|
|
1273
|
+
|
|
1274
|
+
// React Component
|
|
1275
|
+
function CheckoutPage({ linkCode }) {
|
|
1276
|
+
const containerRef = useRef(null);
|
|
1277
|
+
|
|
1278
|
+
useEffect(() => {
|
|
1279
|
+
const checkout = new ZendFiEmbeddedCheckout({
|
|
1280
|
+
linkCode,
|
|
1281
|
+
containerId: 'checkout-container',
|
|
1282
|
+
mode: 'test',
|
|
1283
|
+
|
|
1284
|
+
onSuccess: (payment) => {
|
|
1285
|
+
// Payment successful - redirect or show confirmation
|
|
1286
|
+
router.push(`/success?payment=${payment.paymentId}`);
|
|
1287
|
+
},
|
|
1288
|
+
|
|
1289
|
+
onError: (error) => {
|
|
1290
|
+
// Handle errors
|
|
1291
|
+
setError(error.message);
|
|
1292
|
+
},
|
|
1293
|
+
|
|
1294
|
+
theme: {
|
|
1295
|
+
primaryColor: '#8b5cf6',
|
|
1296
|
+
borderRadius: '16px',
|
|
1297
|
+
},
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
checkout.mount();
|
|
1301
|
+
return () => checkout.unmount();
|
|
1302
|
+
}, [linkCode]);
|
|
1303
|
+
|
|
1304
|
+
return <div id="checkout-container" ref={containerRef} />;
|
|
1305
|
+
}
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1100
1308
|
### E-commerce Checkout
|
|
1101
1309
|
|
|
1102
1310
|
```typescript
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
-
}) : x)(function(x) {
|
|
4
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
1
|
// src/webhook-handler.ts
|
|
9
2
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
10
3
|
var processedWebhooks = /* @__PURE__ */ new Set();
|
|
@@ -147,6 +140,5 @@ async function processWebhook(a, b, c) {
|
|
|
147
140
|
}
|
|
148
141
|
|
|
149
142
|
export {
|
|
150
|
-
__require,
|
|
151
143
|
processWebhook
|
|
152
144
|
};
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// src/helpers/cache.ts
|
|
2
|
+
var SessionKeyCache = class {
|
|
3
|
+
memoryCache = /* @__PURE__ */ new Map();
|
|
4
|
+
config;
|
|
5
|
+
refreshTimers = /* @__PURE__ */ new Map();
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.config = {
|
|
8
|
+
storage: config.storage || "memory",
|
|
9
|
+
ttl: config.ttl || 30 * 60 * 1e3,
|
|
10
|
+
// 30 minutes default
|
|
11
|
+
autoRefresh: config.autoRefresh || false,
|
|
12
|
+
namespace: config.namespace || "zendfi_cache",
|
|
13
|
+
debug: config.debug || false
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Get cached keypair or decrypt and cache
|
|
18
|
+
*/
|
|
19
|
+
async getCached(sessionKeyId, decryptFn, options) {
|
|
20
|
+
this.log(`getCached: ${sessionKeyId}`);
|
|
21
|
+
const memoryCached = this.memoryCache.get(sessionKeyId);
|
|
22
|
+
if (memoryCached && Date.now() < memoryCached.expiry) {
|
|
23
|
+
this.log(`Memory cache HIT: ${sessionKeyId}`);
|
|
24
|
+
return memoryCached.keypair;
|
|
25
|
+
}
|
|
26
|
+
if (this.config.storage !== "memory") {
|
|
27
|
+
const persistentCached = await this.getFromStorage(sessionKeyId);
|
|
28
|
+
if (persistentCached && Date.now() < persistentCached.expiry) {
|
|
29
|
+
if (options?.deviceFingerprint && persistentCached.deviceFingerprint) {
|
|
30
|
+
if (options.deviceFingerprint !== persistentCached.deviceFingerprint) {
|
|
31
|
+
this.log(`Device fingerprint mismatch for ${sessionKeyId}`);
|
|
32
|
+
await this.invalidate(sessionKeyId);
|
|
33
|
+
return await this.decryptAndCache(sessionKeyId, decryptFn, options);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
this.log(`Persistent cache HIT: ${sessionKeyId}`);
|
|
37
|
+
this.memoryCache.set(sessionKeyId, persistentCached);
|
|
38
|
+
return persistentCached.keypair;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this.log(`Cache MISS: ${sessionKeyId}`);
|
|
42
|
+
return await this.decryptAndCache(sessionKeyId, decryptFn, options);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Decrypt keypair and cache it
|
|
46
|
+
*/
|
|
47
|
+
async decryptAndCache(sessionKeyId, decryptFn, options) {
|
|
48
|
+
const keypair = await decryptFn();
|
|
49
|
+
const expiry = Date.now() + this.config.ttl;
|
|
50
|
+
const cached = {
|
|
51
|
+
keypair,
|
|
52
|
+
expiry,
|
|
53
|
+
sessionKeyId,
|
|
54
|
+
deviceFingerprint: options?.deviceFingerprint
|
|
55
|
+
};
|
|
56
|
+
this.memoryCache.set(sessionKeyId, cached);
|
|
57
|
+
if (this.config.storage !== "memory") {
|
|
58
|
+
await this.setInStorage(sessionKeyId, cached);
|
|
59
|
+
}
|
|
60
|
+
if (this.config.autoRefresh) {
|
|
61
|
+
this.setupAutoRefresh(sessionKeyId, decryptFn, options);
|
|
62
|
+
}
|
|
63
|
+
this.log(`Cached: ${sessionKeyId}, expires in ${this.config.ttl}ms`);
|
|
64
|
+
return keypair;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Invalidate cached keypair
|
|
68
|
+
*/
|
|
69
|
+
async invalidate(sessionKeyId) {
|
|
70
|
+
this.log(`Invalidating: ${sessionKeyId}`);
|
|
71
|
+
this.memoryCache.delete(sessionKeyId);
|
|
72
|
+
const timer = this.refreshTimers.get(sessionKeyId);
|
|
73
|
+
if (timer) {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
this.refreshTimers.delete(sessionKeyId);
|
|
76
|
+
}
|
|
77
|
+
if (this.config.storage !== "memory") {
|
|
78
|
+
await this.removeFromStorage(sessionKeyId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Clear all cached keypairs
|
|
83
|
+
*/
|
|
84
|
+
async clear() {
|
|
85
|
+
this.log("Clearing all cache");
|
|
86
|
+
this.memoryCache.clear();
|
|
87
|
+
for (const timer of this.refreshTimers.values()) {
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
}
|
|
90
|
+
this.refreshTimers.clear();
|
|
91
|
+
if (this.config.storage !== "memory") {
|
|
92
|
+
await this.clearStorage();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Get cache statistics
|
|
97
|
+
*/
|
|
98
|
+
getStats() {
|
|
99
|
+
const entries = Array.from(this.memoryCache.entries()).map(([id, cached]) => ({
|
|
100
|
+
sessionKeyId: id,
|
|
101
|
+
expiresIn: Math.max(0, cached.expiry - Date.now())
|
|
102
|
+
}));
|
|
103
|
+
return { size: this.memoryCache.size, entries };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check if a session key is cached and valid
|
|
107
|
+
*/
|
|
108
|
+
isCached(sessionKeyId) {
|
|
109
|
+
const cached = this.memoryCache.get(sessionKeyId);
|
|
110
|
+
return cached ? Date.now() < cached.expiry : false;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Update TTL for a cached session key
|
|
114
|
+
*/
|
|
115
|
+
async extendTTL(sessionKeyId, additionalMs) {
|
|
116
|
+
const cached = this.memoryCache.get(sessionKeyId);
|
|
117
|
+
if (!cached) return false;
|
|
118
|
+
cached.expiry += additionalMs;
|
|
119
|
+
this.memoryCache.set(sessionKeyId, cached);
|
|
120
|
+
if (this.config.storage !== "memory") {
|
|
121
|
+
await this.setInStorage(sessionKeyId, cached);
|
|
122
|
+
}
|
|
123
|
+
this.log(`Extended TTL for ${sessionKeyId} by ${additionalMs}ms`);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
// ============================================
|
|
127
|
+
// Storage Backend Implementations
|
|
128
|
+
// ============================================
|
|
129
|
+
async getFromStorage(sessionKeyId) {
|
|
130
|
+
try {
|
|
131
|
+
const key = this.getStorageKey(sessionKeyId);
|
|
132
|
+
if (this.config.storage === "localStorage") {
|
|
133
|
+
const data = localStorage.getItem(key);
|
|
134
|
+
if (!data) return null;
|
|
135
|
+
const parsed = JSON.parse(data);
|
|
136
|
+
return {
|
|
137
|
+
...parsed,
|
|
138
|
+
keypair: this.deserializeKeypair(parsed.keypair)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (this.config.storage === "indexedDB") {
|
|
142
|
+
return await this.getFromIndexedDB(key);
|
|
143
|
+
}
|
|
144
|
+
if (typeof this.config.storage === "object") {
|
|
145
|
+
const data = await this.config.storage.get(key);
|
|
146
|
+
if (!data) return null;
|
|
147
|
+
const parsed = JSON.parse(data);
|
|
148
|
+
return {
|
|
149
|
+
...parsed,
|
|
150
|
+
keypair: this.deserializeKeypair(parsed.keypair)
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.log(`Error reading from storage: ${error}`);
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
async setInStorage(sessionKeyId, cached) {
|
|
159
|
+
try {
|
|
160
|
+
const key = this.getStorageKey(sessionKeyId);
|
|
161
|
+
const serialized = {
|
|
162
|
+
...cached,
|
|
163
|
+
keypair: this.serializeKeypair(cached.keypair)
|
|
164
|
+
};
|
|
165
|
+
if (this.config.storage === "localStorage") {
|
|
166
|
+
localStorage.setItem(key, JSON.stringify(serialized));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (this.config.storage === "indexedDB") {
|
|
170
|
+
await this.setInIndexedDB(key, serialized);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (typeof this.config.storage === "object") {
|
|
174
|
+
await this.config.storage.set(key, JSON.stringify(serialized));
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.log(`Error writing to storage: ${error}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async removeFromStorage(sessionKeyId) {
|
|
181
|
+
try {
|
|
182
|
+
const key = this.getStorageKey(sessionKeyId);
|
|
183
|
+
if (this.config.storage === "localStorage") {
|
|
184
|
+
localStorage.removeItem(key);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (this.config.storage === "indexedDB") {
|
|
188
|
+
await this.removeFromIndexedDB(key);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (typeof this.config.storage === "object") {
|
|
192
|
+
await this.config.storage.remove(key);
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
this.log(`Error removing from storage: ${error}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async clearStorage() {
|
|
199
|
+
try {
|
|
200
|
+
if (this.config.storage === "localStorage") {
|
|
201
|
+
const keysToRemove = [];
|
|
202
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
203
|
+
const key = localStorage.key(i);
|
|
204
|
+
if (key?.startsWith(this.config.namespace)) {
|
|
205
|
+
keysToRemove.push(key);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (this.config.storage === "indexedDB") {
|
|
212
|
+
await this.clearIndexedDB();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (typeof this.config.storage === "object") {
|
|
216
|
+
await this.config.storage.clear();
|
|
217
|
+
}
|
|
218
|
+
} catch (error) {
|
|
219
|
+
this.log(`Error clearing storage: ${error}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ============================================
|
|
223
|
+
// IndexedDB Helpers
|
|
224
|
+
// ============================================
|
|
225
|
+
async getFromIndexedDB(key) {
|
|
226
|
+
return new Promise((resolve) => {
|
|
227
|
+
const request = indexedDB.open(this.config.namespace, 1);
|
|
228
|
+
request.onerror = () => resolve(null);
|
|
229
|
+
request.onupgradeneeded = (event) => {
|
|
230
|
+
const db = event.target.result;
|
|
231
|
+
if (!db.objectStoreNames.contains("cache")) {
|
|
232
|
+
db.createObjectStore("cache");
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
request.onsuccess = (event) => {
|
|
236
|
+
const db = event.target.result;
|
|
237
|
+
const transaction = db.transaction(["cache"], "readonly");
|
|
238
|
+
const store = transaction.objectStore("cache");
|
|
239
|
+
const getRequest = store.get(key);
|
|
240
|
+
getRequest.onsuccess = () => {
|
|
241
|
+
resolve(getRequest.result || null);
|
|
242
|
+
};
|
|
243
|
+
getRequest.onerror = () => resolve(null);
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async setInIndexedDB(key, value) {
|
|
248
|
+
return new Promise((resolve, reject) => {
|
|
249
|
+
const request = indexedDB.open(this.config.namespace, 1);
|
|
250
|
+
request.onerror = () => reject(new Error("IndexedDB error"));
|
|
251
|
+
request.onupgradeneeded = (event) => {
|
|
252
|
+
const db = event.target.result;
|
|
253
|
+
if (!db.objectStoreNames.contains("cache")) {
|
|
254
|
+
db.createObjectStore("cache");
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
request.onsuccess = (event) => {
|
|
258
|
+
const db = event.target.result;
|
|
259
|
+
const transaction = db.transaction(["cache"], "readwrite");
|
|
260
|
+
const store = transaction.objectStore("cache");
|
|
261
|
+
store.put(value, key);
|
|
262
|
+
transaction.oncomplete = () => resolve();
|
|
263
|
+
transaction.onerror = () => reject(new Error("IndexedDB transaction error"));
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async removeFromIndexedDB(key) {
|
|
268
|
+
return new Promise((resolve) => {
|
|
269
|
+
const request = indexedDB.open(this.config.namespace, 1);
|
|
270
|
+
request.onsuccess = (event) => {
|
|
271
|
+
const db = event.target.result;
|
|
272
|
+
const transaction = db.transaction(["cache"], "readwrite");
|
|
273
|
+
const store = transaction.objectStore("cache");
|
|
274
|
+
store.delete(key);
|
|
275
|
+
transaction.oncomplete = () => resolve();
|
|
276
|
+
};
|
|
277
|
+
request.onerror = () => resolve();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async clearIndexedDB() {
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
const request = indexedDB.open(this.config.namespace, 1);
|
|
283
|
+
request.onsuccess = (event) => {
|
|
284
|
+
const db = event.target.result;
|
|
285
|
+
const transaction = db.transaction(["cache"], "readwrite");
|
|
286
|
+
const store = transaction.objectStore("cache");
|
|
287
|
+
store.clear();
|
|
288
|
+
transaction.oncomplete = () => resolve();
|
|
289
|
+
};
|
|
290
|
+
request.onerror = () => resolve();
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
// ============================================
|
|
294
|
+
// Serialization
|
|
295
|
+
// ============================================
|
|
296
|
+
serializeKeypair(keypair) {
|
|
297
|
+
if (keypair && typeof keypair === "object" && "secretKey" in keypair) {
|
|
298
|
+
return {
|
|
299
|
+
type: "solana",
|
|
300
|
+
secretKey: Array.from(keypair.secretKey)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (keypair instanceof Uint8Array) {
|
|
304
|
+
return {
|
|
305
|
+
type: "uint8array",
|
|
306
|
+
data: Array.from(keypair)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return keypair;
|
|
310
|
+
}
|
|
311
|
+
deserializeKeypair(data) {
|
|
312
|
+
if (!data || typeof data !== "object") return data;
|
|
313
|
+
if (data.type === "solana" && data.secretKey) {
|
|
314
|
+
return new Uint8Array(data.secretKey);
|
|
315
|
+
}
|
|
316
|
+
if (data.type === "uint8array" && data.data) {
|
|
317
|
+
return new Uint8Array(data.data);
|
|
318
|
+
}
|
|
319
|
+
return data;
|
|
320
|
+
}
|
|
321
|
+
// ============================================
|
|
322
|
+
// Auto-Refresh
|
|
323
|
+
// ============================================
|
|
324
|
+
setupAutoRefresh(sessionKeyId, decryptFn, options) {
|
|
325
|
+
const existingTimer = this.refreshTimers.get(sessionKeyId);
|
|
326
|
+
if (existingTimer) {
|
|
327
|
+
clearTimeout(existingTimer);
|
|
328
|
+
}
|
|
329
|
+
const refreshIn = Math.max(0, this.config.ttl - 5 * 60 * 1e3);
|
|
330
|
+
const timer = setTimeout(async () => {
|
|
331
|
+
this.log(`Auto-refreshing: ${sessionKeyId}`);
|
|
332
|
+
try {
|
|
333
|
+
await this.decryptAndCache(sessionKeyId, decryptFn, options);
|
|
334
|
+
} catch (error) {
|
|
335
|
+
this.log(`Auto-refresh failed: ${error}`);
|
|
336
|
+
}
|
|
337
|
+
}, refreshIn);
|
|
338
|
+
this.refreshTimers.set(sessionKeyId, timer);
|
|
339
|
+
}
|
|
340
|
+
// ============================================
|
|
341
|
+
// Utilities
|
|
342
|
+
// ============================================
|
|
343
|
+
getStorageKey(sessionKeyId) {
|
|
344
|
+
return `${this.config.namespace}:${sessionKeyId}`;
|
|
345
|
+
}
|
|
346
|
+
log(message) {
|
|
347
|
+
if (this.config.debug) {
|
|
348
|
+
console.log(`[SessionKeyCache] ${message}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
var QuickCaches = {
|
|
353
|
+
/** Memory-only cache (30 minutes) */
|
|
354
|
+
memory: () => new SessionKeyCache({ storage: "memory", ttl: 30 * 60 * 1e3 }),
|
|
355
|
+
/** Persistent cache (1 hour, survives reload) */
|
|
356
|
+
persistent: () => new SessionKeyCache({ storage: "localStorage", ttl: 60 * 60 * 1e3 }),
|
|
357
|
+
/** Long-term cache (24 hours, IndexedDB) */
|
|
358
|
+
longTerm: () => new SessionKeyCache({ storage: "indexedDB", ttl: 24 * 60 * 60 * 1e3, autoRefresh: true }),
|
|
359
|
+
/** Secure cache (5 minutes, memory-only) */
|
|
360
|
+
secure: () => new SessionKeyCache({ storage: "memory", ttl: 5 * 60 * 1e3 })
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export {
|
|
364
|
+
SessionKeyCache,
|
|
365
|
+
QuickCaches
|
|
366
|
+
};
|