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