@burdenoff/be-sdk 2026.527.1 → 2026.527.3
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/README.md +33 -0
- package/dist/chunk-7BMKOD7N.js +265 -0
- package/dist/chunk-7BMKOD7N.js.map +1 -0
- package/dist/chunk-PHKK7725.js +476 -0
- package/dist/chunk-PHKK7725.js.map +1 -0
- package/dist/index-BQ1yJn1K.d.ts +222 -0
- package/dist/index.d.ts +4 -222
- package/dist/index.js +6 -473
- package/dist/index.js.map +1 -1
- package/dist/platform/index.d.ts +1 -0
- package/dist/platform/index.js +5 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/resource-caps/index.d.ts +51 -0
- package/dist/resource-caps/index.js +5 -0
- package/dist/resource-caps/index.js.map +1 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -134,6 +134,39 @@ await sdk.translation.upsertProductTranslations({
|
|
|
134
134
|
- **translation**: i18n support
|
|
135
135
|
- **payments**: Payment processing
|
|
136
136
|
- **bootstrap**: Server initialization
|
|
137
|
+
- **platform**: Scale primitives (see below)
|
|
138
|
+
|
|
139
|
+
### Platform Primitives (`@burdenoff/be-sdk/platform`)
|
|
140
|
+
|
|
141
|
+
Reusable building blocks consumed fleet-wide as part of the scale refactor:
|
|
142
|
+
|
|
143
|
+
| Primitive | Purpose |
|
|
144
|
+
|-----------|---------|
|
|
145
|
+
| `httpClient` / `createHttpClient` | Hardened `fetch` wrapper: per-request `AbortSignal.timeout` (default 5s), retries with jitter, per-host circuit breaker (opens at 5 consecutive 5xx, half-open after 30s). |
|
|
146
|
+
| `withLeaderLock(redis, name, fn, { ttlMs, renewMs })` | Redis-based leader election via `SET NX EX`; auto-renews while `fn()` runs; releases via token-checked LUA DEL. |
|
|
147
|
+
| `rateLimitIncr(redis, { key, limit, windowMs })` | Atomic INCR + `EXPIRE NX` rate limiter. Returns `{ allowed, remaining, resetAt }`. |
|
|
148
|
+
| `buildLoaders` + `resolveReferenceLoader` | Per-request DataLoader factory + Apollo Federation `__resolveReference` helper. |
|
|
149
|
+
| `clampLimit` + `paginate` | Cursor pagination (`take = limit + 1` to compute `hasMore` and `nextCursor`). |
|
|
150
|
+
| `LLMRouter` | Multi-provider LLM wrapper (Anthropic / OpenAI / OpenRouter) with timeout, retries with jitter, provider fallback chain (env `LLM_PROVIDER_CHAIN`), and token cap. |
|
|
151
|
+
| `ensureMonthlyPartitions` / `dropOldPartitions` | PostgreSQL monthly RANGE partition creation + retention drop. |
|
|
152
|
+
| `buildCacheKey` / `invalidateNamespace` | Versioned-key cache invalidation (`INCR v:{namespace}` — no `KEYS`/`SCAN`). |
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { Platform } from '@burdenoff/be-sdk';
|
|
156
|
+
// or: import { httpClient, withLeaderLock, ... } from '@burdenoff/be-sdk/platform';
|
|
157
|
+
|
|
158
|
+
const response = await Platform.httpClient('https://api.example.com/v1/users');
|
|
159
|
+
|
|
160
|
+
await Platform.withLeaderLock(redis, 'nightly-cleanup', async () => {
|
|
161
|
+
// only one replica runs this
|
|
162
|
+
}, { ttlMs: 60_000 });
|
|
163
|
+
|
|
164
|
+
const { allowed } = await Platform.rateLimitIncr(redis, {
|
|
165
|
+
key: `signup:${ip}`,
|
|
166
|
+
limit: 5,
|
|
167
|
+
windowMs: 60_000,
|
|
168
|
+
});
|
|
169
|
+
```
|
|
137
170
|
|
|
138
171
|
## Documentation
|
|
139
172
|
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { logger } from './chunk-CBZNP6RF.js';
|
|
2
|
+
import { __name } from './chunk-XE4OXN2W.js';
|
|
3
|
+
import { GraphQLError } from 'graphql';
|
|
4
|
+
|
|
5
|
+
var RESOURCE_CAP_DEFAULTS = {
|
|
6
|
+
workspace: {
|
|
7
|
+
free: 3,
|
|
8
|
+
paidDefault: 50,
|
|
9
|
+
scope: "user"
|
|
10
|
+
},
|
|
11
|
+
workspaceMember: {
|
|
12
|
+
free: 5,
|
|
13
|
+
paidDefault: 500,
|
|
14
|
+
scope: "workspace"
|
|
15
|
+
},
|
|
16
|
+
sandbox: {
|
|
17
|
+
free: 2,
|
|
18
|
+
paidDefault: 50,
|
|
19
|
+
scope: "user"
|
|
20
|
+
},
|
|
21
|
+
vibe: {
|
|
22
|
+
free: 10,
|
|
23
|
+
paidDefault: 500,
|
|
24
|
+
scope: "user"
|
|
25
|
+
},
|
|
26
|
+
agent: {
|
|
27
|
+
free: 2,
|
|
28
|
+
paidDefault: 20,
|
|
29
|
+
scope: "workspace"
|
|
30
|
+
},
|
|
31
|
+
conversation: {
|
|
32
|
+
free: 20,
|
|
33
|
+
paidDefault: 2e3,
|
|
34
|
+
scope: "user/day"
|
|
35
|
+
},
|
|
36
|
+
message: {
|
|
37
|
+
free: 200,
|
|
38
|
+
paidDefault: 2e4,
|
|
39
|
+
scope: "user/day"
|
|
40
|
+
},
|
|
41
|
+
tag: {
|
|
42
|
+
free: 50,
|
|
43
|
+
paidDefault: 5e3,
|
|
44
|
+
scope: "workspace"
|
|
45
|
+
},
|
|
46
|
+
file: {
|
|
47
|
+
free: 100,
|
|
48
|
+
paidDefault: 1e4,
|
|
49
|
+
scope: "user"
|
|
50
|
+
},
|
|
51
|
+
group: {
|
|
52
|
+
free: 5,
|
|
53
|
+
paidDefault: 500,
|
|
54
|
+
scope: "workspace"
|
|
55
|
+
},
|
|
56
|
+
calendar: {
|
|
57
|
+
free: 3,
|
|
58
|
+
paidDefault: 100,
|
|
59
|
+
scope: "user"
|
|
60
|
+
},
|
|
61
|
+
scheduledTask: {
|
|
62
|
+
free: 5,
|
|
63
|
+
paidDefault: 500,
|
|
64
|
+
scope: "user"
|
|
65
|
+
},
|
|
66
|
+
form: {
|
|
67
|
+
free: 5,
|
|
68
|
+
paidDefault: 500,
|
|
69
|
+
scope: "user"
|
|
70
|
+
},
|
|
71
|
+
apiKey: {
|
|
72
|
+
free: 2,
|
|
73
|
+
paidDefault: 50,
|
|
74
|
+
scope: "user"
|
|
75
|
+
},
|
|
76
|
+
webhook: {
|
|
77
|
+
free: 2,
|
|
78
|
+
paidDefault: 100,
|
|
79
|
+
scope: "workspace"
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var ResourceCapExceededError = class extends GraphQLError {
|
|
83
|
+
static {
|
|
84
|
+
__name(this, "ResourceCapExceededError");
|
|
85
|
+
}
|
|
86
|
+
constructor(detail) {
|
|
87
|
+
super(`Resource cap exceeded for ${detail.resource}: ${detail.currentCount}/${detail.limit} on plan '${detail.planName}'.`, {
|
|
88
|
+
extensions: {
|
|
89
|
+
code: "RESOURCE_CAP_EXCEEDED",
|
|
90
|
+
resource: detail.resource,
|
|
91
|
+
currentCount: detail.currentCount,
|
|
92
|
+
limit: detail.limit,
|
|
93
|
+
planName: detail.planName
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
this.name = "ResourceCapExceededError";
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var PLAN_QUERY = (
|
|
100
|
+
/* GraphQL */
|
|
101
|
+
`
|
|
102
|
+
query GetUserPlanForCaps($userId: ID!) {
|
|
103
|
+
userBillingPlan(userId: $userId) {
|
|
104
|
+
planName
|
|
105
|
+
tier
|
|
106
|
+
resourceCaps
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`
|
|
110
|
+
);
|
|
111
|
+
var ResourceCapsModule = class {
|
|
112
|
+
static {
|
|
113
|
+
__name(this, "ResourceCapsModule");
|
|
114
|
+
}
|
|
115
|
+
client;
|
|
116
|
+
cacheTtlMs;
|
|
117
|
+
defaultTier;
|
|
118
|
+
planCache = /* @__PURE__ */ new Map();
|
|
119
|
+
constructor(config = {}) {
|
|
120
|
+
this.client = config.client;
|
|
121
|
+
this.cacheTtlMs = config.cacheTtlMs ?? 6e4;
|
|
122
|
+
this.defaultTier = config.defaultTier ?? "free";
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the cap for a given resource for the supplied user.
|
|
126
|
+
* Order: plan-override → plan-tier-default → static free default.
|
|
127
|
+
*/
|
|
128
|
+
async getResourceCap(userId, resource) {
|
|
129
|
+
if (!userId) {
|
|
130
|
+
throw new Error("getResourceCap: userId is required");
|
|
131
|
+
}
|
|
132
|
+
const defaults = RESOURCE_CAP_DEFAULTS[resource];
|
|
133
|
+
if (!defaults) {
|
|
134
|
+
throw new Error(`getResourceCap: unknown resource '${resource}'`);
|
|
135
|
+
}
|
|
136
|
+
const plan = await this.resolvePlan(userId);
|
|
137
|
+
const override = plan.overrides[resource];
|
|
138
|
+
if (typeof override === "number" && override >= 0) {
|
|
139
|
+
return {
|
|
140
|
+
resource,
|
|
141
|
+
limit: override,
|
|
142
|
+
planName: plan.planName,
|
|
143
|
+
planTier: plan.planTier,
|
|
144
|
+
isFallback: false
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const limit = plan.planTier === "paid" ? defaults.paidDefault : defaults.free;
|
|
148
|
+
return {
|
|
149
|
+
resource,
|
|
150
|
+
limit,
|
|
151
|
+
planName: plan.planName,
|
|
152
|
+
planTier: plan.planTier,
|
|
153
|
+
isFallback: plan.planName === "fallback-free" || plan.planName === "fallback-paid"
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Throws ResourceCapExceededError when currentCount >= limit.
|
|
158
|
+
* Use BEFORE the create transaction to avoid orphan rows.
|
|
159
|
+
*/
|
|
160
|
+
async enforceResourceCap(userId, resource, currentCount) {
|
|
161
|
+
const cap = await this.getResourceCap(userId, resource);
|
|
162
|
+
if (currentCount >= cap.limit) {
|
|
163
|
+
throw new ResourceCapExceededError({
|
|
164
|
+
resource,
|
|
165
|
+
currentCount,
|
|
166
|
+
limit: cap.limit,
|
|
167
|
+
planName: cap.planName
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return cap;
|
|
171
|
+
}
|
|
172
|
+
/** Invalidate cache for a user (e.g. after plan upgrade) */
|
|
173
|
+
invalidate(userId) {
|
|
174
|
+
this.planCache.delete(userId);
|
|
175
|
+
}
|
|
176
|
+
/** Clear entire plan cache */
|
|
177
|
+
invalidateAll() {
|
|
178
|
+
this.planCache.clear();
|
|
179
|
+
}
|
|
180
|
+
// -------------------------------------------------------------------------
|
|
181
|
+
// Internals
|
|
182
|
+
// -------------------------------------------------------------------------
|
|
183
|
+
async resolvePlan(userId) {
|
|
184
|
+
const cached = this.planCache.get(userId);
|
|
185
|
+
if (cached && Date.now() - cached.fetchedAt < this.cacheTtlMs) {
|
|
186
|
+
return cached;
|
|
187
|
+
}
|
|
188
|
+
const plan = await this.fetchPlan(userId);
|
|
189
|
+
this.planCache.set(userId, plan);
|
|
190
|
+
return plan;
|
|
191
|
+
}
|
|
192
|
+
async fetchPlan(userId) {
|
|
193
|
+
if (!this.client) {
|
|
194
|
+
return this.fallbackPlan();
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const res = await this.client.query(PLAN_QUERY, {
|
|
198
|
+
userId
|
|
199
|
+
});
|
|
200
|
+
const planNode = res?.userBillingPlan;
|
|
201
|
+
if (!planNode) {
|
|
202
|
+
return this.fallbackPlan();
|
|
203
|
+
}
|
|
204
|
+
const tier = normaliseTier(planNode.tier, this.defaultTier);
|
|
205
|
+
const overrides = sanitizeOverrides(planNode.resourceCaps);
|
|
206
|
+
return {
|
|
207
|
+
planName: planNode.planName ?? (tier === "paid" ? "paid" : "free"),
|
|
208
|
+
planTier: tier,
|
|
209
|
+
overrides,
|
|
210
|
+
fetchedAt: Date.now()
|
|
211
|
+
};
|
|
212
|
+
} catch (err) {
|
|
213
|
+
logger.warn("resource-caps: plan lookup failed; falling back to default tier", {
|
|
214
|
+
err,
|
|
215
|
+
userId
|
|
216
|
+
});
|
|
217
|
+
return this.fallbackPlan();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
fallbackPlan() {
|
|
221
|
+
return {
|
|
222
|
+
planName: this.defaultTier === "paid" ? "fallback-paid" : "fallback-free",
|
|
223
|
+
planTier: this.defaultTier,
|
|
224
|
+
overrides: {},
|
|
225
|
+
fetchedAt: Date.now()
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
function normaliseTier(raw, fallback) {
|
|
230
|
+
if (!raw) return fallback;
|
|
231
|
+
const lc = raw.toLowerCase();
|
|
232
|
+
if (lc === "free" || lc === "paid") return lc;
|
|
233
|
+
if (lc === "enterprise" || lc === "business" || lc === "pro" || lc === "team") {
|
|
234
|
+
return "paid";
|
|
235
|
+
}
|
|
236
|
+
return fallback;
|
|
237
|
+
}
|
|
238
|
+
__name(normaliseTier, "normaliseTier");
|
|
239
|
+
function sanitizeOverrides(raw) {
|
|
240
|
+
if (!raw || typeof raw !== "object") return {};
|
|
241
|
+
const out = {};
|
|
242
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
243
|
+
if (key in RESOURCE_CAP_DEFAULTS && typeof value === "number" && Number.isFinite(value)) {
|
|
244
|
+
out[key] = Math.max(0, Math.floor(value));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
__name(sanitizeOverrides, "sanitizeOverrides");
|
|
250
|
+
var defaultInstance = null;
|
|
251
|
+
function getResourceCaps(config) {
|
|
252
|
+
if (!defaultInstance) {
|
|
253
|
+
defaultInstance = new ResourceCapsModule(config);
|
|
254
|
+
}
|
|
255
|
+
return defaultInstance;
|
|
256
|
+
}
|
|
257
|
+
__name(getResourceCaps, "getResourceCaps");
|
|
258
|
+
function __resetResourceCapsForTests() {
|
|
259
|
+
defaultInstance = null;
|
|
260
|
+
}
|
|
261
|
+
__name(__resetResourceCapsForTests, "__resetResourceCapsForTests");
|
|
262
|
+
|
|
263
|
+
export { RESOURCE_CAP_DEFAULTS, ResourceCapExceededError, ResourceCapsModule, __resetResourceCapsForTests, getResourceCaps };
|
|
264
|
+
//# sourceMappingURL=chunk-7BMKOD7N.js.map
|
|
265
|
+
//# sourceMappingURL=chunk-7BMKOD7N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resource-caps/index.ts"],"names":["RESOURCE_CAP_DEFAULTS","workspace","free","paidDefault","scope","workspaceMember","sandbox","vibe","agent","conversation","message","tag","file","group","calendar","scheduledTask","form","apiKey","webhook","ResourceCapExceededError","GraphQLError","detail","resource","currentCount","limit","planName","extensions","code","name","PLAN_QUERY","ResourceCapsModule","client","cacheTtlMs","defaultTier","planCache","Map","config","getResourceCap","userId","Error","defaults","plan","resolvePlan","override","overrides","planTier","isFallback","enforceResourceCap","cap","invalidate","delete","invalidateAll","clear","cached","get","Date","now","fetchedAt","fetchPlan","set","fallbackPlan","res","query","planNode","userBillingPlan","tier","normaliseTier","sanitizeOverrides","resourceCaps","err","logger","warn","raw","fallback","lc","toLowerCase","out","key","value","Object","entries","Number","isFinite","Math","max","floor","defaultInstance","getResourceCaps","__resetResourceCapsForTests"],"mappings":"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","file":"chunk-7BMKOD7N.js","sourcesContent":["/**\n * Resource Caps Module\n *\n * Enforces per-user / per-workspace resource ceilings to prevent unbounded\n * resource use on a multi-tenant SaaS. Caps come from the user's billing plan\n * (resolved via global-billing-svc) and fall back to static defaults defined\n * here when the plan lookup fails or the resource isn't overridden.\n *\n * Two plan tiers exist by default:\n * - free: tight per-user / per-workspace ceilings\n * - paid (default): ~100x looser; individual paid plans can override via the\n * `Plan.resourceCaps` JSON column in global-billing-svc.\n *\n * @example\n * ```ts\n * import { resourceCaps } from '@burdenoff/be-sdk/resource-caps';\n *\n * const { limit } = await resourceCaps.getResourceCap(userId, 'tag');\n * await resourceCaps.enforceResourceCap(userId, 'tag', currentCount);\n * ```\n */\n\nimport { GraphQLError } from 'graphql';\n\nimport type { BurdenoffGraphQLClient } from '../client/graphql.js';\nimport { logger } from '../logger/index.js';\n\n// ---------------------------------------------------------------------------\n// Resource registry\n// ---------------------------------------------------------------------------\n\n/**\n * Canonical resource names. Adding a new capped resource? Append it here,\n * extend RESOURCE_CAP_DEFAULTS, and reference it from the relevant svc.\n */\nexport type ResourceName =\n | 'workspace' // per user\n | 'workspaceMember' // per workspace\n | 'sandbox' // per user\n | 'vibe' // per user\n | 'agent' // per workspace\n | 'conversation' // per user/day\n | 'message' // per user/day\n | 'tag' // per workspace\n | 'file' // per user (count only)\n | 'group' // per workspace\n | 'calendar' // per user\n | 'scheduledTask' // per user\n | 'form' // per user\n | 'apiKey' // per user\n | 'webhook'; // per workspace\n\nexport type PlanTier = 'free' | 'paid';\n\nexport interface ResourceCapDefaults {\n free: number;\n paidDefault: number;\n /** Optional scope hint used by callers when wiring guards */\n scope: 'user' | 'workspace' | 'user/day';\n}\n\n/**\n * Static fallback caps. Real values may be overridden per plan in\n * global-billing-svc Plan.resourceCaps JSON.\n */\nexport const RESOURCE_CAP_DEFAULTS: Record<ResourceName, ResourceCapDefaults> = {\n workspace: { free: 3, paidDefault: 50, scope: 'user' },\n workspaceMember: { free: 5, paidDefault: 500, scope: 'workspace' },\n sandbox: { free: 2, paidDefault: 50, scope: 'user' },\n vibe: { free: 10, paidDefault: 500, scope: 'user' },\n agent: { free: 2, paidDefault: 20, scope: 'workspace' },\n conversation: { free: 20, paidDefault: 2000, scope: 'user/day' },\n message: { free: 200, paidDefault: 20000, scope: 'user/day' },\n tag: { free: 50, paidDefault: 5000, scope: 'workspace' },\n file: { free: 100, paidDefault: 10000, scope: 'user' },\n group: { free: 5, paidDefault: 500, scope: 'workspace' },\n calendar: { free: 3, paidDefault: 100, scope: 'user' },\n scheduledTask: { free: 5, paidDefault: 500, scope: 'user' },\n form: { free: 5, paidDefault: 500, scope: 'user' },\n apiKey: { free: 2, paidDefault: 50, scope: 'user' },\n webhook: { free: 2, paidDefault: 100, scope: 'workspace' },\n};\n\n// ---------------------------------------------------------------------------\n// Types & errors\n// ---------------------------------------------------------------------------\n\nexport interface ResourceCap {\n resource: ResourceName;\n limit: number;\n planName: string;\n planTier: PlanTier;\n /** True when the cap came from defaults, not from the billing service */\n isFallback: boolean;\n}\n\nexport interface ResourceCapsConfig {\n client?: BurdenoffGraphQLClient;\n /** Optional in-memory TTL for plan lookups; defaults to 60s */\n cacheTtlMs?: number;\n /** Default plan tier when billing lookup fails */\n defaultTier?: PlanTier;\n}\n\ninterface CachedPlan {\n planName: string;\n planTier: PlanTier;\n overrides: Partial<Record<ResourceName, number>>;\n fetchedAt: number;\n}\n\n/**\n * Thrown when an entity create operation would push the user past the cap.\n * The matching GraphQL error has extensions.code = RESOURCE_CAP_EXCEEDED\n * so clients can detect and route to an upgrade flow.\n */\nexport class ResourceCapExceededError extends GraphQLError {\n constructor(detail: { resource: ResourceName; currentCount: number; limit: number; planName: string }) {\n super(\n `Resource cap exceeded for ${detail.resource}: ${detail.currentCount}/${detail.limit} on plan '${detail.planName}'.`,\n {\n extensions: {\n code: 'RESOURCE_CAP_EXCEEDED',\n resource: detail.resource,\n currentCount: detail.currentCount,\n limit: detail.limit,\n planName: detail.planName,\n },\n }\n );\n this.name = 'ResourceCapExceededError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Module\n// ---------------------------------------------------------------------------\n\nconst PLAN_QUERY = /* GraphQL */ `\n query GetUserPlanForCaps($userId: ID!) {\n userBillingPlan(userId: $userId) {\n planName\n tier\n resourceCaps\n }\n }\n`;\n\ninterface PlanQueryResponse {\n userBillingPlan?: {\n planName?: string | null;\n tier?: string | null;\n resourceCaps?: Record<string, number> | null;\n } | null;\n}\n\nexport class ResourceCapsModule {\n private readonly client?: BurdenoffGraphQLClient;\n private readonly cacheTtlMs: number;\n private readonly defaultTier: PlanTier;\n private readonly planCache = new Map<string, CachedPlan>();\n\n constructor(config: ResourceCapsConfig = {}) {\n this.client = config.client;\n this.cacheTtlMs = config.cacheTtlMs ?? 60_000;\n this.defaultTier = config.defaultTier ?? 'free';\n }\n\n /**\n * Resolve the cap for a given resource for the supplied user.\n * Order: plan-override → plan-tier-default → static free default.\n */\n async getResourceCap(userId: string, resource: ResourceName): Promise<ResourceCap> {\n if (!userId) {\n throw new Error('getResourceCap: userId is required');\n }\n const defaults = RESOURCE_CAP_DEFAULTS[resource];\n if (!defaults) {\n throw new Error(`getResourceCap: unknown resource '${resource}'`);\n }\n\n const plan = await this.resolvePlan(userId);\n const override = plan.overrides[resource];\n if (typeof override === 'number' && override >= 0) {\n return {\n resource,\n limit: override,\n planName: plan.planName,\n planTier: plan.planTier,\n isFallback: false,\n };\n }\n\n const limit = plan.planTier === 'paid' ? defaults.paidDefault : defaults.free;\n return {\n resource,\n limit,\n planName: plan.planName,\n planTier: plan.planTier,\n isFallback: plan.planName === 'fallback-free' || plan.planName === 'fallback-paid',\n };\n }\n\n /**\n * Throws ResourceCapExceededError when currentCount >= limit.\n * Use BEFORE the create transaction to avoid orphan rows.\n */\n async enforceResourceCap(userId: string, resource: ResourceName, currentCount: number): Promise<ResourceCap> {\n const cap = await this.getResourceCap(userId, resource);\n if (currentCount >= cap.limit) {\n throw new ResourceCapExceededError({\n resource,\n currentCount,\n limit: cap.limit,\n planName: cap.planName,\n });\n }\n return cap;\n }\n\n /** Invalidate cache for a user (e.g. after plan upgrade) */\n invalidate(userId: string): void {\n this.planCache.delete(userId);\n }\n\n /** Clear entire plan cache */\n invalidateAll(): void {\n this.planCache.clear();\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private async resolvePlan(userId: string): Promise<CachedPlan> {\n const cached = this.planCache.get(userId);\n if (cached && Date.now() - cached.fetchedAt < this.cacheTtlMs) {\n return cached;\n }\n\n const plan = await this.fetchPlan(userId);\n this.planCache.set(userId, plan);\n return plan;\n }\n\n private async fetchPlan(userId: string): Promise<CachedPlan> {\n if (!this.client) {\n return this.fallbackPlan();\n }\n try {\n const res = await this.client.query<PlanQueryResponse>(PLAN_QUERY, { userId });\n const planNode = res?.userBillingPlan;\n if (!planNode) {\n return this.fallbackPlan();\n }\n const tier = normaliseTier(planNode.tier, this.defaultTier);\n const overrides = sanitizeOverrides(planNode.resourceCaps);\n return {\n planName: planNode.planName ?? (tier === 'paid' ? 'paid' : 'free'),\n planTier: tier,\n overrides,\n fetchedAt: Date.now(),\n };\n } catch (err) {\n logger.warn('resource-caps: plan lookup failed; falling back to default tier', {\n err,\n userId,\n });\n return this.fallbackPlan();\n }\n }\n\n private fallbackPlan(): CachedPlan {\n return {\n planName: this.defaultTier === 'paid' ? 'fallback-paid' : 'fallback-free',\n planTier: this.defaultTier,\n overrides: {},\n fetchedAt: Date.now(),\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction normaliseTier(raw: string | null | undefined, fallback: PlanTier): PlanTier {\n if (!raw) return fallback;\n const lc = raw.toLowerCase();\n if (lc === 'free' || lc === 'paid') return lc;\n // Treat anything non-free as paid (enterprise, business, pro, etc.)\n if (lc === 'enterprise' || lc === 'business' || lc === 'pro' || lc === 'team') {\n return 'paid';\n }\n return fallback;\n}\n\nfunction sanitizeOverrides(raw: Record<string, number> | null | undefined): Partial<Record<ResourceName, number>> {\n if (!raw || typeof raw !== 'object') return {};\n const out: Partial<Record<ResourceName, number>> = {};\n for (const [key, value] of Object.entries(raw)) {\n if (key in RESOURCE_CAP_DEFAULTS && typeof value === 'number' && Number.isFinite(value)) {\n out[key as ResourceName] = Math.max(0, Math.floor(value));\n }\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Convenience singleton\n// ---------------------------------------------------------------------------\n\nlet defaultInstance: ResourceCapsModule | null = null;\n\n/**\n * Obtain a process-wide singleton. The first call binds the GraphQL client\n * (typically the SDK's internal client). Later calls return the same instance.\n */\nexport function getResourceCaps(config?: ResourceCapsConfig): ResourceCapsModule {\n if (!defaultInstance) {\n defaultInstance = new ResourceCapsModule(config);\n }\n return defaultInstance;\n}\n\n/** Reset the singleton (for tests) */\nexport function __resetResourceCapsForTests(): void {\n defaultInstance = null;\n}\n"]}
|