@nehorai/credits 2.0.0 → 2.2.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 -21
- package/README.md +384 -377
- package/dist/adapters/index.cjs +12 -8
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.d.cts +3 -2
- package/dist/adapters/index.d.ts +3 -2
- package/dist/adapters/index.js +12 -8
- package/dist/adapters/index.js.map +1 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/core/index.cjs +106 -4
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +92 -5
- package/dist/core/index.d.ts +92 -5
- package/dist/core/index.js +96 -4
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +282 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -7
- package/dist/index.d.ts +9 -7
- package/dist/index.js +270 -45
- package/dist/index.js.map +1 -1
- package/dist/repository/index.cjs +251 -41
- package/dist/repository/index.cjs.map +1 -1
- package/dist/repository/index.d.cts +34 -6
- package/dist/repository/index.d.ts +34 -6
- package/dist/repository/index.js +248 -40
- package/dist/repository/index.js.map +1 -1
- 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.map +1 -1
- package/dist/service/index.cjs +12 -6
- 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 +12 -6
- package/dist/service/index.js.map +1 -1
- package/dist/{types-BmG5Omzj.d.ts → types-3c-IAwZI.d.ts} +38 -3
- package/dist/{types-DJ8yqaVr.d.cts → types-MaViK5tD.d.cts} +23 -1
- package/dist/{types-DJ8yqaVr.d.ts → types-MaViK5tD.d.ts} +23 -1
- package/dist/{types-C8hZMe8a.d.cts → types-XSjNXO5l.d.cts} +38 -3
- package/package.json +1 -1
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/api-key.ts"],"sourcesContent":["import type { ICreditsAuthProvider, CreditsUser } from \"./types.js\";\n\n/**\n * Timing-safe string comparison to prevent timing attacks\n */\nfunction timingSafeEqual(a: string, b: string): boolean {\n if (!a || !b) {\n return false;\n }\n\n // Pad strings to same length to prevent length-based timing attacks\n const maxLength = Math.max(a.length, b.length);\n const paddedA = a.padEnd(maxLength);\n const paddedB = b.padEnd(maxLength);\n\n let result = 0;\n for (let i = 0; i < maxLength; i++) {\n result |= paddedA.charCodeAt(i) ^ paddedB.charCodeAt(i);\n }\n\n // Also check that lengths were originally equal\n return result === 0 && a.length === b.length;\n}\n\n/**\n * Verify a Bearer token from an Authorization header\n *\n * @param header - The Authorization header value (e.g., \"Bearer secret123\")\n * @param secret - The expected secret value\n * @returns True if token is valid\n */\nexport function verifyBearerToken(header: string | null, secret: string): boolean {\n if (!header || !secret) {\n return false;\n }\n\n // Extract token from \"Bearer {token}\" format\n const token = header.replace(/^Bearer\\s+/i, \"\");\n\n // Use timing-safe comparison\n return timingSafeEqual(token, secret);\n}\n\n/**\n * API Key auth provider for admin routes\n *\n * Uses Bearer token authentication for external API access\n * The API key is stored in CREDITS_ADMIN_API_KEY environment variable\n */\nexport class ApiKeyCreditsProvider implements ICreditsAuthProvider {\n private readonly apiKey: string | undefined;\n private isValidated = false;\n\n constructor(authHeader?: string | null, apiKey?: string) {\n this.apiKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;\n\n // Validate the provided auth header using timing-safe comparison\n if (authHeader && this.apiKey) {\n this.isValidated = verifyBearerToken(authHeader, this.apiKey);\n }\n }\n\n async getCurrentUser(): Promise<CreditsUser | null> {\n if (!this.isValidated) {\n return null;\n }\n\n // API key auth returns a special \"admin\" user\n return {\n id: \"api-admin\",\n email: \"api@admin.internal\",\n name: \"API Admin\",\n };\n }\n\n async verifyAdminAccess(): Promise<boolean> {\n return this.isValidated;\n }\n}\n\n/**\n * Verify an API key from a request header\n * @param authHeader - Authorization header value\n * @param apiKey - Optional API key to use (defaults to CREDITS_ADMIN_API_KEY env var)\n * @returns True if API key is valid\n */\nexport function verifyAdminApiKey(authHeader: string | null, apiKey?: string): boolean {\n if (!authHeader) {\n return false;\n }\n\n const expectedKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;\n if (!expectedKey) {\n console.warn(\"CREDITS_ADMIN_API_KEY not configured\");\n return false;\n }\n\n // Use timing-safe comparison to prevent timing attacks\n return verifyBearerToken(authHeader, expectedKey);\n}\n\n/**\n * Create an API key auth provider from a request\n * @param request - Request object with headers\n * @param apiKey - Optional API key to use\n * @returns API key auth provider\n */\nexport function createApiKeyProvider(request: Request, apiKey?: string): ApiKeyCreditsProvider {\n const authHeader = request.headers.get(\"Authorization\");\n return new ApiKeyCreditsProvider(authHeader, apiKey);\n}\n"],"mappings":";AAKA,SAAS,gBAAgB,GAAW,GAAoB;AACtD,MAAI,CAAC,KAAK,CAAC,GAAG;AACZ,WAAO;AAAA,EACT;AAGA,QAAM,YAAY,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM;AAC7C,QAAM,UAAU,EAAE,OAAO,SAAS;AAClC,QAAM,UAAU,EAAE,OAAO,SAAS;AAElC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,cAAU,QAAQ,WAAW,CAAC,IAAI,QAAQ,WAAW,CAAC;AAAA,EACxD;AAGA,SAAO,WAAW,KAAK,EAAE,WAAW,EAAE;AACxC;AASO,SAAS,kBAAkB,QAAuB,QAAyB;AAChF,MAAI,CAAC,UAAU,CAAC,QAAQ;AACtB,WAAO;AAAA,EACT;AAGA,QAAM,QAAQ,OAAO,QAAQ,eAAe,EAAE;AAG9C,SAAO,gBAAgB,OAAO,MAAM;AACtC;AAQO,IAAM,wBAAN,MAA4D;AAAA,EAChD;AAAA,EACT,cAAc;AAAA,EAEtB,YAAY,YAA4B,QAAiB;AACvD,SAAK,SAAS,UAAU,QAAQ,IAAI;AAGpC,QAAI,cAAc,KAAK,QAAQ;AAC7B,WAAK,cAAc,kBAAkB,YAAY,KAAK,MAAM;AAAA,IAC9D;AAAA,EACF;AAAA,EAEA,MAAM,iBAA8C;AAClD,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO;AAAA,IACT;AAGA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,oBAAsC;AAC1C,WAAO,KAAK;AAAA,EACd;AACF;AAQO,SAAS,kBAAkB,YAA2B,QAA0B;AACrF,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,UAAU,QAAQ,IAAI;AAC1C,MAAI,CAAC,aAAa;AAChB,YAAQ,KAAK,sCAAsC;AACnD,WAAO;AAAA,EACT;AAGA,SAAO,kBAAkB,YAAY,WAAW;AAClD;AAQO,SAAS,qBAAqB,SAAkB,QAAwC;AAC7F,QAAM,aAAa,QAAQ,QAAQ,IAAI,eAAe;AACtD,SAAO,IAAI,sBAAsB,YAAY,MAAM;AACrD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/api-key.ts"],"sourcesContent":["import type { ICreditsAuthProvider, CreditsUser } from \"./types.js\";\r\n\r\n/**\r\n * Timing-safe string comparison to prevent timing attacks\r\n */\r\nfunction timingSafeEqual(a: string, b: string): boolean {\r\n if (!a || !b) {\r\n return false;\r\n }\r\n\r\n // Pad strings to same length to prevent length-based timing attacks\r\n const maxLength = Math.max(a.length, b.length);\r\n const paddedA = a.padEnd(maxLength);\r\n const paddedB = b.padEnd(maxLength);\r\n\r\n let result = 0;\r\n for (let i = 0; i < maxLength; i++) {\r\n result |= paddedA.charCodeAt(i) ^ paddedB.charCodeAt(i);\r\n }\r\n\r\n // Also check that lengths were originally equal\r\n return result === 0 && a.length === b.length;\r\n}\r\n\r\n/**\r\n * Verify a Bearer token from an Authorization header\r\n *\r\n * @param header - The Authorization header value (e.g., \"Bearer secret123\")\r\n * @param secret - The expected secret value\r\n * @returns True if token is valid\r\n */\r\nexport function verifyBearerToken(header: string | null, secret: string): boolean {\r\n if (!header || !secret) {\r\n return false;\r\n }\r\n\r\n // Extract token from \"Bearer {token}\" format\r\n const token = header.replace(/^Bearer\\s+/i, \"\");\r\n\r\n // Use timing-safe comparison\r\n return timingSafeEqual(token, secret);\r\n}\r\n\r\n/**\r\n * API Key auth provider for admin routes\r\n *\r\n * Uses Bearer token authentication for external API access\r\n * The API key is stored in CREDITS_ADMIN_API_KEY environment variable\r\n */\r\nexport class ApiKeyCreditsProvider implements ICreditsAuthProvider {\r\n private readonly apiKey: string | undefined;\r\n private isValidated = false;\r\n\r\n constructor(authHeader?: string | null, apiKey?: string) {\r\n this.apiKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;\r\n\r\n // Validate the provided auth header using timing-safe comparison\r\n if (authHeader && this.apiKey) {\r\n this.isValidated = verifyBearerToken(authHeader, this.apiKey);\r\n }\r\n }\r\n\r\n async getCurrentUser(): Promise<CreditsUser | null> {\r\n if (!this.isValidated) {\r\n return null;\r\n }\r\n\r\n // API key auth returns a special \"admin\" user\r\n return {\r\n id: \"api-admin\",\r\n email: \"api@admin.internal\",\r\n name: \"API Admin\",\r\n };\r\n }\r\n\r\n async verifyAdminAccess(): Promise<boolean> {\r\n return this.isValidated;\r\n }\r\n}\r\n\r\n/**\r\n * Verify an API key from a request header\r\n * @param authHeader - Authorization header value\r\n * @param apiKey - Optional API key to use (defaults to CREDITS_ADMIN_API_KEY env var)\r\n * @returns True if API key is valid\r\n */\r\nexport function verifyAdminApiKey(authHeader: string | null, apiKey?: string): boolean {\r\n if (!authHeader) {\r\n return false;\r\n }\r\n\r\n const expectedKey = apiKey ?? process.env.CREDITS_ADMIN_API_KEY;\r\n if (!expectedKey) {\r\n console.warn(\"CREDITS_ADMIN_API_KEY not configured\");\r\n return false;\r\n }\r\n\r\n // Use timing-safe comparison to prevent timing attacks\r\n return verifyBearerToken(authHeader, expectedKey);\r\n}\r\n\r\n/**\r\n * Create an API key auth provider from a request\r\n * @param request - Request object with headers\r\n * @param apiKey - Optional API key to use\r\n * @returns API key auth provider\r\n */\r\nexport function createApiKeyProvider(request: Request, apiKey?: string): ApiKeyCreditsProvider {\r\n const authHeader = request.headers.get(\"Authorization\");\r\n return new ApiKeyCreditsProvider(authHeader, apiKey);\r\n}\r\n"],"mappings":";AAKA,SAAS,gBAAgB,GAAW,GAAoB;AACtD,MAAI,CAAC,KAAK,CAAC,GAAG;AACZ,WAAO;AAAA,EACT;AAGA,QAAM,YAAY,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM;AAC7C,QAAM,UAAU,EAAE,OAAO,SAAS;AAClC,QAAM,UAAU,EAAE,OAAO,SAAS;AAElC,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,cAAU,QAAQ,WAAW,CAAC,IAAI,QAAQ,WAAW,CAAC;AAAA,EACxD;AAGA,SAAO,WAAW,KAAK,EAAE,WAAW,EAAE;AACxC;AASO,SAAS,kBAAkB,QAAuB,QAAyB;AAChF,MAAI,CAAC,UAAU,CAAC,QAAQ;AACtB,WAAO;AAAA,EACT;AAGA,QAAM,QAAQ,OAAO,QAAQ,eAAe,EAAE;AAG9C,SAAO,gBAAgB,OAAO,MAAM;AACtC;AAQO,IAAM,wBAAN,MAA4D;AAAA,EAChD;AAAA,EACT,cAAc;AAAA,EAEtB,YAAY,YAA4B,QAAiB;AACvD,SAAK,SAAS,UAAU,QAAQ,IAAI;AAGpC,QAAI,cAAc,KAAK,QAAQ;AAC7B,WAAK,cAAc,kBAAkB,YAAY,KAAK,MAAM;AAAA,IAC9D;AAAA,EACF;AAAA,EAEA,MAAM,iBAA8C;AAClD,QAAI,CAAC,KAAK,aAAa;AACrB,aAAO;AAAA,IACT;AAGA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO;AAAA,MACP,MAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEA,MAAM,oBAAsC;AAC1C,WAAO,KAAK;AAAA,EACd;AACF;AAQO,SAAS,kBAAkB,YAA2B,QAA0B;AACrF,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,UAAU,QAAQ,IAAI;AAC1C,MAAI,CAAC,aAAa;AAChB,YAAQ,KAAK,sCAAsC;AACnD,WAAO;AAAA,EACT;AAGA,SAAO,kBAAkB,YAAY,WAAW;AAClD;AAQO,SAAS,qBAAqB,SAAkB,QAAwC;AAC7F,QAAM,aAAa,QAAQ,QAAQ,IAAI,eAAe;AACtD,SAAO,IAAI,sBAAsB,YAAY,MAAM;AACrD;","names":[]}
|
package/dist/core/index.cjs
CHANGED
|
@@ -36,12 +36,16 @@ __export(core_exports, {
|
|
|
36
36
|
assertRepresentableTierAmount: () => assertRepresentableTierAmount,
|
|
37
37
|
assertUnkeyedDirectReservation: () => assertUnkeyedDirectReservation,
|
|
38
38
|
assertUnreferencedDirectTransaction: () => assertUnreferencedDirectTransaction,
|
|
39
|
+
assertValidBalanceTarget: () => assertValidBalanceTarget,
|
|
39
40
|
assertValidCreditAmount: () => assertValidCreditAmount,
|
|
40
41
|
assertValidIdempotencyKey: () => assertValidIdempotencyKey,
|
|
41
42
|
assertValidStoredAmount: () => assertValidStoredAmount,
|
|
42
43
|
assertValidStoredAmountRaw: () => assertValidStoredAmountRaw,
|
|
44
|
+
assertValidTierBalanceInput: () => assertValidTierBalanceInput,
|
|
45
|
+
attributeExpiredBacking: () => attributeExpiredBacking,
|
|
43
46
|
backedBalanceFloor: () => backedBalanceFloor,
|
|
44
47
|
calculateAvailableCredits: () => calculateAvailableCredits,
|
|
48
|
+
cappedBalance: () => cappedBalance,
|
|
45
49
|
classifyDatabaseError: () => classifyDatabaseError,
|
|
46
50
|
commitReservationWithJournal: () => commitReservationWithJournal,
|
|
47
51
|
createDeferredExecutor: () => createDeferredExecutor,
|
|
@@ -57,6 +61,7 @@ __export(core_exports, {
|
|
|
57
61
|
createUnsupportedOperationError: () => createUnsupportedOperationError,
|
|
58
62
|
createUserNotFoundError: () => createUserNotFoundError,
|
|
59
63
|
describePaymentMismatch: () => describePaymentMismatch,
|
|
64
|
+
expiredHoldBackingPatch: () => expiredHoldBackingPatch,
|
|
60
65
|
genericDeferred: () => genericDeferred,
|
|
61
66
|
getSqlState: () => getSqlState,
|
|
62
67
|
hasPlacedHold: () => hasPlacedHold,
|
|
@@ -78,9 +83,14 @@ __export(core_exports, {
|
|
|
78
83
|
noopDeferred: () => noopDeferred,
|
|
79
84
|
normalizePaymentRef: () => normalizePaymentRef,
|
|
80
85
|
numericToCents: () => numericToCents,
|
|
86
|
+
planBalanceCap: () => planBalanceCap,
|
|
87
|
+
planBalanceTarget: () => planBalanceTarget,
|
|
88
|
+
planHoldCommit: () => planHoldCommit,
|
|
81
89
|
releaseReservationWithJournal: () => releaseReservationWithJournal,
|
|
82
90
|
reserveCreditsForOperation: () => reserveCreditsForOperation,
|
|
83
91
|
sameAmount: () => sameAmount,
|
|
92
|
+
settleExpiredHoldBacking: () => settleExpiredHoldBacking,
|
|
93
|
+
spendableBalance: () => spendableBalance,
|
|
84
94
|
storedMonthlyLimit: () => storedMonthlyLimit,
|
|
85
95
|
sumAmounts: () => sumAmounts,
|
|
86
96
|
synchronousDeferred: () => synchronousDeferred,
|
|
@@ -312,6 +322,86 @@ function backedBalanceFloor(reserved, bonusCredits) {
|
|
|
312
322
|
assertRepresentableAmount(floor, "backedBalanceFloor", { reserved, bonusCredits });
|
|
313
323
|
return floor;
|
|
314
324
|
}
|
|
325
|
+
function cappedBalance(balance, cap, reserved, bonusCredits) {
|
|
326
|
+
return Math.min(balance, Math.max(cap, backedBalanceFloor(reserved, bonusCredits)));
|
|
327
|
+
}
|
|
328
|
+
function spendableBalance(balance, expiredHoldBacking = 0) {
|
|
329
|
+
return sumAmounts(balance, -Math.min(expiredHoldBacking, balance));
|
|
330
|
+
}
|
|
331
|
+
function planBalanceTarget(target, reserved, bonusCredits) {
|
|
332
|
+
const balance = Math.max(target, backedBalanceFloor(reserved, bonusCredits));
|
|
333
|
+
return { balance, expiredHoldBacking: sumAmounts(balance, -target) };
|
|
334
|
+
}
|
|
335
|
+
function planBalanceCap(balance, cap, reserved, bonusCredits, expiredHoldBacking = 0) {
|
|
336
|
+
const spendable = spendableBalance(balance, expiredHoldBacking);
|
|
337
|
+
const target = Math.min(spendable, cap);
|
|
338
|
+
const kept = Math.max(
|
|
339
|
+
sumAmounts(balance, -spendable),
|
|
340
|
+
sumAmounts(backedBalanceFloor(reserved, bonusCredits), -target)
|
|
341
|
+
);
|
|
342
|
+
const next = Math.min(balance, sumAmounts(target, kept));
|
|
343
|
+
return { balance: next, expiredHoldBacking: Math.max(0, sumAmounts(next, -target)) };
|
|
344
|
+
}
|
|
345
|
+
function settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking) {
|
|
346
|
+
const held = Math.min(expiredHoldBacking, balance);
|
|
347
|
+
const used = Math.min(held, holdBacking);
|
|
348
|
+
return { used, remaining: sumAmounts(held, -used) };
|
|
349
|
+
}
|
|
350
|
+
function planHoldCommit(balance, bonusCredits, expiredHoldBacking, holdBacking, amount) {
|
|
351
|
+
const backing = settleExpiredHoldBacking(balance, expiredHoldBacking, holdBacking);
|
|
352
|
+
const rest = sumAmounts(amount, -backing.used);
|
|
353
|
+
const fromSpendable = Math.min(spendableBalance(balance, expiredHoldBacking), rest);
|
|
354
|
+
const fromBonus = sumAmounts(rest, -fromSpendable);
|
|
355
|
+
return {
|
|
356
|
+
covered: fromBonus <= bonusCredits,
|
|
357
|
+
balance: sumAmounts(balance, -backing.used, -fromSpendable),
|
|
358
|
+
bonusCredits: sumAmounts(bonusCredits, -fromBonus),
|
|
359
|
+
expiredHoldBacking: backing.remaining
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function attributeExpiredBacking(holds, total) {
|
|
363
|
+
const placed = holds.filter((hold) => hold.holdPlacedAt).sort(
|
|
364
|
+
(a, b) => a.holdPlacedAt === b.holdPlacedAt ? a.id < b.id ? -1 : 1 : a.holdPlacedAt < b.holdPlacedAt ? -1 : 1
|
|
365
|
+
);
|
|
366
|
+
const shares = /* @__PURE__ */ new Map();
|
|
367
|
+
let left = total;
|
|
368
|
+
for (const hold of placed) {
|
|
369
|
+
const kept = Math.min(hold.expiredBacking ?? 0, hold.amount, left);
|
|
370
|
+
shares.set(hold.id, kept);
|
|
371
|
+
left = sumAmounts(left, -kept);
|
|
372
|
+
}
|
|
373
|
+
for (const hold of placed) {
|
|
374
|
+
const share = shares.get(hold.id);
|
|
375
|
+
const added = Math.min(sumAmounts(hold.amount, -share), left);
|
|
376
|
+
shares.set(hold.id, sumAmounts(share, added));
|
|
377
|
+
left = sumAmounts(left, -added);
|
|
378
|
+
}
|
|
379
|
+
return shares;
|
|
380
|
+
}
|
|
381
|
+
function expiredHoldBackingPatch(next, stored = 0) {
|
|
382
|
+
return next > 0 || stored > 0 ? { expiredHoldBacking: next } : {};
|
|
383
|
+
}
|
|
384
|
+
function assertValidBalanceTarget(value, field, context) {
|
|
385
|
+
assertRepresentableAmount(value, field, context);
|
|
386
|
+
if (value < 0) {
|
|
387
|
+
throw new CreditError(
|
|
388
|
+
`${field} cannot be negative (got ${value})`,
|
|
389
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
390
|
+
{ field, value, ...context }
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function assertValidTierBalanceInput(input, context) {
|
|
395
|
+
if (input.balanceCap === void 0) return;
|
|
396
|
+
if (input.balance !== void 0) {
|
|
397
|
+
throw new CreditError(
|
|
398
|
+
"A tier update cannot set both balance and balanceCap",
|
|
399
|
+
CreditErrorCode.INVALID_AMOUNT,
|
|
400
|
+
{ balance: input.balance, balanceCap: input.balanceCap, ...context }
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
assertValidBalanceTarget(input.balanceCap, "balanceCap", context);
|
|
404
|
+
}
|
|
315
405
|
function assertRepresentableTierAmount(value, field, context) {
|
|
316
406
|
if (value === Number.POSITIVE_INFINITY) return;
|
|
317
407
|
assertRepresentableAmount(value, field, context);
|
|
@@ -686,11 +776,13 @@ var creditSystemConfigSchema = import_zod.z.object({
|
|
|
686
776
|
/**
|
|
687
777
|
* Operation costs in credits
|
|
688
778
|
* Keys are operation type names (any non-empty string)
|
|
689
|
-
* Values are
|
|
779
|
+
* Values are non-negative numbers representing credit cost. 0 marks a free
|
|
780
|
+
* operation: `withCredits` runs it without touching the ledger. Amounts that
|
|
781
|
+
* move credits stay strictly positive, so `reserveCredits` refuses a 0.
|
|
690
782
|
*/
|
|
691
783
|
operationCosts: import_zod.z.record(
|
|
692
784
|
import_zod.z.string().min(1),
|
|
693
|
-
import_zod.z.number().
|
|
785
|
+
import_zod.z.number().nonnegative()
|
|
694
786
|
),
|
|
695
787
|
/**
|
|
696
788
|
* Display labels for operation types (English canonical labels).
|
|
@@ -712,9 +804,9 @@ var creditSystemConfigSchema = import_zod.z.object({
|
|
|
712
804
|
*/
|
|
713
805
|
reservationExpiryMs: import_zod.z.number().positive().default(5 * 60 * 1e3),
|
|
714
806
|
/**
|
|
715
|
-
* Default credits for new users (free tier)
|
|
807
|
+
* Default credits for new users (free tier). 0 means no signup grant.
|
|
716
808
|
*/
|
|
717
|
-
defaultFreeCredits: import_zod.z.number().
|
|
809
|
+
defaultFreeCredits: import_zod.z.number().min(0).default(25),
|
|
718
810
|
/**
|
|
719
811
|
* Grace period in days after subscription expires before downgrade
|
|
720
812
|
*/
|
|
@@ -1098,12 +1190,16 @@ async function reserveCreditsForOperation(repository, userId, amount, operationT
|
|
|
1098
1190
|
assertRepresentableTierAmount,
|
|
1099
1191
|
assertUnkeyedDirectReservation,
|
|
1100
1192
|
assertUnreferencedDirectTransaction,
|
|
1193
|
+
assertValidBalanceTarget,
|
|
1101
1194
|
assertValidCreditAmount,
|
|
1102
1195
|
assertValidIdempotencyKey,
|
|
1103
1196
|
assertValidStoredAmount,
|
|
1104
1197
|
assertValidStoredAmountRaw,
|
|
1198
|
+
assertValidTierBalanceInput,
|
|
1199
|
+
attributeExpiredBacking,
|
|
1105
1200
|
backedBalanceFloor,
|
|
1106
1201
|
calculateAvailableCredits,
|
|
1202
|
+
cappedBalance,
|
|
1107
1203
|
classifyDatabaseError,
|
|
1108
1204
|
commitReservationWithJournal,
|
|
1109
1205
|
createDeferredExecutor,
|
|
@@ -1119,6 +1215,7 @@ async function reserveCreditsForOperation(repository, userId, amount, operationT
|
|
|
1119
1215
|
createUnsupportedOperationError,
|
|
1120
1216
|
createUserNotFoundError,
|
|
1121
1217
|
describePaymentMismatch,
|
|
1218
|
+
expiredHoldBackingPatch,
|
|
1122
1219
|
genericDeferred,
|
|
1123
1220
|
getSqlState,
|
|
1124
1221
|
hasPlacedHold,
|
|
@@ -1140,9 +1237,14 @@ async function reserveCreditsForOperation(repository, userId, amount, operationT
|
|
|
1140
1237
|
noopDeferred,
|
|
1141
1238
|
normalizePaymentRef,
|
|
1142
1239
|
numericToCents,
|
|
1240
|
+
planBalanceCap,
|
|
1241
|
+
planBalanceTarget,
|
|
1242
|
+
planHoldCommit,
|
|
1143
1243
|
releaseReservationWithJournal,
|
|
1144
1244
|
reserveCreditsForOperation,
|
|
1145
1245
|
sameAmount,
|
|
1246
|
+
settleExpiredHoldBacking,
|
|
1247
|
+
spendableBalance,
|
|
1146
1248
|
storedMonthlyLimit,
|
|
1147
1249
|
sumAmounts,
|
|
1148
1250
|
synchronousDeferred,
|