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