@closeloop/sdk 0.1.6 → 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/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
- function sanitizeMetadata(metadata) {
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
- clean[key] = value;
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
- planId: planIdSchema,
71
- amount: creditAmountSchema
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
- walletAddress: walletAddressSchema,
95
- planId: planIdSchema
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
- planName: import_zod.z.string(),
124
- price: import_zod.z.number(),
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 = this.buildQueryString({
266
+ const query = buildQueryString({
252
267
  walletAddress: validated.walletAddress,
253
- plan: validated.planId
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.planName}: ${balance.remainingCredits} credits`)
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 = this.buildQueryString({
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 = this.buildQueryString({
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("0x1234...")
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(walletAddress) {
350
- validateInput(walletAddressSchema, walletAddress);
351
- const query = this.buildQueryString({ walletAddress });
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.hasEnoughCredits) {
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 single atomic operation.
441
- * This is useful when you want to ensure credits are available
442
- * before consuming them, without race conditions.
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
- planId: validated.planId,
473
- amount: validated.amount
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.hasEnoughCredits) {
517
+ if (!verification.hasEnough) {
544
518
  throw new InsufficientCreditsError(
545
- verification.remainingCredits,
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
- return this.validateEventStructure(parsed);
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
- return result.data;
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 Error("CloseLoop API key is required");
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({