@zendfi/sdk 0.5.8 → 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
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
// src/device-bound-crypto.ts
|
|
2
|
+
import { Keypair } from "@solana/web3.js";
|
|
3
|
+
import * as crypto from "crypto";
|
|
4
|
+
var DeviceFingerprintGenerator = class {
|
|
5
|
+
/**
|
|
6
|
+
* Generate a unique device fingerprint
|
|
7
|
+
* Combines multiple browser attributes for uniqueness
|
|
8
|
+
*/
|
|
9
|
+
static async generate() {
|
|
10
|
+
const components = {};
|
|
11
|
+
try {
|
|
12
|
+
components.canvas = await this.getCanvasFingerprint();
|
|
13
|
+
components.webgl = await this.getWebGLFingerprint();
|
|
14
|
+
components.audio = await this.getAudioFingerprint();
|
|
15
|
+
components.screen = `${screen.width}x${screen.height}x${screen.colorDepth}`;
|
|
16
|
+
components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
17
|
+
components.languages = navigator.languages?.join(",") || navigator.language;
|
|
18
|
+
components.platform = navigator.platform;
|
|
19
|
+
components.hardwareConcurrency = navigator.hardwareConcurrency?.toString() || "unknown";
|
|
20
|
+
const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|");
|
|
21
|
+
const fingerprint = await this.sha256(combined);
|
|
22
|
+
return {
|
|
23
|
+
fingerprint,
|
|
24
|
+
generatedAt: Date.now(),
|
|
25
|
+
components
|
|
26
|
+
};
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn("Device fingerprinting failed, using fallback", error);
|
|
29
|
+
return this.generateFallbackFingerprint();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Graceful fallback fingerprint generation
|
|
34
|
+
* Works in headless browsers, SSR, and restricted environments
|
|
35
|
+
*/
|
|
36
|
+
static async generateFallbackFingerprint() {
|
|
37
|
+
const components = {};
|
|
38
|
+
try {
|
|
39
|
+
if (typeof navigator !== "undefined") {
|
|
40
|
+
components.platform = navigator.platform || "unknown";
|
|
41
|
+
components.languages = navigator.languages?.join(",") || navigator.language || "unknown";
|
|
42
|
+
components.hardwareConcurrency = navigator.hardwareConcurrency?.toString() || "unknown";
|
|
43
|
+
}
|
|
44
|
+
if (typeof screen !== "undefined") {
|
|
45
|
+
components.screen = `${screen.width || 0}x${screen.height || 0}x${screen.colorDepth || 0}`;
|
|
46
|
+
}
|
|
47
|
+
if (typeof Intl !== "undefined") {
|
|
48
|
+
try {
|
|
49
|
+
components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
50
|
+
} catch {
|
|
51
|
+
components.timezone = "unknown";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
components.platform = "fallback";
|
|
56
|
+
}
|
|
57
|
+
let randomEntropy = "";
|
|
58
|
+
try {
|
|
59
|
+
if (typeof window !== "undefined" && window.crypto?.getRandomValues) {
|
|
60
|
+
const arr = new Uint8Array(16);
|
|
61
|
+
window.crypto.getRandomValues(arr);
|
|
62
|
+
randomEntropy = Array.from(arr).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
63
|
+
} else if (typeof crypto !== "undefined" && crypto.randomBytes) {
|
|
64
|
+
randomEntropy = crypto.randomBytes(16).toString("hex");
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
randomEntropy = Date.now().toString(36) + Math.random().toString(36);
|
|
68
|
+
}
|
|
69
|
+
const combined = Object.entries(components).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}:${value}`).join("|") + "|entropy:" + randomEntropy;
|
|
70
|
+
const fingerprint = await this.sha256(combined);
|
|
71
|
+
return {
|
|
72
|
+
fingerprint,
|
|
73
|
+
generatedAt: Date.now(),
|
|
74
|
+
components
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
static async getCanvasFingerprint() {
|
|
78
|
+
const canvas = document.createElement("canvas");
|
|
79
|
+
const ctx = canvas.getContext("2d");
|
|
80
|
+
if (!ctx) return "no-canvas";
|
|
81
|
+
canvas.width = 200;
|
|
82
|
+
canvas.height = 50;
|
|
83
|
+
ctx.textBaseline = "top";
|
|
84
|
+
ctx.font = '14px "Arial"';
|
|
85
|
+
ctx.fillStyle = "#f60";
|
|
86
|
+
ctx.fillRect(0, 0, 100, 50);
|
|
87
|
+
ctx.fillStyle = "#069";
|
|
88
|
+
ctx.fillText("ZendFi \u{1F510}", 2, 2);
|
|
89
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
90
|
+
ctx.fillText("Device-Bound", 4, 17);
|
|
91
|
+
return canvas.toDataURL();
|
|
92
|
+
}
|
|
93
|
+
static async getWebGLFingerprint() {
|
|
94
|
+
const canvas = document.createElement("canvas");
|
|
95
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
96
|
+
if (!gl) return "no-webgl";
|
|
97
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
98
|
+
if (!debugInfo) return "no-debug-info";
|
|
99
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
100
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
101
|
+
return `${vendor}|${renderer}`;
|
|
102
|
+
}
|
|
103
|
+
static async getAudioFingerprint() {
|
|
104
|
+
try {
|
|
105
|
+
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
106
|
+
if (!AudioContext) return "no-audio";
|
|
107
|
+
const context = new AudioContext();
|
|
108
|
+
const oscillator = context.createOscillator();
|
|
109
|
+
const analyser = context.createAnalyser();
|
|
110
|
+
const gainNode = context.createGain();
|
|
111
|
+
const scriptProcessor = context.createScriptProcessor(4096, 1, 1);
|
|
112
|
+
gainNode.gain.value = 0;
|
|
113
|
+
oscillator.connect(analyser);
|
|
114
|
+
analyser.connect(scriptProcessor);
|
|
115
|
+
scriptProcessor.connect(gainNode);
|
|
116
|
+
gainNode.connect(context.destination);
|
|
117
|
+
oscillator.start(0);
|
|
118
|
+
return new Promise((resolve) => {
|
|
119
|
+
scriptProcessor.onaudioprocess = (event) => {
|
|
120
|
+
const output = event.inputBuffer.getChannelData(0);
|
|
121
|
+
const hash = Array.from(output.slice(0, 30)).reduce((acc, val) => acc + Math.abs(val), 0);
|
|
122
|
+
oscillator.stop();
|
|
123
|
+
scriptProcessor.disconnect();
|
|
124
|
+
context.close();
|
|
125
|
+
resolve(hash.toString());
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return "audio-error";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
static async sha256(data) {
|
|
133
|
+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
134
|
+
const encoder = new TextEncoder();
|
|
135
|
+
const dataBuffer = encoder.encode(data);
|
|
136
|
+
const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);
|
|
137
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
138
|
+
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
139
|
+
} else {
|
|
140
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
var SessionKeyCrypto = class {
|
|
145
|
+
/**
|
|
146
|
+
* Encrypt a Solana keypair with PIN + device fingerprint
|
|
147
|
+
* Uses Argon2id for key derivation and AES-256-GCM for encryption
|
|
148
|
+
*/
|
|
149
|
+
static async encrypt(keypair, pin, deviceFingerprint) {
|
|
150
|
+
if (!/^\d{6}$/.test(pin)) {
|
|
151
|
+
throw new Error("PIN must be exactly 6 numeric digits");
|
|
152
|
+
}
|
|
153
|
+
const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
|
|
154
|
+
const nonce = this.generateNonce();
|
|
155
|
+
const secretKey = keypair.secretKey;
|
|
156
|
+
const encryptedData = await this.aesEncrypt(secretKey, encryptionKey, nonce);
|
|
157
|
+
return {
|
|
158
|
+
encryptedData: Buffer.from(encryptedData).toString("base64"),
|
|
159
|
+
nonce: Buffer.from(nonce).toString("base64"),
|
|
160
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
161
|
+
deviceFingerprint,
|
|
162
|
+
version: "argon2id-aes256gcm-v1"
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Decrypt an encrypted session key with PIN + device fingerprint
|
|
167
|
+
*/
|
|
168
|
+
static async decrypt(encrypted, pin, deviceFingerprint) {
|
|
169
|
+
if (!/^\d{6}$/.test(pin)) {
|
|
170
|
+
throw new Error("PIN must be exactly 6 numeric digits");
|
|
171
|
+
}
|
|
172
|
+
if (encrypted.deviceFingerprint !== deviceFingerprint) {
|
|
173
|
+
throw new Error("Device fingerprint mismatch - wrong device or security threat");
|
|
174
|
+
}
|
|
175
|
+
const encryptionKey = await this.deriveKey(pin, deviceFingerprint);
|
|
176
|
+
const encryptedData = Buffer.from(encrypted.encryptedData, "base64");
|
|
177
|
+
const nonce = Buffer.from(encrypted.nonce, "base64");
|
|
178
|
+
try {
|
|
179
|
+
const secretKey = await this.aesDecrypt(encryptedData, encryptionKey, nonce);
|
|
180
|
+
return Keypair.fromSecretKey(secretKey);
|
|
181
|
+
} catch (error) {
|
|
182
|
+
throw new Error("Decryption failed - wrong PIN or corrupted data");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Derive encryption key from PIN + device fingerprint using Argon2id
|
|
187
|
+
*
|
|
188
|
+
* Argon2id parameters (OWASP recommended):
|
|
189
|
+
* - Memory: 64MB (65536 KB)
|
|
190
|
+
* - Iterations: 3
|
|
191
|
+
* - Parallelism: 4
|
|
192
|
+
* - Salt: device fingerprint
|
|
193
|
+
*/
|
|
194
|
+
static async deriveKey(pin, deviceFingerprint) {
|
|
195
|
+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
196
|
+
const encoder = new TextEncoder();
|
|
197
|
+
const keyMaterial = await window.crypto.subtle.importKey(
|
|
198
|
+
"raw",
|
|
199
|
+
encoder.encode(pin),
|
|
200
|
+
{ name: "PBKDF2" },
|
|
201
|
+
false,
|
|
202
|
+
["deriveBits"]
|
|
203
|
+
);
|
|
204
|
+
const derivedBits = await window.crypto.subtle.deriveBits(
|
|
205
|
+
{
|
|
206
|
+
name: "PBKDF2",
|
|
207
|
+
salt: encoder.encode(deviceFingerprint),
|
|
208
|
+
iterations: 1e5,
|
|
209
|
+
// High iteration count for security
|
|
210
|
+
hash: "SHA-256"
|
|
211
|
+
},
|
|
212
|
+
keyMaterial,
|
|
213
|
+
256
|
|
214
|
+
// 256 bits = 32 bytes for AES-256
|
|
215
|
+
);
|
|
216
|
+
return new Uint8Array(derivedBits);
|
|
217
|
+
} else {
|
|
218
|
+
const salt = crypto.createHash("sha256").update(deviceFingerprint).digest();
|
|
219
|
+
return crypto.pbkdf2Sync(pin, salt, 1e5, 32, "sha256");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Generate random nonce for AES-GCM (12 bytes)
|
|
224
|
+
*/
|
|
225
|
+
static generateNonce() {
|
|
226
|
+
if (typeof window !== "undefined" && window.crypto) {
|
|
227
|
+
return window.crypto.getRandomValues(new Uint8Array(12));
|
|
228
|
+
} else {
|
|
229
|
+
return crypto.randomBytes(12);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Encrypt with AES-256-GCM
|
|
234
|
+
*/
|
|
235
|
+
static async aesEncrypt(plaintext, key, nonce) {
|
|
236
|
+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
237
|
+
const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
|
|
238
|
+
const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
|
|
239
|
+
const plaintextBuffer = plaintext.buffer.slice(plaintext.byteOffset, plaintext.byteOffset + plaintext.byteLength);
|
|
240
|
+
const cryptoKey = await window.crypto.subtle.importKey(
|
|
241
|
+
"raw",
|
|
242
|
+
keyBuffer,
|
|
243
|
+
{ name: "AES-GCM" },
|
|
244
|
+
false,
|
|
245
|
+
["encrypt"]
|
|
246
|
+
);
|
|
247
|
+
const encrypted = await window.crypto.subtle.encrypt(
|
|
248
|
+
{
|
|
249
|
+
name: "AES-GCM",
|
|
250
|
+
iv: nonceBuffer
|
|
251
|
+
},
|
|
252
|
+
cryptoKey,
|
|
253
|
+
plaintextBuffer
|
|
254
|
+
);
|
|
255
|
+
return new Uint8Array(encrypted);
|
|
256
|
+
} else {
|
|
257
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);
|
|
258
|
+
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
259
|
+
const authTag = cipher.getAuthTag();
|
|
260
|
+
return new Uint8Array(Buffer.concat([encrypted, authTag]));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Decrypt with AES-256-GCM
|
|
265
|
+
*/
|
|
266
|
+
static async aesDecrypt(ciphertext, key, nonce) {
|
|
267
|
+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
|
|
268
|
+
const keyBuffer = key.buffer.slice(key.byteOffset, key.byteOffset + key.byteLength);
|
|
269
|
+
const nonceBuffer = nonce.buffer.slice(nonce.byteOffset, nonce.byteOffset + nonce.byteLength);
|
|
270
|
+
const ciphertextBuffer = ciphertext.buffer.slice(ciphertext.byteOffset, ciphertext.byteOffset + ciphertext.byteLength);
|
|
271
|
+
const cryptoKey = await window.crypto.subtle.importKey(
|
|
272
|
+
"raw",
|
|
273
|
+
keyBuffer,
|
|
274
|
+
{ name: "AES-GCM" },
|
|
275
|
+
false,
|
|
276
|
+
["decrypt"]
|
|
277
|
+
);
|
|
278
|
+
const decrypted = await window.crypto.subtle.decrypt(
|
|
279
|
+
{
|
|
280
|
+
name: "AES-GCM",
|
|
281
|
+
iv: nonceBuffer
|
|
282
|
+
},
|
|
283
|
+
cryptoKey,
|
|
284
|
+
ciphertextBuffer
|
|
285
|
+
);
|
|
286
|
+
return new Uint8Array(decrypted);
|
|
287
|
+
} else {
|
|
288
|
+
const authTag = ciphertext.slice(-16);
|
|
289
|
+
const encrypted = ciphertext.slice(0, -16);
|
|
290
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);
|
|
291
|
+
decipher.setAuthTag(authTag);
|
|
292
|
+
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
|
293
|
+
return new Uint8Array(decrypted);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var RecoveryQRGenerator = class {
|
|
298
|
+
/**
|
|
299
|
+
* Generate recovery QR data
|
|
300
|
+
* This allows users to recover their session key on a new device
|
|
301
|
+
*/
|
|
302
|
+
static generate(encrypted) {
|
|
303
|
+
return {
|
|
304
|
+
encryptedSessionKey: encrypted.encryptedData,
|
|
305
|
+
nonce: encrypted.nonce,
|
|
306
|
+
publicKey: encrypted.publicKey,
|
|
307
|
+
version: "v1",
|
|
308
|
+
createdAt: Date.now()
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Encode recovery QR as JSON string
|
|
313
|
+
*/
|
|
314
|
+
static encode(recoveryQR) {
|
|
315
|
+
return JSON.stringify(recoveryQR);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Decode recovery QR from JSON string
|
|
319
|
+
*/
|
|
320
|
+
static decode(qrData) {
|
|
321
|
+
try {
|
|
322
|
+
const parsed = JSON.parse(qrData);
|
|
323
|
+
if (!parsed.encryptedSessionKey || !parsed.nonce || !parsed.publicKey) {
|
|
324
|
+
throw new Error("Invalid recovery QR data");
|
|
325
|
+
}
|
|
326
|
+
return parsed;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
throw new Error("Failed to decode recovery QR");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Re-encrypt session key for new device
|
|
333
|
+
*/
|
|
334
|
+
static async reEncryptForNewDevice(recoveryQR, oldPin, oldDeviceFingerprint, newPin, newDeviceFingerprint) {
|
|
335
|
+
const oldEncrypted = {
|
|
336
|
+
encryptedData: recoveryQR.encryptedSessionKey,
|
|
337
|
+
nonce: recoveryQR.nonce,
|
|
338
|
+
publicKey: recoveryQR.publicKey,
|
|
339
|
+
deviceFingerprint: oldDeviceFingerprint,
|
|
340
|
+
version: "argon2id-aes256gcm-v1"
|
|
341
|
+
};
|
|
342
|
+
const keypair = await SessionKeyCrypto.decrypt(oldEncrypted, oldPin, oldDeviceFingerprint);
|
|
343
|
+
return await SessionKeyCrypto.encrypt(keypair, newPin, newDeviceFingerprint);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
var DeviceBoundSessionKey = class _DeviceBoundSessionKey {
|
|
347
|
+
encrypted = null;
|
|
348
|
+
deviceFingerprint = null;
|
|
349
|
+
sessionKeyId = null;
|
|
350
|
+
recoveryQR = null;
|
|
351
|
+
// Auto-signing cache: decrypted keypair stored in memory
|
|
352
|
+
// Enables instant signing without re-entering PIN for subsequent payments
|
|
353
|
+
cachedKeypair = null;
|
|
354
|
+
cacheExpiry = null;
|
|
355
|
+
// Timestamp when cache expires
|
|
356
|
+
DEFAULT_CACHE_TTL_MS = 30 * 60 * 1e3;
|
|
357
|
+
// 30 minutes
|
|
358
|
+
/**
|
|
359
|
+
* Create a new device-bound session key
|
|
360
|
+
*/
|
|
361
|
+
static async create(options) {
|
|
362
|
+
const deviceFingerprint = await DeviceFingerprintGenerator.generate();
|
|
363
|
+
const keypair = Keypair.generate();
|
|
364
|
+
const encrypted = await SessionKeyCrypto.encrypt(
|
|
365
|
+
keypair,
|
|
366
|
+
options.pin,
|
|
367
|
+
deviceFingerprint.fingerprint
|
|
368
|
+
);
|
|
369
|
+
const instance = new _DeviceBoundSessionKey();
|
|
370
|
+
instance.encrypted = encrypted;
|
|
371
|
+
instance.deviceFingerprint = deviceFingerprint;
|
|
372
|
+
if (options.generateRecoveryQR) {
|
|
373
|
+
instance.recoveryQR = RecoveryQRGenerator.generate(encrypted);
|
|
374
|
+
}
|
|
375
|
+
return instance;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Get encrypted data for backend storage
|
|
379
|
+
*/
|
|
380
|
+
getEncryptedData() {
|
|
381
|
+
if (!this.encrypted) {
|
|
382
|
+
throw new Error("Session key not created yet");
|
|
383
|
+
}
|
|
384
|
+
return this.encrypted;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Get device fingerprint
|
|
388
|
+
*/
|
|
389
|
+
getDeviceFingerprint() {
|
|
390
|
+
if (!this.deviceFingerprint) {
|
|
391
|
+
throw new Error("Device fingerprint not generated yet");
|
|
392
|
+
}
|
|
393
|
+
return this.deviceFingerprint.fingerprint;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Get public key
|
|
397
|
+
*/
|
|
398
|
+
getPublicKey() {
|
|
399
|
+
if (!this.encrypted) {
|
|
400
|
+
throw new Error("Session key not created yet");
|
|
401
|
+
}
|
|
402
|
+
return this.encrypted.publicKey;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Get recovery QR data (if generated)
|
|
406
|
+
*/
|
|
407
|
+
getRecoveryQR() {
|
|
408
|
+
return this.recoveryQR;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Decrypt and sign a transaction
|
|
412
|
+
*
|
|
413
|
+
* @param transaction - The transaction to sign
|
|
414
|
+
* @param pin - User's PIN (required only if keypair not cached)
|
|
415
|
+
* @param cacheKeypair - Whether to cache the decrypted keypair for future use (default: true)
|
|
416
|
+
* @param cacheTTL - Cache time-to-live in milliseconds (default: 30 minutes)
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```typescript
|
|
420
|
+
* // First payment: requires PIN, caches keypair
|
|
421
|
+
* await sessionKey.signTransaction(tx1, '123456', true);
|
|
422
|
+
*
|
|
423
|
+
* // Subsequent payments: uses cached keypair, no PIN needed!
|
|
424
|
+
* await sessionKey.signTransaction(tx2, '', false); // PIN ignored if cached
|
|
425
|
+
*
|
|
426
|
+
* // Clear cache when done
|
|
427
|
+
* sessionKey.clearCache();
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
async signTransaction(transaction, pin = "", cacheKeypair = true, cacheTTL) {
|
|
431
|
+
if (!this.encrypted || !this.deviceFingerprint) {
|
|
432
|
+
throw new Error("Session key not initialized");
|
|
433
|
+
}
|
|
434
|
+
let keypair;
|
|
435
|
+
if (this.isCached()) {
|
|
436
|
+
keypair = this.cachedKeypair;
|
|
437
|
+
if (typeof console !== "undefined") {
|
|
438
|
+
console.log("\u{1F680} Using cached keypair - instant signing (no PIN required)");
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
if (!pin) {
|
|
442
|
+
throw new Error("PIN required: no cached keypair available");
|
|
443
|
+
}
|
|
444
|
+
keypair = await SessionKeyCrypto.decrypt(
|
|
445
|
+
this.encrypted,
|
|
446
|
+
pin,
|
|
447
|
+
this.deviceFingerprint.fingerprint
|
|
448
|
+
);
|
|
449
|
+
if (cacheKeypair) {
|
|
450
|
+
const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
|
|
451
|
+
this.cacheKeypair(keypair, ttl);
|
|
452
|
+
if (typeof console !== "undefined") {
|
|
453
|
+
console.log(`\u2705 Keypair decrypted and cached for ${ttl / 1e3 / 60} minutes`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
transaction.sign(keypair);
|
|
458
|
+
return transaction;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Check if keypair is cached and valid
|
|
462
|
+
*/
|
|
463
|
+
isCached() {
|
|
464
|
+
if (!this.cachedKeypair || !this.cacheExpiry) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
if (now > this.cacheExpiry) {
|
|
469
|
+
this.clearCache();
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
return true;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Manually cache a keypair
|
|
476
|
+
* Called internally after PIN decryption
|
|
477
|
+
*/
|
|
478
|
+
cacheKeypair(keypair, ttl) {
|
|
479
|
+
this.cachedKeypair = keypair;
|
|
480
|
+
this.cacheExpiry = Date.now() + ttl;
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Clear cached keypair
|
|
484
|
+
* Should be called when user logs out or session ends
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```typescript
|
|
488
|
+
* // Clear cache on logout
|
|
489
|
+
* sessionKey.clearCache();
|
|
490
|
+
*
|
|
491
|
+
* // Or clear automatically on tab close
|
|
492
|
+
* window.addEventListener('beforeunload', () => {
|
|
493
|
+
* sessionKey.clearCache();
|
|
494
|
+
* });
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
497
|
+
clearCache() {
|
|
498
|
+
this.cachedKeypair = null;
|
|
499
|
+
this.cacheExpiry = null;
|
|
500
|
+
if (typeof console !== "undefined") {
|
|
501
|
+
console.log("\u{1F9F9} Keypair cache cleared");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Decrypt and cache keypair without signing a transaction
|
|
506
|
+
* Useful for pre-warming the cache before user makes payments
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* ```typescript
|
|
510
|
+
* // After session key creation, decrypt and cache
|
|
511
|
+
* await sessionKey.unlockWithPin('123456');
|
|
512
|
+
*
|
|
513
|
+
* // Now all subsequent payments are instant (no PIN)
|
|
514
|
+
* await sessionKey.signTransaction(tx1, '', false); // Instant!
|
|
515
|
+
* await sessionKey.signTransaction(tx2, '', false); // Instant!
|
|
516
|
+
* ```
|
|
517
|
+
*/
|
|
518
|
+
async unlockWithPin(pin, cacheTTL) {
|
|
519
|
+
if (!this.encrypted || !this.deviceFingerprint) {
|
|
520
|
+
throw new Error("Session key not initialized");
|
|
521
|
+
}
|
|
522
|
+
const keypair = await SessionKeyCrypto.decrypt(
|
|
523
|
+
this.encrypted,
|
|
524
|
+
pin,
|
|
525
|
+
this.deviceFingerprint.fingerprint
|
|
526
|
+
);
|
|
527
|
+
const ttl = cacheTTL || this.DEFAULT_CACHE_TTL_MS;
|
|
528
|
+
this.cacheKeypair(keypair, ttl);
|
|
529
|
+
if (typeof console !== "undefined") {
|
|
530
|
+
console.log(`\u{1F513} Session key unlocked and cached for ${ttl / 1e3 / 60} minutes`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Get time remaining until cache expires (in milliseconds)
|
|
535
|
+
* Returns 0 if not cached
|
|
536
|
+
*/
|
|
537
|
+
getCacheTimeRemaining() {
|
|
538
|
+
if (!this.isCached() || !this.cacheExpiry) {
|
|
539
|
+
return 0;
|
|
540
|
+
}
|
|
541
|
+
const remaining = this.cacheExpiry - Date.now();
|
|
542
|
+
return Math.max(0, remaining);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Extend cache expiry time
|
|
546
|
+
* Useful to keep session active during user activity
|
|
547
|
+
*
|
|
548
|
+
* @example
|
|
549
|
+
* ```typescript
|
|
550
|
+
* // Extend cache by 15 minutes on each payment
|
|
551
|
+
* await sessionKey.signTransaction(tx, '');
|
|
552
|
+
* sessionKey.extendCache(15 * 60 * 1000);
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
extendCache(additionalTTL) {
|
|
556
|
+
if (!this.isCached()) {
|
|
557
|
+
throw new Error("Cannot extend cache: no cached keypair");
|
|
558
|
+
}
|
|
559
|
+
this.cacheExpiry += additionalTTL;
|
|
560
|
+
if (typeof console !== "undefined") {
|
|
561
|
+
const remainingMinutes = this.getCacheTimeRemaining() / 1e3 / 60;
|
|
562
|
+
console.log(`\u23F0 Cache extended - ${remainingMinutes.toFixed(1)} minutes remaining`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Set session key ID after backend creation
|
|
567
|
+
*/
|
|
568
|
+
setSessionKeyId(id) {
|
|
569
|
+
this.sessionKeyId = id;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Get session key ID
|
|
573
|
+
*/
|
|
574
|
+
getSessionKeyId() {
|
|
575
|
+
if (!this.sessionKeyId) {
|
|
576
|
+
throw new Error("Session key not registered with backend");
|
|
577
|
+
}
|
|
578
|
+
return this.sessionKeyId;
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
export {
|
|
583
|
+
DeviceFingerprintGenerator,
|
|
584
|
+
SessionKeyCrypto,
|
|
585
|
+
RecoveryQRGenerator,
|
|
586
|
+
DeviceBoundSessionKey
|
|
587
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
__require
|
|
10
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DeviceBoundSessionKey,
|
|
3
|
+
DeviceFingerprintGenerator,
|
|
4
|
+
RecoveryQRGenerator,
|
|
5
|
+
SessionKeyCrypto
|
|
6
|
+
} from "./chunk-XERHBDUK.mjs";
|
|
7
|
+
import "./chunk-Y6FXYEAI.mjs";
|
|
8
|
+
export {
|
|
9
|
+
DeviceBoundSessionKey,
|
|
10
|
+
DeviceFingerprintGenerator,
|
|
11
|
+
RecoveryQRGenerator,
|
|
12
|
+
SessionKeyCrypto
|
|
13
|
+
};
|
package/dist/express.d.mts
CHANGED
package/dist/express.d.ts
CHANGED