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