@closeloop/sdk 0.1.7 → 0.1.8
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 +18 -234
- package/dist/index.d.mts +843 -2
- package/dist/index.d.ts +843 -2
- package/dist/index.js +107 -105
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +107 -105
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -19
- package/dist/errors-CNnLzjDZ.d.mts +0 -901
- package/dist/errors-CNnLzjDZ.d.ts +0 -901
- package/dist/nextjs.d.mts +0 -135
- package/dist/nextjs.d.ts +0 -135
- package/dist/nextjs.js +0 -952
- package/dist/nextjs.js.map +0 -1
- package/dist/nextjs.mjs +0 -917
- package/dist/nextjs.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -39,12 +39,14 @@ module.exports = __toCommonJS(src_exports);
|
|
|
39
39
|
// src/constants.ts
|
|
40
40
|
var DEFAULT_BASE_URL = "https://closeloop.app";
|
|
41
41
|
var DEFAULT_TIMEOUT = 3e4;
|
|
42
|
-
var SDK_VERSION = "0.1.0";
|
|
42
|
+
var SDK_VERSION = true ? "0.1.8" : "0.0.0";
|
|
43
43
|
var USER_AGENT = `closeloop-node/${SDK_VERSION}`;
|
|
44
|
+
var WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
44
45
|
|
|
45
46
|
// src/schemas/validation.ts
|
|
46
47
|
var import_zod = require("zod");
|
|
47
48
|
var walletAddressSchema = import_zod.z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum wallet address");
|
|
49
|
+
var productIdSchema = import_zod.z.string().min(1, "Product ID is required").max(100, "Product ID too long");
|
|
48
50
|
var planIdSchema = import_zod.z.string().min(1, "Plan ID is required").max(100, "Plan ID too long");
|
|
49
51
|
var balanceIdSchema = import_zod.z.string().min(1, "Balance ID is required").max(100, "Balance ID too long");
|
|
50
52
|
var creditAmountSchema = import_zod.z.number().int("Amount must be an integer").positive("Amount must be positive").max(1e9, "Amount exceeds maximum");
|
|
@@ -56,43 +58,43 @@ var transactionTypeSchema = import_zod.z.enum([
|
|
|
56
58
|
"EXPIRATION"
|
|
57
59
|
]);
|
|
58
60
|
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
59
|
-
|
|
61
|
+
var MAX_METADATA_DEPTH = 5;
|
|
62
|
+
function sanitizeMetadata(metadata, depth = 0) {
|
|
63
|
+
if (depth > MAX_METADATA_DEPTH) return void 0;
|
|
60
64
|
if (!metadata) return void 0;
|
|
61
65
|
const clean = /* @__PURE__ */ Object.create(null);
|
|
62
66
|
for (const [key, value] of Object.entries(metadata)) {
|
|
63
67
|
if (DANGEROUS_KEYS.has(key)) continue;
|
|
64
|
-
|
|
68
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
69
|
+
const sanitizedNested = sanitizeMetadata(
|
|
70
|
+
value,
|
|
71
|
+
depth + 1
|
|
72
|
+
);
|
|
73
|
+
if (sanitizedNested !== void 0) {
|
|
74
|
+
clean[key] = sanitizedNested;
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
clean[key] = value;
|
|
78
|
+
}
|
|
65
79
|
}
|
|
66
80
|
return Object.freeze(clean);
|
|
67
81
|
}
|
|
68
82
|
var verifyCreditsSchema = import_zod.z.object({
|
|
69
83
|
walletAddress: walletAddressSchema,
|
|
70
|
-
|
|
71
|
-
|
|
84
|
+
amount: creditAmountSchema,
|
|
85
|
+
productId: productIdSchema
|
|
72
86
|
});
|
|
73
87
|
var consumeCreditsSchema = import_zod.z.object({
|
|
88
|
+
productId: productIdSchema,
|
|
74
89
|
walletAddress: walletAddressSchema,
|
|
75
|
-
planId: planIdSchema,
|
|
76
90
|
amount: creditAmountSchema,
|
|
77
91
|
consumedBy: import_zod.z.string().max(100).optional(),
|
|
78
92
|
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
|
|
79
93
|
idempotencyKey: import_zod.z.string().max(100).optional()
|
|
80
94
|
});
|
|
81
|
-
var batchConsumeSchema = import_zod.z.object({
|
|
82
|
-
operations: import_zod.z.array(
|
|
83
|
-
import_zod.z.object({
|
|
84
|
-
walletAddress: walletAddressSchema,
|
|
85
|
-
planId: planIdSchema,
|
|
86
|
-
amount: creditAmountSchema,
|
|
87
|
-
consumedBy: import_zod.z.string().max(100).optional(),
|
|
88
|
-
metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
|
|
89
|
-
})
|
|
90
|
-
).min(1, "At least one operation required").max(100, "Maximum 100 operations per batch"),
|
|
91
|
-
atomic: import_zod.z.boolean().optional()
|
|
92
|
-
});
|
|
93
95
|
var getBalanceSchema = import_zod.z.object({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
productId: productIdSchema,
|
|
97
|
+
walletAddress: walletAddressSchema
|
|
96
98
|
});
|
|
97
99
|
var listBalancesSchema = import_zod.z.object({
|
|
98
100
|
walletAddress: walletAddressSchema,
|
|
@@ -106,6 +108,10 @@ var listTransactionsSchema = import_zod.z.object({
|
|
|
106
108
|
limit: paginationLimitSchema.optional(),
|
|
107
109
|
cursor: import_zod.z.string().max(200).optional()
|
|
108
110
|
});
|
|
111
|
+
var getStatsSchema = import_zod.z.object({
|
|
112
|
+
productId: productIdSchema,
|
|
113
|
+
walletAddress: walletAddressSchema
|
|
114
|
+
});
|
|
109
115
|
var webhookEventTypeSchema = import_zod.z.enum([
|
|
110
116
|
"payment.success",
|
|
111
117
|
"credits.low",
|
|
@@ -119,21 +125,20 @@ var webhookEventSchema = import_zod.z.object({
|
|
|
119
125
|
});
|
|
120
126
|
var paymentSuccessPayloadSchema = import_zod.z.object({
|
|
121
127
|
type: import_zod.z.string(),
|
|
128
|
+
productId: import_zod.z.string(),
|
|
122
129
|
planId: import_zod.z.string(),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
walletAddress: import_zod.z.string(),
|
|
126
|
-
transactionId: import_zod.z.string()
|
|
130
|
+
transactionId: import_zod.z.string(),
|
|
131
|
+
walletAddress: import_zod.z.string()
|
|
127
132
|
}).loose();
|
|
128
133
|
var creditsLowPayloadSchema = import_zod.z.object({
|
|
134
|
+
productId: import_zod.z.string(),
|
|
129
135
|
walletAddress: import_zod.z.string(),
|
|
130
|
-
planId: import_zod.z.string(),
|
|
131
136
|
remainingCredits: import_zod.z.number(),
|
|
132
137
|
threshold: import_zod.z.number()
|
|
133
138
|
});
|
|
134
139
|
var creditsExpiredPayloadSchema = import_zod.z.object({
|
|
140
|
+
productId: import_zod.z.string(),
|
|
135
141
|
walletAddress: import_zod.z.string(),
|
|
136
|
-
planId: import_zod.z.string(),
|
|
137
142
|
expiredCredits: import_zod.z.number(),
|
|
138
143
|
expiresAt: import_zod.z.string()
|
|
139
144
|
});
|
|
@@ -205,6 +210,17 @@ var ValidationError = class extends CloseLoopError {
|
|
|
205
210
|
}
|
|
206
211
|
};
|
|
207
212
|
|
|
213
|
+
// src/utils/query.ts
|
|
214
|
+
function buildQueryString(params) {
|
|
215
|
+
const query = new URLSearchParams();
|
|
216
|
+
for (const [key, value] of Object.entries(params)) {
|
|
217
|
+
if (value !== void 0) {
|
|
218
|
+
query.set(key, String(value));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return query.toString();
|
|
222
|
+
}
|
|
223
|
+
|
|
208
224
|
// src/utils/validation.ts
|
|
209
225
|
function validateInput(schema, data) {
|
|
210
226
|
const result = schema.safeParse(data);
|
|
@@ -234,8 +250,7 @@ var Balances = class {
|
|
|
234
250
|
* @example
|
|
235
251
|
* ```typescript
|
|
236
252
|
* const balance = await client.balances.get({
|
|
237
|
-
* walletAddress: "0x1234..."
|
|
238
|
-
* planId: "plan_abc123"
|
|
253
|
+
* walletAddress: "0x1234..."
|
|
239
254
|
* })
|
|
240
255
|
*
|
|
241
256
|
* if (balance) {
|
|
@@ -248,9 +263,9 @@ var Balances = class {
|
|
|
248
263
|
*/
|
|
249
264
|
async get(params) {
|
|
250
265
|
const validated = validateInput(getBalanceSchema, params);
|
|
251
|
-
const query =
|
|
266
|
+
const query = buildQueryString({
|
|
252
267
|
walletAddress: validated.walletAddress,
|
|
253
|
-
|
|
268
|
+
productId: validated.productId
|
|
254
269
|
});
|
|
255
270
|
try {
|
|
256
271
|
return await this.http.request({
|
|
@@ -276,7 +291,7 @@ var Balances = class {
|
|
|
276
291
|
* })
|
|
277
292
|
*
|
|
278
293
|
* for (const balance of balances) {
|
|
279
|
-
* console.log(`${balance.
|
|
294
|
+
* console.log(`${balance.totalCredits}: ${balance.remainingCredits} credits`)
|
|
280
295
|
* }
|
|
281
296
|
*
|
|
282
297
|
* // Paginate if needed
|
|
@@ -292,7 +307,7 @@ var Balances = class {
|
|
|
292
307
|
*/
|
|
293
308
|
async list(params) {
|
|
294
309
|
const validated = validateInput(listBalancesSchema, params);
|
|
295
|
-
const query =
|
|
310
|
+
const query = buildQueryString({
|
|
296
311
|
walletAddress: validated.walletAddress,
|
|
297
312
|
activeOnly: validated.activeOnly,
|
|
298
313
|
limit: validated.limit,
|
|
@@ -323,7 +338,7 @@ var Balances = class {
|
|
|
323
338
|
*/
|
|
324
339
|
async transactions(params) {
|
|
325
340
|
const validated = validateInput(listTransactionsSchema, params);
|
|
326
|
-
const query =
|
|
341
|
+
const query = buildQueryString({
|
|
327
342
|
type: validated.type,
|
|
328
343
|
limit: validated.limit,
|
|
329
344
|
cursor: validated.cursor
|
|
@@ -340,42 +355,32 @@ var Balances = class {
|
|
|
340
355
|
*
|
|
341
356
|
* @example
|
|
342
357
|
* ```typescript
|
|
343
|
-
* const stats = await client.balances.stats(
|
|
358
|
+
* const stats = await client.balances.stats({
|
|
359
|
+
* walletAddress: "0x1234...",
|
|
360
|
+
* productId: "prod_123"
|
|
361
|
+
* })
|
|
344
362
|
* console.log(`Total: ${stats.totalCredits}, Used: ${stats.totalUsed}`)
|
|
345
363
|
* ```
|
|
346
364
|
*
|
|
347
365
|
* @throws {CloseLoopError} When input validation fails
|
|
348
366
|
*/
|
|
349
|
-
async stats(
|
|
350
|
-
validateInput(
|
|
351
|
-
const query =
|
|
367
|
+
async stats(params) {
|
|
368
|
+
validateInput(getStatsSchema, params);
|
|
369
|
+
const query = buildQueryString({
|
|
370
|
+
walletAddress: params.walletAddress,
|
|
371
|
+
productId: params.productId
|
|
372
|
+
});
|
|
352
373
|
return this.http.request({
|
|
353
374
|
method: "GET",
|
|
354
375
|
path: `${ENDPOINTS.STATS}?${query}`
|
|
355
376
|
});
|
|
356
377
|
}
|
|
357
|
-
// ==========================================================================
|
|
358
|
-
// Private Helpers
|
|
359
|
-
// ==========================================================================
|
|
360
|
-
/**
|
|
361
|
-
* Build URL query string from params, filtering out undefined values
|
|
362
|
-
*/
|
|
363
|
-
buildQueryString(params) {
|
|
364
|
-
const query = new URLSearchParams();
|
|
365
|
-
for (const [key, value] of Object.entries(params)) {
|
|
366
|
-
if (value !== void 0) {
|
|
367
|
-
query.set(key, String(value));
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
return query.toString();
|
|
371
|
-
}
|
|
372
378
|
};
|
|
373
379
|
|
|
374
380
|
// src/resources/credits.ts
|
|
375
381
|
var ENDPOINTS2 = {
|
|
376
382
|
VERIFY: "/api/credit/verify",
|
|
377
|
-
CONSUME: "/api/credit/consume"
|
|
378
|
-
BATCH_CONSUME: "/api/credit/consume/batch"
|
|
383
|
+
CONSUME: "/api/credit/consume"
|
|
379
384
|
};
|
|
380
385
|
var Credits = class {
|
|
381
386
|
constructor(http) {
|
|
@@ -388,11 +393,10 @@ var Credits = class {
|
|
|
388
393
|
* ```typescript
|
|
389
394
|
* const result = await client.credits.verify({
|
|
390
395
|
* walletAddress: "0x1234...",
|
|
391
|
-
* planId: "plan_abc123",
|
|
392
396
|
* amount: 10
|
|
393
397
|
* })
|
|
394
398
|
*
|
|
395
|
-
* if (result.
|
|
399
|
+
* if (result.hasEnough) {
|
|
396
400
|
* // Proceed with service
|
|
397
401
|
* }
|
|
398
402
|
* ```
|
|
@@ -414,7 +418,6 @@ var Credits = class {
|
|
|
414
418
|
* ```typescript
|
|
415
419
|
* const result = await client.credits.consume({
|
|
416
420
|
* walletAddress: "0x1234...",
|
|
417
|
-
* planId: "plan_abc123",
|
|
418
421
|
* amount: 1,
|
|
419
422
|
* consumedBy: "ai-generation",
|
|
420
423
|
* metadata: { requestId: "req_xyz" }
|
|
@@ -437,18 +440,22 @@ var Credits = class {
|
|
|
437
440
|
});
|
|
438
441
|
}
|
|
439
442
|
/**
|
|
440
|
-
* Verify and consume credits in a
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
+
* Verify and consume credits in a two-step operation.
|
|
444
|
+
*
|
|
445
|
+
* **Important**: This method performs TWO separate API calls (verify, then consume).
|
|
446
|
+
* There is a potential race condition between the verify and consume steps where
|
|
447
|
+
* another request could consume credits. For critical operations, consider:
|
|
448
|
+
* - Using an `idempotencyKey` to prevent duplicate consumption
|
|
449
|
+
* - Implementing server-side locking if your use case requires strict atomicity
|
|
443
450
|
*
|
|
444
451
|
* @example
|
|
445
452
|
* ```typescript
|
|
446
453
|
* try {
|
|
447
454
|
* const result = await client.credits.verifyAndConsume({
|
|
448
455
|
* walletAddress: "0x1234...",
|
|
449
|
-
* planId: "plan_abc123",
|
|
450
456
|
* amount: 5,
|
|
451
|
-
* consumedBy: "batch-processing"
|
|
457
|
+
* consumedBy: "batch-processing",
|
|
458
|
+
* idempotencyKey: "unique-request-id" // Recommended for safety
|
|
452
459
|
* })
|
|
453
460
|
* // Credits were verified and consumed
|
|
454
461
|
* } catch (error) {
|
|
@@ -469,8 +476,8 @@ var Credits = class {
|
|
|
469
476
|
path: ENDPOINTS2.VERIFY,
|
|
470
477
|
body: {
|
|
471
478
|
walletAddress: validated.walletAddress,
|
|
472
|
-
|
|
473
|
-
|
|
479
|
+
amount: validated.amount,
|
|
480
|
+
productId: validated.productId
|
|
474
481
|
}
|
|
475
482
|
});
|
|
476
483
|
this.assertCreditsAvailable(verification, validated.amount);
|
|
@@ -481,39 +488,6 @@ var Credits = class {
|
|
|
481
488
|
headers: this.buildIdempotencyHeaders(validated.idempotencyKey)
|
|
482
489
|
});
|
|
483
490
|
}
|
|
484
|
-
/**
|
|
485
|
-
* Consume credits in batch.
|
|
486
|
-
*
|
|
487
|
-
* @example
|
|
488
|
-
* ```typescript
|
|
489
|
-
* const result = await client.credits.batchConsume({
|
|
490
|
-
* operations: [
|
|
491
|
-
* { walletAddress: "0x1234...", planId: "plan_a", amount: 1 },
|
|
492
|
-
* { walletAddress: "0x5678...", planId: "plan_b", amount: 2 }
|
|
493
|
-
* ],
|
|
494
|
-
* atomic: false // Allow partial success
|
|
495
|
-
* })
|
|
496
|
-
*
|
|
497
|
-
* console.log(`Success: ${result.successCount}, Failed: ${result.failedCount}`)
|
|
498
|
-
* ```
|
|
499
|
-
*
|
|
500
|
-
* @throws {CloseLoopError} When input validation fails
|
|
501
|
-
*/
|
|
502
|
-
async batchConsume(params) {
|
|
503
|
-
const validated = validateInput(batchConsumeSchema, params);
|
|
504
|
-
const sanitizedOperations = validated.operations.map((op) => ({
|
|
505
|
-
...op,
|
|
506
|
-
metadata: sanitizeMetadata(op.metadata)
|
|
507
|
-
}));
|
|
508
|
-
return this.http.request({
|
|
509
|
-
method: "POST",
|
|
510
|
-
path: ENDPOINTS2.BATCH_CONSUME,
|
|
511
|
-
body: {
|
|
512
|
-
operations: sanitizedOperations,
|
|
513
|
-
atomic: validated.atomic
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
491
|
// ==========================================================================
|
|
518
492
|
// Private Helpers
|
|
519
493
|
// ==========================================================================
|
|
@@ -523,8 +497,8 @@ var Credits = class {
|
|
|
523
497
|
buildConsumeBody(validated) {
|
|
524
498
|
return {
|
|
525
499
|
walletAddress: validated.walletAddress,
|
|
526
|
-
planId: validated.planId,
|
|
527
500
|
amount: validated.amount,
|
|
501
|
+
productId: validated.productId,
|
|
528
502
|
consumedBy: validated.consumedBy,
|
|
529
503
|
metadata: sanitizeMetadata(validated.metadata)
|
|
530
504
|
};
|
|
@@ -540,9 +514,9 @@ var Credits = class {
|
|
|
540
514
|
* Assert credits are available, throw typed errors if not
|
|
541
515
|
*/
|
|
542
516
|
assertCreditsAvailable(verification, requiredAmount) {
|
|
543
|
-
if (!verification.
|
|
517
|
+
if (!verification.hasEnough) {
|
|
544
518
|
throw new InsufficientCreditsError(
|
|
545
|
-
verification.
|
|
519
|
+
verification.totalRemaining,
|
|
546
520
|
requiredAmount
|
|
547
521
|
);
|
|
548
522
|
}
|
|
@@ -572,7 +546,8 @@ function verifySignature(payload, signature, secret) {
|
|
|
572
546
|
var ERROR_CODES = {
|
|
573
547
|
VALIDATION: "VALIDATION_ERROR",
|
|
574
548
|
INVALID_SIGNATURE: "INVALID_SIGNATURE",
|
|
575
|
-
INVALID_PAYLOAD: "INVALID_PAYLOAD"
|
|
549
|
+
INVALID_PAYLOAD: "INVALID_PAYLOAD",
|
|
550
|
+
TIMESTAMP_EXPIRED: "TIMESTAMP_EXPIRED"
|
|
576
551
|
};
|
|
577
552
|
var Webhooks = class {
|
|
578
553
|
/**
|
|
@@ -626,7 +601,10 @@ var Webhooks = class {
|
|
|
626
601
|
const payloadString = this.normalizePayload(params.payload);
|
|
627
602
|
this.verifySignatureOrThrow(payloadString, params.signature, params.secret);
|
|
628
603
|
const parsed = this.parseJsonOrThrow(payloadString);
|
|
629
|
-
|
|
604
|
+
const event = this.validateEventStructure(parsed);
|
|
605
|
+
const tolerance = params.toleranceSeconds ?? WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS;
|
|
606
|
+
this.validateTimestamp(event.createdAt, tolerance);
|
|
607
|
+
return event;
|
|
630
608
|
}
|
|
631
609
|
/**
|
|
632
610
|
* Type guard for payment.success events
|
|
@@ -746,7 +724,29 @@ var Webhooks = class {
|
|
|
746
724
|
400
|
|
747
725
|
);
|
|
748
726
|
}
|
|
749
|
-
|
|
727
|
+
const validatedData = result.data;
|
|
728
|
+
return {
|
|
729
|
+
id: validatedData.id,
|
|
730
|
+
type: validatedData.type,
|
|
731
|
+
createdAt: validatedData.createdAt,
|
|
732
|
+
data: validatedData.data
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Validate webhook timestamp to prevent replay attacks
|
|
737
|
+
* Rejects webhooks older than the specified tolerance
|
|
738
|
+
*/
|
|
739
|
+
validateTimestamp(createdAt, toleranceSeconds) {
|
|
740
|
+
const eventTime = new Date(createdAt).getTime();
|
|
741
|
+
const now = Date.now();
|
|
742
|
+
const ageSeconds = (now - eventTime) / 1e3;
|
|
743
|
+
if (ageSeconds > toleranceSeconds || ageSeconds < -60) {
|
|
744
|
+
throw new CloseLoopError(
|
|
745
|
+
`Webhook timestamp expired or invalid. Event age: ${Math.round(ageSeconds)}s, tolerance: ${toleranceSeconds}s`,
|
|
746
|
+
ERROR_CODES.TIMESTAMP_EXPIRED,
|
|
747
|
+
400
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
750
|
}
|
|
751
751
|
};
|
|
752
752
|
|
|
@@ -847,8 +847,10 @@ var CloseLoop = class {
|
|
|
847
847
|
*/
|
|
848
848
|
webhooks;
|
|
849
849
|
constructor(options) {
|
|
850
|
-
if (!options.apiKey) {
|
|
851
|
-
throw new
|
|
850
|
+
if (!options.apiKey || typeof options.apiKey !== "string" || options.apiKey.trim().length < 10) {
|
|
851
|
+
throw new AuthenticationError(
|
|
852
|
+
"CloseLoop API key is required and must be at least 10 characters"
|
|
853
|
+
);
|
|
852
854
|
}
|
|
853
855
|
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
854
856
|
this.httpClient = new HttpClient({
|