@arnaudjnn/billing-tools 0.61.0 → 0.62.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.
@@ -0,0 +1,93 @@
1
+ import { type AllowanceState, type LimitState } from "./allowance.js";
2
+ import { type CycleWindow, type PlanCatalog } from "./plan-model.js";
3
+ import { type UsageLedger } from "./usage-ledger.js";
4
+ import type { BillingAdapter, ResolvedConfig } from "./types.js";
5
+ /** One window with progress. `resetsAt` is null only for an open-ended cycle. */
6
+ export interface UsageWindow extends LimitState {
7
+ /** 0-100, clamped, rounded. `size` 0 reads as 100 — nothing is allowed. */
8
+ percent: number;
9
+ resetsAt: number | null;
10
+ }
11
+ export interface UsageSummary {
12
+ plan: string | null;
13
+ /** The billing cycle the cap is measured over. */
14
+ cycle: CycleWindow;
15
+ /** Declared rate limits, tightest-first is NOT imposed: config order. */
16
+ windows: UsageWindow[];
17
+ /** The plan's included window, as its own progress row when there is one. */
18
+ pool: UsageWindow | null;
19
+ /** The caller's seat pack, when the plan caps per seat and a caller was given. */
20
+ pack: (UsageWindow & {
21
+ seatType: string;
22
+ extra: number;
23
+ }) | null;
24
+ /** Prepaid balance, in the configured currency. */
25
+ wallet: number;
26
+ /** Epoch ms this was read. What a "last updated" line shows. */
27
+ at: number;
28
+ }
29
+ export interface UsageSummaryInput {
30
+ orgId: string;
31
+ plans: PlanCatalog;
32
+ plan?: string | null;
33
+ /**
34
+ * Whose usage. Omit for the workspace as a whole — caller-scoped limits and the
35
+ * seat pack are then left out, because neither means anything without a caller.
36
+ * `seatType` is resolved from the adapter when not supplied.
37
+ */
38
+ caller?: {
39
+ kind: "user" | "api";
40
+ id?: string;
41
+ seatType?: string;
42
+ };
43
+ ledger?: UsageLedger;
44
+ now?: number;
45
+ }
46
+ /**
47
+ * Everything a usage screen shows for one subject (a workspace, or one member).
48
+ *
49
+ * One `resolveAllowance` underneath, so the numbers a screen shows and the numbers
50
+ * the meter enforces are the same numbers, read the same way. A screen that
51
+ * computed its own would eventually disagree with the gate — and the disagreement
52
+ * would be invisible until a customer was refused at 60%.
53
+ */
54
+ export declare function usageSummary(adapter: BillingAdapter, config: ResolvedConfig, input: UsageSummaryInput): Promise<UsageSummary>;
55
+ export interface MemberUsage {
56
+ /** WorkOS member id, or the API-key marker for the shared seat. */
57
+ id: string;
58
+ kind: "user" | "api";
59
+ seatType: string;
60
+ /** The member's pack, when the plan caps per seat. */
61
+ pack: (UsageWindow & {
62
+ extra: number;
63
+ }) | null;
64
+ /** Caller-scoped rate limits, measured for this member. */
65
+ windows: UsageWindow[];
66
+ /** Total counted usage in the billing cycle. */
67
+ usedInCycle: number;
68
+ }
69
+ /**
70
+ * Per-member usage for an admin view.
71
+ *
72
+ * One summary per member, run in parallel: the ledger has no group-by, and adding
73
+ * one to the seam would mean either a Stripe meter dimension per member (there is
74
+ * no such thing) or paging every transaction in the window and bucketing it here.
75
+ * A workspace is tens of members, not thousands, and this is a page, not the hot
76
+ * path — but it IS N round trips, so a caller that renders it on every request
77
+ * should cache it.
78
+ */
79
+ export declare function memberUsage(adapter: BillingAdapter, config: ResolvedConfig, input: {
80
+ orgId: string;
81
+ plans: PlanCatalog;
82
+ plan?: string | null;
83
+ /** The members to report on, in the order they should appear. */
84
+ members: readonly {
85
+ id: string;
86
+ kind?: "user" | "api";
87
+ }[];
88
+ ledger?: UsageLedger;
89
+ now?: number;
90
+ }): Promise<MemberUsage[]>;
91
+ /** Re-exported so a consumer can type a state it passes around. */
92
+ export type { AllowanceState, LimitState };
93
+ //# sourceMappingURL=usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAExF,OAAO,EAAa,KAAK,WAAW,EAAc,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC5F,OAAO,EAA4B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAejE,iFAAiF;AACjF,MAAM,WAAW,WAAY,SAAQ,UAAU;IAC7C,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,kDAAkD;IAClD,KAAK,EAAE,WAAW,CAAC;IACnB,yEAAyE;IACzE,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,6EAA6E;IAC7E,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,kFAAkF;IAClF,IAAI,EAAE,CAAC,WAAW,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC;IACjE,mDAAmD;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,EAAE,EAAE,MAAM,CAAC;CACZ;AA+BD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAClE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAAC,YAAY,CAAC,CAyCvB;AAED,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,GAAG,KAAK,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,IAAI,EAAE,CAAC,WAAW,GAAG;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,GAAG,IAAI,CAAC;IAC/C,2DAA2D;IAC3D,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,cAAc,EACtB,KAAK,EAAE;IACL,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,WAAW,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,iEAAiE;IACjE,OAAO,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;KAAE,EAAE,CAAC;IAC1D,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GACA,OAAO,CAAC,WAAW,EAAE,CAAC,CA8BxB;AAED,mEAAmE;AACnE,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC"}
package/dist/usage.js ADDED
@@ -0,0 +1,110 @@
1
+ import { getBillingCustomerId } from "./billing.js";
2
+ import { resolveAllowance } from "./allowance.js";
3
+ import { getSeatType } from "./seats.js";
4
+ import { planModel } from "./plan-model.js";
5
+ import { stripeBalanceUsageLedger } from "./usage-ledger.js";
6
+ const pct = (used, size) => size <= 0 ? 100 : Math.min(100, Math.max(0, Math.round((used / size) * 100)));
7
+ function toWindow(l) {
8
+ return { ...l, percent: pct(l.used, l.size), resetsAt: l.window.end };
9
+ }
10
+ /** The cap's own window, so a screen can render pool/pack with the same row as a
11
+ * rate limit rather than special-casing three shapes. */
12
+ function capWindow(every, label, size, used, cycle) {
13
+ return {
14
+ every,
15
+ scope: "org",
16
+ label,
17
+ size,
18
+ used,
19
+ remaining: Math.max(0, size - used),
20
+ window: cycle,
21
+ percent: pct(used, size),
22
+ resetsAt: cycle.end,
23
+ };
24
+ }
25
+ /**
26
+ * Everything a usage screen shows for one subject (a workspace, or one member).
27
+ *
28
+ * One `resolveAllowance` underneath, so the numbers a screen shows and the numbers
29
+ * the meter enforces are the same numbers, read the same way. A screen that
30
+ * computed its own would eventually disagree with the gate — and the disagreement
31
+ * would be invisible until a customer was refused at 60%.
32
+ */
33
+ export async function usageSummary(adapter, config, input) {
34
+ const model = planModel(input.plans, input.plan ?? null);
35
+ const at = input.now ?? Date.now();
36
+ // A member's seat decides which pack and which seat-scoped limits apply, so
37
+ // resolve it here rather than making every page do it.
38
+ let caller = input.caller;
39
+ if (caller && !caller.seatType) {
40
+ const seatType = caller.kind === "api"
41
+ ? "api"
42
+ : ((caller.id && (await getSeatType(adapter, input.orgId, caller.id))) || "standard");
43
+ caller = { ...caller, seatType };
44
+ }
45
+ const state = await resolveAllowance(adapter, config, {
46
+ orgId: input.orgId,
47
+ plans: input.plans,
48
+ plan: input.plan,
49
+ caller,
50
+ ledger: input.ledger ?? stripeBalanceUsageLedger(),
51
+ now: at,
52
+ });
53
+ return {
54
+ plan: state.plan,
55
+ cycle: state.cycle,
56
+ windows: state.limits.map(toWindow),
57
+ pool: state.pool
58
+ ? capWindow("cycle", model?.display?.pooled?.title ?? null, state.pool.size, state.pool.used, state.cycle)
59
+ : null,
60
+ pack: state.pack
61
+ ? {
62
+ ...capWindow("cycle", null, state.pack.size + state.pack.extra, state.pack.used, state.cycle),
63
+ seatType: state.pack.seatType,
64
+ extra: state.pack.extra,
65
+ }
66
+ : null,
67
+ wallet: state.wallet,
68
+ at,
69
+ };
70
+ }
71
+ /**
72
+ * Per-member usage for an admin view.
73
+ *
74
+ * One summary per member, run in parallel: the ledger has no group-by, and adding
75
+ * one to the seam would mean either a Stripe meter dimension per member (there is
76
+ * no such thing) or paging every transaction in the window and bucketing it here.
77
+ * A workspace is tens of members, not thousands, and this is a page, not the hot
78
+ * path — but it IS N round trips, so a caller that renders it on every request
79
+ * should cache it.
80
+ */
81
+ export async function memberUsage(adapter, config, input) {
82
+ const at = input.now ?? Date.now();
83
+ const customerId = await getBillingCustomerId(adapter, input.orgId);
84
+ if (!customerId)
85
+ return [];
86
+ return Promise.all(input.members.map(async (m) => {
87
+ const kind = m.kind ?? "user";
88
+ const summary = await usageSummary(adapter, config, {
89
+ orgId: input.orgId,
90
+ plans: input.plans,
91
+ plan: input.plan,
92
+ caller: { kind, id: m.id },
93
+ ledger: input.ledger,
94
+ now: at,
95
+ });
96
+ // The pack is the per-member figure when the plan has one; without a pack
97
+ // (a pool or a pure wallet) fall back to the caller-scoped window, and
98
+ // failing that to zero, which is honest: a pooled plan does not attribute.
99
+ const usedInCycle = summary.pack?.used ?? summary.windows.find((w) => w.scope === "caller")?.used ?? 0;
100
+ return {
101
+ id: m.id,
102
+ kind,
103
+ seatType: summary.pack?.seatType ?? (kind === "api" ? "api" : "standard"),
104
+ pack: summary.pack ? { ...summary.pack, extra: summary.pack.extra } : null,
105
+ windows: summary.windows.filter((w) => w.scope === "caller"),
106
+ usedInCycle,
107
+ };
108
+ }));
109
+ }
110
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usage.js","sourceRoot":"","sources":["../src/usage.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arnaudjnn/billing-tools",
3
- "version": "0.61.0",
3
+ "version": "0.62.1",
4
4
  "description": "The get-paid engine for Stripe + WorkOS apps, for humans and AI agents. Drop-in API-key auth, token/credit + subscription billing, auth.md agent registration, and MPP machine payments, as MCP tools, a REST API, and a CLI. Framework-agnostic, storage-pluggable.",
5
5
  "license": "MIT",
6
6
  "author": "Arnaud Jeannin",