@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 CloseLoop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,478 @@
1
+ # @closeloop/sdk
2
+
3
+ Official Node.js SDK for the [CloseLoop](https://closeloop.app) credit billing system.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@closeloop/sdk.svg)](https://www.npmjs.com/package/@closeloop/sdk)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @closeloop/sdk
13
+ # or
14
+ pnpm add @closeloop/sdk
15
+ # or
16
+ yarn add @closeloop/sdk
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```typescript
22
+ import { CloseLoop } from "@closeloop/sdk"
23
+
24
+ const client = new CloseLoop({
25
+ apiKey: process.env.CLOSELOOP_API_KEY!
26
+ })
27
+
28
+ // Verify credits before processing
29
+ const verification = await client.credits.verify({
30
+ walletAddress: "0x1234...",
31
+ planId: "plan_abc123",
32
+ amount: 1
33
+ })
34
+
35
+ if (verification.hasEnoughCredits) {
36
+ // Process the request...
37
+
38
+ // Consume the credit
39
+ await client.credits.consume({
40
+ walletAddress: "0x1234...",
41
+ planId: "plan_abc123",
42
+ amount: 1,
43
+ consumedBy: "my-service"
44
+ })
45
+ }
46
+ ```
47
+
48
+ ## Features
49
+
50
+ - 🔒 **Secure** - Input validation, HMAC webhook verification, timing-safe comparisons
51
+ - ⚡ **Fast** - Lightweight with minimal dependencies
52
+ - 🔄 **Atomic operations** - Verify and consume in one call
53
+ - 🪝 **Webhook support** - Secure signature verification with schema validation
54
+ - 📦 **Framework helpers** - Next.js middleware with rate limiting docs
55
+ - ✅ **Validated** - All inputs validated with Zod schemas
56
+
57
+ ## API Reference
58
+
59
+ ### Credit Operations
60
+
61
+ #### Verify Credits
62
+
63
+ Check if a user has enough credits without consuming them:
64
+
65
+ ```typescript
66
+ const result = await client.credits.verify({
67
+ walletAddress: "0x...",
68
+ planId: "plan_id",
69
+ amount: 10
70
+ })
71
+
72
+ console.log(result.hasEnoughCredits) // true
73
+ console.log(result.remainingCredits) // 100
74
+ console.log(result.expiresAt) // "2024-12-31T23:59:59Z" or null
75
+ ```
76
+
77
+ #### Consume Credits
78
+
79
+ Deduct credits from a user's balance:
80
+
81
+ ```typescript
82
+ const result = await client.credits.consume({
83
+ walletAddress: "0x...",
84
+ planId: "plan_id",
85
+ amount: 1,
86
+ consumedBy: "ai-generation", // Optional: track what used the credits
87
+ metadata: { requestId: "req_123" }, // Optional: attach metadata
88
+ idempotencyKey: "unique-key-123" // Optional: prevent duplicates
89
+ })
90
+
91
+ console.log(result.success) // true
92
+ console.log(result.remainingCredits) // 99
93
+ console.log(result.transactionId) // "tx_abc123"
94
+ ```
95
+
96
+ #### Verify and Consume (Atomic)
97
+
98
+ Verify and consume in a single operation to prevent race conditions:
99
+
100
+ ```typescript
101
+ import { InsufficientCreditsError, CreditsExpiredError } from "@closeloop/sdk"
102
+
103
+ try {
104
+ const result = await client.credits.verifyAndConsume({
105
+ walletAddress: "0x...",
106
+ planId: "plan_id",
107
+ amount: 5,
108
+ consumedBy: "batch-processing"
109
+ })
110
+ // Credits were verified and consumed
111
+ } catch (error) {
112
+ if (error instanceof InsufficientCreditsError) {
113
+ console.log(`Need ${error.requiredCredits}, have ${error.remainingCredits}`)
114
+ } else if (error instanceof CreditsExpiredError) {
115
+ console.log(`Credits expired at ${error.expiresAt}`)
116
+ }
117
+ }
118
+ ```
119
+
120
+ #### Batch Consume
121
+
122
+ Consume credits for multiple users in one request:
123
+
124
+ ```typescript
125
+ const result = await client.credits.batchConsume({
126
+ operations: [
127
+ { walletAddress: "0x1234...", planId: "plan_a", amount: 1 },
128
+ { walletAddress: "0x5678...", planId: "plan_b", amount: 2 }
129
+ ],
130
+ atomic: false // Allow partial success
131
+ })
132
+
133
+ console.log(`Success: ${result.successCount}, Failed: ${result.failedCount}`)
134
+ ```
135
+
136
+ ### Balance Queries
137
+
138
+ #### Get Specific Balance
139
+
140
+ ```typescript
141
+ const balance = await client.balances.get({
142
+ walletAddress: "0x...",
143
+ planId: "plan_id"
144
+ })
145
+
146
+ if (balance) {
147
+ console.log(`${balance.remainingCredits} / ${balance.totalCredits} credits`)
148
+ console.log(`Active: ${balance.isActive}`)
149
+ console.log(`Expires: ${balance.expiresAt}`)
150
+ }
151
+ ```
152
+
153
+ #### List All Balances
154
+
155
+ ```typescript
156
+ const { balances, nextCursor, totalCount } = await client.balances.list({
157
+ walletAddress: "0x...",
158
+ activeOnly: true,
159
+ limit: 10
160
+ })
161
+
162
+ for (const balance of balances) {
163
+ console.log(`${balance.planName}: ${balance.remainingCredits} credits`)
164
+ }
165
+
166
+ // Paginate
167
+ if (nextCursor) {
168
+ const nextPage = await client.balances.list({
169
+ walletAddress: "0x...",
170
+ cursor: nextCursor
171
+ })
172
+ }
173
+ ```
174
+
175
+ #### Get Transaction History
176
+
177
+ ```typescript
178
+ const { transactions } = await client.balances.transactions({
179
+ balanceId: "bal_id",
180
+ type: "CONSUMPTION", // Optional filter
181
+ limit: 50
182
+ })
183
+
184
+ for (const tx of transactions) {
185
+ console.log(`${tx.type}: ${tx.amount} - ${tx.description}`)
186
+ }
187
+ ```
188
+
189
+ #### Get Aggregated Stats
190
+
191
+ ```typescript
192
+ const stats = await client.balances.stats("0x...")
193
+
194
+ console.log(`Total: ${stats.totalCredits}`)
195
+ console.log(`Used: ${stats.totalUsed}`)
196
+ console.log(`Remaining: ${stats.totalRemaining}`)
197
+ console.log(`Active Balances: ${stats.activeBalances}`)
198
+ ```
199
+
200
+ ### Webhook Verification
201
+
202
+ Securely verify webhook signatures:
203
+
204
+ ```typescript
205
+ // Express
206
+ app.post("/webhook", (req, res) => {
207
+ try {
208
+ const event = client.webhooks.verify({
209
+ payload: req.body, // raw body string
210
+ signature: req.headers["x-closeloop-signature"],
211
+ secret: process.env.WEBHOOK_SECRET!
212
+ })
213
+
214
+ // Type-safe event handling
215
+ if (client.webhooks.isPaymentSuccess(event)) {
216
+ const { creditAmount, payerAddress, planId } = event.data
217
+ console.log(`${payerAddress} purchased ${creditAmount} credits`)
218
+ }
219
+
220
+ if (client.webhooks.isCreditsLow(event)) {
221
+ const { walletAddress, remainingCredits, threshold } = event.data
222
+ console.log(`Low balance alert: ${remainingCredits} < ${threshold}`)
223
+ }
224
+
225
+ if (client.webhooks.isCreditsExpired(event)) {
226
+ const { walletAddress, expiredCredits } = event.data
227
+ console.log(`${expiredCredits} credits expired`)
228
+ }
229
+
230
+ res.json({ received: true })
231
+ } catch (error) {
232
+ res.status(400).json({ error: "Invalid signature" })
233
+ }
234
+ })
235
+ ```
236
+
237
+ ```typescript
238
+ // Next.js App Router
239
+ export async function POST(request: Request) {
240
+ const payload = await request.text()
241
+ const signature = request.headers.get("x-closeloop-signature")!
242
+
243
+ try {
244
+ const event = client.webhooks.verify({
245
+ payload,
246
+ signature,
247
+ secret: process.env.WEBHOOK_SECRET!
248
+ })
249
+
250
+ // Handle event...
251
+ return Response.json({ received: true })
252
+ } catch {
253
+ return Response.json({ error: "Invalid signature" }, { status: 400 })
254
+ }
255
+ }
256
+ ```
257
+
258
+ ## Next.js Integration
259
+
260
+ ### Middleware
261
+
262
+ Protect routes by requiring credits:
263
+
264
+ ```typescript
265
+ // middleware.ts
266
+ import { CloseLoop, creditGate } from "@closeloop/sdk/nextjs"
267
+
268
+ const client = new CloseLoop({ apiKey: process.env.CLOSELOOP_API_KEY! })
269
+
270
+ export default creditGate({
271
+ client,
272
+ planId: "plan_abc123",
273
+ creditsPerRequest: 1,
274
+ getWalletAddress: (req) => req.headers.get("x-wallet-address")
275
+ })
276
+
277
+ export const config = {
278
+ matcher: ["/api/protected/:path*"]
279
+ }
280
+ ```
281
+
282
+ ### API Route Helper
283
+
284
+ Consume credits after processing:
285
+
286
+ ```typescript
287
+ // app/api/ai/route.ts
288
+ import { NextRequest, NextResponse } from "next/server"
289
+ import { CloseLoop, consumeCreditsAfterRequest } from "@closeloop/sdk/nextjs"
290
+
291
+ const client = new CloseLoop({ apiKey: process.env.CLOSELOOP_API_KEY! })
292
+
293
+ export async function POST(request: NextRequest) {
294
+ const wallet = request.headers.get("x-wallet-address")!
295
+
296
+ // Process request...
297
+ const result = await processAIRequest()
298
+
299
+ // Consume credit after successful processing
300
+ await consumeCreditsAfterRequest(client, {
301
+ walletAddress: wallet,
302
+ planId: "plan_abc123",
303
+ amount: 1,
304
+ consumedBy: "ai-text-generation",
305
+ metadata: { requestId: result.id }
306
+ })
307
+
308
+ return NextResponse.json(result)
309
+ }
310
+ ```
311
+
312
+ ## Error Handling
313
+
314
+ All errors extend `CloseLoopError`:
315
+
316
+ ```typescript
317
+ import {
318
+ CloseLoopError,
319
+ InsufficientCreditsError,
320
+ CreditsExpiredError,
321
+ AuthenticationError,
322
+ RateLimitError,
323
+ NetworkError,
324
+ NotFoundError,
325
+ ValidationError
326
+ } from "@closeloop/sdk"
327
+
328
+ try {
329
+ await client.credits.consume({ ... })
330
+ } catch (error) {
331
+ if (error instanceof ValidationError) {
332
+ // 400 - Invalid input (bad wallet address, negative amount, etc.)
333
+ console.log(`Validation error: ${error.message}`)
334
+ } else if (error instanceof InsufficientCreditsError) {
335
+ // 402 - Not enough credits
336
+ console.log(`Need ${error.requiredCredits}, have ${error.remainingCredits}`)
337
+ } else if (error instanceof CreditsExpiredError) {
338
+ // 410 - Credits expired
339
+ console.log(`Expired at ${error.expiresAt}`)
340
+ } else if (error instanceof AuthenticationError) {
341
+ // 401 - Invalid API key
342
+ console.log("Check your API key")
343
+ } else if (error instanceof RateLimitError) {
344
+ // 429 - Too many requests
345
+ console.log(`Retry after ${error.retryAfter} seconds`)
346
+ } else if (error instanceof NotFoundError) {
347
+ // 404 - Resource not found
348
+ console.log("Balance or plan not found")
349
+ } else if (error instanceof NetworkError) {
350
+ // Network issues
351
+ console.log(`Network error: ${error.message}`)
352
+ } else if (error instanceof CloseLoopError) {
353
+ // Other API errors
354
+ console.log(`API error: ${error.code} - ${error.message}`)
355
+ }
356
+ }
357
+ ```
358
+
359
+ ## Security
360
+
361
+ ### Input Validation
362
+
363
+ All inputs are validated using Zod schemas before being sent to the API:
364
+
365
+ - **Wallet addresses**: Must be valid Ethereum addresses (`0x` + 40 hex chars)
366
+ - **Plan IDs**: Non-empty strings with max 100 characters
367
+ - **Credit amounts**: Positive integers up to 1 billion
368
+ - **Metadata**: Sanitized to prevent prototype pollution attacks
369
+
370
+ ### Rate Limiting
371
+
372
+ The `creditGate` middleware does **not** include rate limiting. We strongly recommend combining it with a rate limiter:
373
+
374
+ ```typescript
375
+ // middleware.ts - With rate limiting (RECOMMENDED)
376
+ import { CloseLoop, creditGate } from "@closeloop/sdk/nextjs"
377
+ import { Ratelimit } from "@upstash/ratelimit"
378
+ import { Redis } from "@upstash/redis"
379
+
380
+ const client = new CloseLoop({ apiKey: process.env.CLOSELOOP_API_KEY! })
381
+
382
+ const ratelimit = new Ratelimit({
383
+ redis: Redis.fromEnv(),
384
+ limiter: Ratelimit.slidingWindow(10, "10 s"), // 10 requests per 10 seconds
385
+ })
386
+
387
+ const creditGateMiddleware = creditGate({
388
+ client,
389
+ planId: "plan_abc123",
390
+ creditsPerRequest: 1,
391
+ getWalletAddress: (req) => req.headers.get("x-wallet-address")
392
+ })
393
+
394
+ export default async function middleware(request: NextRequest) {
395
+ // Rate limit by IP first
396
+ const ip = request.ip ?? "127.0.0.1"
397
+ const { success } = await ratelimit.limit(ip)
398
+
399
+ if (!success) {
400
+ return NextResponse.json({ error: "Rate limit exceeded" }, { status: 429 })
401
+ }
402
+
403
+ // Then check credits
404
+ return creditGateMiddleware(request)
405
+ }
406
+
407
+ export const config = {
408
+ matcher: ["/api/protected/:path*"]
409
+ }
410
+ ```
411
+
412
+ ### Webhook Security
413
+
414
+ - HMAC-SHA256 signatures with timing-safe comparison
415
+ - Schema validation for all webhook payloads
416
+ - Type guards validate payload structure before use
417
+
418
+ ## TypeScript
419
+
420
+ All types are exported:
421
+
422
+ ```typescript
423
+ import type {
424
+ // Client
425
+ CloseLoopOptions,
426
+
427
+ // Credits
428
+ VerifyCreditsParams,
429
+ VerifyCreditsResponse,
430
+ ConsumeCreditsParams,
431
+ ConsumeCreditsResponse,
432
+ BatchConsumeParams,
433
+ BatchConsumeResponse,
434
+
435
+ // Balances
436
+ CreditBalance,
437
+ CreditTransaction,
438
+ CreditStats,
439
+ GetBalanceParams,
440
+ ListBalancesParams,
441
+ ListBalancesResponse,
442
+ ListTransactionsParams,
443
+ ListTransactionsResponse,
444
+
445
+ // Webhooks
446
+ WebhookEvent,
447
+ WebhookEventType,
448
+ WebhookPayload,
449
+ PaymentSuccessPayload,
450
+ CreditsLowPayload,
451
+ CreditsExpiredPayload,
452
+ VerifyWebhookParams
453
+ } from "@closeloop/sdk"
454
+ ```
455
+
456
+ ## Configuration
457
+
458
+ ```typescript
459
+ const client = new CloseLoop({
460
+ // Required: Your API key from CloseLoop dashboard
461
+ apiKey: process.env.CLOSELOOP_API_KEY!,
462
+
463
+ // Optional: Custom API URL (for self-hosted instances)
464
+ baseUrl: "https://closeloop.app",
465
+
466
+ // Optional: Request timeout in milliseconds (default: 30000)
467
+ timeout: 30000
468
+ })
469
+ ```
470
+
471
+ ## Requirements
472
+
473
+ - Node.js 18.0.0 or higher
474
+ - Next.js 14.0.0 or higher (for Next.js helpers)
475
+
476
+ ## License
477
+
478
+ MIT