@zendfi/sdk 0.5.7 → 0.6.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 +109 -4
- 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 +1062 -3
- package/dist/index.d.ts +1062 -3
- package/dist/index.js +3185 -609
- package/dist/index.mjs +2181 -586
- 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-DGBeCWT-.d.mts} +20 -0
- package/dist/{webhook-handler-D5CigE9G.d.ts → webhook-handler-DGBeCWT-.d.ts} +20 -0
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
8
11
|
var __export = (target, all) => {
|
|
9
12
|
for (var name in all)
|
|
10
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,31 +30,1015 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
30
|
));
|
|
28
31
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
32
|
|
|
33
|
+
// src/device-bound-crypto.ts
|
|
34
|
+
var device_bound_crypto_exports = {};
|
|
35
|
+
__export(device_bound_crypto_exports, {
|
|
36
|
+
DeviceBoundSessionKey: () => DeviceBoundSessionKey,
|
|
37
|
+
DeviceFingerprintGenerator: () => DeviceFingerprintGenerator,
|
|
38
|
+
RecoveryQRGenerator: () => RecoveryQRGenerator,
|
|
39
|
+
SessionKeyCrypto: () => SessionKeyCrypto
|
|
40
|
+
});
|
|
41
|
+
var import_web3, crypto2, DeviceFingerprintGenerator, SessionKeyCrypto, RecoveryQRGenerator, DeviceBoundSessionKey;
|
|
42
|
+
var init_device_bound_crypto = __esm({
|
|
43
|
+
"src/device-bound-crypto.ts"() {
|
|
44
|
+
"use strict";
|
|
45
|
+
import_web3 = require("@solana/web3.js");
|
|
46
|
+
crypto2 = __toESM(require("crypto"));
|
|
47
|
+
DeviceFingerprintGenerator = class {
|
|
48
|
+
/**
|
|
49
|
+
* Generate a unique device fingerprint
|
|
50
|
+
* Combines multiple browser attributes for uniqueness
|
|
51
|
+
*/
|
|
52
|
+
static async generate() {
|
|
53
|
+
const components = {};
|
|
54
|
+
try {
|
|
55
|
+
components.canvas = await this.getCanvasFingerprint();
|
|
56
|
+
components.webgl = await this.getWebGLFingerprint();
|
|
57
|
+
components.audio = await this.getAudioFingerprint();
|
|
58
|
+
components.screen = `${screen.width}x${screen.height}x${screen.colorDepth}`;
|
|
59
|
+
components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
60
|
+
components.languages = navigator.languages?.join(",") || navigator.language;
|
|
61
|
+
components.platform = navigator.platform;
|
|
62
|
+
components.hardwareConcurrency = navigator.hardwareConcurrency?.toString() || "unknown";
|
|
63
|
+
const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|");
|
|
64
|
+
const fingerprint = await this.sha256(combined);
|
|
65
|
+
return {
|
|
66
|
+
fingerprint,
|
|
67
|
+
generatedAt: Date.now(),
|
|
68
|
+
components
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.warn("Device fingerprinting failed, using fallback", error);
|
|
72
|
+
return this.generateFallbackFingerprint();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Graceful fallback fingerprint generation
|
|
77
|
+
* Works in headless browsers, SSR, and restricted environments
|
|
78
|
+
*/
|
|
79
|
+
static async generateFallbackFingerprint() {
|
|
80
|
+
const components = {};
|
|
81
|
+
try {
|
|
82
|
+
if (typeof navigator !== "undefined") {
|
|
83
|
+
components.platform = navigator.platform || "unknown";
|
|
84
|
+
components.languages = navigator.languages?.join(",") || navigator.language || "unknown";
|
|
85
|
+
components.hardwareConcurrency = navigator.hardwareConcurrency?.toString() || "unknown";
|
|
86
|
+
}
|
|
87
|
+
if (typeof screen !== "undefined") {
|
|
88
|
+
components.screen = `${screen.width || 0}x${screen.height || 0}x${screen.colorDepth || 0}`;
|
|
89
|
+
}
|
|
90
|
+
if (typeof Intl !== "undefined") {
|
|
91
|
+
try {
|
|
92
|
+
components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
93
|
+
} catch {
|
|
94
|
+
components.timezone = "unknown";
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
components.platform = "fallback";
|
|
99
|
+
}
|
|
100
|
+
let randomEntropy = "";
|
|
101
|
+
try {
|
|
102
|
+
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
|
103
|
+
const arr = new Uint8Array(16);
|
|
104
|
+
window.crypto.getRandomValues(arr);
|
|
105
|
+
randomEntropy = Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
106
|
+
} else if (typeof crypto2 !== "undefined" && crypto2.randomBytes) {
|
|
107
|
+
randomEntropy = crypto2.randomBytes(16).toString("hex");
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
randomEntropy = Date.now().toString(36) + Math.random().toString(36);
|
|
111
|
+
}
|
|
112
|
+
const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|") + "|entropy:" + randomEntropy;
|
|
113
|
+
const fingerprint = await this.sha256(combined);
|
|
114
|
+
return {
|
|
115
|
+
fingerprint,
|
|
116
|
+
generatedAt: Date.now(),
|
|
117
|
+
components
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
static async getCanvasFingerprint() {
|
|
121
|
+
const canvas = document.createElement("canvas");
|
|
122
|
+
const ctx = canvas.getContext("2d");
|
|
123
|
+
if (!ctx) return "no-canvas";
|
|
124
|
+
canvas.width = 200;
|
|
125
|
+
canvas.height = 50;
|
|
126
|
+
ctx.textBaseline = "top";
|
|
127
|
+
ctx.font = '14px "Arial"';
|
|
128
|
+
ctx.fillStyle = "#f60";
|
|
129
|
+
ctx.fillRect(0, 0, 100, 50);
|
|
130
|
+
ctx.fillStyle = "#069";
|
|
131
|
+
ctx.fillText("ZendFi \u{1F510}", 2, 2);
|
|
132
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
133
|
+
ctx.fillText("Device-Bound", 4, 17);
|
|
134
|
+
return canvas.toDataURL();
|
|
135
|
+
}
|
|
136
|
+
static async getWebGLFingerprint() {
|
|
137
|
+
const canvas = document.createElement("canvas");
|
|
138
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
139
|
+
if (!gl) return "no-webgl";
|
|
140
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
141
|
+
if (!debugInfo) return "no-debug-info";
|
|
142
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
143
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
144
|
+
return `${vendor}|${renderer}`;
|
|
145
|
+
}
|
|
146
|
+
static async getAudioFingerprint() {
|
|
147
|
+
try {
|
|
148
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
149
|
+
if (!AudioContext) return "no-audio";
|
|
150
|
+
const context = new AudioContext();
|
|
151
|
+
const oscillator = context.createOscillator();
|
|
152
|
+
const analyser = context.createAnalyser();
|
|
153
|
+
const gainNode = context.createGain();
|
|
154
|
+
const scriptProcessor = context.createScriptProcessor(4096, 1, 1);
|
|
155
|
+
gainNode.gain.value = 0;
|
|
156
|
+
oscillator.connect(analyser);
|
|
157
|
+
analyser.connect(scriptProcessor);
|
|
158
|
+
scriptProcessor.connect(gainNode);
|
|
159
|
+
gainNode.connect(context.destination);
|
|
160
|
+
oscillator.start(0);
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
scriptProcessor.onaudioprocess = (event) => {
|
|
163
|
+
const output = event.inputBuffer.getChannelData(0);
|
|
164
|
+
const hash = Array.from(output.slice(0, 30)).reduce((acc, val) => acc + Math.abs(val), 0);
|
|
165
|
+
oscillator.stop();
|
|
166
|
+
scriptProcessor.disconnect();
|
|
167
|
+
context.close();
|
|
168
|
+
resolve(hash.toString());
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return "audio-error";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
static async sha256(data) {
|
|
176
|
+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
177
|
+
const encoder = new TextEncoder();
|
|
178
|
+
const dataBuffer = encoder.encode(data);
|
|
179
|
+
const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);
|
|
180
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
181
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
182
|
+
} else {
|
|
183
|
+
return crypto2.createHash("sha256").update(data).digest("hex");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
SessionKeyCrypto = class {
|
|
188
|
+
/**
|
|
189
|
+
* Encrypt a Solana keypair with PIN + device fingerprint
|
|
190
|
+
* Uses Argon2id for key derivation and AES-256-GCM for encryption
|
|
191
|
+
*/
|
|
192
|
+
static async encrypt(keypair, pin, deviceFingerprint) {
|
|
193
|
+
if (!/^\d{6}$/.test(pin)) {
|
|
194
|
+
throw new Error("PIN must be exactly 6 numeric digits");
|
|
195
|
+
}
|
|
196
|
+
const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
|
|
197
|
+
const nonce = this.generateNonce();
|
|
198
|
+
const secretKey = keypair.secretKey;
|
|
199
|
+
const encryptedData = await this.aesEncrypt(secretKey, encryptionKey, nonce);
|
|
200
|
+
return {
|
|
201
|
+
encryptedData: Buffer.from(encryptedData).toString("base64"),
|
|
202
|
+
nonce: Buffer.from(nonce).toString("base64"),
|
|
203
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
204
|
+
deviceFingerprint,
|
|
205
|
+
version: "argon2id-aes256gcm-v1"
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Decrypt an encrypted session key with PIN + device fingerprint
|
|
210
|
+
*/
|
|
211
|
+
static async decrypt(encrypted, pin, deviceFingerprint) {
|
|
212
|
+
if (!/^\d{6}$/.test(pin)) {
|
|
213
|
+
throw new Error("PIN must be exactly 6 numeric digits");
|
|
214
|
+
}
|
|
215
|
+
if (encrypted.deviceFingerprint !== deviceFingerprint) {
|
|
216
|
+
throw new Error("Device fingerprint mismatch - wrong device or security threat");
|
|
217
|
+
}
|
|
218
|
+
const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
|
|
219
|
+
const encryptedData = Buffer.from(encrypted.encryptedData, "base64");
|
|
220
|
+
const nonce = Buffer.from(encrypted.nonce, "base64");
|
|
221
|
+
try {
|
|
222
|
+
const secretKey = await this.aesDecrypt(encryptedData, encryptionKey, nonce);
|
|
223
|
+
return import_web3.Keypair.fromSecretKey(secretKey);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw new Error("Decryption failed - wrong PIN or corrupted data");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Derive encryption key from PIN + device fingerprint using Argon2id
|
|
230
|
+
*
|
|
231
|
+
* Argon2id parameters (OWASP recommended):
|
|
232
|
+
* - Memory: 64MB (65536 KB)
|
|
233
|
+
* - Iterations: 3
|
|
234
|
+
* - Parallelism: 4
|
|
235
|
+
* - Salt: device fingerprint
|
|
236
|
+
*/
|
|
237
|
+
static async deriveKey(pin, deviceFingerprint) {
|
|
238
|
+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
239
|
+
const encoder = new TextEncoder();
|
|
240
|
+
const keyMaterial = await window.crypto.subtle.importKey(
|
|
241
|
+
"raw",
|
|
242
|
+
encoder.encode(pin),
|
|
243
|
+
{ name: "PBKDF2" },
|
|
244
|
+
false,
|
|
245
|
+
["deriveBits"]
|
|
246
|
+
);
|
|
247
|
+
const derivedBits = await window.crypto.subtle.deriveBits(
|
|
248
|
+
{
|
|
249
|
+
name: "PBKDF2",
|
|
250
|
+
salt: encoder.encode(deviceFingerprint),
|
|
251
|
+
iterations: 1e5,
|
|
252
|
+
// High iteration count for security
|
|
253
|
+
hash: "SHA-256"
|
|
254
|
+
},
|
|
255
|
+
keyMaterial,
|
|
256
|
+
256
|
|
257
|
+
// 256 bits = 32 bytes for AES-256
|
|
258
|
+
);
|
|
259
|
+
return new Uint8Array(derivedBits);
|
|
260
|
+
} else {
|
|
261
|
+
const salt = crypto2.createHash("sha256").update(deviceFingerprint).digest();
|
|
262
|
+
return crypto2.pbkdf2Sync(pin, salt, 1e5, 32, "sha256");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Generate random nonce for AES-GCM (12 bytes)
|
|
267
|
+
*/
|
|
268
|
+
static generateNonce() {
|
|
269
|
+
if (typeof window !== "undefined" && window.crypto) {
|
|
270
|
+
return window.crypto.getRandomValues(new Uint8Array(12));
|
|
271
|
+
} else {
|
|
272
|
+
return crypto2.randomBytes(12);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Encrypt with AES-256-GCM
|
|
277
|
+
*/
|
|
278
|
+
static async aesEncrypt(plaintext, key, nonce) {
|
|
279
|
+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
280
|
+
const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
|
|
281
|
+
const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
|
|
282
|
+
const plaintextBuffer = plaintext.buffer.slice(plaintext.byteOffset, plaintext.byteOffset + plaintext.byteLength);
|
|
283
|
+
const cryptoKey = await window.crypto.subtle.importKey(
|
|
284
|
+
"raw",
|
|
285
|
+
keyBuffer,
|
|
286
|
+
{ name: "AES-GCM" },
|
|
287
|
+
false,
|
|
288
|
+
["encrypt"]
|
|
289
|
+
);
|
|
290
|
+
const encrypted = await window.crypto.subtle.encrypt(
|
|
291
|
+
{
|
|
292
|
+
name: "AES-GCM",
|
|
293
|
+
iv: nonceBuffer
|
|
294
|
+
},
|
|
295
|
+
cryptoKey,
|
|
296
|
+
plaintextBuffer
|
|
297
|
+
);
|
|
298
|
+
return new Uint8Array(encrypted);
|
|
299
|
+
} else {
|
|
300
|
+
const cipher = crypto2.createCipheriv("aes-256-gcm", key, nonce);
|
|
301
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
302
|
+
const authTag = cipher.getAuthTag();
|
|
303
|
+
return new Uint8Array(Buffer.concat([encrypted, authTag]));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Decrypt with AES-256-GCM
|
|
308
|
+
*/
|
|
309
|
+
static async aesDecrypt(ciphertext, key, nonce) {
|
|
310
|
+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
311
|
+
const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
|
|
312
|
+
const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
|
|
313
|
+
const ciphertextBuffer = ciphertext.buffer.slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength);
|
|
314
|
+
const cryptoKey = await window.crypto.subtle.importKey(
|
|
315
|
+
"raw",
|
|
316
|
+
keyBuffer,
|
|
317
|
+
{ name: "AES-GCM" },
|
|
318
|
+
false,
|
|
319
|
+
["decrypt"]
|
|
320
|
+
);
|
|
321
|
+
const decrypted = await window.crypto.subtle.decrypt(
|
|
322
|
+
{
|
|
323
|
+
name: "AES-GCM",
|
|
324
|
+
iv: nonceBuffer
|
|
325
|
+
},
|
|
326
|
+
cryptoKey,
|
|
327
|
+
ciphertextBuffer
|
|
328
|
+
);
|
|
329
|
+
return new Uint8Array(decrypted);
|
|
330
|
+
} else {
|
|
331
|
+
const authTag = ciphertext.slice(-16);
|
|
332
|
+
const encrypted = ciphertext.slice(0, -16);
|
|
333
|
+
const decipher = crypto2.createDecipheriv("aes-256-gcm", key, nonce);
|
|
334
|
+
decipher.setAuthTag(authTag);
|
|
335
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
336
|
+
return new Uint8Array(decrypted);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
RecoveryQRGenerator = class {
|
|
341
|
+
/**
|
|
342
|
+
* Generate recovery QR data
|
|
343
|
+
* This allows users to recover their session key on a new device
|
|
344
|
+
*/
|
|
345
|
+
static generate(encrypted) {
|
|
346
|
+
return {
|
|
347
|
+
encryptedSessionKey: encrypted.encryptedData,
|
|
348
|
+
nonce: encrypted.nonce,
|
|
349
|
+
publicKey: encrypted.publicKey,
|
|
350
|
+
version: "v1",
|
|
351
|
+
createdAt: Date.now()
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Encode recovery QR as JSON string
|
|
356
|
+
*/
|
|
357
|
+
static encode(recoveryQR) {
|
|
358
|
+
return JSON.stringify(recoveryQR);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Decode recovery QR from JSON string
|
|
362
|
+
*/
|
|
363
|
+
static decode(qrData) {
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(qrData);
|
|
366
|
+
if (!parsed.encryptedSessionKey || !parsed.nonce || !parsed.publicKey) {
|
|
367
|
+
throw new Error("Invalid recovery QR data");
|
|
368
|
+
}
|
|
369
|
+
return parsed;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
throw new Error("Failed to decode recovery QR");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Re-encrypt session key for new device
|
|
376
|
+
*/
|
|
377
|
+
static async reEncryptForNewDevice(recoveryQR, oldPin, oldDeviceFingerprint, newPin, newDeviceFingerprint) {
|
|
378
|
+
const oldEncrypted = {
|
|
379
|
+
encryptedData: recoveryQR.encryptedSessionKey,
|
|
380
|
+
nonce: recoveryQR.nonce,
|
|
381
|
+
publicKey: recoveryQR.publicKey,
|
|
382
|
+
deviceFingerprint: oldDeviceFingerprint,
|
|
383
|
+
version: "argon2id-aes256gcm-v1"
|
|
384
|
+
};
|
|
385
|
+
const keypair = await SessionKeyCrypto.decrypt(oldEncrypted, oldPin, oldDeviceFingerprint);
|
|
386
|
+
return await SessionKeyCrypto.encrypt(keypair, newPin, newDeviceFingerprint);
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
DeviceBoundSessionKey = class _DeviceBoundSessionKey {
|
|
390
|
+
encrypted = null;
|
|
391
|
+
deviceFingerprint = null;
|
|
392
|
+
sessionKeyId = null;
|
|
393
|
+
recoveryQR = null;
|
|
394
|
+
// Auto-signing cache: decrypted keypair stored in memory
|
|
395
|
+
// Enables instant signing without re-entering PIN for subsequent payments
|
|
396
|
+
cachedKeypair = null;
|
|
397
|
+
cacheExpiry = null;
|
|
398
|
+
// Timestamp when cache expires
|
|
399
|
+
DEFAULT_CACHE_TTL_MS = 30 * 60 * 1e3;
|
|
400
|
+
// 30 minutes
|
|
401
|
+
/**
|
|
402
|
+
* Create a new device-bound session key
|
|
403
|
+
*/
|
|
404
|
+
static async create(options) {
|
|
405
|
+
const deviceFingerprint = await DeviceFingerprintGenerator.generate();
|
|
406
|
+
const keypair = import_web3.Keypair.generate();
|
|
407
|
+
const encrypted = await SessionKeyCrypto.encrypt(
|
|
408
|
+
keypair,
|
|
409
|
+
options.pin,
|
|
410
|
+
deviceFingerprint.fingerprint
|
|
411
|
+
);
|
|
412
|
+
const instance = new _DeviceBoundSessionKey();
|
|
413
|
+
instance.encrypted = encrypted;
|
|
414
|
+
instance.deviceFingerprint = deviceFingerprint;
|
|
415
|
+
if (options.generateRecoveryQR) {
|
|
416
|
+
instance.recoveryQR = RecoveryQRGenerator.generate(encrypted);
|
|
417
|
+
}
|
|
418
|
+
return instance;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Get encrypted data for backend storage
|
|
422
|
+
*/
|
|
423
|
+
getEncryptedData() {
|
|
424
|
+
if (!this.encrypted) {
|
|
425
|
+
throw new Error("Session key not created yet");
|
|
426
|
+
}
|
|
427
|
+
return this.encrypted;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Get device fingerprint
|
|
431
|
+
*/
|
|
432
|
+
getDeviceFingerprint() {
|
|
433
|
+
if (!this.deviceFingerprint) {
|
|
434
|
+
throw new Error("Device fingerprint not generated yet");
|
|
435
|
+
}
|
|
436
|
+
return this.deviceFingerprint.fingerprint;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Get public key
|
|
440
|
+
*/
|
|
441
|
+
getPublicKey() {
|
|
442
|
+
if (!this.encrypted) {
|
|
443
|
+
throw new Error("Session key not created yet");
|
|
444
|
+
}
|
|
445
|
+
return this.encrypted.publicKey;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Get recovery QR data (if generated)
|
|
449
|
+
*/
|
|
450
|
+
getRecoveryQR() {
|
|
451
|
+
return this.recoveryQR;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Decrypt and sign a transaction
|
|
455
|
+
*
|
|
456
|
+
* @param transaction - The transaction to sign
|
|
457
|
+
* @param pin - User's PIN (required only if keypair not cached)
|
|
458
|
+
* @param cacheKeypair - Whether to cache the decrypted keypair for future use (default: true)
|
|
459
|
+
* @param cacheTTL - Cache time-to-live in milliseconds (default: 30 minutes)
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```typescript
|
|
463
|
+
* // First payment: requires PIN, caches keypair
|
|
464
|
+
* await sessionKey.signTransaction(tx1, '123456', true);
|
|
465
|
+
*
|
|
466
|
+
* // Subsequent payments: uses cached keypair, no PIN needed!
|
|
467
|
+
* await sessionKey.signTransaction(tx2, '', false); // PIN ignored if cached
|
|
468
|
+
*
|
|
469
|
+
* // Clear cache when done
|
|
470
|
+
* sessionKey.clearCache();
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
async signTransaction(transaction, pin = "", cacheKeypair = true, cacheTTL) {
|
|
474
|
+
if (!this.encrypted || !this.deviceFingerprint) {
|
|
475
|
+
throw new Error("Session key not initialized");
|
|
476
|
+
}
|
|
477
|
+
let keypair;
|
|
478
|
+
if (this.isCached()) {
|
|
479
|
+
keypair = this.cachedKeypair;
|
|
480
|
+
if (typeof console !== "undefined") {
|
|
481
|
+
console.log("\u{1F680} Using cached keypair - instant signing (no PIN required)");
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
if (!pin) {
|
|
485
|
+
throw new Error("PIN required: no cached keypair available");
|
|
486
|
+
}
|
|
487
|
+
keypair = await SessionKeyCrypto.decrypt(
|
|
488
|
+
this.encrypted,
|
|
489
|
+
pin,
|
|
490
|
+
this.deviceFingerprint.fingerprint
|
|
491
|
+
);
|
|
492
|
+
if (cacheKeypair) {
|
|
493
|
+
const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
|
|
494
|
+
this.cacheKeypair(keypair, ttl);
|
|
495
|
+
if (typeof console !== "undefined") {
|
|
496
|
+
console.log(`\u2705 Keypair decrypted and cached for ${ttl / 1e3 / 60} minutes`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
transaction.sign(keypair);
|
|
501
|
+
return transaction;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Check if keypair is cached and valid
|
|
505
|
+
*/
|
|
506
|
+
isCached() {
|
|
507
|
+
if (!this.cachedKeypair || !this.cacheExpiry) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
const now = Date.now();
|
|
511
|
+
if (now > this.cacheExpiry) {
|
|
512
|
+
this.clearCache();
|
|
513
|
+
return false;
|
|
514
|
+
}
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Manually cache a keypair
|
|
519
|
+
* Called internally after PIN decryption
|
|
520
|
+
*/
|
|
521
|
+
cacheKeypair(keypair, ttl) {
|
|
522
|
+
this.cachedKeypair = keypair;
|
|
523
|
+
this.cacheExpiry = Date.now() + ttl;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Clear cached keypair
|
|
527
|
+
* Should be called when user logs out or session ends
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```typescript
|
|
531
|
+
* // Clear cache on logout
|
|
532
|
+
* sessionKey.clearCache();
|
|
533
|
+
*
|
|
534
|
+
* // Or clear automatically on tab close
|
|
535
|
+
* window.addEventListener('beforeunload', () => {
|
|
536
|
+
* sessionKey.clearCache();
|
|
537
|
+
* });
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
clearCache() {
|
|
541
|
+
this.cachedKeypair = null;
|
|
542
|
+
this.cacheExpiry = null;
|
|
543
|
+
if (typeof console !== "undefined") {
|
|
544
|
+
console.log("\u{1F9F9} Keypair cache cleared");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Decrypt and cache keypair without signing a transaction
|
|
549
|
+
* Useful for pre-warming the cache before user makes payments
|
|
550
|
+
*
|
|
551
|
+
* @example
|
|
552
|
+
* ```typescript
|
|
553
|
+
* // After session key creation, decrypt and cache
|
|
554
|
+
* await sessionKey.unlockWithPin('123456');
|
|
555
|
+
*
|
|
556
|
+
* // Now all subsequent payments are instant (no PIN)
|
|
557
|
+
* await sessionKey.signTransaction(tx1, '', false); // Instant!
|
|
558
|
+
* await sessionKey.signTransaction(tx2, '', false); // Instant!
|
|
559
|
+
* ```
|
|
560
|
+
*/
|
|
561
|
+
async unlockWithPin(pin, cacheTTL) {
|
|
562
|
+
if (!this.encrypted || !this.deviceFingerprint) {
|
|
563
|
+
throw new Error("Session key not initialized");
|
|
564
|
+
}
|
|
565
|
+
const keypair = await SessionKeyCrypto.decrypt(
|
|
566
|
+
this.encrypted,
|
|
567
|
+
pin,
|
|
568
|
+
this.deviceFingerprint.fingerprint
|
|
569
|
+
);
|
|
570
|
+
const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
|
|
571
|
+
this.cacheKeypair(keypair, ttl);
|
|
572
|
+
if (typeof console !== "undefined") {
|
|
573
|
+
console.log(`\u{1F513} Session key unlocked and cached for ${ttl / 1e3 / 60} minutes`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Get time remaining until cache expires (in milliseconds)
|
|
578
|
+
* Returns 0 if not cached
|
|
579
|
+
*/
|
|
580
|
+
getCacheTimeRemaining() {
|
|
581
|
+
if (!this.isCached() || !this.cacheExpiry) {
|
|
582
|
+
return 0;
|
|
583
|
+
}
|
|
584
|
+
const remaining = this.cacheExpiry - Date.now();
|
|
585
|
+
return Math.max(0, remaining);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Extend cache expiry time
|
|
589
|
+
* Useful to keep session active during user activity
|
|
590
|
+
*
|
|
591
|
+
* @example
|
|
592
|
+
* ```typescript
|
|
593
|
+
* // Extend cache by 15 minutes on each payment
|
|
594
|
+
* await sessionKey.signTransaction(tx, '');
|
|
595
|
+
* sessionKey.extendCache(15 * 60 * 1000);
|
|
596
|
+
* ```
|
|
597
|
+
*/
|
|
598
|
+
extendCache(additionalTTL) {
|
|
599
|
+
if (!this.isCached()) {
|
|
600
|
+
throw new Error("Cannot extend cache: no cached keypair");
|
|
601
|
+
}
|
|
602
|
+
this.cacheExpiry += additionalTTL;
|
|
603
|
+
if (typeof console !== "undefined") {
|
|
604
|
+
const remainingMinutes = this.getCacheTimeRemaining() / 1e3 / 60;
|
|
605
|
+
console.log(`\u23F0 Cache extended - ${remainingMinutes.toFixed(1)} minutes remaining`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Set session key ID after backend creation
|
|
610
|
+
*/
|
|
611
|
+
setSessionKeyId(id) {
|
|
612
|
+
this.sessionKeyId = id;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Get session key ID
|
|
616
|
+
*/
|
|
617
|
+
getSessionKeyId() {
|
|
618
|
+
if (!this.sessionKeyId) {
|
|
619
|
+
throw new Error("Session key not registered with backend");
|
|
620
|
+
}
|
|
621
|
+
return this.sessionKeyId;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// src/helpers/cache.ts
|
|
628
|
+
var cache_exports = {};
|
|
629
|
+
__export(cache_exports, {
|
|
630
|
+
QuickCaches: () => QuickCaches,
|
|
631
|
+
SessionKeyCache: () => SessionKeyCache
|
|
632
|
+
});
|
|
633
|
+
var SessionKeyCache, QuickCaches;
|
|
634
|
+
var init_cache = __esm({
|
|
635
|
+
"src/helpers/cache.ts"() {
|
|
636
|
+
"use strict";
|
|
637
|
+
SessionKeyCache = class {
|
|
638
|
+
memoryCache = /* @__PURE__ */ new Map();
|
|
639
|
+
config;
|
|
640
|
+
refreshTimers = /* @__PURE__ */ new Map();
|
|
641
|
+
constructor(config = {}) {
|
|
642
|
+
this.config = {
|
|
643
|
+
storage: config.storage || "memory",
|
|
644
|
+
ttl: config.ttl || 30 * 60 * 1e3,
|
|
645
|
+
// 30 minutes default
|
|
646
|
+
autoRefresh: config.autoRefresh || false,
|
|
647
|
+
namespace: config.namespace || "zendfi_cache",
|
|
648
|
+
debug: config.debug || false
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Get cached keypair or decrypt and cache
|
|
653
|
+
*/
|
|
654
|
+
async getCached(sessionKeyId, decryptFn, options) {
|
|
655
|
+
this.log(`getCached: ${sessionKeyId}`);
|
|
656
|
+
const memoryCached = this.memoryCache.get(sessionKeyId);
|
|
657
|
+
if (memoryCached && Date.now() < memoryCached.expiry) {
|
|
658
|
+
this.log(`Memory cache HIT: ${sessionKeyId}`);
|
|
659
|
+
return memoryCached.keypair;
|
|
660
|
+
}
|
|
661
|
+
if (this.config.storage !== "memory") {
|
|
662
|
+
const persistentCached = await this.getFromStorage(sessionKeyId);
|
|
663
|
+
if (persistentCached && Date.now() < persistentCached.expiry) {
|
|
664
|
+
if (options?.deviceFingerprint && persistentCached.deviceFingerprint) {
|
|
665
|
+
if (options.deviceFingerprint !== persistentCached.deviceFingerprint) {
|
|
666
|
+
this.log(`Device fingerprint mismatch for ${sessionKeyId}`);
|
|
667
|
+
await this.invalidate(sessionKeyId);
|
|
668
|
+
return await this.decryptAndCache(sessionKeyId, decryptFn, options);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
this.log(`Persistent cache HIT: ${sessionKeyId}`);
|
|
672
|
+
this.memoryCache.set(sessionKeyId, persistentCached);
|
|
673
|
+
return persistentCached.keypair;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
this.log(`Cache MISS: ${sessionKeyId}`);
|
|
677
|
+
return await this.decryptAndCache(sessionKeyId, decryptFn, options);
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Decrypt keypair and cache it
|
|
681
|
+
*/
|
|
682
|
+
async decryptAndCache(sessionKeyId, decryptFn, options) {
|
|
683
|
+
const keypair = await decryptFn();
|
|
684
|
+
const expiry = Date.now() + this.config.ttl;
|
|
685
|
+
const cached = {
|
|
686
|
+
keypair,
|
|
687
|
+
expiry,
|
|
688
|
+
sessionKeyId,
|
|
689
|
+
deviceFingerprint: options?.deviceFingerprint
|
|
690
|
+
};
|
|
691
|
+
this.memoryCache.set(sessionKeyId, cached);
|
|
692
|
+
if (this.config.storage !== "memory") {
|
|
693
|
+
await this.setInStorage(sessionKeyId, cached);
|
|
694
|
+
}
|
|
695
|
+
if (this.config.autoRefresh) {
|
|
696
|
+
this.setupAutoRefresh(sessionKeyId, decryptFn, options);
|
|
697
|
+
}
|
|
698
|
+
this.log(`Cached: ${sessionKeyId}, expires in ${this.config.ttl}ms`);
|
|
699
|
+
return keypair;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Invalidate cached keypair
|
|
703
|
+
*/
|
|
704
|
+
async invalidate(sessionKeyId) {
|
|
705
|
+
this.log(`Invalidating: ${sessionKeyId}`);
|
|
706
|
+
this.memoryCache.delete(sessionKeyId);
|
|
707
|
+
const timer = this.refreshTimers.get(sessionKeyId);
|
|
708
|
+
if (timer) {
|
|
709
|
+
clearTimeout(timer);
|
|
710
|
+
this.refreshTimers.delete(sessionKeyId);
|
|
711
|
+
}
|
|
712
|
+
if (this.config.storage !== "memory") {
|
|
713
|
+
await this.removeFromStorage(sessionKeyId);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Clear all cached keypairs
|
|
718
|
+
*/
|
|
719
|
+
async clear() {
|
|
720
|
+
this.log("Clearing all cache");
|
|
721
|
+
this.memoryCache.clear();
|
|
722
|
+
for (const timer of this.refreshTimers.values()) {
|
|
723
|
+
clearTimeout(timer);
|
|
724
|
+
}
|
|
725
|
+
this.refreshTimers.clear();
|
|
726
|
+
if (this.config.storage !== "memory") {
|
|
727
|
+
await this.clearStorage();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Get cache statistics
|
|
732
|
+
*/
|
|
733
|
+
getStats() {
|
|
734
|
+
const entries = Array.from(this.memoryCache.entries()).map(([id, cached]) => ({
|
|
735
|
+
sessionKeyId: id,
|
|
736
|
+
expiresIn: Math.max(0, cached.expiry - Date.now())
|
|
737
|
+
}));
|
|
738
|
+
return { size: this.memoryCache.size, entries };
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Check if a session key is cached and valid
|
|
742
|
+
*/
|
|
743
|
+
isCached(sessionKeyId) {
|
|
744
|
+
const cached = this.memoryCache.get(sessionKeyId);
|
|
745
|
+
return cached ? Date.now() < cached.expiry : false;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Update TTL for a cached session key
|
|
749
|
+
*/
|
|
750
|
+
async extendTTL(sessionKeyId, additionalMs) {
|
|
751
|
+
const cached = this.memoryCache.get(sessionKeyId);
|
|
752
|
+
if (!cached) return false;
|
|
753
|
+
cached.expiry += additionalMs;
|
|
754
|
+
this.memoryCache.set(sessionKeyId, cached);
|
|
755
|
+
if (this.config.storage !== "memory") {
|
|
756
|
+
await this.setInStorage(sessionKeyId, cached);
|
|
757
|
+
}
|
|
758
|
+
this.log(`Extended TTL for ${sessionKeyId} by ${additionalMs}ms`);
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
// ============================================
|
|
762
|
+
// Storage Backend Implementations
|
|
763
|
+
// ============================================
|
|
764
|
+
async getFromStorage(sessionKeyId) {
|
|
765
|
+
try {
|
|
766
|
+
const key = this.getStorageKey(sessionKeyId);
|
|
767
|
+
if (this.config.storage === "localStorage") {
|
|
768
|
+
const data = localStorage.getItem(key);
|
|
769
|
+
if (!data) return null;
|
|
770
|
+
const parsed = JSON.parse(data);
|
|
771
|
+
return {
|
|
772
|
+
...parsed,
|
|
773
|
+
keypair: this.deserializeKeypair(parsed.keypair)
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
if (this.config.storage === "indexedDB") {
|
|
777
|
+
return await this.getFromIndexedDB(key);
|
|
778
|
+
}
|
|
779
|
+
if (typeof this.config.storage === "object") {
|
|
780
|
+
const data = await this.config.storage.get(key);
|
|
781
|
+
if (!data) return null;
|
|
782
|
+
const parsed = JSON.parse(data);
|
|
783
|
+
return {
|
|
784
|
+
...parsed,
|
|
785
|
+
keypair: this.deserializeKeypair(parsed.keypair)
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
} catch (error) {
|
|
789
|
+
this.log(`Error reading from storage: ${error}`);
|
|
790
|
+
}
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
async setInStorage(sessionKeyId, cached) {
|
|
794
|
+
try {
|
|
795
|
+
const key = this.getStorageKey(sessionKeyId);
|
|
796
|
+
const serialized = {
|
|
797
|
+
...cached,
|
|
798
|
+
keypair: this.serializeKeypair(cached.keypair)
|
|
799
|
+
};
|
|
800
|
+
if (this.config.storage === "localStorage") {
|
|
801
|
+
localStorage.setItem(key, JSON.stringify(serialized));
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (this.config.storage === "indexedDB") {
|
|
805
|
+
await this.setInIndexedDB(key, serialized);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (typeof this.config.storage === "object") {
|
|
809
|
+
await this.config.storage.set(key, JSON.stringify(serialized));
|
|
810
|
+
}
|
|
811
|
+
} catch (error) {
|
|
812
|
+
this.log(`Error writing to storage: ${error}`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async removeFromStorage(sessionKeyId) {
|
|
816
|
+
try {
|
|
817
|
+
const key = this.getStorageKey(sessionKeyId);
|
|
818
|
+
if (this.config.storage === "localStorage") {
|
|
819
|
+
localStorage.removeItem(key);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (this.config.storage === "indexedDB") {
|
|
823
|
+
await this.removeFromIndexedDB(key);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (typeof this.config.storage === "object") {
|
|
827
|
+
await this.config.storage.remove(key);
|
|
828
|
+
}
|
|
829
|
+
} catch (error) {
|
|
830
|
+
this.log(`Error removing from storage: ${error}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async clearStorage() {
|
|
834
|
+
try {
|
|
835
|
+
if (this.config.storage === "localStorage") {
|
|
836
|
+
const keysToRemove = [];
|
|
837
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
838
|
+
const key = localStorage.key(i);
|
|
839
|
+
if (key?.startsWith(this.config.namespace)) {
|
|
840
|
+
keysToRemove.push(key);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (this.config.storage === "indexedDB") {
|
|
847
|
+
await this.clearIndexedDB();
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (typeof this.config.storage === "object") {
|
|
851
|
+
await this.config.storage.clear();
|
|
852
|
+
}
|
|
853
|
+
} catch (error) {
|
|
854
|
+
this.log(`Error clearing storage: ${error}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// ============================================
|
|
858
|
+
// IndexedDB Helpers
|
|
859
|
+
// ============================================
|
|
860
|
+
async getFromIndexedDB(key) {
|
|
861
|
+
return new Promise((resolve) => {
|
|
862
|
+
const request = indexedDB.open(this.config.namespace, 1);
|
|
863
|
+
request.onerror = () => resolve(null);
|
|
864
|
+
request.onupgradeneeded = (event) => {
|
|
865
|
+
const db = event.target.result;
|
|
866
|
+
if (!db.objectStoreNames.contains("cache")) {
|
|
867
|
+
db.createObjectStore("cache");
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
request.onsuccess = (event) => {
|
|
871
|
+
const db = event.target.result;
|
|
872
|
+
const transaction = db.transaction(["cache"], "readonly");
|
|
873
|
+
const store = transaction.objectStore("cache");
|
|
874
|
+
const getRequest = store.get(key);
|
|
875
|
+
getRequest.onsuccess = () => {
|
|
876
|
+
resolve(getRequest.result || null);
|
|
877
|
+
};
|
|
878
|
+
getRequest.onerror = () => resolve(null);
|
|
879
|
+
};
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
async setInIndexedDB(key, value) {
|
|
883
|
+
return new Promise((resolve, reject) => {
|
|
884
|
+
const request = indexedDB.open(this.config.namespace, 1);
|
|
885
|
+
request.onerror = () => reject(new Error("IndexedDB error"));
|
|
886
|
+
request.onupgradeneeded = (event) => {
|
|
887
|
+
const db = event.target.result;
|
|
888
|
+
if (!db.objectStoreNames.contains("cache")) {
|
|
889
|
+
db.createObjectStore("cache");
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
request.onsuccess = (event) => {
|
|
893
|
+
const db = event.target.result;
|
|
894
|
+
const transaction = db.transaction(["cache"], "readwrite");
|
|
895
|
+
const store = transaction.objectStore("cache");
|
|
896
|
+
store.put(value, key);
|
|
897
|
+
transaction.oncomplete = () => resolve();
|
|
898
|
+
transaction.onerror = () => reject(new Error("IndexedDB transaction error"));
|
|
899
|
+
};
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
async removeFromIndexedDB(key) {
|
|
903
|
+
return new Promise((resolve) => {
|
|
904
|
+
const request = indexedDB.open(this.config.namespace, 1);
|
|
905
|
+
request.onsuccess = (event) => {
|
|
906
|
+
const db = event.target.result;
|
|
907
|
+
const transaction = db.transaction(["cache"], "readwrite");
|
|
908
|
+
const store = transaction.objectStore("cache");
|
|
909
|
+
store.delete(key);
|
|
910
|
+
transaction.oncomplete = () => resolve();
|
|
911
|
+
};
|
|
912
|
+
request.onerror = () => resolve();
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
async clearIndexedDB() {
|
|
916
|
+
return new Promise((resolve) => {
|
|
917
|
+
const request = indexedDB.open(this.config.namespace, 1);
|
|
918
|
+
request.onsuccess = (event) => {
|
|
919
|
+
const db = event.target.result;
|
|
920
|
+
const transaction = db.transaction(["cache"], "readwrite");
|
|
921
|
+
const store = transaction.objectStore("cache");
|
|
922
|
+
store.clear();
|
|
923
|
+
transaction.oncomplete = () => resolve();
|
|
924
|
+
};
|
|
925
|
+
request.onerror = () => resolve();
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
// ============================================
|
|
929
|
+
// Serialization
|
|
930
|
+
// ============================================
|
|
931
|
+
serializeKeypair(keypair) {
|
|
932
|
+
if (keypair && typeof keypair === "object" && "secretKey" in keypair) {
|
|
933
|
+
return {
|
|
934
|
+
type: "solana",
|
|
935
|
+
secretKey: Array.from(keypair.secretKey)
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
if (keypair instanceof Uint8Array) {
|
|
939
|
+
return {
|
|
940
|
+
type: "uint8array",
|
|
941
|
+
data: Array.from(keypair)
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
return keypair;
|
|
945
|
+
}
|
|
946
|
+
deserializeKeypair(data) {
|
|
947
|
+
if (!data || typeof data !== "object") return data;
|
|
948
|
+
if (data.type === "solana" && data.secretKey) {
|
|
949
|
+
return new Uint8Array(data.secretKey);
|
|
950
|
+
}
|
|
951
|
+
if (data.type === "uint8array" && data.data) {
|
|
952
|
+
return new Uint8Array(data.data);
|
|
953
|
+
}
|
|
954
|
+
return data;
|
|
955
|
+
}
|
|
956
|
+
// ============================================
|
|
957
|
+
// Auto-Refresh
|
|
958
|
+
// ============================================
|
|
959
|
+
setupAutoRefresh(sessionKeyId, decryptFn, options) {
|
|
960
|
+
const existingTimer = this.refreshTimers.get(sessionKeyId);
|
|
961
|
+
if (existingTimer) {
|
|
962
|
+
clearTimeout(existingTimer);
|
|
963
|
+
}
|
|
964
|
+
const refreshIn = Math.max(0, this.config.ttl - 5 * 60 * 1e3);
|
|
965
|
+
const timer = setTimeout(async () => {
|
|
966
|
+
this.log(`Auto-refreshing: ${sessionKeyId}`);
|
|
967
|
+
try {
|
|
968
|
+
await this.decryptAndCache(sessionKeyId, decryptFn, options);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
this.log(`Auto-refresh failed: ${error}`);
|
|
971
|
+
}
|
|
972
|
+
}, refreshIn);
|
|
973
|
+
this.refreshTimers.set(sessionKeyId, timer);
|
|
974
|
+
}
|
|
975
|
+
// ============================================
|
|
976
|
+
// Utilities
|
|
977
|
+
// ============================================
|
|
978
|
+
getStorageKey(sessionKeyId) {
|
|
979
|
+
return `${this.config.namespace}:${sessionKeyId}`;
|
|
980
|
+
}
|
|
981
|
+
log(message) {
|
|
982
|
+
if (this.config.debug) {
|
|
983
|
+
console.log(`[SessionKeyCache] ${message}`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
QuickCaches = {
|
|
988
|
+
/** Memory-only cache (30 minutes) */
|
|
989
|
+
memory: () => new SessionKeyCache({ storage: "memory", ttl: 30 * 60 * 1e3 }),
|
|
990
|
+
/** Persistent cache (1 hour, survives reload) */
|
|
991
|
+
persistent: () => new SessionKeyCache({ storage: "localStorage", ttl: 60 * 60 * 1e3 }),
|
|
992
|
+
/** Long-term cache (24 hours, IndexedDB) */
|
|
993
|
+
longTerm: () => new SessionKeyCache({ storage: "indexedDB", ttl: 24 * 60 * 60 * 1e3, autoRefresh: true }),
|
|
994
|
+
/** Secure cache (5 minutes, memory-only) */
|
|
995
|
+
secure: () => new SessionKeyCache({ storage: "memory", ttl: 5 * 60 * 1e3 })
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
});
|
|
999
|
+
|
|
30
1000
|
// src/index.ts
|
|
31
1001
|
var index_exports = {};
|
|
32
1002
|
__export(index_exports, {
|
|
33
1003
|
AgentAPI: () => AgentAPI,
|
|
1004
|
+
AnthropicAdapter: () => AnthropicAdapter,
|
|
34
1005
|
ApiError: () => ApiError,
|
|
35
1006
|
AuthenticationError: () => AuthenticationError2,
|
|
36
1007
|
AutonomyAPI: () => AutonomyAPI,
|
|
37
1008
|
ConfigLoader: () => ConfigLoader,
|
|
1009
|
+
DevTools: () => DevTools,
|
|
38
1010
|
DeviceBoundSessionKey: () => DeviceBoundSessionKey,
|
|
39
1011
|
DeviceFingerprintGenerator: () => DeviceFingerprintGenerator,
|
|
40
1012
|
ERROR_CODES: () => ERROR_CODES,
|
|
1013
|
+
ErrorRecovery: () => ErrorRecovery,
|
|
1014
|
+
GeminiAdapter: () => GeminiAdapter,
|
|
41
1015
|
InterceptorManager: () => InterceptorManager,
|
|
42
1016
|
LitCryptoSigner: () => LitCryptoSigner,
|
|
43
1017
|
NetworkError: () => NetworkError2,
|
|
1018
|
+
OpenAIAdapter: () => OpenAIAdapter,
|
|
1019
|
+
PINRateLimiter: () => PINRateLimiter,
|
|
1020
|
+
PINValidator: () => PINValidator,
|
|
44
1021
|
PaymentError: () => PaymentError,
|
|
1022
|
+
PaymentIntentParser: () => PaymentIntentParser,
|
|
45
1023
|
PaymentIntentsAPI: () => PaymentIntentsAPI,
|
|
1024
|
+
PerformanceMonitor: () => PerformanceMonitor,
|
|
46
1025
|
PricingAPI: () => PricingAPI,
|
|
1026
|
+
QuickCaches: () => QuickCaches,
|
|
47
1027
|
RateLimitError: () => RateLimitError2,
|
|
48
1028
|
RateLimiter: () => RateLimiter,
|
|
49
1029
|
RecoveryQRGenerator: () => RecoveryQRGenerator,
|
|
1030
|
+
RetryStrategy: () => RetryStrategy,
|
|
50
1031
|
SPENDING_LIMIT_ACTION_CID: () => SPENDING_LIMIT_ACTION_CID,
|
|
1032
|
+
SecureStorage: () => SecureStorage,
|
|
1033
|
+
SessionKeyCache: () => SessionKeyCache,
|
|
51
1034
|
SessionKeyCrypto: () => SessionKeyCrypto,
|
|
1035
|
+
SessionKeyLifecycle: () => SessionKeyLifecycle,
|
|
52
1036
|
SessionKeysAPI: () => SessionKeysAPI,
|
|
53
1037
|
SmartPaymentsAPI: () => SmartPaymentsAPI,
|
|
1038
|
+
TransactionMonitor: () => TransactionMonitor,
|
|
1039
|
+
TransactionPoller: () => TransactionPoller,
|
|
54
1040
|
ValidationError: () => ValidationError2,
|
|
1041
|
+
WalletConnector: () => WalletConnector,
|
|
55
1042
|
WebhookError: () => WebhookError,
|
|
56
1043
|
ZendFiClient: () => ZendFiClient,
|
|
57
1044
|
ZendFiError: () => ZendFiError2,
|
|
@@ -66,6 +1053,7 @@ __export(index_exports, {
|
|
|
66
1053
|
asPaymentLinkCode: () => asPaymentLinkCode,
|
|
67
1054
|
asSessionId: () => asSessionId,
|
|
68
1055
|
asSubscriptionId: () => asSubscriptionId,
|
|
1056
|
+
createWalletHook: () => createWalletHook,
|
|
69
1057
|
createZendFiError: () => createZendFiError,
|
|
70
1058
|
decodeSignatureFromLit: () => decodeSignatureFromLit,
|
|
71
1059
|
encodeTransactionForLit: () => encodeTransactionForLit,
|
|
@@ -73,6 +1061,7 @@ __export(index_exports, {
|
|
|
73
1061
|
isZendFiError: () => isZendFiError,
|
|
74
1062
|
processWebhook: () => processWebhook,
|
|
75
1063
|
requiresLitSigning: () => requiresLitSigning,
|
|
1064
|
+
setupQuickSessionKey: () => setupQuickSessionKey,
|
|
76
1065
|
sleep: () => sleep,
|
|
77
1066
|
verifyExpressWebhook: () => verifyExpressWebhook,
|
|
78
1067
|
verifyNextWebhook: () => verifyNextWebhook,
|
|
@@ -1277,6 +2266,43 @@ var AutonomyAPI = class {
|
|
|
1277
2266
|
throw new Error("delegation_signature must be base64 encoded");
|
|
1278
2267
|
}
|
|
1279
2268
|
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Get spending attestations for a delegate (audit trail)
|
|
2271
|
+
*
|
|
2272
|
+
* Returns all cryptographically signed attestations ZendFi created for
|
|
2273
|
+
* this delegate. Each attestation contains:
|
|
2274
|
+
* - The spending state at the time of payment
|
|
2275
|
+
* - ZendFi's Ed25519 signature
|
|
2276
|
+
* - Timestamp and nonce (for replay protection)
|
|
2277
|
+
*
|
|
2278
|
+
* These attestations can be independently verified using ZendFi's public key
|
|
2279
|
+
* to confirm spending limit enforcement was applied correctly.
|
|
2280
|
+
*
|
|
2281
|
+
* @param delegateId - UUID of the autonomous delegate
|
|
2282
|
+
* @returns Attestation audit response with all signed attestations
|
|
2283
|
+
*
|
|
2284
|
+
* @example
|
|
2285
|
+
* ```typescript
|
|
2286
|
+
* const audit = await zendfi.autonomy.getAttestations('delegate_123...');
|
|
2287
|
+
*
|
|
2288
|
+
* console.log(`Found ${audit.attestation_count} attestations`);
|
|
2289
|
+
* console.log(`ZendFi public key: ${audit.zendfi_attestation_public_key}`);
|
|
2290
|
+
*
|
|
2291
|
+
* // Verify each attestation independently
|
|
2292
|
+
* for (const signed of audit.attestations) {
|
|
2293
|
+
* console.log(`Payment ${signed.attestation.payment_id}:`);
|
|
2294
|
+
* console.log(` Requested: $${signed.attestation.requested_usd}`);
|
|
2295
|
+
* console.log(` Remaining after: $${signed.attestation.remaining_after_usd}`);
|
|
2296
|
+
* // Verify signature with nacl.sign.detached.verify()
|
|
2297
|
+
* }
|
|
2298
|
+
* ```
|
|
2299
|
+
*/
|
|
2300
|
+
async getAttestations(delegateId) {
|
|
2301
|
+
return this.request(
|
|
2302
|
+
"GET",
|
|
2303
|
+
`/api/v1/ai/delegates/${delegateId}/attestations`
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
1280
2306
|
};
|
|
1281
2307
|
|
|
1282
2308
|
// src/api/smart-payments.ts
|
|
@@ -2464,616 +3490,39 @@ function verifyWebhookSignature(payload, signature, secret) {
|
|
|
2464
3490
|
});
|
|
2465
3491
|
}
|
|
2466
3492
|
|
|
2467
|
-
// src/device-bound-
|
|
2468
|
-
|
|
2469
|
-
var
|
|
2470
|
-
var
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
components.canvas = await this.getCanvasFingerprint();
|
|
2479
|
-
components.webgl = await this.getWebGLFingerprint();
|
|
2480
|
-
components.audio = await this.getAudioFingerprint();
|
|
2481
|
-
components.screen = `${screen.width}x${screen.height}x${screen.colorDepth}`;
|
|
2482
|
-
components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2483
|
-
components.languages = navigator.languages?.join(",") || navigator.language;
|
|
2484
|
-
components.platform = navigator.platform;
|
|
2485
|
-
components.hardwareConcurrency = navigator.hardwareConcurrency?.toString() || "unknown";
|
|
2486
|
-
const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|");
|
|
2487
|
-
const fingerprint = await this.sha256(combined);
|
|
2488
|
-
return {
|
|
2489
|
-
fingerprint,
|
|
2490
|
-
generatedAt: Date.now(),
|
|
2491
|
-
components
|
|
2492
|
-
};
|
|
2493
|
-
} catch (error) {
|
|
2494
|
-
console.warn("Device fingerprinting failed, using fallback", error);
|
|
2495
|
-
return this.generateFallbackFingerprint();
|
|
2496
|
-
}
|
|
3493
|
+
// src/device-bound-session-keys.ts
|
|
3494
|
+
init_device_bound_crypto();
|
|
3495
|
+
var import_web32 = require("@solana/web3.js");
|
|
3496
|
+
var ZendFiSessionKeyManager = class {
|
|
3497
|
+
baseURL;
|
|
3498
|
+
apiKey;
|
|
3499
|
+
sessionKey = null;
|
|
3500
|
+
sessionKeyId = null;
|
|
3501
|
+
constructor(apiKey, baseURL = "https://api.zendfi.com") {
|
|
3502
|
+
this.apiKey = apiKey;
|
|
3503
|
+
this.baseURL = baseURL;
|
|
2497
3504
|
}
|
|
2498
3505
|
/**
|
|
2499
|
-
*
|
|
2500
|
-
*
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
}
|
|
2520
|
-
} catch {
|
|
2521
|
-
components.platform = "fallback";
|
|
2522
|
-
}
|
|
2523
|
-
let randomEntropy = "";
|
|
2524
|
-
try {
|
|
2525
|
-
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
|
2526
|
-
const arr = new Uint8Array(16);
|
|
2527
|
-
window.crypto.getRandomValues(arr);
|
|
2528
|
-
randomEntropy = Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2529
|
-
} else if (typeof crypto !== "undefined" && crypto.randomBytes) {
|
|
2530
|
-
randomEntropy = crypto.randomBytes(16).toString("hex");
|
|
2531
|
-
}
|
|
2532
|
-
} catch {
|
|
2533
|
-
randomEntropy = Date.now().toString(36) + Math.random().toString(36);
|
|
2534
|
-
}
|
|
2535
|
-
const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|") + "|entropy:" + randomEntropy;
|
|
2536
|
-
const fingerprint = await this.sha256(combined);
|
|
2537
|
-
return {
|
|
2538
|
-
fingerprint,
|
|
2539
|
-
generatedAt: Date.now(),
|
|
2540
|
-
components
|
|
2541
|
-
};
|
|
2542
|
-
}
|
|
2543
|
-
static async getCanvasFingerprint() {
|
|
2544
|
-
const canvas = document.createElement("canvas");
|
|
2545
|
-
const ctx = canvas.getContext("2d");
|
|
2546
|
-
if (!ctx) return "no-canvas";
|
|
2547
|
-
canvas.width = 200;
|
|
2548
|
-
canvas.height = 50;
|
|
2549
|
-
ctx.textBaseline = "top";
|
|
2550
|
-
ctx.font = '14px "Arial"';
|
|
2551
|
-
ctx.fillStyle = "#f60";
|
|
2552
|
-
ctx.fillRect(0, 0, 100, 50);
|
|
2553
|
-
ctx.fillStyle = "#069";
|
|
2554
|
-
ctx.fillText("ZendFi \u{1F510}", 2, 2);
|
|
2555
|
-
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
2556
|
-
ctx.fillText("Device-Bound", 4, 17);
|
|
2557
|
-
return canvas.toDataURL();
|
|
2558
|
-
}
|
|
2559
|
-
static async getWebGLFingerprint() {
|
|
2560
|
-
const canvas = document.createElement("canvas");
|
|
2561
|
-
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
2562
|
-
if (!gl) return "no-webgl";
|
|
2563
|
-
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
2564
|
-
if (!debugInfo) return "no-debug-info";
|
|
2565
|
-
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
2566
|
-
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
2567
|
-
return `${vendor}|${renderer}`;
|
|
2568
|
-
}
|
|
2569
|
-
static async getAudioFingerprint() {
|
|
2570
|
-
try {
|
|
2571
|
-
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
2572
|
-
if (!AudioContext) return "no-audio";
|
|
2573
|
-
const context = new AudioContext();
|
|
2574
|
-
const oscillator = context.createOscillator();
|
|
2575
|
-
const analyser = context.createAnalyser();
|
|
2576
|
-
const gainNode = context.createGain();
|
|
2577
|
-
const scriptProcessor = context.createScriptProcessor(4096, 1, 1);
|
|
2578
|
-
gainNode.gain.value = 0;
|
|
2579
|
-
oscillator.connect(analyser);
|
|
2580
|
-
analyser.connect(scriptProcessor);
|
|
2581
|
-
scriptProcessor.connect(gainNode);
|
|
2582
|
-
gainNode.connect(context.destination);
|
|
2583
|
-
oscillator.start(0);
|
|
2584
|
-
return new Promise((resolve) => {
|
|
2585
|
-
scriptProcessor.onaudioprocess = (event) => {
|
|
2586
|
-
const output = event.inputBuffer.getChannelData(0);
|
|
2587
|
-
const hash = Array.from(output.slice(0, 30)).reduce((acc, val) => acc + Math.abs(val), 0);
|
|
2588
|
-
oscillator.stop();
|
|
2589
|
-
scriptProcessor.disconnect();
|
|
2590
|
-
context.close();
|
|
2591
|
-
resolve(hash.toString());
|
|
2592
|
-
};
|
|
2593
|
-
});
|
|
2594
|
-
} catch (error) {
|
|
2595
|
-
return "audio-error";
|
|
2596
|
-
}
|
|
2597
|
-
}
|
|
2598
|
-
static async sha256(data) {
|
|
2599
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
2600
|
-
const encoder = new TextEncoder();
|
|
2601
|
-
const dataBuffer = encoder.encode(data);
|
|
2602
|
-
const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);
|
|
2603
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
2604
|
-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2605
|
-
} else {
|
|
2606
|
-
return crypto.createHash("sha256").update(data).digest("hex");
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2609
|
-
};
|
|
2610
|
-
var SessionKeyCrypto = class {
|
|
2611
|
-
/**
|
|
2612
|
-
* Encrypt a Solana keypair with PIN + device fingerprint
|
|
2613
|
-
* Uses Argon2id for key derivation and AES-256-GCM for encryption
|
|
2614
|
-
*/
|
|
2615
|
-
static async encrypt(keypair, pin, deviceFingerprint) {
|
|
2616
|
-
if (!/^\d{6}$/.test(pin)) {
|
|
2617
|
-
throw new Error("PIN must be exactly 6 numeric digits");
|
|
2618
|
-
}
|
|
2619
|
-
const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
|
|
2620
|
-
const nonce = this.generateNonce();
|
|
2621
|
-
const secretKey = keypair.secretKey;
|
|
2622
|
-
const encryptedData = await this.aesEncrypt(secretKey, encryptionKey, nonce);
|
|
2623
|
-
return {
|
|
2624
|
-
encryptedData: Buffer.from(encryptedData).toString("base64"),
|
|
2625
|
-
nonce: Buffer.from(nonce).toString("base64"),
|
|
2626
|
-
publicKey: keypair.publicKey.toBase58(),
|
|
2627
|
-
deviceFingerprint,
|
|
2628
|
-
version: "argon2id-aes256gcm-v1"
|
|
2629
|
-
};
|
|
2630
|
-
}
|
|
2631
|
-
/**
|
|
2632
|
-
* Decrypt an encrypted session key with PIN + device fingerprint
|
|
2633
|
-
*/
|
|
2634
|
-
static async decrypt(encrypted, pin, deviceFingerprint) {
|
|
2635
|
-
if (!/^\d{6}$/.test(pin)) {
|
|
2636
|
-
throw new Error("PIN must be exactly 6 numeric digits");
|
|
2637
|
-
}
|
|
2638
|
-
if (encrypted.deviceFingerprint !== deviceFingerprint) {
|
|
2639
|
-
throw new Error("Device fingerprint mismatch - wrong device or security threat");
|
|
2640
|
-
}
|
|
2641
|
-
const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
|
|
2642
|
-
const encryptedData = Buffer.from(encrypted.encryptedData, "base64");
|
|
2643
|
-
const nonce = Buffer.from(encrypted.nonce, "base64");
|
|
2644
|
-
try {
|
|
2645
|
-
const secretKey = await this.aesDecrypt(encryptedData, encryptionKey, nonce);
|
|
2646
|
-
return import_web3.Keypair.fromSecretKey(secretKey);
|
|
2647
|
-
} catch (error) {
|
|
2648
|
-
throw new Error("Decryption failed - wrong PIN or corrupted data");
|
|
2649
|
-
}
|
|
2650
|
-
}
|
|
2651
|
-
/**
|
|
2652
|
-
* Derive encryption key from PIN + device fingerprint using Argon2id
|
|
2653
|
-
*
|
|
2654
|
-
* Argon2id parameters (OWASP recommended):
|
|
2655
|
-
* - Memory: 64MB (65536 KB)
|
|
2656
|
-
* - Iterations: 3
|
|
2657
|
-
* - Parallelism: 4
|
|
2658
|
-
* - Salt: device fingerprint
|
|
2659
|
-
*/
|
|
2660
|
-
static async deriveKey(pin, deviceFingerprint) {
|
|
2661
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
2662
|
-
const encoder = new TextEncoder();
|
|
2663
|
-
const keyMaterial = await window.crypto.subtle.importKey(
|
|
2664
|
-
"raw",
|
|
2665
|
-
encoder.encode(pin),
|
|
2666
|
-
{ name: "PBKDF2" },
|
|
2667
|
-
false,
|
|
2668
|
-
["deriveBits"]
|
|
2669
|
-
);
|
|
2670
|
-
const derivedBits = await window.crypto.subtle.deriveBits(
|
|
2671
|
-
{
|
|
2672
|
-
name: "PBKDF2",
|
|
2673
|
-
salt: encoder.encode(deviceFingerprint),
|
|
2674
|
-
iterations: 1e5,
|
|
2675
|
-
// High iteration count for security
|
|
2676
|
-
hash: "SHA-256"
|
|
2677
|
-
},
|
|
2678
|
-
keyMaterial,
|
|
2679
|
-
256
|
|
2680
|
-
// 256 bits = 32 bytes for AES-256
|
|
2681
|
-
);
|
|
2682
|
-
return new Uint8Array(derivedBits);
|
|
2683
|
-
} else {
|
|
2684
|
-
const salt = crypto.createHash("sha256").update(deviceFingerprint).digest();
|
|
2685
|
-
return crypto.pbkdf2Sync(pin, salt, 1e5, 32, "sha256");
|
|
2686
|
-
}
|
|
2687
|
-
}
|
|
2688
|
-
/**
|
|
2689
|
-
* Generate random nonce for AES-GCM (12 bytes)
|
|
2690
|
-
*/
|
|
2691
|
-
static generateNonce() {
|
|
2692
|
-
if (typeof window !== "undefined" && window.crypto) {
|
|
2693
|
-
return window.crypto.getRandomValues(new Uint8Array(12));
|
|
2694
|
-
} else {
|
|
2695
|
-
return crypto.randomBytes(12);
|
|
2696
|
-
}
|
|
2697
|
-
}
|
|
2698
|
-
/**
|
|
2699
|
-
* Encrypt with AES-256-GCM
|
|
2700
|
-
*/
|
|
2701
|
-
static async aesEncrypt(plaintext, key, nonce) {
|
|
2702
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
2703
|
-
const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
|
|
2704
|
-
const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
|
|
2705
|
-
const plaintextBuffer = plaintext.buffer.slice(plaintext.byteOffset, plaintext.byteOffset + plaintext.byteLength);
|
|
2706
|
-
const cryptoKey = await window.crypto.subtle.importKey(
|
|
2707
|
-
"raw",
|
|
2708
|
-
keyBuffer,
|
|
2709
|
-
{ name: "AES-GCM" },
|
|
2710
|
-
false,
|
|
2711
|
-
["encrypt"]
|
|
2712
|
-
);
|
|
2713
|
-
const encrypted = await window.crypto.subtle.encrypt(
|
|
2714
|
-
{
|
|
2715
|
-
name: "AES-GCM",
|
|
2716
|
-
iv: nonceBuffer
|
|
2717
|
-
},
|
|
2718
|
-
cryptoKey,
|
|
2719
|
-
plaintextBuffer
|
|
2720
|
-
);
|
|
2721
|
-
return new Uint8Array(encrypted);
|
|
2722
|
-
} else {
|
|
2723
|
-
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
|
|
2724
|
-
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
2725
|
-
const authTag = cipher.getAuthTag();
|
|
2726
|
-
return new Uint8Array(Buffer.concat([encrypted, authTag]));
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
/**
|
|
2730
|
-
* Decrypt with AES-256-GCM
|
|
2731
|
-
*/
|
|
2732
|
-
static async aesDecrypt(ciphertext, key, nonce) {
|
|
2733
|
-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
2734
|
-
const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
|
|
2735
|
-
const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
|
|
2736
|
-
const ciphertextBuffer = ciphertext.buffer.slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength);
|
|
2737
|
-
const cryptoKey = await window.crypto.subtle.importKey(
|
|
2738
|
-
"raw",
|
|
2739
|
-
keyBuffer,
|
|
2740
|
-
{ name: "AES-GCM" },
|
|
2741
|
-
false,
|
|
2742
|
-
["decrypt"]
|
|
2743
|
-
);
|
|
2744
|
-
const decrypted = await window.crypto.subtle.decrypt(
|
|
2745
|
-
{
|
|
2746
|
-
name: "AES-GCM",
|
|
2747
|
-
iv: nonceBuffer
|
|
2748
|
-
},
|
|
2749
|
-
cryptoKey,
|
|
2750
|
-
ciphertextBuffer
|
|
2751
|
-
);
|
|
2752
|
-
return new Uint8Array(decrypted);
|
|
2753
|
-
} else {
|
|
2754
|
-
const authTag = ciphertext.slice(-16);
|
|
2755
|
-
const encrypted = ciphertext.slice(0, -16);
|
|
2756
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
2757
|
-
decipher.setAuthTag(authTag);
|
|
2758
|
-
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
2759
|
-
return new Uint8Array(decrypted);
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
};
|
|
2763
|
-
var RecoveryQRGenerator = class {
|
|
2764
|
-
/**
|
|
2765
|
-
* Generate recovery QR data
|
|
2766
|
-
* This allows users to recover their session key on a new device
|
|
2767
|
-
*/
|
|
2768
|
-
static generate(encrypted) {
|
|
2769
|
-
return {
|
|
2770
|
-
encryptedSessionKey: encrypted.encryptedData,
|
|
2771
|
-
nonce: encrypted.nonce,
|
|
2772
|
-
publicKey: encrypted.publicKey,
|
|
2773
|
-
version: "v1",
|
|
2774
|
-
createdAt: Date.now()
|
|
2775
|
-
};
|
|
2776
|
-
}
|
|
2777
|
-
/**
|
|
2778
|
-
* Encode recovery QR as JSON string
|
|
2779
|
-
*/
|
|
2780
|
-
static encode(recoveryQR) {
|
|
2781
|
-
return JSON.stringify(recoveryQR);
|
|
2782
|
-
}
|
|
2783
|
-
/**
|
|
2784
|
-
* Decode recovery QR from JSON string
|
|
2785
|
-
*/
|
|
2786
|
-
static decode(qrData) {
|
|
2787
|
-
try {
|
|
2788
|
-
const parsed = JSON.parse(qrData);
|
|
2789
|
-
if (!parsed.encryptedSessionKey || !parsed.nonce || !parsed.publicKey) {
|
|
2790
|
-
throw new Error("Invalid recovery QR data");
|
|
2791
|
-
}
|
|
2792
|
-
return parsed;
|
|
2793
|
-
} catch (error) {
|
|
2794
|
-
throw new Error("Failed to decode recovery QR");
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2797
|
-
/**
|
|
2798
|
-
* Re-encrypt session key for new device
|
|
2799
|
-
*/
|
|
2800
|
-
static async reEncryptForNewDevice(recoveryQR, oldPin, oldDeviceFingerprint, newPin, newDeviceFingerprint) {
|
|
2801
|
-
const oldEncrypted = {
|
|
2802
|
-
encryptedData: recoveryQR.encryptedSessionKey,
|
|
2803
|
-
nonce: recoveryQR.nonce,
|
|
2804
|
-
publicKey: recoveryQR.publicKey,
|
|
2805
|
-
deviceFingerprint: oldDeviceFingerprint,
|
|
2806
|
-
version: "argon2id-aes256gcm-v1"
|
|
2807
|
-
};
|
|
2808
|
-
const keypair = await SessionKeyCrypto.decrypt(oldEncrypted, oldPin, oldDeviceFingerprint);
|
|
2809
|
-
return await SessionKeyCrypto.encrypt(keypair, newPin, newDeviceFingerprint);
|
|
2810
|
-
}
|
|
2811
|
-
};
|
|
2812
|
-
var DeviceBoundSessionKey = class _DeviceBoundSessionKey {
|
|
2813
|
-
encrypted = null;
|
|
2814
|
-
deviceFingerprint = null;
|
|
2815
|
-
sessionKeyId = null;
|
|
2816
|
-
recoveryQR = null;
|
|
2817
|
-
// Auto-signing cache: decrypted keypair stored in memory
|
|
2818
|
-
// Enables instant signing without re-entering PIN for subsequent payments
|
|
2819
|
-
cachedKeypair = null;
|
|
2820
|
-
cacheExpiry = null;
|
|
2821
|
-
// Timestamp when cache expires
|
|
2822
|
-
DEFAULT_CACHE_TTL_MS = 30 * 60 * 1e3;
|
|
2823
|
-
// 30 minutes
|
|
2824
|
-
/**
|
|
2825
|
-
* Create a new device-bound session key
|
|
2826
|
-
*/
|
|
2827
|
-
static async create(options) {
|
|
2828
|
-
const deviceFingerprint = await DeviceFingerprintGenerator.generate();
|
|
2829
|
-
const keypair = import_web3.Keypair.generate();
|
|
2830
|
-
const encrypted = await SessionKeyCrypto.encrypt(
|
|
2831
|
-
keypair,
|
|
2832
|
-
options.pin,
|
|
2833
|
-
deviceFingerprint.fingerprint
|
|
2834
|
-
);
|
|
2835
|
-
const instance = new _DeviceBoundSessionKey();
|
|
2836
|
-
instance.encrypted = encrypted;
|
|
2837
|
-
instance.deviceFingerprint = deviceFingerprint;
|
|
2838
|
-
if (options.generateRecoveryQR) {
|
|
2839
|
-
instance.recoveryQR = RecoveryQRGenerator.generate(encrypted);
|
|
2840
|
-
}
|
|
2841
|
-
return instance;
|
|
2842
|
-
}
|
|
2843
|
-
/**
|
|
2844
|
-
* Get encrypted data for backend storage
|
|
2845
|
-
*/
|
|
2846
|
-
getEncryptedData() {
|
|
2847
|
-
if (!this.encrypted) {
|
|
2848
|
-
throw new Error("Session key not created yet");
|
|
2849
|
-
}
|
|
2850
|
-
return this.encrypted;
|
|
2851
|
-
}
|
|
2852
|
-
/**
|
|
2853
|
-
* Get device fingerprint
|
|
2854
|
-
*/
|
|
2855
|
-
getDeviceFingerprint() {
|
|
2856
|
-
if (!this.deviceFingerprint) {
|
|
2857
|
-
throw new Error("Device fingerprint not generated yet");
|
|
2858
|
-
}
|
|
2859
|
-
return this.deviceFingerprint.fingerprint;
|
|
2860
|
-
}
|
|
2861
|
-
/**
|
|
2862
|
-
* Get public key
|
|
2863
|
-
*/
|
|
2864
|
-
getPublicKey() {
|
|
2865
|
-
if (!this.encrypted) {
|
|
2866
|
-
throw new Error("Session key not created yet");
|
|
2867
|
-
}
|
|
2868
|
-
return this.encrypted.publicKey;
|
|
2869
|
-
}
|
|
2870
|
-
/**
|
|
2871
|
-
* Get recovery QR data (if generated)
|
|
2872
|
-
*/
|
|
2873
|
-
getRecoveryQR() {
|
|
2874
|
-
return this.recoveryQR;
|
|
2875
|
-
}
|
|
2876
|
-
/**
|
|
2877
|
-
* Decrypt and sign a transaction
|
|
2878
|
-
*
|
|
2879
|
-
* @param transaction - The transaction to sign
|
|
2880
|
-
* @param pin - User's PIN (required only if keypair not cached)
|
|
2881
|
-
* @param cacheKeypair - Whether to cache the decrypted keypair for future use (default: true)
|
|
2882
|
-
* @param cacheTTL - Cache time-to-live in milliseconds (default: 30 minutes)
|
|
2883
|
-
*
|
|
2884
|
-
* @example
|
|
2885
|
-
* ```typescript
|
|
2886
|
-
* // First payment: requires PIN, caches keypair
|
|
2887
|
-
* await sessionKey.signTransaction(tx1, '123456', true);
|
|
2888
|
-
*
|
|
2889
|
-
* // Subsequent payments: uses cached keypair, no PIN needed!
|
|
2890
|
-
* await sessionKey.signTransaction(tx2, '', false); // PIN ignored if cached
|
|
2891
|
-
*
|
|
2892
|
-
* // Clear cache when done
|
|
2893
|
-
* sessionKey.clearCache();
|
|
2894
|
-
* ```
|
|
2895
|
-
*/
|
|
2896
|
-
async signTransaction(transaction, pin = "", cacheKeypair = true, cacheTTL) {
|
|
2897
|
-
if (!this.encrypted || !this.deviceFingerprint) {
|
|
2898
|
-
throw new Error("Session key not initialized");
|
|
2899
|
-
}
|
|
2900
|
-
let keypair;
|
|
2901
|
-
if (this.isCached()) {
|
|
2902
|
-
keypair = this.cachedKeypair;
|
|
2903
|
-
if (typeof console !== "undefined") {
|
|
2904
|
-
console.log("\u{1F680} Using cached keypair - instant signing (no PIN required)");
|
|
2905
|
-
}
|
|
2906
|
-
} else {
|
|
2907
|
-
if (!pin) {
|
|
2908
|
-
throw new Error("PIN required: no cached keypair available");
|
|
2909
|
-
}
|
|
2910
|
-
keypair = await SessionKeyCrypto.decrypt(
|
|
2911
|
-
this.encrypted,
|
|
2912
|
-
pin,
|
|
2913
|
-
this.deviceFingerprint.fingerprint
|
|
2914
|
-
);
|
|
2915
|
-
if (cacheKeypair) {
|
|
2916
|
-
const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
|
|
2917
|
-
this.cacheKeypair(keypair, ttl);
|
|
2918
|
-
if (typeof console !== "undefined") {
|
|
2919
|
-
console.log(`\u2705 Keypair decrypted and cached for ${ttl / 1e3 / 60} minutes`);
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
transaction.sign(keypair);
|
|
2924
|
-
return transaction;
|
|
2925
|
-
}
|
|
2926
|
-
/**
|
|
2927
|
-
* Check if keypair is cached and valid
|
|
2928
|
-
*/
|
|
2929
|
-
isCached() {
|
|
2930
|
-
if (!this.cachedKeypair || !this.cacheExpiry) {
|
|
2931
|
-
return false;
|
|
2932
|
-
}
|
|
2933
|
-
const now = Date.now();
|
|
2934
|
-
if (now > this.cacheExpiry) {
|
|
2935
|
-
this.clearCache();
|
|
2936
|
-
return false;
|
|
2937
|
-
}
|
|
2938
|
-
return true;
|
|
2939
|
-
}
|
|
2940
|
-
/**
|
|
2941
|
-
* Manually cache a keypair
|
|
2942
|
-
* Called internally after PIN decryption
|
|
2943
|
-
*/
|
|
2944
|
-
cacheKeypair(keypair, ttl) {
|
|
2945
|
-
this.cachedKeypair = keypair;
|
|
2946
|
-
this.cacheExpiry = Date.now() + ttl;
|
|
2947
|
-
}
|
|
2948
|
-
/**
|
|
2949
|
-
* Clear cached keypair
|
|
2950
|
-
* Should be called when user logs out or session ends
|
|
2951
|
-
*
|
|
2952
|
-
* @example
|
|
2953
|
-
* ```typescript
|
|
2954
|
-
* // Clear cache on logout
|
|
2955
|
-
* sessionKey.clearCache();
|
|
2956
|
-
*
|
|
2957
|
-
* // Or clear automatically on tab close
|
|
2958
|
-
* window.addEventListener('beforeunload', () => {
|
|
2959
|
-
* sessionKey.clearCache();
|
|
2960
|
-
* });
|
|
2961
|
-
* ```
|
|
2962
|
-
*/
|
|
2963
|
-
clearCache() {
|
|
2964
|
-
this.cachedKeypair = null;
|
|
2965
|
-
this.cacheExpiry = null;
|
|
2966
|
-
if (typeof console !== "undefined") {
|
|
2967
|
-
console.log("\u{1F9F9} Keypair cache cleared");
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
/**
|
|
2971
|
-
* Decrypt and cache keypair without signing a transaction
|
|
2972
|
-
* Useful for pre-warming the cache before user makes payments
|
|
2973
|
-
*
|
|
2974
|
-
* @example
|
|
2975
|
-
* ```typescript
|
|
2976
|
-
* // After session key creation, decrypt and cache
|
|
2977
|
-
* await sessionKey.unlockWithPin('123456');
|
|
2978
|
-
*
|
|
2979
|
-
* // Now all subsequent payments are instant (no PIN)
|
|
2980
|
-
* await sessionKey.signTransaction(tx1, '', false); // Instant!
|
|
2981
|
-
* await sessionKey.signTransaction(tx2, '', false); // Instant!
|
|
2982
|
-
* ```
|
|
2983
|
-
*/
|
|
2984
|
-
async unlockWithPin(pin, cacheTTL) {
|
|
2985
|
-
if (!this.encrypted || !this.deviceFingerprint) {
|
|
2986
|
-
throw new Error("Session key not initialized");
|
|
2987
|
-
}
|
|
2988
|
-
const keypair = await SessionKeyCrypto.decrypt(
|
|
2989
|
-
this.encrypted,
|
|
2990
|
-
pin,
|
|
2991
|
-
this.deviceFingerprint.fingerprint
|
|
2992
|
-
);
|
|
2993
|
-
const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
|
|
2994
|
-
this.cacheKeypair(keypair, ttl);
|
|
2995
|
-
if (typeof console !== "undefined") {
|
|
2996
|
-
console.log(`\u{1F513} Session key unlocked and cached for ${ttl / 1e3 / 60} minutes`);
|
|
2997
|
-
}
|
|
2998
|
-
}
|
|
2999
|
-
/**
|
|
3000
|
-
* Get time remaining until cache expires (in milliseconds)
|
|
3001
|
-
* Returns 0 if not cached
|
|
3002
|
-
*/
|
|
3003
|
-
getCacheTimeRemaining() {
|
|
3004
|
-
if (!this.isCached() || !this.cacheExpiry) {
|
|
3005
|
-
return 0;
|
|
3006
|
-
}
|
|
3007
|
-
const remaining = this.cacheExpiry - Date.now();
|
|
3008
|
-
return Math.max(0, remaining);
|
|
3009
|
-
}
|
|
3010
|
-
/**
|
|
3011
|
-
* Extend cache expiry time
|
|
3012
|
-
* Useful to keep session active during user activity
|
|
3013
|
-
*
|
|
3014
|
-
* @example
|
|
3015
|
-
* ```typescript
|
|
3016
|
-
* // Extend cache by 15 minutes on each payment
|
|
3017
|
-
* await sessionKey.signTransaction(tx, '');
|
|
3018
|
-
* sessionKey.extendCache(15 * 60 * 1000);
|
|
3019
|
-
* ```
|
|
3020
|
-
*/
|
|
3021
|
-
extendCache(additionalTTL) {
|
|
3022
|
-
if (!this.isCached()) {
|
|
3023
|
-
throw new Error("Cannot extend cache: no cached keypair");
|
|
3024
|
-
}
|
|
3025
|
-
this.cacheExpiry += additionalTTL;
|
|
3026
|
-
if (typeof console !== "undefined") {
|
|
3027
|
-
const remainingMinutes = this.getCacheTimeRemaining() / 1e3 / 60;
|
|
3028
|
-
console.log(`\u23F0 Cache extended - ${remainingMinutes.toFixed(1)} minutes remaining`);
|
|
3029
|
-
}
|
|
3030
|
-
}
|
|
3031
|
-
/**
|
|
3032
|
-
* Set session key ID after backend creation
|
|
3033
|
-
*/
|
|
3034
|
-
setSessionKeyId(id) {
|
|
3035
|
-
this.sessionKeyId = id;
|
|
3036
|
-
}
|
|
3037
|
-
/**
|
|
3038
|
-
* Get session key ID
|
|
3039
|
-
*/
|
|
3040
|
-
getSessionKeyId() {
|
|
3041
|
-
if (!this.sessionKeyId) {
|
|
3042
|
-
throw new Error("Session key not registered with backend");
|
|
3043
|
-
}
|
|
3044
|
-
return this.sessionKeyId;
|
|
3045
|
-
}
|
|
3046
|
-
};
|
|
3047
|
-
|
|
3048
|
-
// src/device-bound-session-keys.ts
|
|
3049
|
-
var import_web32 = require("@solana/web3.js");
|
|
3050
|
-
var ZendFiSessionKeyManager = class {
|
|
3051
|
-
baseURL;
|
|
3052
|
-
apiKey;
|
|
3053
|
-
sessionKey = null;
|
|
3054
|
-
sessionKeyId = null;
|
|
3055
|
-
constructor(apiKey, baseURL = "https://api.zendfi.com") {
|
|
3056
|
-
this.apiKey = apiKey;
|
|
3057
|
-
this.baseURL = baseURL;
|
|
3058
|
-
}
|
|
3059
|
-
/**
|
|
3060
|
-
* Create a new device-bound session key
|
|
3061
|
-
*
|
|
3062
|
-
* @example
|
|
3063
|
-
* ```typescript
|
|
3064
|
-
* const manager = new ZendFiSessionKeyManager('your-api-key');
|
|
3065
|
-
*
|
|
3066
|
-
* const sessionKey = await manager.createSessionKey({
|
|
3067
|
-
* userWallet: '7xKNH....',
|
|
3068
|
-
* limitUSDC: 100,
|
|
3069
|
-
* durationDays: 7,
|
|
3070
|
-
* pin: '123456',
|
|
3071
|
-
* generateRecoveryQR: true,
|
|
3072
|
-
* });
|
|
3073
|
-
*
|
|
3074
|
-
* console.log('Session key created:', sessionKey.sessionKeyId);
|
|
3075
|
-
* console.log('Recovery QR:', sessionKey.recoveryQR);
|
|
3076
|
-
* ```
|
|
3506
|
+
* Create a new device-bound session key
|
|
3507
|
+
*
|
|
3508
|
+
* @example
|
|
3509
|
+
* ```typescript
|
|
3510
|
+
* const manager = new ZendFiSessionKeyManager('your-api-key');
|
|
3511
|
+
*
|
|
3512
|
+
* const sessionKey = await manager.createSessionKey({
|
|
3513
|
+
* userWallet: '7xKNH....',
|
|
3514
|
+
* agentId: 'shopping-assistant-v1',
|
|
3515
|
+
* agentName: 'AI Shopping Assistant',
|
|
3516
|
+
* limitUSDC: 100,
|
|
3517
|
+
* durationDays: 7,
|
|
3518
|
+
* pin: '123456',
|
|
3519
|
+
* generateRecoveryQR: true,
|
|
3520
|
+
* });
|
|
3521
|
+
*
|
|
3522
|
+
* console.log('Session key created:', sessionKey.sessionKeyId);
|
|
3523
|
+
* console.log('Works across all apps with agent:', sessionKey.agentId);
|
|
3524
|
+
* console.log('Recovery QR:', sessionKey.recoveryQR);
|
|
3525
|
+
* ```
|
|
3077
3526
|
*/
|
|
3078
3527
|
async createSessionKey(options) {
|
|
3079
3528
|
const sessionKey = await DeviceBoundSessionKey.create({
|
|
@@ -3091,6 +3540,8 @@ var ZendFiSessionKeyManager = class {
|
|
|
3091
3540
|
}
|
|
3092
3541
|
const request = {
|
|
3093
3542
|
userWallet: options.userWallet,
|
|
3543
|
+
agentId: options.agentId,
|
|
3544
|
+
agentName: options.agentName,
|
|
3094
3545
|
limitUsdc: options.limitUSDC,
|
|
3095
3546
|
durationDays: options.durationDays,
|
|
3096
3547
|
encryptedSessionKey: encrypted.encryptedData,
|
|
@@ -3109,10 +3560,13 @@ var ZendFiSessionKeyManager = class {
|
|
|
3109
3560
|
sessionKey.setSessionKeyId(response.sessionKeyId);
|
|
3110
3561
|
return {
|
|
3111
3562
|
sessionKeyId: response.sessionKeyId,
|
|
3563
|
+
agentId: response.agentId,
|
|
3564
|
+
agentName: response.agentName,
|
|
3112
3565
|
sessionWallet: response.sessionWallet,
|
|
3113
3566
|
expiresAt: response.expiresAt,
|
|
3114
3567
|
recoveryQR,
|
|
3115
|
-
limitUsdc: response.limitUsdc
|
|
3568
|
+
limitUsdc: response.limitUsdc,
|
|
3569
|
+
crossAppCompatible: response.crossAppCompatible
|
|
3116
3570
|
};
|
|
3117
3571
|
}
|
|
3118
3572
|
/**
|
|
@@ -3667,30 +4121,2150 @@ function decodeSignatureFromLit(result) {
|
|
|
3667
4121
|
}
|
|
3668
4122
|
return bytes;
|
|
3669
4123
|
}
|
|
4124
|
+
|
|
4125
|
+
// src/helpers/index.ts
|
|
4126
|
+
init_cache();
|
|
4127
|
+
|
|
4128
|
+
// src/helpers/ai.ts
|
|
4129
|
+
var PaymentIntentParser = class {
|
|
4130
|
+
/**
|
|
4131
|
+
* Parse natural language into structured intent
|
|
4132
|
+
*/
|
|
4133
|
+
static parse(text) {
|
|
4134
|
+
if (!text || typeof text !== "string") return null;
|
|
4135
|
+
const lowerText = text.toLowerCase().trim();
|
|
4136
|
+
let action = "chat_only";
|
|
4137
|
+
let confidence = 0;
|
|
4138
|
+
if (this.containsPaymentKeywords(lowerText)) {
|
|
4139
|
+
action = "payment";
|
|
4140
|
+
confidence = 0.7;
|
|
4141
|
+
const amount = this.extractAmount(lowerText);
|
|
4142
|
+
const description = this.extractDescription(lowerText);
|
|
4143
|
+
if (amount && description) {
|
|
4144
|
+
confidence = 0.9;
|
|
4145
|
+
return { action, amount, description, confidence, rawText: text };
|
|
4146
|
+
}
|
|
4147
|
+
if (amount) {
|
|
4148
|
+
confidence = 0.8;
|
|
4149
|
+
return { action, amount, confidence, rawText: text };
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
if (this.containsSessionKeywords(lowerText)) {
|
|
4153
|
+
action = "create_session";
|
|
4154
|
+
confidence = 0.8;
|
|
4155
|
+
const amount = this.extractAmount(lowerText);
|
|
4156
|
+
return { action, amount, confidence, rawText: text, description: "Session key budget" };
|
|
4157
|
+
}
|
|
4158
|
+
if (this.containsStatusKeywords(lowerText)) {
|
|
4159
|
+
action = lowerText.includes("session") || lowerText.includes("key") ? "check_status" : "check_balance";
|
|
4160
|
+
confidence = 0.9;
|
|
4161
|
+
return { action, confidence, rawText: text };
|
|
4162
|
+
}
|
|
4163
|
+
if (this.containsTopUpKeywords(lowerText)) {
|
|
4164
|
+
action = "topup";
|
|
4165
|
+
confidence = 0.8;
|
|
4166
|
+
const amount = this.extractAmount(lowerText);
|
|
4167
|
+
return { action, amount, confidence, rawText: text };
|
|
4168
|
+
}
|
|
4169
|
+
if (this.containsRevokeKeywords(lowerText)) {
|
|
4170
|
+
action = "revoke";
|
|
4171
|
+
confidence = 0.9;
|
|
4172
|
+
return { action, confidence, rawText: text };
|
|
4173
|
+
}
|
|
4174
|
+
if (this.containsAutonomyKeywords(lowerText)) {
|
|
4175
|
+
action = "enable_autonomy";
|
|
4176
|
+
confidence = 0.8;
|
|
4177
|
+
const amount = this.extractAmount(lowerText);
|
|
4178
|
+
return { action, amount, confidence, rawText: text, description: "Autonomous delegate limit" };
|
|
4179
|
+
}
|
|
4180
|
+
return null;
|
|
4181
|
+
}
|
|
4182
|
+
/**
|
|
4183
|
+
* Generate system prompt for AI models
|
|
4184
|
+
*/
|
|
4185
|
+
static generateSystemPrompt(capabilities = {}) {
|
|
4186
|
+
const enabledFeatures = Object.entries({
|
|
4187
|
+
createPayment: capabilities.createPayment !== false,
|
|
4188
|
+
createSessionKey: capabilities.createSessionKey !== false,
|
|
4189
|
+
checkBalance: capabilities.checkBalance !== false,
|
|
4190
|
+
checkStatus: capabilities.checkStatus !== false,
|
|
4191
|
+
topUpSession: capabilities.topUpSession !== false,
|
|
4192
|
+
revokeSession: capabilities.revokeSession !== false,
|
|
4193
|
+
enableAutonomy: capabilities.enableAutonomy !== false
|
|
4194
|
+
}).filter(([_, enabled]) => enabled).map(([feature]) => feature);
|
|
4195
|
+
return `You are a ZendFi payment assistant. You can help users with crypto payments on Solana.
|
|
4196
|
+
|
|
4197
|
+
Available Actions:
|
|
4198
|
+
${enabledFeatures.includes("createPayment") ? '- Make payments (e.g., "Buy coffee for $5", "Send $20 to merchant")' : ""}
|
|
4199
|
+
${enabledFeatures.includes("createSessionKey") ? '- Create session keys (e.g., "Create a session key with $100 budget")' : ""}
|
|
4200
|
+
${enabledFeatures.includes("checkBalance") ? `- Check balances (e.g., "What's my balance?", "How much do I have?")` : ""}
|
|
4201
|
+
${enabledFeatures.includes("checkStatus") ? '- Check session status (e.g., "Session key status", "How much is left?")' : ""}
|
|
4202
|
+
${enabledFeatures.includes("topUpSession") ? '- Top up session keys (e.g., "Add $50 to session key", "Top up with $100")' : ""}
|
|
4203
|
+
${enabledFeatures.includes("revokeSession") ? '- Revoke session keys (e.g., "Revoke session key", "Cancel my session")' : ""}
|
|
4204
|
+
${enabledFeatures.includes("enableAutonomy") ? '- Enable autonomous mode (e.g., "Enable auto-pay with $25 limit")' : ""}
|
|
4205
|
+
|
|
4206
|
+
Response Format:
|
|
4207
|
+
Always respond with valid JSON:
|
|
4208
|
+
{
|
|
4209
|
+
"action": "payment" | "create_session" | "check_balance" | "check_status" | "topup" | "revoke" | "enable_autonomy" | "chat_only",
|
|
4210
|
+
"amount_usd": <number if applicable>,
|
|
4211
|
+
"description": "<description>",
|
|
4212
|
+
"message": "<friendly response to user>"
|
|
4213
|
+
}
|
|
4214
|
+
|
|
4215
|
+
Examples:
|
|
4216
|
+
User: "Buy coffee for $5"
|
|
4217
|
+
You: {"action": "payment", "amount_usd": 5, "description": "coffee", "message": "Sure! Processing your $5 coffee payment..."}
|
|
4218
|
+
|
|
4219
|
+
User: "Create a session key with $100"
|
|
4220
|
+
You: {"action": "create_session", "amount_usd": 100, "message": "I'll create a session key with a $100 budget..."}
|
|
4221
|
+
|
|
4222
|
+
User: "What's my balance?"
|
|
4223
|
+
You: {"action": "check_balance", "message": "Let me check your balance..."}
|
|
4224
|
+
|
|
4225
|
+
Be helpful, concise, and always respond in valid JSON format.`;
|
|
4226
|
+
}
|
|
4227
|
+
// ============================================
|
|
4228
|
+
// Keyword Detection
|
|
4229
|
+
// ============================================
|
|
4230
|
+
static containsPaymentKeywords(text) {
|
|
4231
|
+
const keywords = [
|
|
4232
|
+
"buy",
|
|
4233
|
+
"purchase",
|
|
4234
|
+
"pay",
|
|
4235
|
+
"send",
|
|
4236
|
+
"transfer",
|
|
4237
|
+
"payment",
|
|
4238
|
+
"order",
|
|
4239
|
+
"checkout",
|
|
4240
|
+
"subscribe",
|
|
4241
|
+
"donate",
|
|
4242
|
+
"tip"
|
|
4243
|
+
];
|
|
4244
|
+
return keywords.some((kw) => text.includes(kw));
|
|
4245
|
+
}
|
|
4246
|
+
static containsSessionKeywords(text) {
|
|
4247
|
+
const keywords = [
|
|
4248
|
+
"create session",
|
|
4249
|
+
"new session",
|
|
4250
|
+
"session key",
|
|
4251
|
+
"setup session",
|
|
4252
|
+
"generate session",
|
|
4253
|
+
"make session",
|
|
4254
|
+
"start session"
|
|
4255
|
+
];
|
|
4256
|
+
return keywords.some((kw) => text.includes(kw));
|
|
4257
|
+
}
|
|
4258
|
+
static containsStatusKeywords(text) {
|
|
4259
|
+
const keywords = [
|
|
4260
|
+
"status",
|
|
4261
|
+
"balance",
|
|
4262
|
+
"remaining",
|
|
4263
|
+
"left",
|
|
4264
|
+
"how much",
|
|
4265
|
+
"check",
|
|
4266
|
+
"show",
|
|
4267
|
+
"view",
|
|
4268
|
+
"display"
|
|
4269
|
+
];
|
|
4270
|
+
return keywords.some((kw) => text.includes(kw));
|
|
4271
|
+
}
|
|
4272
|
+
static containsTopUpKeywords(text) {
|
|
4273
|
+
const keywords = [
|
|
4274
|
+
"top up",
|
|
4275
|
+
"topup",
|
|
4276
|
+
"add funds",
|
|
4277
|
+
"add money",
|
|
4278
|
+
"fund",
|
|
4279
|
+
"increase",
|
|
4280
|
+
"reload",
|
|
4281
|
+
"refill"
|
|
4282
|
+
];
|
|
4283
|
+
return keywords.some((kw) => text.includes(kw));
|
|
4284
|
+
}
|
|
4285
|
+
static containsRevokeKeywords(text) {
|
|
4286
|
+
const keywords = [
|
|
4287
|
+
"revoke",
|
|
4288
|
+
"cancel",
|
|
4289
|
+
"disable",
|
|
4290
|
+
"remove",
|
|
4291
|
+
"delete",
|
|
4292
|
+
"stop",
|
|
4293
|
+
"end",
|
|
4294
|
+
"terminate"
|
|
4295
|
+
];
|
|
4296
|
+
return keywords.some((kw) => text.includes(kw));
|
|
4297
|
+
}
|
|
4298
|
+
static containsAutonomyKeywords(text) {
|
|
4299
|
+
const keywords = [
|
|
4300
|
+
"autonomous",
|
|
4301
|
+
"auto",
|
|
4302
|
+
"automatic",
|
|
4303
|
+
"delegate",
|
|
4304
|
+
"enable auto",
|
|
4305
|
+
"auto-sign",
|
|
4306
|
+
"auto sign",
|
|
4307
|
+
"auto-pay",
|
|
4308
|
+
"auto pay"
|
|
4309
|
+
];
|
|
4310
|
+
return keywords.some((kw) => text.includes(kw));
|
|
4311
|
+
}
|
|
4312
|
+
// ============================================
|
|
4313
|
+
// Amount Extraction
|
|
4314
|
+
// ============================================
|
|
4315
|
+
static extractAmount(text) {
|
|
4316
|
+
const dollarMatch = text.match(/\$\s*(\d+(?:\.\d{1,2})?)/);
|
|
4317
|
+
if (dollarMatch) {
|
|
4318
|
+
return parseFloat(dollarMatch[1]);
|
|
4319
|
+
}
|
|
4320
|
+
const usdMatch = text.match(/(\d+(?:\.\d{1,2})?)\s*(?:usd|usdc|dollars?)/i);
|
|
4321
|
+
if (usdMatch) {
|
|
4322
|
+
return parseFloat(usdMatch[1]);
|
|
4323
|
+
}
|
|
4324
|
+
const numberMatch = text.match(/(\d+(?:\.\d{1,2})?)/);
|
|
4325
|
+
if (numberMatch) {
|
|
4326
|
+
const num = parseFloat(numberMatch[1]);
|
|
4327
|
+
if (num > 0 && num < 1e5) {
|
|
4328
|
+
return num;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
return void 0;
|
|
4332
|
+
}
|
|
4333
|
+
// ============================================
|
|
4334
|
+
// Description Extraction
|
|
4335
|
+
// ============================================
|
|
4336
|
+
static extractDescription(text) {
|
|
4337
|
+
const lowerText = text.toLowerCase();
|
|
4338
|
+
const items = {
|
|
4339
|
+
"coffee": ["coffee", "espresso", "latte", "cappuccino"],
|
|
4340
|
+
"food": ["food", "meal", "lunch", "dinner", "breakfast"],
|
|
4341
|
+
"drink": ["drink", "beverage", "soda", "juice"],
|
|
4342
|
+
"book": ["book", "ebook", "magazine"],
|
|
4343
|
+
"subscription": ["subscription", "membership", "plan"],
|
|
4344
|
+
"tip": ["tip", "gratuity"],
|
|
4345
|
+
"donation": ["donate", "donation", "contribute"],
|
|
4346
|
+
"game": ["game", "gaming"],
|
|
4347
|
+
"music": ["music", "song", "album"],
|
|
4348
|
+
"video": ["video", "movie", "film"]
|
|
4349
|
+
};
|
|
4350
|
+
for (const [item, keywords] of Object.entries(items)) {
|
|
4351
|
+
if (keywords.some((kw) => lowerText.includes(kw))) {
|
|
4352
|
+
return item;
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
const forMatch = text.match(/for\s+(.+?)(?:\s+\$|\s+usd|$)/i);
|
|
4356
|
+
if (forMatch && forMatch[1]) {
|
|
4357
|
+
const desc = forMatch[1].trim();
|
|
4358
|
+
if (desc.length > 0 && desc.length < 50) {
|
|
4359
|
+
return desc;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
return void 0;
|
|
4363
|
+
}
|
|
4364
|
+
};
|
|
4365
|
+
var OpenAIAdapter = class {
|
|
4366
|
+
constructor(apiKey, model = "gpt-4o-mini", capabilities) {
|
|
4367
|
+
this.apiKey = apiKey;
|
|
4368
|
+
this.model = model;
|
|
4369
|
+
this.capabilities = capabilities;
|
|
4370
|
+
}
|
|
4371
|
+
async chat(message, conversationHistory = []) {
|
|
4372
|
+
const systemPrompt = PaymentIntentParser.generateSystemPrompt(this.capabilities);
|
|
4373
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
4374
|
+
method: "POST",
|
|
4375
|
+
headers: {
|
|
4376
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
4377
|
+
"Content-Type": "application/json"
|
|
4378
|
+
},
|
|
4379
|
+
body: JSON.stringify({
|
|
4380
|
+
model: this.model,
|
|
4381
|
+
messages: [
|
|
4382
|
+
{ role: "system", content: systemPrompt },
|
|
4383
|
+
...conversationHistory,
|
|
4384
|
+
{ role: "user", content: message }
|
|
4385
|
+
],
|
|
4386
|
+
temperature: 0.7,
|
|
4387
|
+
max_tokens: 500
|
|
4388
|
+
})
|
|
4389
|
+
});
|
|
4390
|
+
if (!response.ok) {
|
|
4391
|
+
throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`);
|
|
4392
|
+
}
|
|
4393
|
+
const data = await response.json();
|
|
4394
|
+
const text = data.choices[0]?.message?.content || "";
|
|
4395
|
+
let intent = null;
|
|
4396
|
+
try {
|
|
4397
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
4398
|
+
if (jsonMatch) {
|
|
4399
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
4400
|
+
intent = {
|
|
4401
|
+
action: parsed.action || "chat_only",
|
|
4402
|
+
amount: parsed.amount_usd,
|
|
4403
|
+
description: parsed.description,
|
|
4404
|
+
confidence: 0.9,
|
|
4405
|
+
rawText: text,
|
|
4406
|
+
metadata: { message: parsed.message }
|
|
4407
|
+
};
|
|
4408
|
+
}
|
|
4409
|
+
} catch {
|
|
4410
|
+
intent = PaymentIntentParser.parse(text);
|
|
4411
|
+
}
|
|
4412
|
+
return { text, intent, raw: data };
|
|
4413
|
+
}
|
|
4414
|
+
};
|
|
4415
|
+
var AnthropicAdapter = class {
|
|
4416
|
+
constructor(apiKey, model = "claude-3-5-sonnet-20241022", capabilities) {
|
|
4417
|
+
this.apiKey = apiKey;
|
|
4418
|
+
this.model = model;
|
|
4419
|
+
this.capabilities = capabilities;
|
|
4420
|
+
}
|
|
4421
|
+
async chat(message, conversationHistory = []) {
|
|
4422
|
+
const systemPrompt = PaymentIntentParser.generateSystemPrompt(this.capabilities);
|
|
4423
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
4424
|
+
method: "POST",
|
|
4425
|
+
headers: {
|
|
4426
|
+
"x-api-key": this.apiKey,
|
|
4427
|
+
"anthropic-version": "2023-06-01",
|
|
4428
|
+
"Content-Type": "application/json"
|
|
4429
|
+
},
|
|
4430
|
+
body: JSON.stringify({
|
|
4431
|
+
model: this.model,
|
|
4432
|
+
system: systemPrompt,
|
|
4433
|
+
messages: [
|
|
4434
|
+
...conversationHistory.map((msg) => ({
|
|
4435
|
+
role: msg.role === "user" ? "user" : "assistant",
|
|
4436
|
+
content: msg.content
|
|
4437
|
+
})),
|
|
4438
|
+
{ role: "user", content: message }
|
|
4439
|
+
],
|
|
4440
|
+
max_tokens: 500
|
|
4441
|
+
})
|
|
4442
|
+
});
|
|
4443
|
+
if (!response.ok) {
|
|
4444
|
+
throw new Error(`Anthropic API error: ${response.status} ${response.statusText}`);
|
|
4445
|
+
}
|
|
4446
|
+
const data = await response.json();
|
|
4447
|
+
const text = data.content[0]?.text || "";
|
|
4448
|
+
let intent = null;
|
|
4449
|
+
try {
|
|
4450
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
4451
|
+
if (jsonMatch) {
|
|
4452
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
4453
|
+
intent = {
|
|
4454
|
+
action: parsed.action || "chat_only",
|
|
4455
|
+
amount: parsed.amount_usd,
|
|
4456
|
+
description: parsed.description,
|
|
4457
|
+
confidence: 0.9,
|
|
4458
|
+
rawText: text,
|
|
4459
|
+
metadata: { message: parsed.message }
|
|
4460
|
+
};
|
|
4461
|
+
}
|
|
4462
|
+
} catch {
|
|
4463
|
+
intent = PaymentIntentParser.parse(text);
|
|
4464
|
+
}
|
|
4465
|
+
return { text, intent, raw: data };
|
|
4466
|
+
}
|
|
4467
|
+
};
|
|
4468
|
+
var GeminiAdapter = class {
|
|
4469
|
+
constructor(apiKey, model = "gemini-2.0-flash-exp", capabilities) {
|
|
4470
|
+
this.apiKey = apiKey;
|
|
4471
|
+
this.model = model;
|
|
4472
|
+
this.capabilities = capabilities;
|
|
4473
|
+
}
|
|
4474
|
+
async chat(message) {
|
|
4475
|
+
const systemPrompt = PaymentIntentParser.generateSystemPrompt(this.capabilities);
|
|
4476
|
+
const fullPrompt = `${systemPrompt}
|
|
4477
|
+
|
|
4478
|
+
User: ${message}`;
|
|
4479
|
+
const response = await fetch(
|
|
4480
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`,
|
|
4481
|
+
{
|
|
4482
|
+
method: "POST",
|
|
4483
|
+
headers: { "Content-Type": "application/json" },
|
|
4484
|
+
body: JSON.stringify({
|
|
4485
|
+
contents: [{ parts: [{ text: fullPrompt }] }]
|
|
4486
|
+
})
|
|
4487
|
+
}
|
|
4488
|
+
);
|
|
4489
|
+
if (!response.ok) {
|
|
4490
|
+
throw new Error(`Gemini API error: ${response.status} ${response.statusText}`);
|
|
4491
|
+
}
|
|
4492
|
+
const data = await response.json();
|
|
4493
|
+
const text = data.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
|
4494
|
+
let intent = null;
|
|
4495
|
+
try {
|
|
4496
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
4497
|
+
if (jsonMatch) {
|
|
4498
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
4499
|
+
intent = {
|
|
4500
|
+
action: parsed.action || "chat_only",
|
|
4501
|
+
amount: parsed.amount_usd,
|
|
4502
|
+
description: parsed.description,
|
|
4503
|
+
confidence: 0.9,
|
|
4504
|
+
rawText: text,
|
|
4505
|
+
metadata: { message: parsed.message }
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4508
|
+
} catch {
|
|
4509
|
+
intent = PaymentIntentParser.parse(text);
|
|
4510
|
+
}
|
|
4511
|
+
return { text, intent, raw: data };
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
|
|
4515
|
+
// src/helpers/wallet.ts
|
|
4516
|
+
var WalletConnector = class _WalletConnector {
|
|
4517
|
+
static connectedWallet = null;
|
|
4518
|
+
/**
|
|
4519
|
+
* Detect and connect to a Solana wallet
|
|
4520
|
+
*/
|
|
4521
|
+
static async detectAndConnect(config = {}) {
|
|
4522
|
+
if (this.connectedWallet && this.connectedWallet.isConnected()) {
|
|
4523
|
+
return this.connectedWallet;
|
|
4524
|
+
}
|
|
4525
|
+
const detected = this.detectWallets();
|
|
4526
|
+
if (detected.length === 0) {
|
|
4527
|
+
if (config.showInstallPrompt !== false) {
|
|
4528
|
+
this.showInstallPrompt();
|
|
4529
|
+
}
|
|
4530
|
+
throw new Error("No Solana wallet detected. Please install Phantom, Solflare, or Backpack.");
|
|
4531
|
+
}
|
|
4532
|
+
let selectedProvider = detected[0];
|
|
4533
|
+
if (config.preferredProvider && detected.includes(config.preferredProvider)) {
|
|
4534
|
+
selectedProvider = config.preferredProvider;
|
|
4535
|
+
}
|
|
4536
|
+
const wallet = await this.connectToProvider(selectedProvider);
|
|
4537
|
+
this.connectedWallet = wallet;
|
|
4538
|
+
return wallet;
|
|
4539
|
+
}
|
|
4540
|
+
/**
|
|
4541
|
+
* Detect available Solana wallets
|
|
4542
|
+
*/
|
|
4543
|
+
static detectWallets() {
|
|
4544
|
+
const detected = [];
|
|
4545
|
+
if (typeof window === "undefined") return detected;
|
|
4546
|
+
if (window.solana?.isPhantom) detected.push("phantom");
|
|
4547
|
+
if (window.solflare?.isSolflare) detected.push("solflare");
|
|
4548
|
+
if (window.backpack?.isBackpack) detected.push("backpack");
|
|
4549
|
+
if (window.coinbaseSolana) detected.push("coinbase");
|
|
4550
|
+
if (window.trustwallet?.solana) detected.push("trust");
|
|
4551
|
+
return detected;
|
|
4552
|
+
}
|
|
4553
|
+
/**
|
|
4554
|
+
* Connect to a specific wallet provider
|
|
4555
|
+
*/
|
|
4556
|
+
static async connectToProvider(provider) {
|
|
4557
|
+
if (typeof window === "undefined") {
|
|
4558
|
+
throw new Error("Wallet connection only works in browser environment");
|
|
4559
|
+
}
|
|
4560
|
+
let adapter;
|
|
4561
|
+
switch (provider) {
|
|
4562
|
+
case "phantom":
|
|
4563
|
+
adapter = window.solana;
|
|
4564
|
+
if (!adapter?.isPhantom) {
|
|
4565
|
+
throw new Error("Phantom wallet not found");
|
|
4566
|
+
}
|
|
4567
|
+
break;
|
|
4568
|
+
case "solflare":
|
|
4569
|
+
adapter = window.solflare;
|
|
4570
|
+
if (!adapter?.isSolflare) {
|
|
4571
|
+
throw new Error("Solflare wallet not found");
|
|
4572
|
+
}
|
|
4573
|
+
break;
|
|
4574
|
+
case "backpack":
|
|
4575
|
+
adapter = window.backpack;
|
|
4576
|
+
if (!adapter?.isBackpack) {
|
|
4577
|
+
throw new Error("Backpack wallet not found");
|
|
4578
|
+
}
|
|
4579
|
+
break;
|
|
4580
|
+
case "coinbase":
|
|
4581
|
+
adapter = window.coinbaseSolana;
|
|
4582
|
+
if (!adapter) {
|
|
4583
|
+
throw new Error("Coinbase Wallet not found");
|
|
4584
|
+
}
|
|
4585
|
+
break;
|
|
4586
|
+
case "trust":
|
|
4587
|
+
adapter = window.trustwallet?.solana;
|
|
4588
|
+
if (!adapter) {
|
|
4589
|
+
throw new Error("Trust Wallet not found");
|
|
4590
|
+
}
|
|
4591
|
+
break;
|
|
4592
|
+
default:
|
|
4593
|
+
throw new Error(`Unknown wallet provider: ${provider}`);
|
|
4594
|
+
}
|
|
4595
|
+
try {
|
|
4596
|
+
const response = await adapter.connect();
|
|
4597
|
+
const publicKey = response.publicKey || adapter.publicKey;
|
|
4598
|
+
if (!publicKey) {
|
|
4599
|
+
throw new Error("Failed to get wallet public key");
|
|
4600
|
+
}
|
|
4601
|
+
const connectedWallet = {
|
|
4602
|
+
address: publicKey.toString(),
|
|
4603
|
+
provider,
|
|
4604
|
+
publicKey,
|
|
4605
|
+
signTransaction: async (tx) => {
|
|
4606
|
+
return await adapter.signTransaction(tx);
|
|
4607
|
+
},
|
|
4608
|
+
signAllTransactions: async (txs) => {
|
|
4609
|
+
if (adapter.signAllTransactions) {
|
|
4610
|
+
return await adapter.signAllTransactions(txs);
|
|
4611
|
+
}
|
|
4612
|
+
const signed = [];
|
|
4613
|
+
for (const tx of txs) {
|
|
4614
|
+
signed.push(await adapter.signTransaction(tx));
|
|
4615
|
+
}
|
|
4616
|
+
return signed;
|
|
4617
|
+
},
|
|
4618
|
+
signMessage: async (message) => {
|
|
4619
|
+
if (adapter.signMessage) {
|
|
4620
|
+
return await adapter.signMessage(message);
|
|
4621
|
+
}
|
|
4622
|
+
throw new Error(`${provider} does not support message signing`);
|
|
4623
|
+
},
|
|
4624
|
+
disconnect: async () => {
|
|
4625
|
+
if (adapter.disconnect) {
|
|
4626
|
+
await adapter.disconnect();
|
|
4627
|
+
}
|
|
4628
|
+
_WalletConnector.connectedWallet = null;
|
|
4629
|
+
},
|
|
4630
|
+
isConnected: () => {
|
|
4631
|
+
return adapter.isConnected ?? false;
|
|
4632
|
+
},
|
|
4633
|
+
raw: adapter
|
|
4634
|
+
};
|
|
4635
|
+
return connectedWallet;
|
|
4636
|
+
} catch (error) {
|
|
4637
|
+
throw new Error(`Failed to connect to ${provider}: ${error.message}`);
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Sign and submit a transaction
|
|
4642
|
+
*/
|
|
4643
|
+
static async signAndSubmit(transaction, wallet, connection) {
|
|
4644
|
+
const signedTx = await wallet.signTransaction(transaction);
|
|
4645
|
+
const signature = await connection.sendRawTransaction(signedTx.serialize(), {
|
|
4646
|
+
skipPreflight: false,
|
|
4647
|
+
preflightCommitment: "confirmed"
|
|
4648
|
+
});
|
|
4649
|
+
return { signature };
|
|
4650
|
+
}
|
|
4651
|
+
/**
|
|
4652
|
+
* Get current connected wallet
|
|
4653
|
+
*/
|
|
4654
|
+
static getConnectedWallet() {
|
|
4655
|
+
return this.connectedWallet;
|
|
4656
|
+
}
|
|
4657
|
+
/**
|
|
4658
|
+
* Disconnect current wallet
|
|
4659
|
+
*/
|
|
4660
|
+
static async disconnect() {
|
|
4661
|
+
if (this.connectedWallet) {
|
|
4662
|
+
await this.connectedWallet.disconnect();
|
|
4663
|
+
this.connectedWallet = null;
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
/**
|
|
4667
|
+
* Listen for wallet connection changes
|
|
4668
|
+
*/
|
|
4669
|
+
static onAccountChange(callback) {
|
|
4670
|
+
if (typeof window === "undefined") {
|
|
4671
|
+
return () => {
|
|
4672
|
+
};
|
|
4673
|
+
}
|
|
4674
|
+
const cleanupFns = [];
|
|
4675
|
+
if (window.solana?.on) {
|
|
4676
|
+
window.solana.on("accountChanged", callback);
|
|
4677
|
+
cleanupFns.push(() => {
|
|
4678
|
+
window.solana?.removeListener("accountChanged", callback);
|
|
4679
|
+
});
|
|
4680
|
+
}
|
|
4681
|
+
if (window.solflare?.on) {
|
|
4682
|
+
window.solflare.on("accountChanged", callback);
|
|
4683
|
+
cleanupFns.push(() => {
|
|
4684
|
+
window.solflare?.removeListener("accountChanged", callback);
|
|
4685
|
+
});
|
|
4686
|
+
}
|
|
4687
|
+
return () => {
|
|
4688
|
+
cleanupFns.forEach((fn) => fn());
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4691
|
+
/**
|
|
4692
|
+
* Listen for wallet disconnection
|
|
4693
|
+
*/
|
|
4694
|
+
static onDisconnect(callback) {
|
|
4695
|
+
if (typeof window === "undefined") {
|
|
4696
|
+
return () => {
|
|
4697
|
+
};
|
|
4698
|
+
}
|
|
4699
|
+
const cleanupFns = [];
|
|
4700
|
+
if (window.solana?.on) {
|
|
4701
|
+
window.solana.on("disconnect", callback);
|
|
4702
|
+
cleanupFns.push(() => {
|
|
4703
|
+
window.solana?.removeListener("disconnect", callback);
|
|
4704
|
+
});
|
|
4705
|
+
}
|
|
4706
|
+
if (window.solflare?.on) {
|
|
4707
|
+
window.solflare.on("disconnect", callback);
|
|
4708
|
+
cleanupFns.push(() => {
|
|
4709
|
+
window.solflare?.removeListener("disconnect", callback);
|
|
4710
|
+
});
|
|
4711
|
+
}
|
|
4712
|
+
return () => {
|
|
4713
|
+
cleanupFns.forEach((fn) => fn());
|
|
4714
|
+
};
|
|
4715
|
+
}
|
|
4716
|
+
/**
|
|
4717
|
+
* Show install prompt UI
|
|
4718
|
+
*/
|
|
4719
|
+
static showInstallPrompt() {
|
|
4720
|
+
const message = `
|
|
4721
|
+
No Solana wallet detected!
|
|
4722
|
+
|
|
4723
|
+
Install one of these wallets:
|
|
4724
|
+
\u2022 Phantom: https://phantom.app
|
|
4725
|
+
\u2022 Solflare: https://solflare.com
|
|
4726
|
+
\u2022 Backpack: https://backpack.app
|
|
4727
|
+
`.trim();
|
|
4728
|
+
console.warn(message);
|
|
4729
|
+
if (typeof window !== "undefined") {
|
|
4730
|
+
const userChoice = window.confirm(
|
|
4731
|
+
"No Solana wallet detected.\n\nWould you like to install Phantom wallet?"
|
|
4732
|
+
);
|
|
4733
|
+
if (userChoice) {
|
|
4734
|
+
window.open("https://phantom.app", "_blank");
|
|
4735
|
+
}
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
/**
|
|
4739
|
+
* Check if wallet is installed
|
|
4740
|
+
*/
|
|
4741
|
+
static isWalletInstalled(provider) {
|
|
4742
|
+
return this.detectWallets().includes(provider);
|
|
4743
|
+
}
|
|
4744
|
+
/**
|
|
4745
|
+
* Get wallet download URL
|
|
4746
|
+
*/
|
|
4747
|
+
static getWalletUrl(provider) {
|
|
4748
|
+
const urls = {
|
|
4749
|
+
phantom: "https://phantom.app",
|
|
4750
|
+
solflare: "https://solflare.com",
|
|
4751
|
+
backpack: "https://backpack.app",
|
|
4752
|
+
coinbase: "https://www.coinbase.com/wallet",
|
|
4753
|
+
trust: "https://trustwallet.com"
|
|
4754
|
+
};
|
|
4755
|
+
return urls[provider];
|
|
4756
|
+
}
|
|
4757
|
+
};
|
|
4758
|
+
function createWalletHook() {
|
|
4759
|
+
let useState;
|
|
4760
|
+
let useEffect;
|
|
4761
|
+
try {
|
|
4762
|
+
const React = require("react");
|
|
4763
|
+
useState = React.useState;
|
|
4764
|
+
useEffect = React.useEffect;
|
|
4765
|
+
} catch {
|
|
4766
|
+
throw new Error("React not found. Install react to use wallet hooks.");
|
|
4767
|
+
}
|
|
4768
|
+
return function useWallet() {
|
|
4769
|
+
const [wallet, setWallet] = useState(null);
|
|
4770
|
+
const [connecting, setConnecting] = useState(false);
|
|
4771
|
+
const [error, setError] = useState(null);
|
|
4772
|
+
const connect = async (config) => {
|
|
4773
|
+
setConnecting(true);
|
|
4774
|
+
setError(null);
|
|
4775
|
+
try {
|
|
4776
|
+
const connected = await WalletConnector.detectAndConnect(config);
|
|
4777
|
+
setWallet(connected);
|
|
4778
|
+
} catch (err) {
|
|
4779
|
+
setError(err);
|
|
4780
|
+
} finally {
|
|
4781
|
+
setConnecting(false);
|
|
4782
|
+
}
|
|
4783
|
+
};
|
|
4784
|
+
const disconnect = async () => {
|
|
4785
|
+
await WalletConnector.disconnect();
|
|
4786
|
+
setWallet(null);
|
|
4787
|
+
};
|
|
4788
|
+
useEffect(() => {
|
|
4789
|
+
const cleanup = WalletConnector.onAccountChange((publicKey) => {
|
|
4790
|
+
if (wallet) {
|
|
4791
|
+
setWallet({ ...wallet, publicKey, address: publicKey.toString() });
|
|
4792
|
+
}
|
|
4793
|
+
});
|
|
4794
|
+
return cleanup;
|
|
4795
|
+
}, [wallet]);
|
|
4796
|
+
return {
|
|
4797
|
+
wallet,
|
|
4798
|
+
connecting,
|
|
4799
|
+
error,
|
|
4800
|
+
connect,
|
|
4801
|
+
disconnect,
|
|
4802
|
+
isConnected: wallet !== null
|
|
4803
|
+
};
|
|
4804
|
+
};
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
// src/helpers/security.ts
|
|
4808
|
+
var PINValidator = class {
|
|
4809
|
+
/**
|
|
4810
|
+
* Validate PIN strength and format
|
|
4811
|
+
*/
|
|
4812
|
+
static validate(pin) {
|
|
4813
|
+
const errors = [];
|
|
4814
|
+
const suggestions = [];
|
|
4815
|
+
if (!pin || pin.length < 4) {
|
|
4816
|
+
errors.push("PIN must be at least 4 digits");
|
|
4817
|
+
}
|
|
4818
|
+
if (pin.length > 12) {
|
|
4819
|
+
errors.push("PIN must be at most 12 digits");
|
|
4820
|
+
}
|
|
4821
|
+
if (!/^\d+$/.test(pin)) {
|
|
4822
|
+
errors.push("PIN must contain only digits");
|
|
4823
|
+
}
|
|
4824
|
+
if (this.isWeakPattern(pin)) {
|
|
4825
|
+
errors.push("PIN is too simple");
|
|
4826
|
+
suggestions.push("Avoid sequential numbers (1234) or repeated digits (1111)");
|
|
4827
|
+
}
|
|
4828
|
+
let strength = "strong";
|
|
4829
|
+
if (errors.length > 0) {
|
|
4830
|
+
strength = "weak";
|
|
4831
|
+
} else if (pin.length <= 4 || this.hasRepeatedDigits(pin)) {
|
|
4832
|
+
strength = "weak";
|
|
4833
|
+
suggestions.push("Use at least 6 digits for better security");
|
|
4834
|
+
} else if (pin.length <= 6) {
|
|
4835
|
+
strength = "medium";
|
|
4836
|
+
suggestions.push("Use 8+ digits for maximum security");
|
|
4837
|
+
}
|
|
4838
|
+
return {
|
|
4839
|
+
valid: errors.length === 0,
|
|
4840
|
+
strength,
|
|
4841
|
+
errors,
|
|
4842
|
+
suggestions
|
|
4843
|
+
};
|
|
4844
|
+
}
|
|
4845
|
+
/**
|
|
4846
|
+
* Check PIN strength (0-100 score)
|
|
4847
|
+
*/
|
|
4848
|
+
static strengthScore(pin) {
|
|
4849
|
+
if (!pin) return 0;
|
|
4850
|
+
let score = 0;
|
|
4851
|
+
score += Math.min(pin.length * 10, 50);
|
|
4852
|
+
const uniqueDigits = new Set(pin).size;
|
|
4853
|
+
score += uniqueDigits * 5;
|
|
4854
|
+
if (!this.isSequential(pin)) {
|
|
4855
|
+
score += 20;
|
|
4856
|
+
}
|
|
4857
|
+
if (!this.hasRepeatedDigits(pin)) {
|
|
4858
|
+
score += 10;
|
|
4859
|
+
}
|
|
4860
|
+
return Math.min(score, 100);
|
|
4861
|
+
}
|
|
4862
|
+
/**
|
|
4863
|
+
* Generate a secure random PIN
|
|
4864
|
+
*/
|
|
4865
|
+
static generateSecureRandom(length = 6) {
|
|
4866
|
+
if (length < 4 || length > 12) {
|
|
4867
|
+
throw new Error("PIN length must be between 4 and 12");
|
|
4868
|
+
}
|
|
4869
|
+
const array = new Uint32Array(length);
|
|
4870
|
+
crypto.getRandomValues(array);
|
|
4871
|
+
const pin = Array.from(array).map((num) => num % 10).join("");
|
|
4872
|
+
if (this.isWeakPattern(pin)) {
|
|
4873
|
+
return this.generateSecureRandom(length);
|
|
4874
|
+
}
|
|
4875
|
+
return pin;
|
|
4876
|
+
}
|
|
4877
|
+
/**
|
|
4878
|
+
* Check for weak patterns
|
|
4879
|
+
*/
|
|
4880
|
+
static isWeakPattern(pin) {
|
|
4881
|
+
if (this.isSequential(pin)) return true;
|
|
4882
|
+
if (/^(\d)\1+$/.test(pin)) return true;
|
|
4883
|
+
const commonPatterns = [
|
|
4884
|
+
"1234",
|
|
4885
|
+
"4321",
|
|
4886
|
+
"1111",
|
|
4887
|
+
"2222",
|
|
4888
|
+
"3333",
|
|
4889
|
+
"4444",
|
|
4890
|
+
"5555",
|
|
4891
|
+
"6666",
|
|
4892
|
+
"7777",
|
|
4893
|
+
"8888",
|
|
4894
|
+
"9999",
|
|
4895
|
+
"0000",
|
|
4896
|
+
"1212",
|
|
4897
|
+
"2323",
|
|
4898
|
+
"0123",
|
|
4899
|
+
"3210",
|
|
4900
|
+
"9876",
|
|
4901
|
+
"6789"
|
|
4902
|
+
];
|
|
4903
|
+
return commonPatterns.some((pattern) => pin.includes(pattern));
|
|
4904
|
+
}
|
|
4905
|
+
/**
|
|
4906
|
+
* Check if PIN is sequential
|
|
4907
|
+
*/
|
|
4908
|
+
static isSequential(pin) {
|
|
4909
|
+
for (let i = 1; i < pin.length; i++) {
|
|
4910
|
+
const diff = parseInt(pin[i]) - parseInt(pin[i - 1]);
|
|
4911
|
+
if (Math.abs(diff) !== 1) return false;
|
|
4912
|
+
}
|
|
4913
|
+
return true;
|
|
4914
|
+
}
|
|
4915
|
+
/**
|
|
4916
|
+
* Check if PIN has repeated digits
|
|
4917
|
+
*/
|
|
4918
|
+
static hasRepeatedDigits(pin) {
|
|
4919
|
+
return /(\d)\1{2,}/.test(pin);
|
|
4920
|
+
}
|
|
4921
|
+
/**
|
|
4922
|
+
* Hash PIN for storage (if needed)
|
|
4923
|
+
* Note: For device-bound keys, the PIN is used for key derivation, not storage
|
|
4924
|
+
*/
|
|
4925
|
+
static async hashPIN(pin, salt) {
|
|
4926
|
+
const actualSalt = salt || crypto.getRandomValues(new Uint8Array(16));
|
|
4927
|
+
const encoder = new TextEncoder();
|
|
4928
|
+
const data = encoder.encode(pin + actualSalt);
|
|
4929
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
4930
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
4931
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4932
|
+
}
|
|
4933
|
+
};
|
|
4934
|
+
var PINRateLimiter = class {
|
|
4935
|
+
attempts = /* @__PURE__ */ new Map();
|
|
4936
|
+
locked = /* @__PURE__ */ new Map();
|
|
4937
|
+
maxAttempts;
|
|
4938
|
+
windowMs;
|
|
4939
|
+
lockoutMs;
|
|
4940
|
+
constructor(config = {}) {
|
|
4941
|
+
this.maxAttempts = config.maxAttempts || 3;
|
|
4942
|
+
this.windowMs = config.windowMs || 6e4;
|
|
4943
|
+
this.lockoutMs = config.lockoutMs || 3e5;
|
|
4944
|
+
}
|
|
4945
|
+
/**
|
|
4946
|
+
* Check if attempt is allowed
|
|
4947
|
+
*/
|
|
4948
|
+
async checkAttempt(sessionKeyId) {
|
|
4949
|
+
const now = Date.now();
|
|
4950
|
+
const lockoutUntil = this.locked.get(sessionKeyId);
|
|
4951
|
+
if (lockoutUntil && now < lockoutUntil) {
|
|
4952
|
+
const lockoutSeconds = Math.ceil((lockoutUntil - now) / 1e3);
|
|
4953
|
+
return {
|
|
4954
|
+
allowed: false,
|
|
4955
|
+
remainingAttempts: 0,
|
|
4956
|
+
lockoutSeconds
|
|
4957
|
+
};
|
|
4958
|
+
}
|
|
4959
|
+
if (lockoutUntil && now >= lockoutUntil) {
|
|
4960
|
+
this.locked.delete(sessionKeyId);
|
|
4961
|
+
this.attempts.delete(sessionKeyId);
|
|
4962
|
+
}
|
|
4963
|
+
const recentAttempts = this.getRecentAttempts(sessionKeyId, now);
|
|
4964
|
+
const remainingAttempts = this.maxAttempts - recentAttempts.length;
|
|
4965
|
+
if (remainingAttempts <= 0) {
|
|
4966
|
+
this.locked.set(sessionKeyId, now + this.lockoutMs);
|
|
4967
|
+
return {
|
|
4968
|
+
allowed: false,
|
|
4969
|
+
remainingAttempts: 0,
|
|
4970
|
+
lockoutSeconds: Math.ceil(this.lockoutMs / 1e3)
|
|
4971
|
+
};
|
|
4972
|
+
}
|
|
4973
|
+
return {
|
|
4974
|
+
allowed: true,
|
|
4975
|
+
remainingAttempts
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
/**
|
|
4979
|
+
* Record a failed attempt
|
|
4980
|
+
*/
|
|
4981
|
+
recordFailedAttempt(sessionKeyId) {
|
|
4982
|
+
const now = Date.now();
|
|
4983
|
+
const attempts = this.attempts.get(sessionKeyId) || [];
|
|
4984
|
+
attempts.push(now);
|
|
4985
|
+
this.attempts.set(sessionKeyId, attempts);
|
|
4986
|
+
}
|
|
4987
|
+
/**
|
|
4988
|
+
* Record a successful attempt (clears history)
|
|
4989
|
+
*/
|
|
4990
|
+
recordSuccessfulAttempt(sessionKeyId) {
|
|
4991
|
+
this.attempts.delete(sessionKeyId);
|
|
4992
|
+
this.locked.delete(sessionKeyId);
|
|
4993
|
+
}
|
|
4994
|
+
/**
|
|
4995
|
+
* Get recent attempts within window
|
|
4996
|
+
*/
|
|
4997
|
+
getRecentAttempts(sessionKeyId, now) {
|
|
4998
|
+
const attempts = this.attempts.get(sessionKeyId) || [];
|
|
4999
|
+
const cutoff = now - this.windowMs;
|
|
5000
|
+
const recent = attempts.filter((timestamp) => timestamp > cutoff);
|
|
5001
|
+
if (recent.length !== attempts.length) {
|
|
5002
|
+
this.attempts.set(sessionKeyId, recent);
|
|
5003
|
+
}
|
|
5004
|
+
return recent;
|
|
5005
|
+
}
|
|
5006
|
+
/**
|
|
5007
|
+
* Clear all rate limit data
|
|
5008
|
+
*/
|
|
5009
|
+
clear() {
|
|
5010
|
+
this.attempts.clear();
|
|
5011
|
+
this.locked.clear();
|
|
5012
|
+
}
|
|
5013
|
+
/**
|
|
5014
|
+
* Check lockout status
|
|
5015
|
+
*/
|
|
5016
|
+
isLockedOut(sessionKeyId) {
|
|
5017
|
+
const lockoutUntil = this.locked.get(sessionKeyId);
|
|
5018
|
+
return lockoutUntil ? Date.now() < lockoutUntil : false;
|
|
5019
|
+
}
|
|
5020
|
+
/**
|
|
5021
|
+
* Get remaining lockout time
|
|
5022
|
+
*/
|
|
5023
|
+
getRemainingLockoutTime(sessionKeyId) {
|
|
5024
|
+
const lockoutUntil = this.locked.get(sessionKeyId);
|
|
5025
|
+
if (!lockoutUntil) return 0;
|
|
5026
|
+
return Math.max(0, lockoutUntil - Date.now());
|
|
5027
|
+
}
|
|
5028
|
+
};
|
|
5029
|
+
var SecureStorage = class {
|
|
5030
|
+
/**
|
|
5031
|
+
* Store data with encryption (basic)
|
|
5032
|
+
* For production, consider using Web Crypto API with user-derived keys
|
|
5033
|
+
*/
|
|
5034
|
+
static async setEncrypted(key, value, secret) {
|
|
5035
|
+
const encrypted = await this.encrypt(value, secret);
|
|
5036
|
+
localStorage.setItem(key, JSON.stringify(encrypted));
|
|
5037
|
+
}
|
|
5038
|
+
/**
|
|
5039
|
+
* Retrieve encrypted data
|
|
5040
|
+
*/
|
|
5041
|
+
static async getEncrypted(key, secret) {
|
|
5042
|
+
const stored = localStorage.getItem(key);
|
|
5043
|
+
if (!stored) return null;
|
|
5044
|
+
try {
|
|
5045
|
+
const encrypted = JSON.parse(stored);
|
|
5046
|
+
return await this.decrypt(encrypted, secret);
|
|
5047
|
+
} catch {
|
|
5048
|
+
return null;
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
/**
|
|
5052
|
+
* Encrypt string with AES-GCM
|
|
5053
|
+
*/
|
|
5054
|
+
static async encrypt(plaintext, secret) {
|
|
5055
|
+
const encoder = new TextEncoder();
|
|
5056
|
+
const data = encoder.encode(plaintext);
|
|
5057
|
+
const key = await this.deriveKey(secret);
|
|
5058
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
5059
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
5060
|
+
{ name: "AES-GCM", iv },
|
|
5061
|
+
key,
|
|
5062
|
+
data
|
|
5063
|
+
);
|
|
5064
|
+
return {
|
|
5065
|
+
ciphertext: btoa(String.fromCharCode(...new Uint8Array(encrypted))),
|
|
5066
|
+
iv: btoa(String.fromCharCode(...iv))
|
|
5067
|
+
};
|
|
5068
|
+
}
|
|
5069
|
+
/**
|
|
5070
|
+
* Decrypt AES-GCM ciphertext
|
|
5071
|
+
*/
|
|
5072
|
+
static async decrypt(encrypted, secret) {
|
|
5073
|
+
const key = await this.deriveKey(secret);
|
|
5074
|
+
const iv = Uint8Array.from(atob(encrypted.iv), (c) => c.charCodeAt(0));
|
|
5075
|
+
const ciphertext = Uint8Array.from(atob(encrypted.ciphertext), (c) => c.charCodeAt(0));
|
|
5076
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
5077
|
+
{ name: "AES-GCM", iv },
|
|
5078
|
+
key,
|
|
5079
|
+
ciphertext
|
|
5080
|
+
);
|
|
5081
|
+
const decoder = new TextDecoder();
|
|
5082
|
+
return decoder.decode(decrypted);
|
|
5083
|
+
}
|
|
5084
|
+
/**
|
|
5085
|
+
* Derive encryption key from secret
|
|
5086
|
+
*/
|
|
5087
|
+
static async deriveKey(secret) {
|
|
5088
|
+
const encoder = new TextEncoder();
|
|
5089
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
5090
|
+
"raw",
|
|
5091
|
+
encoder.encode(secret),
|
|
5092
|
+
{ name: "PBKDF2" },
|
|
5093
|
+
false,
|
|
5094
|
+
["deriveBits", "deriveKey"]
|
|
5095
|
+
);
|
|
5096
|
+
return await crypto.subtle.deriveKey(
|
|
5097
|
+
{
|
|
5098
|
+
name: "PBKDF2",
|
|
5099
|
+
salt: encoder.encode("zendfi-secure-storage"),
|
|
5100
|
+
iterations: 1e5,
|
|
5101
|
+
hash: "SHA-256"
|
|
5102
|
+
},
|
|
5103
|
+
keyMaterial,
|
|
5104
|
+
{ name: "AES-GCM", length: 256 },
|
|
5105
|
+
false,
|
|
5106
|
+
["encrypt", "decrypt"]
|
|
5107
|
+
);
|
|
5108
|
+
}
|
|
5109
|
+
/**
|
|
5110
|
+
* Clear all secure storage
|
|
5111
|
+
*/
|
|
5112
|
+
static clearAll(namespace = "zendfi") {
|
|
5113
|
+
const keysToRemove = [];
|
|
5114
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
5115
|
+
const key = localStorage.key(i);
|
|
5116
|
+
if (key?.startsWith(namespace)) {
|
|
5117
|
+
keysToRemove.push(key);
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
|
5121
|
+
}
|
|
5122
|
+
};
|
|
5123
|
+
|
|
5124
|
+
// src/helpers/polling.ts
|
|
5125
|
+
var TransactionPoller = class {
|
|
5126
|
+
/**
|
|
5127
|
+
* Wait for transaction confirmation
|
|
5128
|
+
*/
|
|
5129
|
+
static async waitForConfirmation(signature, options = {}) {
|
|
5130
|
+
const {
|
|
5131
|
+
timeout = 6e4,
|
|
5132
|
+
interval = 2e3,
|
|
5133
|
+
maxInterval = 1e4,
|
|
5134
|
+
maxAttempts = 30,
|
|
5135
|
+
commitment = "confirmed",
|
|
5136
|
+
rpcUrl
|
|
5137
|
+
} = options;
|
|
5138
|
+
const startTime = Date.now();
|
|
5139
|
+
let currentInterval = interval;
|
|
5140
|
+
let attempts = 0;
|
|
5141
|
+
while (true) {
|
|
5142
|
+
attempts++;
|
|
5143
|
+
if (Date.now() - startTime > timeout) {
|
|
5144
|
+
return {
|
|
5145
|
+
confirmed: false,
|
|
5146
|
+
signature,
|
|
5147
|
+
error: `Transaction confirmation timeout after ${timeout}ms`
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
5150
|
+
if (attempts > maxAttempts) {
|
|
5151
|
+
return {
|
|
5152
|
+
confirmed: false,
|
|
5153
|
+
signature,
|
|
5154
|
+
error: `Maximum polling attempts (${maxAttempts}) exceeded`
|
|
5155
|
+
};
|
|
5156
|
+
}
|
|
5157
|
+
try {
|
|
5158
|
+
const status = await this.checkTransactionStatus(signature, commitment, rpcUrl);
|
|
5159
|
+
if (status.confirmed) {
|
|
5160
|
+
return status;
|
|
5161
|
+
}
|
|
5162
|
+
if (status.error) {
|
|
5163
|
+
return status;
|
|
5164
|
+
}
|
|
5165
|
+
await this.sleep(currentInterval);
|
|
5166
|
+
currentInterval = Math.min(currentInterval * 1.5, maxInterval);
|
|
5167
|
+
} catch (error) {
|
|
5168
|
+
await this.sleep(currentInterval);
|
|
5169
|
+
currentInterval = Math.min(currentInterval * 1.5, maxInterval);
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
/**
|
|
5174
|
+
* Check transaction status via RPC
|
|
5175
|
+
*/
|
|
5176
|
+
static async checkTransactionStatus(signature, commitment = "confirmed", rpcUrl) {
|
|
5177
|
+
const endpoint = rpcUrl || this.getDefaultRpcUrl();
|
|
5178
|
+
const response = await fetch(endpoint, {
|
|
5179
|
+
method: "POST",
|
|
5180
|
+
headers: { "Content-Type": "application/json" },
|
|
5181
|
+
body: JSON.stringify({
|
|
5182
|
+
jsonrpc: "2.0",
|
|
5183
|
+
id: 1,
|
|
5184
|
+
method: "getSignatureStatuses",
|
|
5185
|
+
params: [[signature], { searchTransactionHistory: true }]
|
|
5186
|
+
})
|
|
5187
|
+
});
|
|
5188
|
+
if (!response.ok) {
|
|
5189
|
+
throw new Error(`RPC error: ${response.status} ${response.statusText}`);
|
|
5190
|
+
}
|
|
5191
|
+
const data = await response.json();
|
|
5192
|
+
if (data.error) {
|
|
5193
|
+
return {
|
|
5194
|
+
confirmed: false,
|
|
5195
|
+
signature,
|
|
5196
|
+
error: data.error.message || "RPC error"
|
|
5197
|
+
};
|
|
5198
|
+
}
|
|
5199
|
+
const status = data.result?.value?.[0];
|
|
5200
|
+
if (!status) {
|
|
5201
|
+
return {
|
|
5202
|
+
confirmed: false,
|
|
5203
|
+
signature
|
|
5204
|
+
};
|
|
5205
|
+
}
|
|
5206
|
+
const isConfirmed = this.isCommitmentReached(status, commitment);
|
|
5207
|
+
return {
|
|
5208
|
+
confirmed: isConfirmed,
|
|
5209
|
+
signature,
|
|
5210
|
+
slot: status.slot,
|
|
5211
|
+
confirmations: status.confirmations,
|
|
5212
|
+
error: status.err ? JSON.stringify(status.err) : void 0
|
|
5213
|
+
};
|
|
5214
|
+
}
|
|
5215
|
+
/**
|
|
5216
|
+
* Check if commitment level is reached
|
|
5217
|
+
*/
|
|
5218
|
+
static isCommitmentReached(status, commitment) {
|
|
5219
|
+
if (status.err) return false;
|
|
5220
|
+
switch (commitment) {
|
|
5221
|
+
case "processed":
|
|
5222
|
+
return true;
|
|
5223
|
+
// Any status means processed
|
|
5224
|
+
case "confirmed":
|
|
5225
|
+
return status.confirmationStatus === "confirmed" || status.confirmationStatus === "finalized";
|
|
5226
|
+
case "finalized":
|
|
5227
|
+
return status.confirmationStatus === "finalized";
|
|
5228
|
+
default:
|
|
5229
|
+
return false;
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
/**
|
|
5233
|
+
* Get default RPC URL based on environment
|
|
5234
|
+
*/
|
|
5235
|
+
static getDefaultRpcUrl() {
|
|
5236
|
+
if (typeof window !== "undefined" && window.location) {
|
|
5237
|
+
const hostname = window.location.hostname;
|
|
5238
|
+
if (hostname.includes("localhost") || hostname.includes("dev")) {
|
|
5239
|
+
return "https://api.devnet.solana.com";
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
return "https://api.mainnet-beta.solana.com";
|
|
5243
|
+
}
|
|
5244
|
+
/**
|
|
5245
|
+
* Poll multiple transactions in parallel
|
|
5246
|
+
*/
|
|
5247
|
+
static async waitForMultiple(signatures, options = {}) {
|
|
5248
|
+
return await Promise.all(
|
|
5249
|
+
signatures.map((sig) => this.waitForConfirmation(sig, options))
|
|
5250
|
+
);
|
|
5251
|
+
}
|
|
5252
|
+
/**
|
|
5253
|
+
* Get transaction details after confirmation
|
|
5254
|
+
*/
|
|
5255
|
+
static async getTransactionDetails(signature, rpcUrl) {
|
|
5256
|
+
const endpoint = rpcUrl || this.getDefaultRpcUrl();
|
|
5257
|
+
const response = await fetch(endpoint, {
|
|
5258
|
+
method: "POST",
|
|
5259
|
+
headers: { "Content-Type": "application/json" },
|
|
5260
|
+
body: JSON.stringify({
|
|
5261
|
+
jsonrpc: "2.0",
|
|
5262
|
+
id: 1,
|
|
5263
|
+
method: "getTransaction",
|
|
5264
|
+
params: [
|
|
5265
|
+
signature,
|
|
5266
|
+
{
|
|
5267
|
+
encoding: "jsonParsed",
|
|
5268
|
+
commitment: "confirmed",
|
|
5269
|
+
maxSupportedTransactionVersion: 0
|
|
5270
|
+
}
|
|
5271
|
+
]
|
|
5272
|
+
})
|
|
5273
|
+
});
|
|
5274
|
+
if (!response.ok) {
|
|
5275
|
+
throw new Error(`RPC error: ${response.status}`);
|
|
5276
|
+
}
|
|
5277
|
+
const data = await response.json();
|
|
5278
|
+
if (data.error) {
|
|
5279
|
+
throw new Error(data.error.message || "Failed to get transaction");
|
|
5280
|
+
}
|
|
5281
|
+
return data.result;
|
|
5282
|
+
}
|
|
5283
|
+
/**
|
|
5284
|
+
* Check if transaction exists on chain
|
|
5285
|
+
*/
|
|
5286
|
+
static async exists(signature, rpcUrl) {
|
|
5287
|
+
try {
|
|
5288
|
+
const status = await this.checkTransactionStatus(signature, "confirmed", rpcUrl);
|
|
5289
|
+
return status.confirmed || !!status.slot;
|
|
5290
|
+
} catch {
|
|
5291
|
+
return false;
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
/**
|
|
5295
|
+
* Get recent blockhash (useful for transaction building)
|
|
5296
|
+
*/
|
|
5297
|
+
static async getRecentBlockhash(rpcUrl) {
|
|
5298
|
+
const endpoint = rpcUrl || this.getDefaultRpcUrl();
|
|
5299
|
+
const response = await fetch(endpoint, {
|
|
5300
|
+
method: "POST",
|
|
5301
|
+
headers: { "Content-Type": "application/json" },
|
|
5302
|
+
body: JSON.stringify({
|
|
5303
|
+
jsonrpc: "2.0",
|
|
5304
|
+
id: 1,
|
|
5305
|
+
method: "getLatestBlockhash",
|
|
5306
|
+
params: [{ commitment: "finalized" }]
|
|
5307
|
+
})
|
|
5308
|
+
});
|
|
5309
|
+
if (!response.ok) {
|
|
5310
|
+
throw new Error(`RPC error: ${response.status}`);
|
|
5311
|
+
}
|
|
5312
|
+
const data = await response.json();
|
|
5313
|
+
if (data.error) {
|
|
5314
|
+
throw new Error(data.error.message);
|
|
5315
|
+
}
|
|
5316
|
+
return data.result.value;
|
|
5317
|
+
}
|
|
5318
|
+
/**
|
|
5319
|
+
* Sleep utility
|
|
5320
|
+
*/
|
|
5321
|
+
static sleep(ms) {
|
|
5322
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5323
|
+
}
|
|
5324
|
+
};
|
|
5325
|
+
var TransactionMonitor = class {
|
|
5326
|
+
monitors = /* @__PURE__ */ new Map();
|
|
5327
|
+
/**
|
|
5328
|
+
* Start monitoring a transaction
|
|
5329
|
+
*/
|
|
5330
|
+
monitor(signature, callbacks, options = {}) {
|
|
5331
|
+
this.stopMonitoring(signature);
|
|
5332
|
+
const { timeout = 6e4, interval = 2e3 } = options;
|
|
5333
|
+
const startTime = Date.now();
|
|
5334
|
+
const intervalId = setInterval(async () => {
|
|
5335
|
+
if (Date.now() - startTime > timeout) {
|
|
5336
|
+
this.stopMonitoring(signature);
|
|
5337
|
+
callbacks.onTimeout?.();
|
|
5338
|
+
return;
|
|
5339
|
+
}
|
|
5340
|
+
try {
|
|
5341
|
+
const status = await TransactionPoller.waitForConfirmation(signature, {
|
|
5342
|
+
...options,
|
|
5343
|
+
maxAttempts: 1
|
|
5344
|
+
// Single check per interval
|
|
5345
|
+
});
|
|
5346
|
+
if (status.confirmed) {
|
|
5347
|
+
this.stopMonitoring(signature);
|
|
5348
|
+
callbacks.onConfirmed?.(status);
|
|
5349
|
+
} else if (status.error) {
|
|
5350
|
+
this.stopMonitoring(signature);
|
|
5351
|
+
callbacks.onFailed?.(status);
|
|
5352
|
+
}
|
|
5353
|
+
} catch (error) {
|
|
5354
|
+
}
|
|
5355
|
+
}, interval);
|
|
5356
|
+
this.monitors.set(signature, { interval: intervalId, callbacks });
|
|
5357
|
+
}
|
|
5358
|
+
/**
|
|
5359
|
+
* Stop monitoring a transaction
|
|
5360
|
+
*/
|
|
5361
|
+
stopMonitoring(signature) {
|
|
5362
|
+
const monitor = this.monitors.get(signature);
|
|
5363
|
+
if (monitor) {
|
|
5364
|
+
clearInterval(monitor.interval);
|
|
5365
|
+
this.monitors.delete(signature);
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
/**
|
|
5369
|
+
* Stop all monitors
|
|
5370
|
+
*/
|
|
5371
|
+
stopAll() {
|
|
5372
|
+
for (const [signature] of this.monitors) {
|
|
5373
|
+
this.stopMonitoring(signature);
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
/**
|
|
5377
|
+
* Get active monitors
|
|
5378
|
+
*/
|
|
5379
|
+
getActiveMonitors() {
|
|
5380
|
+
return Array.from(this.monitors.keys());
|
|
5381
|
+
}
|
|
5382
|
+
};
|
|
5383
|
+
|
|
5384
|
+
// src/helpers/recovery.ts
|
|
5385
|
+
var RetryStrategy = class {
|
|
5386
|
+
/**
|
|
5387
|
+
* Execute function with retry logic
|
|
5388
|
+
*/
|
|
5389
|
+
static async withRetry(fn, options = {}) {
|
|
5390
|
+
const {
|
|
5391
|
+
maxAttempts = 3,
|
|
5392
|
+
backoffMs = 1e3,
|
|
5393
|
+
backoffMultiplier = 2,
|
|
5394
|
+
maxBackoffMs = 3e4,
|
|
5395
|
+
shouldRetry = () => true,
|
|
5396
|
+
onRetry
|
|
5397
|
+
} = options;
|
|
5398
|
+
let lastError = null;
|
|
5399
|
+
let currentBackoff = backoffMs;
|
|
5400
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
5401
|
+
try {
|
|
5402
|
+
return await fn();
|
|
5403
|
+
} catch (error) {
|
|
5404
|
+
lastError = error;
|
|
5405
|
+
if (attempt === maxAttempts || !shouldRetry(error, attempt)) {
|
|
5406
|
+
throw error;
|
|
5407
|
+
}
|
|
5408
|
+
const jitter = Math.random() * 0.3 * currentBackoff;
|
|
5409
|
+
const nextDelay = Math.min(currentBackoff + jitter, maxBackoffMs);
|
|
5410
|
+
onRetry?.(error, attempt, nextDelay);
|
|
5411
|
+
await this.sleep(nextDelay);
|
|
5412
|
+
currentBackoff *= backoffMultiplier;
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
throw lastError || new Error("Retry failed");
|
|
5416
|
+
}
|
|
5417
|
+
/**
|
|
5418
|
+
* Retry with linear backoff
|
|
5419
|
+
*/
|
|
5420
|
+
static async withLinearRetry(fn, maxAttempts = 3, delayMs = 1e3) {
|
|
5421
|
+
return this.withRetry(fn, {
|
|
5422
|
+
maxAttempts,
|
|
5423
|
+
backoffMs: delayMs,
|
|
5424
|
+
backoffMultiplier: 1
|
|
5425
|
+
// Linear
|
|
5426
|
+
});
|
|
5427
|
+
}
|
|
5428
|
+
/**
|
|
5429
|
+
* Retry with custom backoff function
|
|
5430
|
+
*/
|
|
5431
|
+
static async withCustomBackoff(fn, backoffFn, maxAttempts = 3) {
|
|
5432
|
+
let lastError = null;
|
|
5433
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
5434
|
+
try {
|
|
5435
|
+
return await fn();
|
|
5436
|
+
} catch (error) {
|
|
5437
|
+
lastError = error;
|
|
5438
|
+
if (attempt === maxAttempts) {
|
|
5439
|
+
throw error;
|
|
5440
|
+
}
|
|
5441
|
+
const delay = backoffFn(attempt);
|
|
5442
|
+
await this.sleep(delay);
|
|
5443
|
+
}
|
|
5444
|
+
}
|
|
5445
|
+
throw lastError || new Error("Retry failed");
|
|
5446
|
+
}
|
|
5447
|
+
/**
|
|
5448
|
+
* Check if error is retryable
|
|
5449
|
+
*/
|
|
5450
|
+
static isRetryableError(error) {
|
|
5451
|
+
if (error.name === "NetworkError" || error.message?.includes("network")) {
|
|
5452
|
+
return true;
|
|
5453
|
+
}
|
|
5454
|
+
if (error.name === "TimeoutError" || error.message?.includes("timeout")) {
|
|
5455
|
+
return true;
|
|
5456
|
+
}
|
|
5457
|
+
if (error.status === 429 || error.code === "RATE_LIMIT_EXCEEDED") {
|
|
5458
|
+
return true;
|
|
5459
|
+
}
|
|
5460
|
+
if (error.status >= 500 && error.status < 600) {
|
|
5461
|
+
return true;
|
|
5462
|
+
}
|
|
5463
|
+
if (error.message?.includes("blockhash") || error.message?.includes("recent")) {
|
|
5464
|
+
return true;
|
|
5465
|
+
}
|
|
5466
|
+
return false;
|
|
5467
|
+
}
|
|
5468
|
+
static sleep(ms) {
|
|
5469
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5470
|
+
}
|
|
5471
|
+
};
|
|
5472
|
+
var ErrorRecovery = class {
|
|
5473
|
+
/**
|
|
5474
|
+
* Recover from network errors with retry
|
|
5475
|
+
*/
|
|
5476
|
+
static async recoverFromNetworkError(fn, maxAttempts = 3) {
|
|
5477
|
+
return RetryStrategy.withRetry(fn, {
|
|
5478
|
+
maxAttempts,
|
|
5479
|
+
backoffMs: 2e3,
|
|
5480
|
+
shouldRetry: (error) => {
|
|
5481
|
+
return error.name === "NetworkError" || error.message?.includes("network") || error.message?.includes("fetch");
|
|
5482
|
+
},
|
|
5483
|
+
onRetry: (error, attempt, nextDelay) => {
|
|
5484
|
+
console.warn(
|
|
5485
|
+
`Network error (attempt ${attempt}), retrying in ${nextDelay}ms:`,
|
|
5486
|
+
error.message
|
|
5487
|
+
);
|
|
5488
|
+
}
|
|
5489
|
+
});
|
|
5490
|
+
}
|
|
5491
|
+
/**
|
|
5492
|
+
* Recover from rate limit errors with exponential backoff
|
|
5493
|
+
*/
|
|
5494
|
+
static async recoverFromRateLimit(fn, maxAttempts = 5) {
|
|
5495
|
+
return RetryStrategy.withRetry(fn, {
|
|
5496
|
+
maxAttempts,
|
|
5497
|
+
backoffMs: 5e3,
|
|
5498
|
+
// Start with 5 seconds
|
|
5499
|
+
backoffMultiplier: 2,
|
|
5500
|
+
maxBackoffMs: 6e4,
|
|
5501
|
+
// Cap at 1 minute
|
|
5502
|
+
shouldRetry: (error) => {
|
|
5503
|
+
return error.status === 429 || error.code === "RATE_LIMIT_EXCEEDED";
|
|
5504
|
+
},
|
|
5505
|
+
onRetry: (attempt, nextDelay) => {
|
|
5506
|
+
console.warn(
|
|
5507
|
+
`Rate limited (attempt ${attempt}), waiting ${nextDelay}ms before retry`
|
|
5508
|
+
);
|
|
5509
|
+
}
|
|
5510
|
+
});
|
|
5511
|
+
}
|
|
5512
|
+
/**
|
|
5513
|
+
* Recover from Solana RPC errors (blockhash, etc.)
|
|
5514
|
+
*/
|
|
5515
|
+
static async recoverFromRPCError(fn, maxAttempts = 3) {
|
|
5516
|
+
return RetryStrategy.withRetry(fn, {
|
|
5517
|
+
maxAttempts,
|
|
5518
|
+
backoffMs: 1e3,
|
|
5519
|
+
shouldRetry: (error) => {
|
|
5520
|
+
const message = error.message?.toLowerCase() || "";
|
|
5521
|
+
return message.includes("blockhash") || message.includes("recent") || message.includes("slot") || message.includes("rpc");
|
|
5522
|
+
},
|
|
5523
|
+
onRetry: (error, attempt, nextDelay) => {
|
|
5524
|
+
console.warn(
|
|
5525
|
+
`RPC error (attempt ${attempt}), retrying in ${nextDelay}ms:`,
|
|
5526
|
+
error.message
|
|
5527
|
+
);
|
|
5528
|
+
}
|
|
5529
|
+
});
|
|
5530
|
+
}
|
|
5531
|
+
/**
|
|
5532
|
+
* Recover from timeout errors
|
|
5533
|
+
*/
|
|
5534
|
+
static async recoverFromTimeout(fn, timeoutMs = 3e4, maxAttempts = 2) {
|
|
5535
|
+
return RetryStrategy.withRetry(
|
|
5536
|
+
() => this.withTimeout(fn, timeoutMs),
|
|
5537
|
+
{
|
|
5538
|
+
maxAttempts,
|
|
5539
|
+
backoffMs: 5e3,
|
|
5540
|
+
shouldRetry: (error) => {
|
|
5541
|
+
return error.name === "TimeoutError" || error.message?.includes("timeout");
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
);
|
|
5545
|
+
}
|
|
5546
|
+
/**
|
|
5547
|
+
* Add timeout to async function
|
|
5548
|
+
*/
|
|
5549
|
+
static async withTimeout(fn, timeoutMs) {
|
|
5550
|
+
return Promise.race([
|
|
5551
|
+
fn(),
|
|
5552
|
+
new Promise((_, reject) => {
|
|
5553
|
+
setTimeout(() => {
|
|
5554
|
+
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
5555
|
+
}, timeoutMs);
|
|
5556
|
+
})
|
|
5557
|
+
]);
|
|
5558
|
+
}
|
|
5559
|
+
/**
|
|
5560
|
+
* Circuit breaker pattern for repeated failures
|
|
5561
|
+
*/
|
|
5562
|
+
static createCircuitBreaker(fn, options = {}) {
|
|
5563
|
+
const {
|
|
5564
|
+
failureThreshold = 5,
|
|
5565
|
+
resetTimeoutMs = 6e4,
|
|
5566
|
+
onStateChange
|
|
5567
|
+
} = options;
|
|
5568
|
+
let state = "closed";
|
|
5569
|
+
let failureCount = 0;
|
|
5570
|
+
let lastFailureTime = 0;
|
|
5571
|
+
let resetTimer = null;
|
|
5572
|
+
return async () => {
|
|
5573
|
+
if (state === "open" && Date.now() - lastFailureTime > resetTimeoutMs) {
|
|
5574
|
+
state = "half-open";
|
|
5575
|
+
onStateChange?.("half-open");
|
|
5576
|
+
}
|
|
5577
|
+
if (state === "open") {
|
|
5578
|
+
throw new Error("Circuit breaker is OPEN - too many failures");
|
|
5579
|
+
}
|
|
5580
|
+
try {
|
|
5581
|
+
const result = await fn();
|
|
5582
|
+
if (state === "half-open") {
|
|
5583
|
+
state = "closed";
|
|
5584
|
+
onStateChange?.("closed");
|
|
5585
|
+
}
|
|
5586
|
+
failureCount = 0;
|
|
5587
|
+
return result;
|
|
5588
|
+
} catch (error) {
|
|
5589
|
+
failureCount++;
|
|
5590
|
+
lastFailureTime = Date.now();
|
|
5591
|
+
if (failureCount >= failureThreshold) {
|
|
5592
|
+
state = "open";
|
|
5593
|
+
onStateChange?.("open");
|
|
5594
|
+
if (resetTimer) clearTimeout(resetTimer);
|
|
5595
|
+
resetTimer = setTimeout(() => {
|
|
5596
|
+
state = "half-open";
|
|
5597
|
+
onStateChange?.("half-open");
|
|
5598
|
+
}, resetTimeoutMs);
|
|
5599
|
+
}
|
|
5600
|
+
throw error;
|
|
5601
|
+
}
|
|
5602
|
+
};
|
|
5603
|
+
}
|
|
5604
|
+
/**
|
|
5605
|
+
* Fallback to alternative function on error
|
|
5606
|
+
*/
|
|
5607
|
+
static async withFallback(primaryFn, fallbackFn, shouldFallback = () => true) {
|
|
5608
|
+
try {
|
|
5609
|
+
return await primaryFn();
|
|
5610
|
+
} catch (error) {
|
|
5611
|
+
if (shouldFallback(error)) {
|
|
5612
|
+
console.warn("Primary function failed, using fallback:", error.message);
|
|
5613
|
+
return await fallbackFn();
|
|
5614
|
+
}
|
|
5615
|
+
throw error;
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
/**
|
|
5619
|
+
* Graceful degradation - return partial result on error
|
|
5620
|
+
*/
|
|
5621
|
+
static async withGracefulDegradation(fn, defaultValue, onError) {
|
|
5622
|
+
try {
|
|
5623
|
+
return await fn();
|
|
5624
|
+
} catch (error) {
|
|
5625
|
+
onError?.(error);
|
|
5626
|
+
console.warn("Operation failed, returning default value:", error.message);
|
|
5627
|
+
return defaultValue;
|
|
5628
|
+
}
|
|
5629
|
+
}
|
|
5630
|
+
};
|
|
5631
|
+
|
|
5632
|
+
// src/helpers/lifecycle.ts
|
|
5633
|
+
var SessionKeyLifecycle = class {
|
|
5634
|
+
constructor(client, config = {}) {
|
|
5635
|
+
this.client = client;
|
|
5636
|
+
this.config = config;
|
|
5637
|
+
if (config.autoCleanup && typeof window !== "undefined") {
|
|
5638
|
+
window.addEventListener("beforeunload", () => {
|
|
5639
|
+
this.cleanup().catch(console.error);
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
sessionKeyId = null;
|
|
5644
|
+
sessionWallet = null;
|
|
5645
|
+
encryptedKey = null;
|
|
5646
|
+
deviceFingerprint = null;
|
|
5647
|
+
/**
|
|
5648
|
+
* Create and fund session key in one call
|
|
5649
|
+
* Handles: keypair generation → encryption → backend registration → funding
|
|
5650
|
+
*/
|
|
5651
|
+
async createAndFund(config) {
|
|
5652
|
+
const fingerprint = this.config.deviceFingerprintProvider ? await this.config.deviceFingerprintProvider() : await this.generateDeviceFingerprint();
|
|
5653
|
+
this.deviceFingerprint = fingerprint;
|
|
5654
|
+
const pin = this.config.pinProvider ? await this.config.pinProvider() : await this.promptForPIN("Create PIN for session key");
|
|
5655
|
+
const { Keypair: Keypair2 } = await this.getSolanaWeb3();
|
|
5656
|
+
const { SessionKeyCrypto: SessionKeyCrypto2 } = await Promise.resolve().then(() => (init_device_bound_crypto(), device_bound_crypto_exports));
|
|
5657
|
+
const keypair = Keypair2.generate();
|
|
5658
|
+
const encrypted = await SessionKeyCrypto2.encrypt(
|
|
5659
|
+
keypair.secretKey,
|
|
5660
|
+
pin,
|
|
5661
|
+
fingerprint
|
|
5662
|
+
);
|
|
5663
|
+
this.encryptedKey = {
|
|
5664
|
+
ciphertext: encrypted.encryptedData,
|
|
5665
|
+
nonce: encrypted.nonce
|
|
5666
|
+
};
|
|
5667
|
+
const response = await this.client.sessionKeys.createDeviceBound({
|
|
5668
|
+
user_wallet: config.userWallet,
|
|
5669
|
+
agent_id: config.agentId,
|
|
5670
|
+
agent_name: config.agentName,
|
|
5671
|
+
limit_usdc: config.limitUsdc,
|
|
5672
|
+
duration_days: config.durationDays || 7,
|
|
5673
|
+
encrypted_session_key: encrypted.encryptedData,
|
|
5674
|
+
nonce: encrypted.nonce,
|
|
5675
|
+
session_public_key: keypair.publicKey.toBase58(),
|
|
5676
|
+
device_fingerprint: fingerprint
|
|
5677
|
+
});
|
|
5678
|
+
this.sessionKeyId = response.session_key_id;
|
|
5679
|
+
this.sessionWallet = response.session_wallet;
|
|
5680
|
+
if (this.config.cache) {
|
|
5681
|
+
await this.config.cache.getCached(
|
|
5682
|
+
this.sessionKeyId,
|
|
5683
|
+
async () => keypair,
|
|
5684
|
+
{ deviceFingerprint: fingerprint }
|
|
5685
|
+
);
|
|
5686
|
+
}
|
|
5687
|
+
if (config.onApprovalNeeded) {
|
|
5688
|
+
const topupResponse = await this.client.sessionKeys.topUp(
|
|
5689
|
+
this.sessionKeyId,
|
|
5690
|
+
{
|
|
5691
|
+
user_wallet: config.userWallet,
|
|
5692
|
+
amount_usdc: config.limitUsdc,
|
|
5693
|
+
device_fingerprint: fingerprint
|
|
5694
|
+
}
|
|
5695
|
+
);
|
|
5696
|
+
await config.onApprovalNeeded(topupResponse.top_up_transaction);
|
|
5697
|
+
}
|
|
5698
|
+
return {
|
|
5699
|
+
sessionKeyId: this.sessionKeyId,
|
|
5700
|
+
sessionWallet: this.sessionWallet
|
|
5701
|
+
};
|
|
5702
|
+
}
|
|
5703
|
+
/**
|
|
5704
|
+
* Make a payment using the session key
|
|
5705
|
+
* Auto-handles caching, PIN prompts, and signing
|
|
5706
|
+
*/
|
|
5707
|
+
async pay(amount, description) {
|
|
5708
|
+
if (!this.sessionKeyId) {
|
|
5709
|
+
throw new Error("No active session key. Call createAndFund() first.");
|
|
5710
|
+
}
|
|
5711
|
+
let keypair;
|
|
5712
|
+
if (this.config.cache) {
|
|
5713
|
+
keypair = await this.config.cache.getCached(
|
|
5714
|
+
this.sessionKeyId,
|
|
5715
|
+
async () => {
|
|
5716
|
+
const pin = this.config.pinProvider ? await this.config.pinProvider() : await this.promptForPIN("Enter PIN to sign payment");
|
|
5717
|
+
return await this.decryptKeypair(pin);
|
|
5718
|
+
},
|
|
5719
|
+
{ deviceFingerprint: this.deviceFingerprint || void 0 }
|
|
5720
|
+
);
|
|
5721
|
+
} else {
|
|
5722
|
+
const pin = this.config.pinProvider ? await this.config.pinProvider() : await this.promptForPIN("Enter PIN to sign payment");
|
|
5723
|
+
keypair = await this.decryptKeypair(pin);
|
|
5724
|
+
}
|
|
5725
|
+
const paymentResponse = await this.client.smart.execute({
|
|
5726
|
+
user_wallet: this.sessionWallet,
|
|
5727
|
+
// Session wallet, not user wallet
|
|
5728
|
+
amount_usd: amount,
|
|
5729
|
+
description,
|
|
5730
|
+
agent_id: "session-lifecycle",
|
|
5731
|
+
auto_detect_gasless: true
|
|
5732
|
+
});
|
|
5733
|
+
if (paymentResponse.requires_signature && paymentResponse.unsigned_transaction) {
|
|
5734
|
+
const { Transaction: Transaction3 } = await this.getSolanaWeb3();
|
|
5735
|
+
const txBuffer = Uint8Array.from(atob(paymentResponse.unsigned_transaction), (c) => c.charCodeAt(0));
|
|
5736
|
+
const tx = Transaction3.from(txBuffer);
|
|
5737
|
+
tx.partialSign(keypair);
|
|
5738
|
+
const submitUrl = paymentResponse.submit_url || `/api/v1/ai/payments/${paymentResponse.payment_id}/submit-signed`;
|
|
5739
|
+
const submitResponse = await fetch(`${this.client["config"].baseURL}${submitUrl}`, {
|
|
5740
|
+
method: "POST",
|
|
5741
|
+
headers: {
|
|
5742
|
+
"Authorization": `Bearer ${this.client["config"].apiKey}`,
|
|
5743
|
+
"Content-Type": "application/json"
|
|
5744
|
+
},
|
|
5745
|
+
body: JSON.stringify({
|
|
5746
|
+
signed_transaction: btoa(String.fromCharCode(...tx.serialize()))
|
|
5747
|
+
})
|
|
5748
|
+
});
|
|
5749
|
+
if (!submitResponse.ok) {
|
|
5750
|
+
throw new Error(`Failed to submit signed transaction: ${submitResponse.statusText}`);
|
|
5751
|
+
}
|
|
5752
|
+
const submitData = await submitResponse.json();
|
|
5753
|
+
return {
|
|
5754
|
+
paymentId: paymentResponse.payment_id,
|
|
5755
|
+
status: submitData.status,
|
|
5756
|
+
signature: submitData.transaction_signature,
|
|
5757
|
+
confirmedInMs: submitData.confirmed_in_ms
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5760
|
+
return {
|
|
5761
|
+
paymentId: paymentResponse.payment_id,
|
|
5762
|
+
status: paymentResponse.status,
|
|
5763
|
+
signature: paymentResponse.transaction_signature,
|
|
5764
|
+
confirmedInMs: paymentResponse.confirmed_in_ms
|
|
5765
|
+
};
|
|
5766
|
+
}
|
|
5767
|
+
/**
|
|
5768
|
+
* Check session key status
|
|
5769
|
+
*/
|
|
5770
|
+
async getStatus() {
|
|
5771
|
+
if (!this.sessionKeyId) {
|
|
5772
|
+
throw new Error("No active session key");
|
|
5773
|
+
}
|
|
5774
|
+
return await this.client.sessionKeys.getStatus(this.sessionKeyId);
|
|
5775
|
+
}
|
|
5776
|
+
/**
|
|
5777
|
+
* Top up session key
|
|
5778
|
+
*/
|
|
5779
|
+
async topUp(amount, userWallet, onApprovalNeeded) {
|
|
5780
|
+
if (!this.sessionKeyId || !this.deviceFingerprint) {
|
|
5781
|
+
throw new Error("No active session key");
|
|
5782
|
+
}
|
|
5783
|
+
const response = await this.client.sessionKeys.topUp(
|
|
5784
|
+
this.sessionKeyId,
|
|
5785
|
+
{
|
|
5786
|
+
user_wallet: userWallet,
|
|
5787
|
+
amount_usdc: amount,
|
|
5788
|
+
device_fingerprint: this.deviceFingerprint
|
|
5789
|
+
}
|
|
5790
|
+
);
|
|
5791
|
+
if (onApprovalNeeded) {
|
|
5792
|
+
await onApprovalNeeded(response.top_up_transaction);
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
/**
|
|
5796
|
+
* Revoke session key
|
|
5797
|
+
*/
|
|
5798
|
+
async revoke() {
|
|
5799
|
+
if (!this.sessionKeyId) {
|
|
5800
|
+
throw new Error("No active session key");
|
|
5801
|
+
}
|
|
5802
|
+
await this.client.sessionKeys.revoke(this.sessionKeyId);
|
|
5803
|
+
await this.cleanup();
|
|
5804
|
+
}
|
|
5805
|
+
/**
|
|
5806
|
+
* Cleanup (clear cache, reset state)
|
|
5807
|
+
*/
|
|
5808
|
+
async cleanup() {
|
|
5809
|
+
if (this.config.cache && this.sessionKeyId) {
|
|
5810
|
+
await this.config.cache.invalidate(this.sessionKeyId);
|
|
5811
|
+
}
|
|
5812
|
+
this.sessionKeyId = null;
|
|
5813
|
+
this.sessionWallet = null;
|
|
5814
|
+
this.encryptedKey = null;
|
|
5815
|
+
this.deviceFingerprint = null;
|
|
5816
|
+
}
|
|
5817
|
+
/**
|
|
5818
|
+
* Get current session key ID
|
|
5819
|
+
*/
|
|
5820
|
+
getSessionKeyId() {
|
|
5821
|
+
return this.sessionKeyId;
|
|
5822
|
+
}
|
|
5823
|
+
/**
|
|
5824
|
+
* Check if session is active
|
|
5825
|
+
*/
|
|
5826
|
+
isActive() {
|
|
5827
|
+
return this.sessionKeyId !== null;
|
|
5828
|
+
}
|
|
5829
|
+
// ============================================
|
|
5830
|
+
// Private Helpers
|
|
5831
|
+
// ============================================
|
|
5832
|
+
async decryptKeypair(pin) {
|
|
5833
|
+
if (!this.encryptedKey || !this.deviceFingerprint) {
|
|
5834
|
+
throw new Error("No encrypted key available");
|
|
5835
|
+
}
|
|
5836
|
+
const { SessionKeyCrypto: SessionKeyCrypto2 } = await Promise.resolve().then(() => (init_device_bound_crypto(), device_bound_crypto_exports));
|
|
5837
|
+
const encrypted = {
|
|
5838
|
+
encryptedData: this.encryptedKey.ciphertext,
|
|
5839
|
+
nonce: this.encryptedKey.nonce,
|
|
5840
|
+
publicKey: "",
|
|
5841
|
+
// Not needed for decryption
|
|
5842
|
+
deviceFingerprint: this.deviceFingerprint,
|
|
5843
|
+
version: "argon2id-aes256gcm-v1"
|
|
5844
|
+
};
|
|
5845
|
+
return await SessionKeyCrypto2.decrypt(encrypted, pin, this.deviceFingerprint);
|
|
5846
|
+
}
|
|
5847
|
+
async generateDeviceFingerprint() {
|
|
5848
|
+
const { DeviceFingerprintGenerator: DeviceFingerprintGenerator2 } = await Promise.resolve().then(() => (init_device_bound_crypto(), device_bound_crypto_exports));
|
|
5849
|
+
const fingerprint = await DeviceFingerprintGenerator2.generate();
|
|
5850
|
+
return fingerprint.fingerprint;
|
|
5851
|
+
}
|
|
5852
|
+
async promptForPIN(message) {
|
|
5853
|
+
if (typeof window !== "undefined" && window.prompt) {
|
|
5854
|
+
const pin = window.prompt(message);
|
|
5855
|
+
if (!pin) {
|
|
5856
|
+
throw new Error("PIN required");
|
|
5857
|
+
}
|
|
5858
|
+
return pin;
|
|
5859
|
+
}
|
|
5860
|
+
throw new Error("PIN provider not configured and no browser prompt available");
|
|
5861
|
+
}
|
|
5862
|
+
async getSolanaWeb3() {
|
|
5863
|
+
try {
|
|
5864
|
+
return await import("@solana/web3.js");
|
|
5865
|
+
} catch {
|
|
5866
|
+
throw new Error("@solana/web3.js not installed. Install it to use device-bound payments.");
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
};
|
|
5870
|
+
async function setupQuickSessionKey(client, config) {
|
|
5871
|
+
const { SessionKeyCache: SessionKeyCache2 } = await Promise.resolve().then(() => (init_cache(), cache_exports));
|
|
5872
|
+
const lifecycle = new SessionKeyLifecycle(client, {
|
|
5873
|
+
cache: new SessionKeyCache2({ storage: "localStorage", ttl: 36e5 }),
|
|
5874
|
+
autoCleanup: true
|
|
5875
|
+
});
|
|
5876
|
+
await lifecycle.createAndFund({
|
|
5877
|
+
userWallet: config.userWallet,
|
|
5878
|
+
agentId: config.agentId,
|
|
5879
|
+
limitUsdc: config.budgetUsdc,
|
|
5880
|
+
onApprovalNeeded: config.onApproval
|
|
5881
|
+
});
|
|
5882
|
+
return lifecycle;
|
|
5883
|
+
}
|
|
5884
|
+
|
|
5885
|
+
// src/helpers/dev.ts
|
|
5886
|
+
var DevTools = class {
|
|
5887
|
+
static debugEnabled = false;
|
|
5888
|
+
static requestLog = [];
|
|
5889
|
+
/**
|
|
5890
|
+
* Enable debug mode (logs all API requests/responses)
|
|
5891
|
+
*/
|
|
5892
|
+
static enableDebugMode() {
|
|
5893
|
+
if (this.isDevelopment()) {
|
|
5894
|
+
this.debugEnabled = true;
|
|
5895
|
+
console.log("\u{1F527} ZendFi Debug Mode: ENABLED");
|
|
5896
|
+
console.log("All API requests will be logged to console");
|
|
5897
|
+
} else {
|
|
5898
|
+
console.warn("Debug mode can only be enabled in development environment");
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
/**
|
|
5902
|
+
* Disable debug mode
|
|
5903
|
+
*/
|
|
5904
|
+
static disableDebugMode() {
|
|
5905
|
+
this.debugEnabled = false;
|
|
5906
|
+
console.log("\u{1F527} ZendFi Debug Mode: DISABLED");
|
|
5907
|
+
}
|
|
5908
|
+
/**
|
|
5909
|
+
* Check if debug mode is enabled
|
|
5910
|
+
*/
|
|
5911
|
+
static isDebugEnabled() {
|
|
5912
|
+
return this.debugEnabled;
|
|
5913
|
+
}
|
|
5914
|
+
/**
|
|
5915
|
+
* Log API request
|
|
5916
|
+
*/
|
|
5917
|
+
static logRequest(method, url, body) {
|
|
5918
|
+
if (!this.debugEnabled) return;
|
|
5919
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
5920
|
+
console.group(`\u{1F4E4} API Request: ${method} ${url}`);
|
|
5921
|
+
console.log("Time:", timestamp.toISOString());
|
|
5922
|
+
if (body) {
|
|
5923
|
+
console.log("Body:", body);
|
|
5924
|
+
}
|
|
5925
|
+
console.groupEnd();
|
|
5926
|
+
this.requestLog.push({ timestamp, method, url });
|
|
5927
|
+
}
|
|
5928
|
+
/**
|
|
5929
|
+
* Log API response
|
|
5930
|
+
*/
|
|
5931
|
+
static logResponse(method, url, status, data, duration) {
|
|
5932
|
+
if (!this.debugEnabled) return;
|
|
5933
|
+
const emoji = status >= 200 && status < 300 ? "\u2705" : "\u274C";
|
|
5934
|
+
console.group(`${emoji} API Response: ${method} ${url} [${status}]`);
|
|
5935
|
+
if (duration) {
|
|
5936
|
+
console.log("Duration:", `${duration}ms`);
|
|
5937
|
+
}
|
|
5938
|
+
console.log("Data:", data);
|
|
5939
|
+
console.groupEnd();
|
|
5940
|
+
const lastRequest = this.requestLog[this.requestLog.length - 1];
|
|
5941
|
+
if (lastRequest && lastRequest.method === method && lastRequest.url === url) {
|
|
5942
|
+
lastRequest.status = status;
|
|
5943
|
+
lastRequest.duration = duration;
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
/**
|
|
5947
|
+
* Get request log
|
|
5948
|
+
*/
|
|
5949
|
+
static getRequestLog() {
|
|
5950
|
+
return [...this.requestLog];
|
|
5951
|
+
}
|
|
5952
|
+
/**
|
|
5953
|
+
* Clear request log
|
|
5954
|
+
*/
|
|
5955
|
+
static clearRequestLog() {
|
|
5956
|
+
this.requestLog = [];
|
|
5957
|
+
console.log("\u{1F5D1}\uFE0F Request log cleared");
|
|
5958
|
+
}
|
|
5959
|
+
/**
|
|
5960
|
+
* Create a test session key (devnet only)
|
|
5961
|
+
*/
|
|
5962
|
+
static async createTestSessionKey() {
|
|
5963
|
+
if (!this.isDevelopment()) {
|
|
5964
|
+
throw new Error("Test session keys can only be created in development");
|
|
5965
|
+
}
|
|
5966
|
+
const { Keypair: Keypair2 } = await this.getSolanaWeb3();
|
|
5967
|
+
const keypair = Keypair2.generate();
|
|
5968
|
+
return {
|
|
5969
|
+
sessionKeyId: this.generateTestId("sk_test"),
|
|
5970
|
+
sessionWallet: keypair.publicKey.toString(),
|
|
5971
|
+
privateKey: keypair.secretKey,
|
|
5972
|
+
budget: 10
|
|
5973
|
+
// $10 test budget
|
|
5974
|
+
};
|
|
5975
|
+
}
|
|
5976
|
+
/**
|
|
5977
|
+
* Create a mock wallet for testing
|
|
5978
|
+
*/
|
|
5979
|
+
static mockWallet(address) {
|
|
5980
|
+
const mockAddress = address || this.generateTestAddress();
|
|
5981
|
+
return {
|
|
5982
|
+
address: mockAddress,
|
|
5983
|
+
publicKey: { toString: () => mockAddress },
|
|
5984
|
+
signTransaction: async (tx) => {
|
|
5985
|
+
console.log("\u{1F527} Mock wallet: Signing transaction");
|
|
5986
|
+
return tx;
|
|
5987
|
+
},
|
|
5988
|
+
signMessage: async (_msg) => {
|
|
5989
|
+
console.log("\u{1F527} Mock wallet: Signing message");
|
|
5990
|
+
return {
|
|
5991
|
+
signature: new Uint8Array(64)
|
|
5992
|
+
// Mock signature
|
|
5993
|
+
};
|
|
5994
|
+
},
|
|
5995
|
+
isConnected: () => true,
|
|
5996
|
+
disconnect: async () => {
|
|
5997
|
+
console.log("\u{1F527} Mock wallet: Disconnected");
|
|
5998
|
+
}
|
|
5999
|
+
};
|
|
6000
|
+
}
|
|
6001
|
+
/**
|
|
6002
|
+
* Log transaction flow (visual diagram in console)
|
|
6003
|
+
*/
|
|
6004
|
+
static logTransactionFlow(paymentId) {
|
|
6005
|
+
console.log(`
|
|
6006
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
6007
|
+
\u2551 TRANSACTION FLOW \u2551
|
|
6008
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
6009
|
+
\u2551 \u2551
|
|
6010
|
+
\u2551 Payment ID: ${paymentId} \u2551
|
|
6011
|
+
\u2551 \u2551
|
|
6012
|
+
\u2551 1. \u{1F3D7}\uFE0F Create Payment Intent \u2551
|
|
6013
|
+
\u2551 \u2514\u2500> POST /api/v1/ai/smart-payment \u2551
|
|
6014
|
+
\u2551 \u2551
|
|
6015
|
+
\u2551 2. \u{1F510} Sign Transaction (Device-Bound) \u2551
|
|
6016
|
+
\u2551 \u2514\u2500> Client-side signing with cached keypair \u2551
|
|
6017
|
+
\u2551 \u2551
|
|
6018
|
+
\u2551 3. \u{1F4E4} Submit Signed Transaction \u2551
|
|
6019
|
+
\u2551 \u2514\u2500> POST /api/v1/ai/payments/{id}/submit-signed \u2551
|
|
6020
|
+
\u2551 \u2551
|
|
6021
|
+
\u2551 4. \u23F3 Wait for Blockchain Confirmation \u2551
|
|
6022
|
+
\u2551 \u2514\u2500> Poll Solana RPC (~30-60 seconds) \u2551
|
|
6023
|
+
\u2551 \u2551
|
|
6024
|
+
\u2551 5. \u2705 Payment Confirmed \u2551
|
|
6025
|
+
\u2551 \u2514\u2500> Webhook fired: payment.confirmed \u2551
|
|
6026
|
+
\u2551 \u2551
|
|
6027
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
6028
|
+
`);
|
|
6029
|
+
}
|
|
6030
|
+
/**
|
|
6031
|
+
* Log session key lifecycle
|
|
6032
|
+
*/
|
|
6033
|
+
static logSessionKeyLifecycle(sessionKeyId) {
|
|
6034
|
+
console.log(`
|
|
6035
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
6036
|
+
\u2551 SESSION KEY LIFECYCLE \u2551
|
|
6037
|
+
\u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563
|
|
6038
|
+
\u2551 \u2551
|
|
6039
|
+
\u2551 Session Key ID: ${sessionKeyId} \u2551
|
|
6040
|
+
\u2551 \u2551
|
|
6041
|
+
\u2551 Phase 1: CREATION \u2551
|
|
6042
|
+
\u2551 \u251C\u2500 Generate keypair (client-side) \u2551
|
|
6043
|
+
\u2551 \u251C\u2500 Encrypt with PIN + device fingerprint \u2551
|
|
6044
|
+
\u2551 \u251C\u2500 Send encrypted blob to backend \u2551
|
|
6045
|
+
\u2551 \u2514\u2500 Session key record created \u2551
|
|
6046
|
+
\u2551 \u2551
|
|
6047
|
+
\u2551 Phase 2: FUNDING \u2551
|
|
6048
|
+
\u2551 \u251C\u2500 Create top-up transaction \u2551
|
|
6049
|
+
\u2551 \u251C\u2500 User signs with main wallet \u2551
|
|
6050
|
+
\u2551 \u251C\u2500 Submit to Solana \u2551
|
|
6051
|
+
\u2551 \u2514\u2500 Session wallet funded \u2551
|
|
6052
|
+
\u2551 \u2551
|
|
6053
|
+
\u2551 Phase 3: USAGE \u2551
|
|
6054
|
+
\u2551 \u251C\u2500 Decrypt keypair with PIN \u2551
|
|
6055
|
+
\u2551 \u251C\u2500 Cache for 30min/1hr/24hr \u2551
|
|
6056
|
+
\u2551 \u251C\u2500 Sign payments automatically \u2551
|
|
6057
|
+
\u2551 \u2514\u2500 Track spending against limit \u2551
|
|
6058
|
+
\u2551 \u2551
|
|
6059
|
+
\u2551 Phase 4: REVOCATION \u2551
|
|
6060
|
+
\u2551 \u251C\u2500 Mark as inactive in DB \u2551
|
|
6061
|
+
\u2551 \u251C\u2500 Clear local cache \u2551
|
|
6062
|
+
\u2551 \u2514\u2500 Remaining funds locked \u2551
|
|
6063
|
+
\u2551 \u2551
|
|
6064
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
6065
|
+
`);
|
|
6066
|
+
}
|
|
6067
|
+
/**
|
|
6068
|
+
* Benchmark API request
|
|
6069
|
+
*/
|
|
6070
|
+
static async benchmarkRequest(name, fn) {
|
|
6071
|
+
const start = performance.now();
|
|
6072
|
+
const result = await fn();
|
|
6073
|
+
const duration = performance.now() - start;
|
|
6074
|
+
console.log(`\u23F1\uFE0F Benchmark [${name}]: ${duration.toFixed(2)}ms`);
|
|
6075
|
+
return {
|
|
6076
|
+
result,
|
|
6077
|
+
durationMs: duration
|
|
6078
|
+
};
|
|
6079
|
+
}
|
|
6080
|
+
/**
|
|
6081
|
+
* Stress test (send multiple concurrent requests)
|
|
6082
|
+
*/
|
|
6083
|
+
static async stressTest(name, fn, concurrency = 10, iterations = 100) {
|
|
6084
|
+
console.log(`\u{1F525} Stress Test: ${name}`);
|
|
6085
|
+
console.log(`Concurrency: ${concurrency}, Iterations: ${iterations}`);
|
|
6086
|
+
const durations = [];
|
|
6087
|
+
let successful = 0;
|
|
6088
|
+
let failed = 0;
|
|
6089
|
+
for (let i = 0; i < iterations; i += concurrency) {
|
|
6090
|
+
const batch = Array(Math.min(concurrency, iterations - i)).fill(null).map(() => this.benchmarkRequest(`${name}-${i}`, fn));
|
|
6091
|
+
const results = await Promise.allSettled(batch);
|
|
6092
|
+
results.forEach((result) => {
|
|
6093
|
+
if (result.status === "fulfilled") {
|
|
6094
|
+
successful++;
|
|
6095
|
+
durations.push(result.value.durationMs);
|
|
6096
|
+
} else {
|
|
6097
|
+
failed++;
|
|
6098
|
+
}
|
|
6099
|
+
});
|
|
6100
|
+
}
|
|
6101
|
+
const stats = {
|
|
6102
|
+
totalRequests: iterations,
|
|
6103
|
+
successful,
|
|
6104
|
+
failed,
|
|
6105
|
+
avgDurationMs: durations.reduce((a, b) => a + b, 0) / durations.length,
|
|
6106
|
+
minDurationMs: Math.min(...durations),
|
|
6107
|
+
maxDurationMs: Math.max(...durations)
|
|
6108
|
+
};
|
|
6109
|
+
console.table(stats);
|
|
6110
|
+
return stats;
|
|
6111
|
+
}
|
|
6112
|
+
/**
|
|
6113
|
+
* Inspect ZendFi SDK configuration
|
|
6114
|
+
*/
|
|
6115
|
+
static inspectConfig(client) {
|
|
6116
|
+
console.group("\u{1F50D} ZendFi SDK Configuration");
|
|
6117
|
+
console.log("Base URL:", client.config?.baseURL || "Unknown");
|
|
6118
|
+
console.log("API Key:", client.config?.apiKey ? `${client.config.apiKey.slice(0, 10)}...` : "Not set");
|
|
6119
|
+
console.log("Mode:", client.config?.mode || "Unknown");
|
|
6120
|
+
console.log("Environment:", client.config?.environment || "Unknown");
|
|
6121
|
+
console.log("Timeout:", client.config?.timeout || "Default");
|
|
6122
|
+
console.groupEnd();
|
|
6123
|
+
}
|
|
6124
|
+
/**
|
|
6125
|
+
* Generate test data
|
|
6126
|
+
*/
|
|
6127
|
+
static generateTestData() {
|
|
6128
|
+
return {
|
|
6129
|
+
userWallet: this.generateTestAddress(),
|
|
6130
|
+
agentId: `test-agent-${Date.now()}`,
|
|
6131
|
+
sessionKeyId: this.generateTestId("sk_test"),
|
|
6132
|
+
paymentId: this.generateTestId("pay_test")
|
|
6133
|
+
};
|
|
6134
|
+
}
|
|
6135
|
+
/**
|
|
6136
|
+
* Check if running in development environment
|
|
6137
|
+
*/
|
|
6138
|
+
static isDevelopment() {
|
|
6139
|
+
if (typeof process !== "undefined" && process.env) {
|
|
6140
|
+
return process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
6141
|
+
}
|
|
6142
|
+
if (typeof window !== "undefined" && window.location) {
|
|
6143
|
+
return window.location.hostname === "localhost" || window.location.hostname.includes("dev") || window.location.hostname.includes("staging");
|
|
6144
|
+
}
|
|
6145
|
+
return false;
|
|
6146
|
+
}
|
|
6147
|
+
/**
|
|
6148
|
+
* Generate test Solana address
|
|
6149
|
+
*/
|
|
6150
|
+
static generateTestAddress() {
|
|
6151
|
+
const chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
6152
|
+
let address = "";
|
|
6153
|
+
for (let i = 0; i < 44; i++) {
|
|
6154
|
+
address += chars[Math.floor(Math.random() * chars.length)];
|
|
6155
|
+
}
|
|
6156
|
+
return address;
|
|
6157
|
+
}
|
|
6158
|
+
/**
|
|
6159
|
+
* Generate test ID with prefix
|
|
6160
|
+
*/
|
|
6161
|
+
static generateTestId(prefix) {
|
|
6162
|
+
const id = Array(32).fill(null).map(() => Math.floor(Math.random() * 16).toString(16)).join("");
|
|
6163
|
+
return `${prefix}_${id}`;
|
|
6164
|
+
}
|
|
6165
|
+
/**
|
|
6166
|
+
* Get Solana Web3.js
|
|
6167
|
+
*/
|
|
6168
|
+
static async getSolanaWeb3() {
|
|
6169
|
+
try {
|
|
6170
|
+
return await import("@solana/web3.js");
|
|
6171
|
+
} catch {
|
|
6172
|
+
throw new Error("@solana/web3.js not installed");
|
|
6173
|
+
}
|
|
6174
|
+
}
|
|
6175
|
+
};
|
|
6176
|
+
var PerformanceMonitor = class {
|
|
6177
|
+
metrics = /* @__PURE__ */ new Map();
|
|
6178
|
+
/**
|
|
6179
|
+
* Record a metric
|
|
6180
|
+
*/
|
|
6181
|
+
record(name, value) {
|
|
6182
|
+
const values = this.metrics.get(name) || [];
|
|
6183
|
+
values.push(value);
|
|
6184
|
+
this.metrics.set(name, values);
|
|
6185
|
+
}
|
|
6186
|
+
/**
|
|
6187
|
+
* Get statistics for a metric
|
|
6188
|
+
*/
|
|
6189
|
+
getStats(name) {
|
|
6190
|
+
const values = this.metrics.get(name);
|
|
6191
|
+
if (!values || values.length === 0) return null;
|
|
6192
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
6193
|
+
const count = values.length;
|
|
6194
|
+
return {
|
|
6195
|
+
count,
|
|
6196
|
+
avg: values.reduce((a, b) => a + b, 0) / count,
|
|
6197
|
+
min: sorted[0],
|
|
6198
|
+
max: sorted[count - 1],
|
|
6199
|
+
p50: sorted[Math.floor(count * 0.5)],
|
|
6200
|
+
p95: sorted[Math.floor(count * 0.95)],
|
|
6201
|
+
p99: sorted[Math.floor(count * 0.99)]
|
|
6202
|
+
};
|
|
6203
|
+
}
|
|
6204
|
+
/**
|
|
6205
|
+
* Get all metrics
|
|
6206
|
+
*/
|
|
6207
|
+
getAllStats() {
|
|
6208
|
+
const stats = {};
|
|
6209
|
+
for (const [name] of this.metrics) {
|
|
6210
|
+
stats[name] = this.getStats(name);
|
|
6211
|
+
}
|
|
6212
|
+
return stats;
|
|
6213
|
+
}
|
|
6214
|
+
/**
|
|
6215
|
+
* Print report
|
|
6216
|
+
*/
|
|
6217
|
+
printReport() {
|
|
6218
|
+
console.table(this.getAllStats());
|
|
6219
|
+
}
|
|
6220
|
+
/**
|
|
6221
|
+
* Clear all metrics
|
|
6222
|
+
*/
|
|
6223
|
+
clear() {
|
|
6224
|
+
this.metrics.clear();
|
|
6225
|
+
}
|
|
6226
|
+
};
|
|
3670
6227
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3671
6228
|
0 && (module.exports = {
|
|
3672
6229
|
AgentAPI,
|
|
6230
|
+
AnthropicAdapter,
|
|
3673
6231
|
ApiError,
|
|
3674
6232
|
AuthenticationError,
|
|
3675
6233
|
AutonomyAPI,
|
|
3676
6234
|
ConfigLoader,
|
|
6235
|
+
DevTools,
|
|
3677
6236
|
DeviceBoundSessionKey,
|
|
3678
6237
|
DeviceFingerprintGenerator,
|
|
3679
6238
|
ERROR_CODES,
|
|
6239
|
+
ErrorRecovery,
|
|
6240
|
+
GeminiAdapter,
|
|
3680
6241
|
InterceptorManager,
|
|
3681
6242
|
LitCryptoSigner,
|
|
3682
6243
|
NetworkError,
|
|
6244
|
+
OpenAIAdapter,
|
|
6245
|
+
PINRateLimiter,
|
|
6246
|
+
PINValidator,
|
|
3683
6247
|
PaymentError,
|
|
6248
|
+
PaymentIntentParser,
|
|
3684
6249
|
PaymentIntentsAPI,
|
|
6250
|
+
PerformanceMonitor,
|
|
3685
6251
|
PricingAPI,
|
|
6252
|
+
QuickCaches,
|
|
3686
6253
|
RateLimitError,
|
|
3687
6254
|
RateLimiter,
|
|
3688
6255
|
RecoveryQRGenerator,
|
|
6256
|
+
RetryStrategy,
|
|
3689
6257
|
SPENDING_LIMIT_ACTION_CID,
|
|
6258
|
+
SecureStorage,
|
|
6259
|
+
SessionKeyCache,
|
|
3690
6260
|
SessionKeyCrypto,
|
|
6261
|
+
SessionKeyLifecycle,
|
|
3691
6262
|
SessionKeysAPI,
|
|
3692
6263
|
SmartPaymentsAPI,
|
|
6264
|
+
TransactionMonitor,
|
|
6265
|
+
TransactionPoller,
|
|
3693
6266
|
ValidationError,
|
|
6267
|
+
WalletConnector,
|
|
3694
6268
|
WebhookError,
|
|
3695
6269
|
ZendFiClient,
|
|
3696
6270
|
ZendFiError,
|
|
@@ -3705,6 +6279,7 @@ function decodeSignatureFromLit(result) {
|
|
|
3705
6279
|
asPaymentLinkCode,
|
|
3706
6280
|
asSessionId,
|
|
3707
6281
|
asSubscriptionId,
|
|
6282
|
+
createWalletHook,
|
|
3708
6283
|
createZendFiError,
|
|
3709
6284
|
decodeSignatureFromLit,
|
|
3710
6285
|
encodeTransactionForLit,
|
|
@@ -3712,6 +6287,7 @@ function decodeSignatureFromLit(result) {
|
|
|
3712
6287
|
isZendFiError,
|
|
3713
6288
|
processWebhook,
|
|
3714
6289
|
requiresLitSigning,
|
|
6290
|
+
setupQuickSessionKey,
|
|
3715
6291
|
sleep,
|
|
3716
6292
|
verifyExpressWebhook,
|
|
3717
6293
|
verifyNextWebhook,
|