@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 +21 -0
- package/README.md +478 -0
- package/dist/errors-B0i1OvBd.d.mts +909 -0
- package/dist/errors-B0i1OvBd.d.ts +909 -0
- package/dist/index.d.mts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +883 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +844 -0
- package/dist/index.mjs.map +1 -0
- package/dist/nextjs.d.mts +135 -0
- package/dist/nextjs.d.ts +135 -0
- package/dist/nextjs.js +955 -0
- package/dist/nextjs.js.map +1 -0
- package/dist/nextjs.mjs +920 -0
- package/dist/nextjs.mjs.map +1 -0
- package/package.json +66 -0
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
|