usage-sdk-test 0.1.0 → 0.1.1

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.
Files changed (96) hide show
  1. package/README.md +2 -1
  2. package/dist/errors.d.ts +20 -0
  3. package/dist/errors.d.ts.map +1 -0
  4. package/dist/errors.js +35 -0
  5. package/dist/errors.js.map +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +24 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/persistence/MemoryPersistenceProvider.d.ts +4 -0
  11. package/dist/persistence/MemoryPersistenceProvider.js +19 -0
  12. package/dist/persistence/MemoryPersistenceProvider.js.map +1 -0
  13. package/dist/persistence/PersistenceProvider.d.ts +22 -0
  14. package/dist/persistence/PersistenceProvider.js +3 -0
  15. package/dist/persistence/PersistenceProvider.js.map +1 -0
  16. package/dist/pricing/matchRule.d.ts +2 -0
  17. package/dist/pricing/matchRule.d.ts.map +1 -0
  18. package/dist/pricing/matchRule.js +47 -0
  19. package/dist/pricing/matchRule.js.map +1 -0
  20. package/dist/pricing/quote.d.ts +2 -0
  21. package/dist/pricing/quote.d.ts.map +1 -0
  22. package/dist/pricing/quote.js +35 -0
  23. package/dist/pricing/quote.js.map +1 -0
  24. package/dist/providers/memory/MemoryLedgerStore.d.ts +16 -0
  25. package/dist/providers/memory/MemoryLedgerStore.d.ts.map +1 -0
  26. package/dist/providers/memory/MemoryLedgerStore.js +80 -0
  27. package/dist/providers/memory/MemoryLedgerStore.js.map +1 -0
  28. package/dist/providers/memory/MemoryLimitStore.d.ts +24 -0
  29. package/dist/providers/memory/MemoryLimitStore.d.ts.map +1 -0
  30. package/dist/providers/memory/MemoryLimitStore.js +88 -0
  31. package/dist/providers/memory/MemoryLimitStore.js.map +1 -0
  32. package/dist/providers/memory/MemoryPlanStore.d.ts +7 -0
  33. package/dist/providers/memory/MemoryPlanStore.d.ts.map +1 -0
  34. package/dist/providers/memory/MemoryPlanStore.js +15 -0
  35. package/dist/providers/memory/MemoryPlanStore.js.map +1 -0
  36. package/dist/providers/memory/MemoryUsageStore.d.ts +11 -0
  37. package/dist/providers/memory/MemoryUsageStore.d.ts.map +1 -0
  38. package/dist/providers/memory/MemoryUsageStore.js +16 -0
  39. package/dist/providers/memory/MemoryUsageStore.js.map +1 -0
  40. package/dist/sdk/UsageSdk.d.ts +40 -0
  41. package/dist/sdk/UsageSdk.d.ts.map +1 -0
  42. package/dist/sdk/UsageSdk.js +156 -0
  43. package/dist/sdk/UsageSdk.js.map +1 -0
  44. package/dist/sdk/createUsageSdk.d.ts +41 -0
  45. package/dist/sdk/createUsageSdk.d.ts.map +1 -0
  46. package/dist/sdk/createUsageSdk.js +24 -0
  47. package/dist/sdk/createUsageSdk.js.map +1 -0
  48. package/dist/stores/LedgerStore.d.ts +29 -0
  49. package/dist/stores/LedgerStore.d.ts.map +1 -0
  50. package/dist/stores/LedgerStore.js +3 -0
  51. package/dist/stores/LedgerStore.js.map +1 -0
  52. package/dist/stores/LimitStore.d.ts +20 -0
  53. package/dist/stores/LimitStore.d.ts.map +1 -0
  54. package/dist/stores/LimitStore.js +3 -0
  55. package/dist/stores/LimitStore.js.map +1 -0
  56. package/dist/stores/PlanStore.d.ts +4 -0
  57. package/dist/stores/PlanStore.d.ts.map +1 -0
  58. package/dist/stores/PlanStore.js +3 -0
  59. package/dist/stores/PlanStore.js.map +1 -0
  60. package/dist/stores/UsageStore.d.ts +9 -0
  61. package/dist/stores/UsageStore.d.ts.map +1 -0
  62. package/dist/stores/UsageStore.js +3 -0
  63. package/dist/stores/UsageStore.js.map +1 -0
  64. package/dist/types.d.ts +115 -0
  65. package/dist/types.d.ts.map +1 -0
  66. package/dist/types.js +3 -0
  67. package/dist/types.js.map +1 -0
  68. package/dist/utils/fingerprint.d.ts +2 -0
  69. package/dist/utils/fingerprint.d.ts.map +1 -0
  70. package/dist/utils/fingerprint.js +20 -0
  71. package/dist/utils/fingerprint.js.map +1 -0
  72. package/dist/utils/guards.d.ts +5 -0
  73. package/dist/utils/guards.d.ts.map +1 -0
  74. package/dist/utils/guards.js +32 -0
  75. package/dist/utils/guards.js.map +1 -0
  76. package/package.json +9 -9
  77. package/examples/basic.ts +0 -35
  78. package/src/errors.ts +0 -19
  79. package/src/index.ts +0 -3
  80. package/src/pricing/matchRule.ts +0 -36
  81. package/src/pricing/quote.ts +0 -38
  82. package/src/providers/memory/MemoryLedgerStore.ts +0 -90
  83. package/src/providers/memory/MemoryLimitStore.ts +0 -102
  84. package/src/providers/memory/MemoryPlanStore.ts +0 -14
  85. package/src/providers/memory/MemoryUsageStore.ts +0 -10
  86. package/src/sdk/UsageSdk.ts +0 -117
  87. package/src/sdk/createUsageSdk.ts +0 -38
  88. package/src/stores/LedgerStore.ts +0 -30
  89. package/src/stores/LimitStore.ts +0 -5
  90. package/src/stores/PlanStore.ts +0 -5
  91. package/src/stores/UsageStore.ts +0 -5
  92. package/src/types.ts +0 -116
  93. package/src/utils/fingerprint.ts +0 -14
  94. package/src/utils/guards.ts +0 -23
  95. package/tsconfig.build.json +0 -14
  96. package/tsconfig.json +0 -13
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "usage-sdk-test",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
- "type": "module",
6
- "exports": {
7
- ".": {
8
- "types": "./src/index.ts",
9
- "default": "./src/index.ts"
10
- }
11
- },
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "README.md"
10
+ ],
12
11
  "scripts": {
13
12
  "build": "tsc -p tsconfig.build.json",
14
- "typecheck": "tsc -p tsconfig.json"
13
+ "typecheck": "tsc -p tsconfig.json",
14
+ "prepublishOnly": "bun run build"
15
15
  },
16
16
  "devDependencies": {
17
17
  "typescript": "^5.9.2"
package/examples/basic.ts DELETED
@@ -1,35 +0,0 @@
1
- import { createUsageSdk } from "../src/index.ts";
2
-
3
- const sdk = createUsageSdk({
4
- plans: [
5
- {
6
- planId: "default",
7
- rules: [
8
- { meter: "tokens.input", when: { model: "*", provider: "*" }, unitPriceMinor: 1, currency: "CNY", ruleId: "in" },
9
- { meter: "tokens.output", when: { model: "*", provider: "*" }, unitPriceMinor: 2, currency: "CNY", ruleId: "out" },
10
- ],
11
- },
12
- ],
13
- initialBalances: [{ accountId: "u1", money: { currency: "CNY", amountMinor: 10_000 } }],
14
- });
15
-
16
- const run = async () => {
17
- const before = await sdk.getBalance({ accountId: "u1", currency: "CNY" });
18
- console.log("before", before);
19
-
20
- const result = await sdk.enforce({
21
- accountId: "u1",
22
- planId: "default",
23
- idempotencyKey: "req_001",
24
- limits: { rpm: 60, dailyLimitMinor: 5000, monthlyLimitMinor: 50_000 },
25
- events: [
26
- { meter: "tokens.input", quantity: 1000, dimensions: { model: "gpt-4.1", provider: "openai" } },
27
- { meter: "tokens.output", quantity: 300, dimensions: { model: "gpt-4.1", provider: "openai" } },
28
- ],
29
- });
30
-
31
- console.log("enforce", result);
32
- };
33
-
34
- await run();
35
-
package/src/errors.ts DELETED
@@ -1,19 +0,0 @@
1
- import type { EnforceErrorCode } from "./types.ts";
2
-
3
- export class UsageError extends Error {
4
- readonly code: EnforceErrorCode;
5
- readonly retryAfterMs?: number;
6
-
7
- constructor(code: EnforceErrorCode, message?: string, retryAfterMs?: number) {
8
- super(message ?? code);
9
- this.code = code;
10
- this.retryAfterMs = retryAfterMs;
11
- }
12
- }
13
-
14
- export const toEnforceResultError = (err: unknown) => {
15
- if (err instanceof UsageError) {
16
- return { ok: false as const, code: err.code, message: err.message, retryAfterMs: err.retryAfterMs };
17
- }
18
- return { ok: false as const, code: "INVALID_ARGUMENT" as const, message: err instanceof Error ? err.message : String(err) };
19
- };
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from "./types.ts";
2
- export * from "./errors.ts";
3
- export { createUsageSdk } from "./sdk/createUsageSdk.ts";
@@ -1,36 +0,0 @@
1
- import type { PriceRule, UsageEvent } from "../types.ts";
2
-
3
- const matchWhen = (when: PriceRule["when"] | undefined, dimensions: UsageEvent["dimensions"] | undefined) => {
4
- if (!when) return { ok: true, score: 0 };
5
- const dims = dimensions ?? {};
6
- let score = 0;
7
- for (const [key, expected] of Object.entries(when)) {
8
- const actual = dims[key];
9
- if (expected === "*") continue;
10
- if (Array.isArray(expected)) {
11
- if (!actual || !expected.includes(actual)) return { ok: false, score: 0 };
12
- score += 1;
13
- continue;
14
- }
15
- if (actual !== expected) return { ok: false, score: 0 };
16
- score += 1;
17
- }
18
- return { ok: true, score };
19
- };
20
-
21
- export const pickBestRule = (rules: PriceRule[], event: UsageEvent): PriceRule | null => {
22
- const candidates: Array<{ rule: PriceRule; score: number; priority: number }> = [];
23
- for (const rule of rules) {
24
- if (rule.meter !== event.meter) continue;
25
- const matched = matchWhen(rule.when, event.dimensions);
26
- if (!matched.ok) continue;
27
- candidates.push({ rule, score: matched.score, priority: rule.priority ?? 0 });
28
- }
29
- if (candidates.length === 0) return null;
30
- candidates.sort((a, b) => {
31
- if (b.score !== a.score) return b.score - a.score;
32
- if (b.priority !== a.priority) return b.priority - a.priority;
33
- return 0;
34
- });
35
- return candidates[0]?.rule ?? null;
36
- };
@@ -1,38 +0,0 @@
1
- import { UsageError } from "../errors.ts";
2
- import type { Money, PricePlan, Quote, QuoteInput, QuoteItem } from "../types.ts";
3
- import { assertNonNegativeInteger, assertUsageEvent } from "../utils/guards.ts";
4
- import { pickBestRule } from "./matchRule.ts";
5
-
6
- export const quoteWithPlan = (input: QuoteInput, plan: PricePlan): Quote => {
7
- if (!plan.rules.length) throw new UsageError("NO_PRICE_RULE", `plan ${plan.planId} has no rules`);
8
-
9
- const items: QuoteItem[] = [];
10
- let currency: Money["currency"] | null = null;
11
- let totalMinor = 0;
12
-
13
- for (let i = 0; i < input.events.length; i++) {
14
- const event = input.events[i]!;
15
- assertUsageEvent(event, `events[${i}]`);
16
-
17
- const rule = pickBestRule(plan.rules, event);
18
- if (!rule) {
19
- throw new UsageError("NO_PRICE_RULE", `no price rule for meter=${event.meter}`);
20
- }
21
-
22
- if (!currency) currency = rule.currency;
23
- if (currency !== rule.currency) {
24
- throw new UsageError("CURRENCY_MISMATCH", `mixed currencies in a single quote are not supported (got ${currency} and ${rule.currency})`);
25
- }
26
-
27
- assertNonNegativeInteger(rule.unitPriceMinor, "rule.unitPriceMinor");
28
-
29
- const costMinor = rule.unitPriceMinor * event.quantity;
30
- assertNonNegativeInteger(costMinor, "computed costMinor");
31
- totalMinor += costMinor;
32
-
33
- items.push({ event, costMinor, ruleId: rule.ruleId });
34
- }
35
-
36
- const total: Money = { currency: currency ?? "CNY", amountMinor: totalMinor };
37
- return { total, items };
38
- };
@@ -1,90 +0,0 @@
1
- import { UsageError } from "../../errors.ts";
2
- import type { LedgerApplyResult, LedgerCreditInput, LedgerDebitInput, LedgerStore } from "../../stores/LedgerStore.ts";
3
- import type { Money } from "../../types.ts";
4
- import { assertMoney, assertNonNegativeInteger } from "../../utils/guards.ts";
5
-
6
- const makeBalanceKey = (accountId: string, currency: Money["currency"]) => `${accountId}::${currency}`;
7
-
8
- const randomId = () => {
9
- const c = globalThis.crypto as Crypto | undefined;
10
- if (c?.randomUUID) return c.randomUUID();
11
- return `tx_${Date.now()}_${Math.random().toString(16).slice(2)}`;
12
- };
13
-
14
- type IdempotencyRecord = {
15
- fingerprint: string;
16
- result: LedgerApplyResult;
17
- };
18
-
19
- export class MemoryLedgerStore implements LedgerStore {
20
- private readonly balances = new Map<string, number>();
21
- private readonly idempotency = new Map<string, IdempotencyRecord>();
22
-
23
- constructor(initialBalances?: Array<{ accountId: string; money: Money }>) {
24
- for (const b of initialBalances ?? []) {
25
- assertMoney(b.money, "initialBalances[].money");
26
- const key = makeBalanceKey(b.accountId, b.money.currency);
27
- this.balances.set(key, b.money.amountMinor);
28
- }
29
- }
30
-
31
- async getBalance(input: { accountId: string; currency: Money["currency"] }): Promise<Money> {
32
- const key = makeBalanceKey(input.accountId, input.currency);
33
- const amountMinor = this.balances.get(key) ?? 0;
34
- return { currency: input.currency, amountMinor };
35
- }
36
-
37
- async debit(input: LedgerDebitInput): Promise<LedgerApplyResult> {
38
- assertMoney(input.money, "money");
39
- if (!input.idempotencyKey) throw new UsageError("INVALID_ARGUMENT", "idempotencyKey is required");
40
- if (!input.fingerprint) throw new UsageError("INVALID_ARGUMENT", "fingerprint is required");
41
- assertNonNegativeInteger(input.nowMs, "nowMs");
42
-
43
- const existing = this.idempotency.get(input.idempotencyKey);
44
- if (existing) {
45
- if (existing.fingerprint !== input.fingerprint) throw new UsageError("IDEMPOTENCY_CONFLICT", "idempotencyKey already used with different input");
46
- return existing.result;
47
- }
48
-
49
- const key = makeBalanceKey(input.accountId, input.money.currency);
50
- const current = this.balances.get(key) ?? 0;
51
- if (current < input.money.amountMinor) throw new UsageError("INSUFFICIENT_BALANCE", "insufficient balance");
52
-
53
- const next = current - input.money.amountMinor;
54
- this.balances.set(key, next);
55
-
56
- const result: LedgerApplyResult = {
57
- txId: randomId(),
58
- balanceAfter: { currency: input.money.currency, amountMinor: next },
59
- };
60
-
61
- this.idempotency.set(input.idempotencyKey, { fingerprint: input.fingerprint, result });
62
- return result;
63
- }
64
-
65
- async credit(input: LedgerCreditInput): Promise<LedgerApplyResult> {
66
- assertMoney(input.money, "money");
67
- if (!input.idempotencyKey) throw new UsageError("INVALID_ARGUMENT", "idempotencyKey is required");
68
- if (!input.fingerprint) throw new UsageError("INVALID_ARGUMENT", "fingerprint is required");
69
- assertNonNegativeInteger(input.nowMs, "nowMs");
70
-
71
- const existing = this.idempotency.get(input.idempotencyKey);
72
- if (existing) {
73
- if (existing.fingerprint !== input.fingerprint) throw new UsageError("IDEMPOTENCY_CONFLICT", "idempotencyKey already used with different input");
74
- return existing.result;
75
- }
76
-
77
- const key = makeBalanceKey(input.accountId, input.money.currency);
78
- const current = this.balances.get(key) ?? 0;
79
- const next = current + input.money.amountMinor;
80
- this.balances.set(key, next);
81
-
82
- const result: LedgerApplyResult = {
83
- txId: randomId(),
84
- balanceAfter: { currency: input.money.currency, amountMinor: next },
85
- };
86
-
87
- this.idempotency.set(input.idempotencyKey, { fingerprint: input.fingerprint, result });
88
- return result;
89
- }
90
- }
@@ -1,102 +0,0 @@
1
- import { UsageError } from "../../errors.ts";
2
- import type { LimitStore } from "../../stores/LimitStore.ts";
3
- import { assertNonNegativeInteger } from "../../utils/guards.ts";
4
-
5
- type Counter = { value: number; expiresAtMs: number };
6
-
7
- const pad2 = (n: number) => String(n).padStart(2, "0");
8
-
9
- const getMinuteKey = (nowMs: number) => {
10
- const d = new Date(nowMs);
11
- return `${d.getUTCFullYear()}${pad2(d.getUTCMonth() + 1)}${pad2(d.getUTCDate())}${pad2(d.getUTCHours())}${pad2(d.getUTCMinutes())}`;
12
- };
13
-
14
- const getDayKey = (nowMs: number) => {
15
- const d = new Date(nowMs);
16
- return `${d.getUTCFullYear()}${pad2(d.getUTCMonth() + 1)}${pad2(d.getUTCDate())}`;
17
- };
18
-
19
- const getMonthKey = (nowMs: number) => {
20
- const d = new Date(nowMs);
21
- return `${d.getUTCFullYear()}${pad2(d.getUTCMonth() + 1)}`;
22
- };
23
-
24
- const endOfUtcMinute = (nowMs: number) => Math.floor(nowMs / 60000) * 60000 + 60000;
25
-
26
- const endOfUtcDay = (nowMs: number) => {
27
- const d = new Date(nowMs);
28
- const next = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1, 0, 0, 0, 0);
29
- return next;
30
- };
31
-
32
- const endOfUtcMonth = (nowMs: number) => {
33
- const d = new Date(nowMs);
34
- const next = Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1, 0, 0, 0, 0);
35
- return next;
36
- };
37
-
38
- export class MemoryLimitStore implements LimitStore {
39
- private readonly counters = new Map<string, Counter>();
40
-
41
- private getCounter(key: string, nowMs: number): Counter | null {
42
- const c = this.counters.get(key);
43
- if (!c) return null;
44
- if (c.expiresAtMs <= nowMs) {
45
- this.counters.delete(key);
46
- return null;
47
- }
48
- return c;
49
- }
50
-
51
- private setCounter(key: string, value: number, expiresAtMs: number) {
52
- this.counters.set(key, { value, expiresAtMs });
53
- }
54
-
55
- async consumeRpm(input: { accountId: string; rpm: number; requestCount: number; nowMs: number }): Promise<void> {
56
- assertNonNegativeInteger(input.nowMs, "nowMs");
57
- assertNonNegativeInteger(input.requestCount, "requestCount");
58
- if (input.rpm <= 0) throw new UsageError("RPM_LIMIT_EXCEEDED", "rpm limit exceeded", 60_000);
59
-
60
- const window = getMinuteKey(input.nowMs);
61
- const key = `rpm::${input.accountId}::${window}`;
62
- const current = this.getCounter(key, input.nowMs)?.value ?? 0;
63
-
64
- const next = current + input.requestCount;
65
- if (next > input.rpm) {
66
- const retryAfterMs = Math.max(0, endOfUtcMinute(input.nowMs) - input.nowMs);
67
- throw new UsageError("RPM_LIMIT_EXCEEDED", "rpm limit exceeded", retryAfterMs);
68
- }
69
-
70
- this.setCounter(key, next, endOfUtcMinute(input.nowMs));
71
- }
72
-
73
- async consumeDailyMinor(input: { accountId: string; amountMinor: number; limitMinor: number; nowMs: number }): Promise<void> {
74
- assertNonNegativeInteger(input.nowMs, "nowMs");
75
- assertNonNegativeInteger(input.amountMinor, "amountMinor");
76
- if (input.limitMinor <= 0) throw new UsageError("DAILY_LIMIT_EXCEEDED", "daily limit exceeded");
77
-
78
- const window = getDayKey(input.nowMs);
79
- const key = `dailyMinor::${input.accountId}::${window}`;
80
- const current = this.getCounter(key, input.nowMs)?.value ?? 0;
81
-
82
- const next = current + input.amountMinor;
83
- if (next > input.limitMinor) throw new UsageError("DAILY_LIMIT_EXCEEDED", "daily limit exceeded");
84
-
85
- this.setCounter(key, next, endOfUtcDay(input.nowMs));
86
- }
87
-
88
- async consumeMonthlyMinor(input: { accountId: string; amountMinor: number; limitMinor: number; nowMs: number }): Promise<void> {
89
- assertNonNegativeInteger(input.nowMs, "nowMs");
90
- assertNonNegativeInteger(input.amountMinor, "amountMinor");
91
- if (input.limitMinor <= 0) throw new UsageError("MONTHLY_LIMIT_EXCEEDED", "monthly limit exceeded");
92
-
93
- const window = getMonthKey(input.nowMs);
94
- const key = `monthlyMinor::${input.accountId}::${window}`;
95
- const current = this.getCounter(key, input.nowMs)?.value ?? 0;
96
-
97
- const next = current + input.amountMinor;
98
- if (next > input.limitMinor) throw new UsageError("MONTHLY_LIMIT_EXCEEDED", "monthly limit exceeded");
99
-
100
- this.setCounter(key, next, endOfUtcMonth(input.nowMs));
101
- }
102
- }
@@ -1,14 +0,0 @@
1
- import type { PlanStore } from "../../stores/PlanStore.ts";
2
- import type { PricePlan } from "../../types.ts";
3
-
4
- export class MemoryPlanStore implements PlanStore {
5
- private readonly plans = new Map<string, PricePlan>();
6
-
7
- constructor(plans: PricePlan[]) {
8
- for (const plan of plans) this.plans.set(plan.planId, plan);
9
- }
10
-
11
- async getPlan(planId: string): Promise<PricePlan | null> {
12
- return this.plans.get(planId) ?? null;
13
- }
14
- }
@@ -1,10 +0,0 @@
1
- import type { UsageStore } from "../../stores/UsageStore.ts";
2
- import type { UsageRecord } from "../../types.ts";
3
-
4
- export class MemoryUsageStore implements UsageStore {
5
- readonly records: UsageRecord[] = [];
6
-
7
- async append(record: UsageRecord): Promise<void> {
8
- this.records.push(record);
9
- }
10
- }
@@ -1,117 +0,0 @@
1
- import type { LedgerStore } from "../stores/LedgerStore.ts";
2
- import type { LimitStore } from "../stores/LimitStore.ts";
3
- import type { PlanStore } from "../stores/PlanStore.ts";
4
- import type { UsageStore } from "../stores/UsageStore.ts";
5
- import type { EnforceInput, EnforceResult, Money, Quote, QuoteInput } from "../types.ts";
6
- import { toEnforceResultError, UsageError } from "../errors.ts";
7
- import { quoteWithPlan } from "../pricing/quote.ts";
8
- import { assertNonNegativeInteger } from "../utils/guards.ts";
9
- import { makeFingerprint } from "../utils/fingerprint.ts";
10
-
11
- export type UsageSdkDeps = {
12
- planStore: PlanStore;
13
- ledgerStore: LedgerStore;
14
- limitStore: LimitStore;
15
- usageStore: UsageStore;
16
- };
17
-
18
- export class UsageSdk {
19
- constructor(private readonly deps: UsageSdkDeps) {}
20
-
21
- async quote(input: QuoteInput): Promise<Quote> {
22
- const plan = await this.deps.planStore.getPlan(input.planId);
23
- if (!plan) throw new UsageError("INVALID_ARGUMENT", `unknown planId: ${input.planId}`);
24
- return quoteWithPlan(input, plan);
25
- }
26
-
27
- async getBalance(input: { accountId: string; currency: Money["currency"] }): Promise<Money> {
28
- return this.deps.ledgerStore.getBalance(input);
29
- }
30
-
31
- async enforce(input: EnforceInput): Promise<EnforceResult> {
32
- try {
33
- if (!input.idempotencyKey) throw new UsageError("INVALID_ARGUMENT", "idempotencyKey is required");
34
- const nowMs = input.nowMs ?? Date.now();
35
- assertNonNegativeInteger(nowMs, "nowMs");
36
-
37
- const quote = await this.quote(input);
38
-
39
- const debitFingerprint = makeFingerprint({
40
- op: "debit",
41
- accountId: input.accountId,
42
- planId: input.planId,
43
- idempotencyKey: input.idempotencyKey,
44
- total: quote.total,
45
- events: quote.items.map((i) => ({ meter: i.event.meter, quantity: i.event.quantity, dimensions: i.event.dimensions ?? {} })),
46
- limits: input.limits ?? null,
47
- });
48
-
49
- const debitResult = await this.deps.ledgerStore.debit({
50
- accountId: input.accountId,
51
- money: quote.total,
52
- idempotencyKey: input.idempotencyKey,
53
- fingerprint: debitFingerprint,
54
- metadata: input.metadata,
55
- nowMs,
56
- });
57
-
58
- try {
59
- const requestCount = input.requestCount ?? 1;
60
- assertNonNegativeInteger(requestCount, "requestCount");
61
- const limits = input.limits;
62
- if (limits?.rpm !== undefined) {
63
- await this.deps.limitStore.consumeRpm({ accountId: input.accountId, rpm: limits.rpm, requestCount, nowMs });
64
- }
65
- if (limits?.dailyLimitMinor !== undefined) {
66
- await this.deps.limitStore.consumeDailyMinor({
67
- accountId: input.accountId,
68
- amountMinor: quote.total.amountMinor,
69
- limitMinor: limits.dailyLimitMinor,
70
- nowMs,
71
- });
72
- }
73
- if (limits?.monthlyLimitMinor !== undefined) {
74
- await this.deps.limitStore.consumeMonthlyMinor({
75
- accountId: input.accountId,
76
- amountMinor: quote.total.amountMinor,
77
- limitMinor: limits.monthlyLimitMinor,
78
- nowMs,
79
- });
80
- }
81
- } catch (limitErr) {
82
- const refundKey = `${input.idempotencyKey}::refund`;
83
- const refundFingerprint = makeFingerprint({
84
- op: "refund",
85
- accountId: input.accountId,
86
- currency: quote.total.currency,
87
- amountMinor: quote.total.amountMinor,
88
- for: input.idempotencyKey,
89
- });
90
- await this.deps.ledgerStore.credit({
91
- accountId: input.accountId,
92
- money: quote.total,
93
- idempotencyKey: refundKey,
94
- fingerprint: refundFingerprint,
95
- metadata: { reason: "limits_failed", for: input.idempotencyKey },
96
- nowMs,
97
- });
98
- throw limitErr;
99
- }
100
-
101
- await this.deps.usageStore.append({
102
- accountId: input.accountId,
103
- planId: input.planId,
104
- idempotencyKey: input.idempotencyKey,
105
- txId: debitResult.txId,
106
- cost: quote.total,
107
- events: input.events,
108
- tsMs: nowMs,
109
- metadata: input.metadata,
110
- });
111
-
112
- return { ok: true, txId: debitResult.txId, cost: quote.total, balanceAfter: debitResult.balanceAfter };
113
- } catch (err) {
114
- return toEnforceResultError(err);
115
- }
116
- }
117
- }
@@ -1,38 +0,0 @@
1
- import type { Limits, Money, PricePlan } from "../types.ts";
2
- import { MemoryLedgerStore } from "../providers/memory/MemoryLedgerStore.ts";
3
- import { MemoryLimitStore } from "../providers/memory/MemoryLimitStore.ts";
4
- import { MemoryPlanStore } from "../providers/memory/MemoryPlanStore.ts";
5
- import { MemoryUsageStore } from "../providers/memory/MemoryUsageStore.ts";
6
- import { UsageSdk } from "./UsageSdk.ts";
7
-
8
- export type CreateUsageSdkInput = {
9
- plans: PricePlan[];
10
- initialBalances?: Array<{ accountId: string; money: Money }>;
11
- /**
12
- * Optional: default limits applied by callers (you can also pass per-call limits in enforce()).
13
- */
14
- defaultLimits?: Limits;
15
- };
16
-
17
- export const createUsageSdk = (input: CreateUsageSdkInput) => {
18
- const planStore = new MemoryPlanStore(input.plans);
19
- const ledgerStore = new MemoryLedgerStore(input.initialBalances);
20
- const limitStore = new MemoryLimitStore();
21
- const usageStore = new MemoryUsageStore();
22
-
23
- const sdk = new UsageSdk({ planStore, ledgerStore, limitStore, usageStore });
24
-
25
- return {
26
- quote: sdk.quote.bind(sdk),
27
- enforce: async (enforceInput: Parameters<typeof sdk.enforce>[0]) =>
28
- sdk.enforce({
29
- ...enforceInput,
30
- limits: enforceInput.limits ?? input.defaultLimits,
31
- }),
32
- getBalance: sdk.getBalance.bind(sdk),
33
- /**
34
- * Memory-only escape hatch for debugging/analytics.
35
- */
36
- __unsafe: { usageStore, ledgerStore, limitStore, planStore },
37
- };
38
- };
@@ -1,30 +0,0 @@
1
- import type { Money } from "../types.ts";
2
-
3
- export type LedgerDebitInput = {
4
- accountId: string;
5
- money: Money;
6
- idempotencyKey: string;
7
- fingerprint: string;
8
- metadata?: Record<string, unknown>;
9
- nowMs: number;
10
- };
11
-
12
- export type LedgerCreditInput = {
13
- accountId: string;
14
- money: Money;
15
- idempotencyKey: string;
16
- fingerprint: string;
17
- metadata?: Record<string, unknown>;
18
- nowMs: number;
19
- };
20
-
21
- export type LedgerApplyResult = {
22
- txId: string;
23
- balanceAfter: Money;
24
- };
25
-
26
- export interface LedgerStore {
27
- getBalance(input: { accountId: string; currency: Money["currency"] }): Promise<Money>;
28
- debit(input: LedgerDebitInput): Promise<LedgerApplyResult>;
29
- credit(input: LedgerCreditInput): Promise<LedgerApplyResult>;
30
- }
@@ -1,5 +0,0 @@
1
- export interface LimitStore {
2
- consumeRpm(input: { accountId: string; rpm: number; requestCount: number; nowMs: number }): Promise<void>;
3
- consumeDailyMinor(input: { accountId: string; amountMinor: number; limitMinor: number; nowMs: number }): Promise<void>;
4
- consumeMonthlyMinor(input: { accountId: string; amountMinor: number; limitMinor: number; nowMs: number }): Promise<void>;
5
- }
@@ -1,5 +0,0 @@
1
- import type { PricePlan } from "../types.ts";
2
-
3
- export interface PlanStore {
4
- getPlan(planId: string): Promise<PricePlan | null>;
5
- }
@@ -1,5 +0,0 @@
1
- import type { UsageRecord } from "../types.ts";
2
-
3
- export interface UsageStore {
4
- append(record: UsageRecord): Promise<void>;
5
- }