@closeloop/sdk 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,844 @@
1
+ // src/constants.ts
2
+ var DEFAULT_BASE_URL = "https://closeloop.app";
3
+ var DEFAULT_TIMEOUT = 3e4;
4
+ var SDK_VERSION = "0.1.0";
5
+ var USER_AGENT = `closeloop-node/${SDK_VERSION}`;
6
+
7
+ // src/schemas/validation.ts
8
+ import { z } from "zod";
9
+ var walletAddressSchema = z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum wallet address");
10
+ var planIdSchema = z.string().min(1, "Plan ID is required").max(100, "Plan ID too long");
11
+ var balanceIdSchema = z.string().min(1, "Balance ID is required").max(100, "Balance ID too long");
12
+ var creditAmountSchema = z.number().int("Amount must be an integer").positive("Amount must be positive").max(1e9, "Amount exceeds maximum");
13
+ var paginationLimitSchema = z.number().int().positive().max(100, "Limit cannot exceed 100");
14
+ var transactionTypeSchema = z.enum([
15
+ "PURCHASE",
16
+ "CONSUMPTION",
17
+ "REFUND",
18
+ "EXPIRATION"
19
+ ]);
20
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
21
+ function sanitizeMetadata(metadata) {
22
+ if (!metadata) return void 0;
23
+ const clean = /* @__PURE__ */ Object.create(null);
24
+ for (const [key, value] of Object.entries(metadata)) {
25
+ if (DANGEROUS_KEYS.has(key)) continue;
26
+ clean[key] = value;
27
+ }
28
+ return Object.freeze(clean);
29
+ }
30
+ var verifyCreditsSchema = z.object({
31
+ walletAddress: walletAddressSchema,
32
+ planId: planIdSchema,
33
+ amount: creditAmountSchema
34
+ });
35
+ var consumeCreditsSchema = z.object({
36
+ walletAddress: walletAddressSchema,
37
+ planId: planIdSchema,
38
+ amount: creditAmountSchema,
39
+ consumedBy: z.string().max(100).optional(),
40
+ metadata: z.record(z.string(), z.unknown()).optional(),
41
+ idempotencyKey: z.string().max(100).optional()
42
+ });
43
+ var batchConsumeSchema = z.object({
44
+ operations: z.array(
45
+ z.object({
46
+ walletAddress: walletAddressSchema,
47
+ planId: planIdSchema,
48
+ amount: creditAmountSchema,
49
+ consumedBy: z.string().max(100).optional(),
50
+ metadata: z.record(z.string(), z.unknown()).optional()
51
+ })
52
+ ).min(1, "At least one operation required").max(100, "Maximum 100 operations per batch"),
53
+ atomic: z.boolean().optional()
54
+ });
55
+ var getBalanceSchema = z.object({
56
+ walletAddress: walletAddressSchema,
57
+ planId: planIdSchema
58
+ });
59
+ var listBalancesSchema = z.object({
60
+ walletAddress: walletAddressSchema,
61
+ activeOnly: z.boolean().optional(),
62
+ limit: paginationLimitSchema.optional(),
63
+ cursor: z.string().max(200).optional()
64
+ });
65
+ var listTransactionsSchema = z.object({
66
+ balanceId: balanceIdSchema,
67
+ type: transactionTypeSchema.optional(),
68
+ limit: paginationLimitSchema.optional(),
69
+ cursor: z.string().max(200).optional()
70
+ });
71
+ var webhookEventTypeSchema = z.enum([
72
+ "payment.success",
73
+ "credits.low",
74
+ "credits.expired"
75
+ ]);
76
+ var webhookEventSchema = z.object({
77
+ type: webhookEventTypeSchema,
78
+ id: z.string().min(1),
79
+ createdAt: z.string(),
80
+ data: z.record(z.string(), z.unknown())
81
+ });
82
+ var paymentSuccessPayloadSchema = z.object({
83
+ transactionId: z.string(),
84
+ planId: z.string(),
85
+ planName: z.string(),
86
+ amount: z.number(),
87
+ requests: z.number().nullable(),
88
+ creditAmount: z.number().nullable(),
89
+ transactionHash: z.string(),
90
+ payerAddress: z.string(),
91
+ accessType: z.string()
92
+ });
93
+ var creditsLowPayloadSchema = z.object({
94
+ walletAddress: z.string(),
95
+ planId: z.string(),
96
+ remainingCredits: z.number(),
97
+ threshold: z.number()
98
+ });
99
+ var creditsExpiredPayloadSchema = z.object({
100
+ walletAddress: z.string(),
101
+ planId: z.string(),
102
+ expiredCredits: z.number(),
103
+ expiresAt: z.string()
104
+ });
105
+
106
+ // src/utils/errors.ts
107
+ var CloseLoopError = class extends Error {
108
+ constructor(message, code, statusCode, details) {
109
+ super(message);
110
+ this.code = code;
111
+ this.statusCode = statusCode;
112
+ this.details = details;
113
+ this.name = "CloseLoopError";
114
+ }
115
+ };
116
+ var InsufficientCreditsError = class extends CloseLoopError {
117
+ constructor(remainingCredits, requiredCredits) {
118
+ super(
119
+ `Insufficient credits: ${remainingCredits} available, ${requiredCredits} required`,
120
+ "INSUFFICIENT_CREDITS",
121
+ 402
122
+ );
123
+ this.remainingCredits = remainingCredits;
124
+ this.requiredCredits = requiredCredits;
125
+ this.name = "InsufficientCreditsError";
126
+ }
127
+ };
128
+ var CreditsExpiredError = class extends CloseLoopError {
129
+ constructor(expiresAt) {
130
+ super(
131
+ `Credits expired at ${expiresAt.toISOString()}`,
132
+ "CREDITS_EXPIRED",
133
+ 410
134
+ );
135
+ this.expiresAt = expiresAt;
136
+ this.name = "CreditsExpiredError";
137
+ }
138
+ };
139
+ var AuthenticationError = class extends CloseLoopError {
140
+ constructor(message = "Invalid API key") {
141
+ super(message, "AUTHENTICATION_ERROR", 401);
142
+ this.name = "AuthenticationError";
143
+ }
144
+ };
145
+ var RateLimitError = class extends CloseLoopError {
146
+ constructor(retryAfter) {
147
+ super("Rate limit exceeded", "RATE_LIMIT", 429);
148
+ this.retryAfter = retryAfter;
149
+ this.name = "RateLimitError";
150
+ }
151
+ };
152
+ var NetworkError = class extends CloseLoopError {
153
+ constructor(message, cause) {
154
+ super(message, "NETWORK_ERROR");
155
+ this.cause = cause;
156
+ this.name = "NetworkError";
157
+ }
158
+ };
159
+ var NotFoundError = class extends CloseLoopError {
160
+ constructor(resource) {
161
+ super(`${resource} not found`, "NOT_FOUND", 404);
162
+ this.name = "NotFoundError";
163
+ }
164
+ };
165
+ var ValidationError = class extends CloseLoopError {
166
+ constructor(message, field) {
167
+ super(message, "VALIDATION_ERROR", 400);
168
+ this.field = field;
169
+ this.name = "ValidationError";
170
+ }
171
+ };
172
+
173
+ // src/utils/validation.ts
174
+ function validateInput(schema, data) {
175
+ const result = schema.safeParse(data);
176
+ if (!result.success) {
177
+ throw new CloseLoopError(
178
+ `Validation error: ${result.error?.message || "Invalid input"}`,
179
+ "VALIDATION_ERROR",
180
+ 400
181
+ );
182
+ }
183
+ return result.data;
184
+ }
185
+
186
+ // src/resources/balances.ts
187
+ var ENDPOINTS = {
188
+ BALANCE: "/api/credit/balance",
189
+ BALANCES: "/api/credit/balances",
190
+ STATS: "/api/credit/stats"
191
+ };
192
+ var Balances = class {
193
+ constructor(http) {
194
+ this.http = http;
195
+ }
196
+ /**
197
+ * Get a specific credit balance for a wallet and plan.
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * const balance = await client.balances.get({
202
+ * walletAddress: "0x1234...",
203
+ * planId: "plan_abc123"
204
+ * })
205
+ *
206
+ * if (balance) {
207
+ * console.log(`Credits: ${balance.remainingCredits}/${balance.totalCredits}`)
208
+ * }
209
+ * ```
210
+ *
211
+ * @returns The credit balance, or null if not found
212
+ * @throws {CloseLoopError} When input validation fails
213
+ */
214
+ async get(params) {
215
+ const validated = validateInput(getBalanceSchema, params);
216
+ const query = this.buildQueryString({
217
+ wallet: validated.walletAddress,
218
+ plan: validated.planId
219
+ });
220
+ try {
221
+ return await this.http.request({
222
+ method: "GET",
223
+ path: `${ENDPOINTS.BALANCE}?${query}`
224
+ });
225
+ } catch (error) {
226
+ if (error instanceof NotFoundError) {
227
+ return null;
228
+ }
229
+ throw error;
230
+ }
231
+ }
232
+ /**
233
+ * List all credit balances for a wallet.
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * const { balances, nextCursor } = await client.balances.list({
238
+ * walletAddress: "0x1234...",
239
+ * activeOnly: true,
240
+ * limit: 10
241
+ * })
242
+ *
243
+ * for (const balance of balances) {
244
+ * console.log(`${balance.planName}: ${balance.remainingCredits} credits`)
245
+ * }
246
+ *
247
+ * // Paginate if needed
248
+ * if (nextCursor) {
249
+ * const nextPage = await client.balances.list({
250
+ * walletAddress: "0x1234...",
251
+ * cursor: nextCursor
252
+ * })
253
+ * }
254
+ * ```
255
+ *
256
+ * @throws {CloseLoopError} When input validation fails
257
+ */
258
+ async list(params) {
259
+ const validated = validateInput(listBalancesSchema, params);
260
+ const query = this.buildQueryString({
261
+ wallet: validated.walletAddress,
262
+ activeOnly: validated.activeOnly,
263
+ limit: validated.limit,
264
+ cursor: validated.cursor
265
+ });
266
+ return this.http.request({
267
+ method: "GET",
268
+ path: `${ENDPOINTS.BALANCES}?${query}`
269
+ });
270
+ }
271
+ /**
272
+ * Get transaction history for a balance.
273
+ *
274
+ * @example
275
+ * ```typescript
276
+ * const { transactions } = await client.balances.transactions({
277
+ * balanceId: "bal_xyz",
278
+ * type: "CONSUMPTION",
279
+ * limit: 50
280
+ * })
281
+ *
282
+ * for (const tx of transactions) {
283
+ * console.log(`${tx.type}: ${tx.amount} - ${tx.description}`)
284
+ * }
285
+ * ```
286
+ *
287
+ * @throws {CloseLoopError} When input validation fails
288
+ */
289
+ async transactions(params) {
290
+ const validated = validateInput(listTransactionsSchema, params);
291
+ const query = this.buildQueryString({
292
+ type: validated.type,
293
+ limit: validated.limit,
294
+ cursor: validated.cursor
295
+ });
296
+ const basePath = `${ENDPOINTS.BALANCE}/${encodeURIComponent(validated.balanceId)}/transactions`;
297
+ const path = query ? `${basePath}?${query}` : basePath;
298
+ return this.http.request({
299
+ method: "GET",
300
+ path
301
+ });
302
+ }
303
+ /**
304
+ * Get aggregated stats for a wallet's credits.
305
+ *
306
+ * @example
307
+ * ```typescript
308
+ * const stats = await client.balances.stats("0x1234...")
309
+ * console.log(`Total: ${stats.totalCredits}, Used: ${stats.totalUsed}`)
310
+ * ```
311
+ *
312
+ * @throws {CloseLoopError} When input validation fails
313
+ */
314
+ async stats(walletAddress) {
315
+ validateInput(walletAddressSchema, walletAddress);
316
+ const query = this.buildQueryString({ wallet: walletAddress });
317
+ return this.http.request({
318
+ method: "GET",
319
+ path: `${ENDPOINTS.STATS}?${query}`
320
+ });
321
+ }
322
+ // ==========================================================================
323
+ // Private Helpers
324
+ // ==========================================================================
325
+ /**
326
+ * Build URL query string from params, filtering out undefined values
327
+ */
328
+ buildQueryString(params) {
329
+ const query = new URLSearchParams();
330
+ for (const [key, value] of Object.entries(params)) {
331
+ if (value !== void 0) {
332
+ query.set(key, String(value));
333
+ }
334
+ }
335
+ return query.toString();
336
+ }
337
+ };
338
+
339
+ // src/resources/credits.ts
340
+ var ENDPOINTS2 = {
341
+ VERIFY: "/api/credit/verify",
342
+ CONSUME: "/api/credit/consume",
343
+ BATCH_CONSUME: "/api/credit/consume/batch"
344
+ };
345
+ var Credits = class {
346
+ constructor(http) {
347
+ this.http = http;
348
+ }
349
+ /**
350
+ * Verify if a user has enough credits without consuming them.
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * const result = await client.credits.verify({
355
+ * walletAddress: "0x1234...",
356
+ * planId: "plan_abc123",
357
+ * amount: 10
358
+ * })
359
+ *
360
+ * if (result.hasEnoughCredits) {
361
+ * // Proceed with service
362
+ * }
363
+ * ```
364
+ *
365
+ * @throws {CloseLoopError} When input validation fails
366
+ */
367
+ async verify(params) {
368
+ const validated = validateInput(verifyCreditsSchema, params);
369
+ return this.http.request({
370
+ method: "POST",
371
+ path: ENDPOINTS2.VERIFY,
372
+ body: validated
373
+ });
374
+ }
375
+ /**
376
+ * Consume credits from a user's balance.
377
+ *
378
+ * @example
379
+ * ```typescript
380
+ * const result = await client.credits.consume({
381
+ * walletAddress: "0x1234...",
382
+ * planId: "plan_abc123",
383
+ * amount: 1,
384
+ * consumedBy: "ai-generation",
385
+ * metadata: { requestId: "req_xyz" }
386
+ * })
387
+ *
388
+ * console.log(`Remaining: ${result.remainingCredits}`)
389
+ * ```
390
+ *
391
+ * @throws {InsufficientCreditsError} When user doesn't have enough credits
392
+ * @throws {CreditsExpiredError} When credits have expired
393
+ * @throws {CloseLoopError} When input validation fails
394
+ */
395
+ async consume(params) {
396
+ const validated = validateInput(consumeCreditsSchema, params);
397
+ return this.http.request({
398
+ method: "POST",
399
+ path: ENDPOINTS2.CONSUME,
400
+ body: this.buildConsumeBody(validated),
401
+ headers: this.buildIdempotencyHeaders(validated.idempotencyKey)
402
+ });
403
+ }
404
+ /**
405
+ * Verify and consume credits in a single atomic operation.
406
+ * This is useful when you want to ensure credits are available
407
+ * before consuming them, without race conditions.
408
+ *
409
+ * @example
410
+ * ```typescript
411
+ * try {
412
+ * const result = await client.credits.verifyAndConsume({
413
+ * walletAddress: "0x1234...",
414
+ * planId: "plan_abc123",
415
+ * amount: 5,
416
+ * consumedBy: "batch-processing"
417
+ * })
418
+ * // Credits were verified and consumed
419
+ * } catch (error) {
420
+ * if (error instanceof InsufficientCreditsError) {
421
+ * // Handle insufficient credits
422
+ * }
423
+ * }
424
+ * ```
425
+ *
426
+ * @throws {InsufficientCreditsError} When user doesn't have enough credits
427
+ * @throws {CreditsExpiredError} When credits have expired
428
+ * @throws {CloseLoopError} When input validation fails
429
+ */
430
+ async verifyAndConsume(params) {
431
+ const validated = validateInput(consumeCreditsSchema, params);
432
+ const verification = await this.http.request({
433
+ method: "POST",
434
+ path: ENDPOINTS2.VERIFY,
435
+ body: {
436
+ walletAddress: validated.walletAddress,
437
+ planId: validated.planId,
438
+ amount: validated.amount
439
+ }
440
+ });
441
+ this.assertCreditsAvailable(verification, validated.amount);
442
+ return this.http.request({
443
+ method: "POST",
444
+ path: ENDPOINTS2.CONSUME,
445
+ body: this.buildConsumeBody(validated),
446
+ headers: this.buildIdempotencyHeaders(validated.idempotencyKey)
447
+ });
448
+ }
449
+ /**
450
+ * Consume credits in batch.
451
+ *
452
+ * @example
453
+ * ```typescript
454
+ * const result = await client.credits.batchConsume({
455
+ * operations: [
456
+ * { walletAddress: "0x1234...", planId: "plan_a", amount: 1 },
457
+ * { walletAddress: "0x5678...", planId: "plan_b", amount: 2 }
458
+ * ],
459
+ * atomic: false // Allow partial success
460
+ * })
461
+ *
462
+ * console.log(`Success: ${result.successCount}, Failed: ${result.failedCount}`)
463
+ * ```
464
+ *
465
+ * @throws {CloseLoopError} When input validation fails
466
+ */
467
+ async batchConsume(params) {
468
+ const validated = validateInput(batchConsumeSchema, params);
469
+ const sanitizedOperations = validated.operations.map((op) => ({
470
+ ...op,
471
+ metadata: sanitizeMetadata(op.metadata)
472
+ }));
473
+ return this.http.request({
474
+ method: "POST",
475
+ path: ENDPOINTS2.BATCH_CONSUME,
476
+ body: {
477
+ operations: sanitizedOperations,
478
+ atomic: validated.atomic
479
+ }
480
+ });
481
+ }
482
+ // ==========================================================================
483
+ // Private Helpers
484
+ // ==========================================================================
485
+ /**
486
+ * Build consumption request body with sanitized metadata
487
+ */
488
+ buildConsumeBody(validated) {
489
+ return {
490
+ walletAddress: validated.walletAddress,
491
+ planId: validated.planId,
492
+ amount: validated.amount,
493
+ consumedBy: validated.consumedBy,
494
+ metadata: sanitizeMetadata(validated.metadata)
495
+ };
496
+ }
497
+ /**
498
+ * Build idempotency headers if key is provided
499
+ */
500
+ buildIdempotencyHeaders(idempotencyKey) {
501
+ if (!idempotencyKey) return {};
502
+ return { "Idempotency-Key": idempotencyKey };
503
+ }
504
+ /**
505
+ * Assert credits are available, throw typed errors if not
506
+ */
507
+ assertCreditsAvailable(verification, requiredAmount) {
508
+ if (!verification.hasEnoughCredits) {
509
+ throw new InsufficientCreditsError(
510
+ verification.remainingCredits,
511
+ requiredAmount
512
+ );
513
+ }
514
+ if (verification.expiresAt) {
515
+ const expirationDate = new Date(verification.expiresAt);
516
+ if (expirationDate < /* @__PURE__ */ new Date()) {
517
+ throw new CreditsExpiredError(expirationDate);
518
+ }
519
+ }
520
+ }
521
+ };
522
+
523
+ // src/utils/crypto.ts
524
+ import { createHmac, timingSafeEqual } from "crypto";
525
+ function generateSignature(payload, secret) {
526
+ return createHmac("sha256", secret).update(payload).digest("hex");
527
+ }
528
+ function verifySignature(payload, signature, secret) {
529
+ const expectedSignature = generateSignature(payload, secret);
530
+ if (signature.length !== expectedSignature.length) {
531
+ return false;
532
+ }
533
+ return timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
534
+ }
535
+
536
+ // src/resources/webhooks.ts
537
+ var ERROR_CODES = {
538
+ VALIDATION: "VALIDATION_ERROR",
539
+ INVALID_SIGNATURE: "INVALID_SIGNATURE",
540
+ INVALID_PAYLOAD: "INVALID_PAYLOAD"
541
+ };
542
+ var Webhooks = class {
543
+ /**
544
+ * Verify a webhook signature and parse the event.
545
+ *
546
+ * @example
547
+ * ```typescript
548
+ * // Express handler
549
+ * app.post("/webhook", (req, res) => {
550
+ * try {
551
+ * const event = client.webhooks.verify({
552
+ * payload: req.body,
553
+ * signature: req.headers["x-closeloop-signature"],
554
+ * secret: process.env.WEBHOOK_SECRET!
555
+ * })
556
+ *
557
+ * if (client.webhooks.isPaymentSuccess(event)) {
558
+ * const { creditAmount, payerAddress } = event.data
559
+ * // Handle credit purchase
560
+ * }
561
+ *
562
+ * res.json({ received: true })
563
+ * } catch (error) {
564
+ * res.status(400).json({ error: "Invalid signature" })
565
+ * }
566
+ * })
567
+ * ```
568
+ *
569
+ * @example
570
+ * ```typescript
571
+ * // Next.js App Router
572
+ * export async function POST(request: Request) {
573
+ * const payload = await request.text()
574
+ * const signature = request.headers.get("x-closeloop-signature")!
575
+ *
576
+ * const event = client.webhooks.verify({
577
+ * payload,
578
+ * signature,
579
+ * secret: process.env.WEBHOOK_SECRET!
580
+ * })
581
+ *
582
+ * // Handle event...
583
+ * return Response.json({ received: true })
584
+ * }
585
+ * ```
586
+ *
587
+ * @throws {CloseLoopError} When signature is invalid or payload is malformed
588
+ */
589
+ verify(params) {
590
+ this.validateRequiredParams(params);
591
+ const payloadString = this.normalizePayload(params.payload);
592
+ this.verifySignatureOrThrow(payloadString, params.signature, params.secret);
593
+ const parsed = this.parseJsonOrThrow(payloadString);
594
+ return this.validateEventStructure(parsed);
595
+ }
596
+ /**
597
+ * Type guard for payment.success events
598
+ * Also validates the payload structure
599
+ *
600
+ * @example
601
+ * ```typescript
602
+ * if (client.webhooks.isPaymentSuccess(event)) {
603
+ * // TypeScript knows event.data is PaymentSuccessPayload
604
+ * console.log(event.data.creditAmount)
605
+ * }
606
+ * ```
607
+ */
608
+ isPaymentSuccess(event) {
609
+ return event.type === "payment.success" && paymentSuccessPayloadSchema.safeParse(event.data).success;
610
+ }
611
+ /**
612
+ * Type guard for credits.low events
613
+ * Also validates the payload structure
614
+ *
615
+ * @example
616
+ * ```typescript
617
+ * if (client.webhooks.isCreditsLow(event)) {
618
+ * // TypeScript knows event.data is CreditsLowPayload
619
+ * console.log(`Low balance: ${event.data.remainingCredits}`)
620
+ * }
621
+ * ```
622
+ */
623
+ isCreditsLow(event) {
624
+ return event.type === "credits.low" && creditsLowPayloadSchema.safeParse(event.data).success;
625
+ }
626
+ /**
627
+ * Type guard for credits.expired events
628
+ * Also validates the payload structure
629
+ *
630
+ * @example
631
+ * ```typescript
632
+ * if (client.webhooks.isCreditsExpired(event)) {
633
+ * // TypeScript knows event.data is CreditsExpiredPayload
634
+ * console.log(`Expired: ${event.data.expiredCredits} credits`)
635
+ * }
636
+ * ```
637
+ */
638
+ isCreditsExpired(event) {
639
+ return event.type === "credits.expired" && creditsExpiredPayloadSchema.safeParse(event.data).success;
640
+ }
641
+ // ==========================================================================
642
+ // Private Helpers
643
+ // ==========================================================================
644
+ /**
645
+ * Validate that all required parameters are present
646
+ */
647
+ validateRequiredParams(params) {
648
+ if (!params.payload) {
649
+ throw new CloseLoopError(
650
+ "Webhook payload is required",
651
+ ERROR_CODES.VALIDATION,
652
+ 400
653
+ );
654
+ }
655
+ if (!params.signature) {
656
+ throw new CloseLoopError(
657
+ "Webhook signature is required",
658
+ ERROR_CODES.VALIDATION,
659
+ 400
660
+ );
661
+ }
662
+ if (!params.secret) {
663
+ throw new CloseLoopError(
664
+ "Webhook secret is required",
665
+ ERROR_CODES.VALIDATION,
666
+ 400
667
+ );
668
+ }
669
+ }
670
+ /**
671
+ * Convert payload to string regardless of input type
672
+ */
673
+ normalizePayload(payload) {
674
+ return typeof payload === "string" ? payload : payload.toString("utf8");
675
+ }
676
+ /**
677
+ * Verify HMAC signature or throw error
678
+ */
679
+ verifySignatureOrThrow(payload, signature, secret) {
680
+ if (!verifySignature(payload, signature, secret)) {
681
+ throw new CloseLoopError(
682
+ "Invalid webhook signature",
683
+ ERROR_CODES.INVALID_SIGNATURE,
684
+ 401
685
+ );
686
+ }
687
+ }
688
+ /**
689
+ * Parse JSON payload or throw error
690
+ */
691
+ parseJsonOrThrow(payload) {
692
+ try {
693
+ return JSON.parse(payload);
694
+ } catch {
695
+ throw new CloseLoopError(
696
+ "Invalid webhook payload: could not parse JSON",
697
+ ERROR_CODES.INVALID_PAYLOAD,
698
+ 400
699
+ );
700
+ }
701
+ }
702
+ /**
703
+ * Validate event structure using Zod schema
704
+ */
705
+ validateEventStructure(parsed) {
706
+ const result = webhookEventSchema.safeParse(parsed);
707
+ if (!result.success) {
708
+ throw new CloseLoopError(
709
+ "Invalid webhook payload structure",
710
+ ERROR_CODES.INVALID_PAYLOAD,
711
+ 400
712
+ );
713
+ }
714
+ return result.data;
715
+ }
716
+ };
717
+
718
+ // src/utils/http.ts
719
+ var HttpClient = class {
720
+ baseUrl;
721
+ apiKey;
722
+ timeout;
723
+ constructor(options) {
724
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
725
+ this.apiKey = options.apiKey;
726
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
727
+ }
728
+ /**
729
+ * Make an HTTP request to the CloseLoop API
730
+ */
731
+ async request(options) {
732
+ const url = `${this.baseUrl}${options.path}`;
733
+ const controller = new AbortController();
734
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
735
+ try {
736
+ const response = await fetch(url, {
737
+ method: options.method,
738
+ headers: {
739
+ "Content-Type": "application/json",
740
+ "User-Agent": USER_AGENT,
741
+ "X-API-Key": this.apiKey,
742
+ ...options.headers
743
+ },
744
+ body: options.body ? JSON.stringify(options.body) : void 0,
745
+ signal: controller.signal
746
+ });
747
+ clearTimeout(timeoutId);
748
+ if (!response.ok) {
749
+ await this.handleErrorResponse(response);
750
+ }
751
+ if (response.status === 204) {
752
+ return {};
753
+ }
754
+ return await response.json();
755
+ } catch (error) {
756
+ clearTimeout(timeoutId);
757
+ if (error instanceof CloseLoopError) {
758
+ throw error;
759
+ }
760
+ if (error instanceof Error && error.name === "AbortError") {
761
+ throw new NetworkError("Request timeout");
762
+ }
763
+ throw new NetworkError(
764
+ error instanceof Error ? error.message : "Network request failed",
765
+ error instanceof Error ? error : void 0
766
+ );
767
+ }
768
+ }
769
+ async handleErrorResponse(response) {
770
+ let errorData = {};
771
+ try {
772
+ const json = await response.json();
773
+ errorData = json;
774
+ } catch {
775
+ }
776
+ const message = errorData.message || `Request failed with status ${response.status}`;
777
+ switch (response.status) {
778
+ case 401:
779
+ throw new AuthenticationError(message);
780
+ case 404:
781
+ throw new NotFoundError(message);
782
+ case 429: {
783
+ const retryAfter = response.headers.get("Retry-After");
784
+ throw new RateLimitError(
785
+ retryAfter ? parseInt(retryAfter, 10) : void 0
786
+ );
787
+ }
788
+ default:
789
+ throw new CloseLoopError(
790
+ message,
791
+ errorData.code || "API_ERROR",
792
+ response.status,
793
+ errorData.details
794
+ );
795
+ }
796
+ }
797
+ };
798
+
799
+ // src/client.ts
800
+ var CloseLoop = class {
801
+ httpClient;
802
+ /**
803
+ * Credit consumption and verification operations
804
+ */
805
+ credits;
806
+ /**
807
+ * Credit balance queries
808
+ */
809
+ balances;
810
+ /**
811
+ * Webhook signature verification utilities
812
+ */
813
+ webhooks;
814
+ constructor(options) {
815
+ if (!options.apiKey) {
816
+ throw new Error("CloseLoop API key is required");
817
+ }
818
+ const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
819
+ this.httpClient = new HttpClient({
820
+ baseUrl,
821
+ apiKey: options.apiKey,
822
+ timeout: options.timeout
823
+ });
824
+ this.credits = new Credits(this.httpClient);
825
+ this.balances = new Balances(this.httpClient);
826
+ this.webhooks = new Webhooks();
827
+ }
828
+ };
829
+ export {
830
+ AuthenticationError,
831
+ Balances,
832
+ CloseLoop,
833
+ CloseLoopError,
834
+ Credits,
835
+ CreditsExpiredError,
836
+ InsufficientCreditsError,
837
+ NetworkError,
838
+ NotFoundError,
839
+ RateLimitError,
840
+ ValidationError,
841
+ Webhooks,
842
+ validateInput
843
+ };
844
+ //# sourceMappingURL=index.mjs.map