@nehorai/credits 2.2.0 → 3.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/adapters/index.cjs +169 -155
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +2 -2
- package/dist/adapters/index.d.ts +2 -2
- package/dist/adapters/index.js +169 -155
- package/dist/adapters/index.js.map +1 -1
- package/dist/core/index.cjs +148 -24
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +113 -22
- package/dist/core/index.d.ts +113 -22
- package/dist/core/index.js +137 -22
- package/dist/core/index.js.map +1 -1
- package/dist/index-BrogG0Gc.d.cts +277 -0
- package/dist/index-DOvJES2m.d.ts +277 -0
- package/dist/index.cjs +324 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +311 -69
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +254 -49
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +3 -254
- package/dist/repository/index.d.ts +3 -254
- package/dist/repository/index.js +254 -49
- package/dist/repository/index.js.map +1 -1
- package/dist/sdk/index.cjs +1 -0
- package/dist/sdk/index.cjs.map +1 -1
- package/dist/sdk/index.d.cts +2 -2
- package/dist/sdk/index.d.ts +2 -2
- package/dist/sdk/index.js +1 -0
- package/dist/sdk/index.js.map +1 -1
- package/dist/service/index.cjs +42 -19
- package/dist/service/index.cjs.map +1 -1
- package/dist/service/index.d.cts +2 -2
- package/dist/service/index.d.ts +2 -2
- package/dist/service/index.js +42 -19
- package/dist/service/index.js.map +1 -1
- package/dist/{types-3c-IAwZI.d.ts → types-BL3YqxUD.d.ts} +28 -6
- package/dist/{types-MaViK5tD.d.cts → types-C3sxDvNb.d.cts} +29 -6
- package/dist/{types-MaViK5tD.d.ts → types-C3sxDvNb.d.ts} +29 -6
- package/dist/{types-XSjNXO5l.d.cts → types-YLAB8bFk.d.cts} +28 -6
- package/package.json +1 -1
package/dist/adapters/index.cjs
CHANGED
|
@@ -27,6 +27,166 @@ module.exports = __toCommonJS(adapters_exports);
|
|
|
27
27
|
// src/config/index.ts
|
|
28
28
|
var import_zod = require("zod");
|
|
29
29
|
|
|
30
|
+
// src/core/errors.ts
|
|
31
|
+
var CreditErrorCode = {
|
|
32
|
+
INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
|
|
33
|
+
RESERVATION_NOT_FOUND: "RESERVATION_NOT_FOUND",
|
|
34
|
+
RESERVATION_EXPIRED: "RESERVATION_EXPIRED",
|
|
35
|
+
RESERVATION_ALREADY_PROCESSED: "RESERVATION_ALREADY_PROCESSED",
|
|
36
|
+
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
37
|
+
INVALID_OPERATION_TYPE: "INVALID_OPERATION_TYPE",
|
|
38
|
+
CONFIGURATION_ERROR: "CONFIGURATION_ERROR",
|
|
39
|
+
DATABASE_ERROR: "DATABASE_ERROR",
|
|
40
|
+
/** Caller reused an idempotency key with a different immutable payload. */
|
|
41
|
+
IDEMPOTENCY_CONFLICT: "IDEMPOTENCY_CONFLICT",
|
|
42
|
+
/**
|
|
43
|
+
* Retryable infrastructure failure — serialization/deadlock, lock timeout,
|
|
44
|
+
* connection loss, pool exhaustion. Distinct from `DATABASE_ERROR`, which is
|
|
45
|
+
* a deterministic failure that will fail again identically on retry.
|
|
46
|
+
*/
|
|
47
|
+
TRANSIENT_ERROR: "TRANSIENT_ERROR",
|
|
48
|
+
/** The configured repository adapter does not implement the requested V2 method. */
|
|
49
|
+
UNSUPPORTED_OPERATION: "UNSUPPORTED_OPERATION",
|
|
50
|
+
/** Amount was not a finite positive number. */
|
|
51
|
+
INVALID_AMOUNT: "INVALID_AMOUNT",
|
|
52
|
+
/** Idempotency key was supplied but empty or whitespace-only. */
|
|
53
|
+
INVALID_IDEMPOTENCY_KEY: "INVALID_IDEMPOTENCY_KEY",
|
|
54
|
+
/**
|
|
55
|
+
* A persisted reservation holds a status this library does not define, so no
|
|
56
|
+
* transition can reason about it. Quarantined rather than cast; nothing moved.
|
|
57
|
+
*/
|
|
58
|
+
CORRUPT_RESERVATION_STATUS: "CORRUPT_RESERVATION_STATUS",
|
|
59
|
+
/**
|
|
60
|
+
* A persisted reservation does not record that its hold was placed
|
|
61
|
+
* atomically, so the credits it claims are not known to be reserved. Moving
|
|
62
|
+
* it would spend another hold's coverage; nothing moved.
|
|
63
|
+
*/
|
|
64
|
+
UNBACKED_RESERVATION: "UNBACKED_RESERVATION"
|
|
65
|
+
};
|
|
66
|
+
var CreditError = class _CreditError extends Error {
|
|
67
|
+
code;
|
|
68
|
+
details;
|
|
69
|
+
constructor(message, code, details) {
|
|
70
|
+
super(message);
|
|
71
|
+
this.name = "CreditError";
|
|
72
|
+
this.code = code;
|
|
73
|
+
this.details = details;
|
|
74
|
+
if (Error.captureStackTrace) {
|
|
75
|
+
Error.captureStackTrace(this, _CreditError);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Convert to JSON-serializable object
|
|
80
|
+
*/
|
|
81
|
+
toJSON() {
|
|
82
|
+
return {
|
|
83
|
+
name: this.name,
|
|
84
|
+
code: this.code,
|
|
85
|
+
message: this.message,
|
|
86
|
+
...this.details && { details: this.details }
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function isCreditError(error) {
|
|
91
|
+
return error instanceof CreditError;
|
|
92
|
+
}
|
|
93
|
+
function isInsufficientCreditsError(error) {
|
|
94
|
+
if (isCreditError(error)) {
|
|
95
|
+
return error.code === CreditErrorCode.INSUFFICIENT_CREDITS;
|
|
96
|
+
}
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
return error.message.toLowerCase().includes("insufficient credits");
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
function createInsufficientCreditsError(required, available) {
|
|
103
|
+
return new CreditError(
|
|
104
|
+
`Insufficient credits: available ${available}, required ${required}`,
|
|
105
|
+
CreditErrorCode.INSUFFICIENT_CREDITS,
|
|
106
|
+
{ required, available, shortfall: sumAmounts(required, -available) }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
function createReservationNotFoundError(reservationId) {
|
|
110
|
+
return new CreditError(
|
|
111
|
+
`Reservation ${reservationId} not found`,
|
|
112
|
+
CreditErrorCode.RESERVATION_NOT_FOUND,
|
|
113
|
+
{ reservationId }
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
function createReservationAlreadyProcessedError(reservationId, status) {
|
|
117
|
+
return new CreditError(
|
|
118
|
+
`Reservation ${reservationId} has already been ${status}`,
|
|
119
|
+
CreditErrorCode.RESERVATION_ALREADY_PROCESSED,
|
|
120
|
+
{ reservationId, status }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
function createUnsupportedOperationError(operation, hint) {
|
|
124
|
+
return new CreditError(
|
|
125
|
+
hint ? `Repository does not support ${operation}. ${hint}` : `Repository does not support ${operation}`,
|
|
126
|
+
CreditErrorCode.UNSUPPORTED_OPERATION,
|
|
127
|
+
{ operation, ...hint ? { hint } : {} }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/core/amount.ts
|
|
132
|
+
var CREDIT_AMOUNT_SCALE = 2;
|
|
133
|
+
var CREDIT_AMOUNT_MAX = 999999999999e-2;
|
|
134
|
+
var MAX_CENTS = 999999999999;
|
|
135
|
+
function toCents(value) {
|
|
136
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
137
|
+
const cents = Math.round(value * 100);
|
|
138
|
+
if (!Number.isSafeInteger(cents)) return null;
|
|
139
|
+
return value === cents / 100 ? cents : null;
|
|
140
|
+
}
|
|
141
|
+
function sumAmounts(...values) {
|
|
142
|
+
let cents = 0;
|
|
143
|
+
for (const value of values) {
|
|
144
|
+
const valueCents = toCents(value);
|
|
145
|
+
if (valueCents === null) return floatSum(values);
|
|
146
|
+
cents += valueCents;
|
|
147
|
+
}
|
|
148
|
+
return Number.isSafeInteger(cents) ? cents / 100 : floatSum(values);
|
|
149
|
+
}
|
|
150
|
+
function floatSum(values) {
|
|
151
|
+
let total = 0;
|
|
152
|
+
for (const value of values) total += value;
|
|
153
|
+
return total;
|
|
154
|
+
}
|
|
155
|
+
var FLOAT_NOISE_RELATIVE = 64 * Number.EPSILON;
|
|
156
|
+
function isValidCreditAmount(value) {
|
|
157
|
+
if (typeof value !== "number") return false;
|
|
158
|
+
const cents = toCents(value);
|
|
159
|
+
return cents !== null && cents > 0 && cents <= MAX_CENTS;
|
|
160
|
+
}
|
|
161
|
+
function assertValidCreditAmount(value, context) {
|
|
162
|
+
if (isValidCreditAmount(value)) return;
|
|
163
|
+
throw new CreditError(
|
|
164
|
+
creditAmountReason(value),
|
|
165
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
166
|
+
{ amount: value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
function creditAmountReason(value) {
|
|
170
|
+
if (typeof value !== "number") {
|
|
171
|
+
return `Credit amount must be a number (got ${typeof value})`;
|
|
172
|
+
}
|
|
173
|
+
if (!Number.isFinite(value)) {
|
|
174
|
+
return `Credit amount must be finite (got ${String(value)})`;
|
|
175
|
+
}
|
|
176
|
+
if (value <= 0) {
|
|
177
|
+
return `Credit amount must be greater than zero (got ${value})`;
|
|
178
|
+
}
|
|
179
|
+
if (value > CREDIT_AMOUNT_MAX) {
|
|
180
|
+
return `Credit amount ${value} exceeds the maximum of ${CREDIT_AMOUNT_MAX}`;
|
|
181
|
+
}
|
|
182
|
+
return `Credit amount ${value} is not exact to ${CREDIT_AMOUNT_SCALE} decimal places`;
|
|
183
|
+
}
|
|
184
|
+
function isRepresentableAmount(value) {
|
|
185
|
+
if (typeof value !== "number") return false;
|
|
186
|
+
const cents = toCents(value);
|
|
187
|
+
return cents !== null && Math.abs(cents) <= MAX_CENTS;
|
|
188
|
+
}
|
|
189
|
+
|
|
30
190
|
// src/config/provider.ts
|
|
31
191
|
var configProvider = null;
|
|
32
192
|
function getConfigProvider() {
|
|
@@ -183,17 +343,25 @@ var DEFAULT_CONFIG = {
|
|
|
183
343
|
}
|
|
184
344
|
};
|
|
185
345
|
var currentConfig = DEFAULT_CONFIG;
|
|
346
|
+
function isOperationCostMap(value) {
|
|
347
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
348
|
+
return Object.values(value).every((cost) => isRepresentableAmount(cost) && cost >= 0);
|
|
349
|
+
}
|
|
186
350
|
function loadConfigFromEnv() {
|
|
187
351
|
const envConfig = {};
|
|
188
352
|
const operationCostsEnv = process.env.CREDITS_OPERATION_COSTS;
|
|
189
353
|
if (operationCostsEnv) {
|
|
190
354
|
try {
|
|
191
355
|
const parsed = JSON.parse(operationCostsEnv);
|
|
192
|
-
if (
|
|
356
|
+
if (isOperationCostMap(parsed)) {
|
|
193
357
|
envConfig.operationCosts = {
|
|
194
358
|
...DEFAULT_CONFIG.operationCosts,
|
|
195
359
|
...parsed
|
|
196
360
|
};
|
|
361
|
+
} else {
|
|
362
|
+
console.warn(
|
|
363
|
+
"[Credits Config] Ignoring CREDITS_OPERATION_COSTS: it must be a JSON object whose costs are numbers >= 0, exact to the cent"
|
|
364
|
+
);
|
|
197
365
|
}
|
|
198
366
|
} catch {
|
|
199
367
|
console.warn("[Credits Config] Failed to parse CREDITS_OPERATION_COSTS env var as JSON");
|
|
@@ -287,160 +455,6 @@ function getOperationLabel(operationType) {
|
|
|
287
455
|
}
|
|
288
456
|
initializeConfig();
|
|
289
457
|
|
|
290
|
-
// src/core/errors.ts
|
|
291
|
-
var CreditErrorCode = {
|
|
292
|
-
INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
|
|
293
|
-
RESERVATION_NOT_FOUND: "RESERVATION_NOT_FOUND",
|
|
294
|
-
RESERVATION_EXPIRED: "RESERVATION_EXPIRED",
|
|
295
|
-
RESERVATION_ALREADY_PROCESSED: "RESERVATION_ALREADY_PROCESSED",
|
|
296
|
-
USER_NOT_FOUND: "USER_NOT_FOUND",
|
|
297
|
-
INVALID_OPERATION_TYPE: "INVALID_OPERATION_TYPE",
|
|
298
|
-
CONFIGURATION_ERROR: "CONFIGURATION_ERROR",
|
|
299
|
-
DATABASE_ERROR: "DATABASE_ERROR",
|
|
300
|
-
/** Caller reused an idempotency key with a different immutable payload. */
|
|
301
|
-
IDEMPOTENCY_CONFLICT: "IDEMPOTENCY_CONFLICT",
|
|
302
|
-
/**
|
|
303
|
-
* Retryable infrastructure failure — serialization/deadlock, lock timeout,
|
|
304
|
-
* connection loss, pool exhaustion. Distinct from `DATABASE_ERROR`, which is
|
|
305
|
-
* a deterministic failure that will fail again identically on retry.
|
|
306
|
-
*/
|
|
307
|
-
TRANSIENT_ERROR: "TRANSIENT_ERROR",
|
|
308
|
-
/** The configured repository adapter does not implement the requested V2 method. */
|
|
309
|
-
UNSUPPORTED_OPERATION: "UNSUPPORTED_OPERATION",
|
|
310
|
-
/** Amount was not a finite positive number. */
|
|
311
|
-
INVALID_AMOUNT: "INVALID_AMOUNT",
|
|
312
|
-
/** Idempotency key was supplied but empty or whitespace-only. */
|
|
313
|
-
INVALID_IDEMPOTENCY_KEY: "INVALID_IDEMPOTENCY_KEY",
|
|
314
|
-
/**
|
|
315
|
-
* A persisted reservation holds a status this library does not define, so no
|
|
316
|
-
* transition can reason about it. Quarantined rather than cast; nothing moved.
|
|
317
|
-
*/
|
|
318
|
-
CORRUPT_RESERVATION_STATUS: "CORRUPT_RESERVATION_STATUS",
|
|
319
|
-
/**
|
|
320
|
-
* A persisted reservation does not record that its hold was placed
|
|
321
|
-
* atomically, so the credits it claims are not known to be reserved. Moving
|
|
322
|
-
* it would spend another hold's coverage; nothing moved.
|
|
323
|
-
*/
|
|
324
|
-
UNBACKED_RESERVATION: "UNBACKED_RESERVATION"
|
|
325
|
-
};
|
|
326
|
-
var CreditError = class _CreditError extends Error {
|
|
327
|
-
code;
|
|
328
|
-
details;
|
|
329
|
-
constructor(message, code, details) {
|
|
330
|
-
super(message);
|
|
331
|
-
this.name = "CreditError";
|
|
332
|
-
this.code = code;
|
|
333
|
-
this.details = details;
|
|
334
|
-
if (Error.captureStackTrace) {
|
|
335
|
-
Error.captureStackTrace(this, _CreditError);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Convert to JSON-serializable object
|
|
340
|
-
*/
|
|
341
|
-
toJSON() {
|
|
342
|
-
return {
|
|
343
|
-
name: this.name,
|
|
344
|
-
code: this.code,
|
|
345
|
-
message: this.message,
|
|
346
|
-
...this.details && { details: this.details }
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
function isCreditError(error) {
|
|
351
|
-
return error instanceof CreditError;
|
|
352
|
-
}
|
|
353
|
-
function isInsufficientCreditsError(error) {
|
|
354
|
-
if (isCreditError(error)) {
|
|
355
|
-
return error.code === CreditErrorCode.INSUFFICIENT_CREDITS;
|
|
356
|
-
}
|
|
357
|
-
if (error instanceof Error) {
|
|
358
|
-
return error.message.toLowerCase().includes("insufficient credits");
|
|
359
|
-
}
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
function createInsufficientCreditsError(required, available) {
|
|
363
|
-
return new CreditError(
|
|
364
|
-
`Insufficient credits: available ${available}, required ${required}`,
|
|
365
|
-
CreditErrorCode.INSUFFICIENT_CREDITS,
|
|
366
|
-
{ required, available, shortfall: sumAmounts(required, -available) }
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
function createReservationNotFoundError(reservationId) {
|
|
370
|
-
return new CreditError(
|
|
371
|
-
`Reservation ${reservationId} not found`,
|
|
372
|
-
CreditErrorCode.RESERVATION_NOT_FOUND,
|
|
373
|
-
{ reservationId }
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
function createReservationAlreadyProcessedError(reservationId, status) {
|
|
377
|
-
return new CreditError(
|
|
378
|
-
`Reservation ${reservationId} has already been ${status}`,
|
|
379
|
-
CreditErrorCode.RESERVATION_ALREADY_PROCESSED,
|
|
380
|
-
{ reservationId, status }
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
function createUnsupportedOperationError(operation, hint) {
|
|
384
|
-
return new CreditError(
|
|
385
|
-
hint ? `Repository does not support ${operation}. ${hint}` : `Repository does not support ${operation}`,
|
|
386
|
-
CreditErrorCode.UNSUPPORTED_OPERATION,
|
|
387
|
-
{ operation, ...hint ? { hint } : {} }
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// src/core/amount.ts
|
|
392
|
-
var CREDIT_AMOUNT_SCALE = 2;
|
|
393
|
-
var CREDIT_AMOUNT_MAX = 999999999999e-2;
|
|
394
|
-
var MAX_CENTS = 999999999999;
|
|
395
|
-
function toCents(value) {
|
|
396
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
397
|
-
const cents = Math.round(value * 100);
|
|
398
|
-
if (!Number.isSafeInteger(cents)) return null;
|
|
399
|
-
return value === cents / 100 ? cents : null;
|
|
400
|
-
}
|
|
401
|
-
function sumAmounts(...values) {
|
|
402
|
-
let cents = 0;
|
|
403
|
-
for (const value of values) {
|
|
404
|
-
const valueCents = toCents(value);
|
|
405
|
-
if (valueCents === null) return floatSum(values);
|
|
406
|
-
cents += valueCents;
|
|
407
|
-
}
|
|
408
|
-
return Number.isSafeInteger(cents) ? cents / 100 : floatSum(values);
|
|
409
|
-
}
|
|
410
|
-
function floatSum(values) {
|
|
411
|
-
let total = 0;
|
|
412
|
-
for (const value of values) total += value;
|
|
413
|
-
return total;
|
|
414
|
-
}
|
|
415
|
-
function isValidCreditAmount(value) {
|
|
416
|
-
if (typeof value !== "number") return false;
|
|
417
|
-
const cents = toCents(value);
|
|
418
|
-
return cents !== null && cents > 0 && cents <= MAX_CENTS;
|
|
419
|
-
}
|
|
420
|
-
function assertValidCreditAmount(value, context) {
|
|
421
|
-
if (isValidCreditAmount(value)) return;
|
|
422
|
-
throw new CreditError(
|
|
423
|
-
creditAmountReason(value),
|
|
424
|
-
CreditErrorCode.INVALID_AMOUNT,
|
|
425
|
-
{ amount: value, max: CREDIT_AMOUNT_MAX, scale: CREDIT_AMOUNT_SCALE, ...context }
|
|
426
|
-
);
|
|
427
|
-
}
|
|
428
|
-
function creditAmountReason(value) {
|
|
429
|
-
if (typeof value !== "number") {
|
|
430
|
-
return `Credit amount must be a number (got ${typeof value})`;
|
|
431
|
-
}
|
|
432
|
-
if (!Number.isFinite(value)) {
|
|
433
|
-
return `Credit amount must be finite (got ${String(value)})`;
|
|
434
|
-
}
|
|
435
|
-
if (value <= 0) {
|
|
436
|
-
return `Credit amount must be greater than zero (got ${value})`;
|
|
437
|
-
}
|
|
438
|
-
if (value > CREDIT_AMOUNT_MAX) {
|
|
439
|
-
return `Credit amount ${value} exceeds the maximum of ${CREDIT_AMOUNT_MAX}`;
|
|
440
|
-
}
|
|
441
|
-
return `Credit amount ${value} is not exact to ${CREDIT_AMOUNT_SCALE} decimal places`;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
458
|
// src/repository/types.ts
|
|
445
459
|
function supportsCreditsV2(repository) {
|
|
446
460
|
return typeof repository.reserveCreditsV2 === "function" && typeof repository.commitReservationV2 === "function" && typeof repository.releaseReservationV2 === "function" && typeof repository.expireReservationV2 === "function";
|