@burdenoff/be-sdk 2026.803.1 → 2026.803.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.
@@ -1,3 +1,4 @@
1
+ import { createCounter } from './chunk-Q6UAOZFZ.js';
1
2
  import { logger } from './chunk-CBZNP6RF.js';
2
3
  import { __name } from './chunk-XE4OXN2W.js';
3
4
  import { GraphQLError } from 'graphql';
@@ -117,16 +118,24 @@ var ResourceCapsModule = class {
117
118
  client;
118
119
  transport;
119
120
  cacheTtlMs;
120
- defaultTier;
121
+ clientlessTier;
122
+ billingFailureTier;
121
123
  onFallback;
124
+ onBillingFailure;
122
125
  warnedClientless = false;
123
126
  planCache = /* @__PURE__ */ new Map();
127
+ planResolutionCounter;
128
+ enforcementCounter;
124
129
  constructor(config = {}) {
125
130
  this.client = config.client;
126
131
  this.transport = config.transport;
127
132
  this.cacheTtlMs = config.cacheTtlMs ?? 6e4;
128
- this.defaultTier = config.defaultTier ?? "free";
133
+ this.clientlessTier = config.clientlessTier ?? "free";
134
+ this.billingFailureTier = config.billingFailureTier ?? "paid";
129
135
  this.onFallback = config.onFallback;
136
+ this.onBillingFailure = config.onBillingFailure;
137
+ this.planResolutionCounter = createCounter("resource_caps.plan_resolution_total", "Total number of billing plan resolutions for resource caps");
138
+ this.enforcementCounter = createCounter("resource_caps.enforcement_total", "Total number of resource cap enforcement checks");
130
139
  }
131
140
  /**
132
141
  * (Re)bind runtime dependencies on an already-constructed module. The SDK
@@ -139,8 +148,10 @@ var ResourceCapsModule = class {
139
148
  if (config.client !== void 0) this.client = config.client;
140
149
  if (config.transport !== void 0) this.transport = config.transport;
141
150
  if (config.onFallback !== void 0) this.onFallback = config.onFallback;
151
+ if (config.onBillingFailure !== void 0) this.onBillingFailure = config.onBillingFailure;
142
152
  if (typeof config.cacheTtlMs === "number") this.cacheTtlMs = config.cacheTtlMs;
143
- if (config.defaultTier) this.defaultTier = config.defaultTier;
153
+ if (config.clientlessTier) this.clientlessTier = config.clientlessTier;
154
+ if (config.billingFailureTier) this.billingFailureTier = config.billingFailureTier;
144
155
  if (this.hasBillingSource()) this.warnedClientless = false;
145
156
  this.planCache.clear();
146
157
  }
@@ -168,7 +179,8 @@ var ResourceCapsModule = class {
168
179
  limit: override,
169
180
  planName: plan.planName,
170
181
  planTier: plan.planTier,
171
- isFallback: false
182
+ isFallback: plan.isFallback,
183
+ fallbackReason: plan.fallbackReason
172
184
  };
173
185
  }
174
186
  const limit = plan.planTier === "paid" ? defaults.paidDefault : defaults.free;
@@ -177,7 +189,8 @@ var ResourceCapsModule = class {
177
189
  limit,
178
190
  planName: plan.planName,
179
191
  planTier: plan.planTier,
180
- isFallback: plan.planName === "fallback-free" || plan.planName === "fallback-paid"
192
+ isFallback: plan.isFallback,
193
+ fallbackReason: plan.fallbackReason
181
194
  };
182
195
  }
183
196
  /**
@@ -187,6 +200,11 @@ var ResourceCapsModule = class {
187
200
  async enforceResourceCap(userId, resource, currentCount) {
188
201
  const cap = await this.getResourceCap(userId, resource);
189
202
  if (currentCount >= cap.limit) {
203
+ this.enforcementCounter.inc({
204
+ resource,
205
+ plan_tier: cap.planTier,
206
+ result: "denied"
207
+ });
190
208
  throw new ResourceCapExceededError({
191
209
  resource,
192
210
  currentCount,
@@ -194,12 +212,94 @@ var ResourceCapsModule = class {
194
212
  planName: cap.planName
195
213
  });
196
214
  }
215
+ this.enforcementCounter.inc({
216
+ resource,
217
+ plan_tier: cap.planTier,
218
+ result: "allowed"
219
+ });
220
+ return cap;
221
+ }
222
+ /**
223
+ * Resolve a workspace-scoped resource cap from the workspace's owner
224
+ * organization plan, regardless of which actor triggered the action.
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * const { limit } = await caps.getWorkspaceResourceCap(
229
+ * { workspaceId: 'ws-1', organizationId: 'org-1' },
230
+ * 'group'
231
+ * );
232
+ * ```
233
+ */
234
+ async getWorkspaceResourceCap(scope, resource) {
235
+ const { workspaceId, organizationId } = scope;
236
+ if (!workspaceId) {
237
+ throw new Error("getWorkspaceResourceCap: workspaceId is required");
238
+ }
239
+ if (!organizationId) {
240
+ throw new Error("getWorkspaceResourceCap: organizationId is required");
241
+ }
242
+ const defaults = RESOURCE_CAP_DEFAULTS[resource];
243
+ if (!defaults) {
244
+ throw new Error(`getWorkspaceResourceCap: unknown resource '${resource}'`);
245
+ }
246
+ const plan = await this.resolvePlan(organizationId, this.workspaceCacheKey(workspaceId));
247
+ const override = plan.overrides[resource];
248
+ if (typeof override === "number" && override >= 0) {
249
+ return {
250
+ resource,
251
+ limit: override,
252
+ planName: plan.planName,
253
+ planTier: plan.planTier,
254
+ isFallback: plan.isFallback,
255
+ fallbackReason: plan.fallbackReason
256
+ };
257
+ }
258
+ const limit = plan.planTier === "paid" ? defaults.paidDefault : defaults.free;
259
+ return {
260
+ resource,
261
+ limit,
262
+ planName: plan.planName,
263
+ planTier: plan.planTier,
264
+ isFallback: plan.isFallback,
265
+ fallbackReason: plan.fallbackReason
266
+ };
267
+ }
268
+ /**
269
+ * Throws ResourceCapExceededError when currentCount >= limit for a
270
+ * workspace-scoped resource. The cap is resolved from the workspace owner's
271
+ * organization plan, so the answer is stable across members.
272
+ */
273
+ async enforceWorkspaceResourceCap(scope, resource, currentCount) {
274
+ const cap = await this.getWorkspaceResourceCap(scope, resource);
275
+ if (currentCount >= cap.limit) {
276
+ this.enforcementCounter.inc({
277
+ resource,
278
+ plan_tier: cap.planTier,
279
+ result: "denied"
280
+ });
281
+ throw new ResourceCapExceededError({
282
+ resource,
283
+ currentCount,
284
+ limit: cap.limit,
285
+ planName: cap.planName
286
+ });
287
+ }
288
+ this.enforcementCounter.inc({
289
+ resource,
290
+ plan_tier: cap.planTier,
291
+ result: "allowed"
292
+ });
197
293
  return cap;
198
294
  }
199
295
  /** Invalidate cache for a user (e.g. after plan upgrade) */
200
296
  invalidate(userId) {
201
297
  this.planCache.delete(userId);
202
298
  }
299
+ /** Invalidate cache for a workspace (e.g. after workspace plan change) */
300
+ invalidateWorkspace(workspaceId) {
301
+ this.planCache.delete(this.workspaceCacheKey(workspaceId));
302
+ }
203
303
  /** Clear entire plan cache */
204
304
  invalidateAll() {
205
305
  this.planCache.clear();
@@ -207,39 +307,64 @@ var ResourceCapsModule = class {
207
307
  // -------------------------------------------------------------------------
208
308
  // Internals
209
309
  // -------------------------------------------------------------------------
210
- async resolvePlan(userId) {
211
- const cached = this.planCache.get(userId);
310
+ workspaceCacheKey(workspaceId) {
311
+ return `workspace:${workspaceId}`;
312
+ }
313
+ async resolvePlan(userId, cacheKey) {
314
+ const key = cacheKey ?? userId;
315
+ const cached = this.planCache.get(key);
212
316
  if (cached && Date.now() - cached.fetchedAt < this.cacheTtlMs) {
213
317
  return cached;
214
318
  }
215
319
  const plan = await this.fetchPlan(userId);
216
- this.planCache.set(userId, plan);
320
+ this.planCache.set(key, plan);
217
321
  return plan;
218
322
  }
219
323
  async fetchPlan(userId) {
220
324
  if (!this.hasBillingSource()) {
221
325
  this.warnClientlessOnce();
222
- return this.fallbackPlan();
326
+ this.planResolutionCounter.inc({
327
+ result: "fallback",
328
+ reason: "clientless"
329
+ });
330
+ return this.fallbackPlan(this.clientlessTier, "clientless");
223
331
  }
224
332
  try {
225
333
  const planNode = await this.fetchPlanNode(userId);
226
334
  if (!planNode) {
227
- return this.fallbackPlan();
335
+ logger.warn("resource-caps: plan lookup returned null; falling back to paid defaults", {
336
+ userId
337
+ });
338
+ this.onBillingFailure?.();
339
+ this.planResolutionCounter.inc({
340
+ result: "fallback",
341
+ reason: "billing_error"
342
+ });
343
+ return this.fallbackPlan(this.billingFailureTier, "billing_error");
228
344
  }
229
- const tier = normaliseTier(planNode.tier, this.defaultTier);
345
+ const tier = normaliseTier(planNode.tier, this.billingFailureTier);
230
346
  const overrides = sanitizeOverrides(planNode.resourceCaps);
347
+ this.planResolutionCounter.inc({
348
+ result: "billing"
349
+ });
231
350
  return {
232
351
  planName: planNode.planName ?? (tier === "paid" ? "paid" : "free"),
233
352
  planTier: tier,
234
353
  overrides,
235
- fetchedAt: Date.now()
354
+ fetchedAt: Date.now(),
355
+ isFallback: false
236
356
  };
237
357
  } catch (err) {
238
- logger.warn("resource-caps: plan lookup failed; falling back to default tier", {
358
+ logger.warn("resource-caps: plan lookup failed; falling back to paid defaults", {
239
359
  err,
240
360
  userId
241
361
  });
242
- return this.fallbackPlan();
362
+ this.onBillingFailure?.();
363
+ this.planResolutionCounter.inc({
364
+ result: "fallback",
365
+ reason: "billing_error"
366
+ });
367
+ return this.fallbackPlan(this.billingFailureTier, "billing_error");
243
368
  }
244
369
  }
245
370
  /**
@@ -268,12 +393,14 @@ var ResourceCapsModule = class {
268
393
  logger.warn("resource-caps: running WITHOUT a billing transport/client \u2014 every resource cap will use the free-tier fallback regardless of the customer plan. Ensure initializeBurdenoffSDK() ran with a layer/gateway configured (BOFF-4805).");
269
394
  this.onFallback?.();
270
395
  }
271
- fallbackPlan() {
396
+ fallbackPlan(tier, reason) {
272
397
  return {
273
- planName: this.defaultTier === "paid" ? "fallback-paid" : "fallback-free",
274
- planTier: this.defaultTier,
398
+ planName: tier === "paid" ? "fallback-paid" : "fallback-free",
399
+ planTier: tier,
275
400
  overrides: {},
276
- fetchedAt: Date.now()
401
+ fetchedAt: Date.now(),
402
+ isFallback: true,
403
+ fallbackReason: reason
277
404
  };
278
405
  }
279
406
  };
@@ -320,5 +447,5 @@ function __resetResourceCapsForTests() {
320
447
  __name(__resetResourceCapsForTests, "__resetResourceCapsForTests");
321
448
 
322
449
  export { RESOURCE_CAP_DEFAULTS, ResourceCapExceededError, ResourceCapsModule, __resetResourceCapsForTests, configureResourceCaps, getResourceCaps };
323
- //# sourceMappingURL=chunk-KUM4N4CO.js.map
324
- //# sourceMappingURL=chunk-KUM4N4CO.js.map
450
+ //# sourceMappingURL=chunk-RJK4MPDW.js.map
451
+ //# sourceMappingURL=chunk-RJK4MPDW.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","userMessage","technicalMessage","name","PLAN_QUERY","ResourceCapsModule","client","transport","cacheTtlMs","clientlessTier","billingFailureTier","onFallback","onBillingFailure","warnedClientless","planCache","Map","planResolutionCounter","enforcementCounter","config","createCounter","configure","undefined","hasBillingSource","clear","Boolean","getResourceCap","userId","Error","defaults","plan","resolvePlan","override","overrides","planTier","isFallback","fallbackReason","enforceResourceCap","cap","inc","plan_tier","result","getWorkspaceResourceCap","workspaceId","organizationId","workspaceCacheKey","enforceWorkspaceResourceCap","invalidate","delete","invalidateWorkspace","invalidateAll","cacheKey","key","cached","get","Date","now","fetchedAt","fetchPlan","set","warnClientlessOnce","reason","fallbackPlan","planNode","fetchPlanNode","logger","warn","tier","normaliseTier","sanitizeOverrides","resourceCaps","err","data","userBillingPlan","res","query","raw","fallback","lc","toLowerCase","out","value","Object","entries","Number","isFinite","Math","max","floor","defaultInstance","getResourceCaps","configureResourceCaps","instance","__resetResourceCapsForTests"],"mappings":"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","file":"chunk-RJK4MPDW.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 { createCounter } from '../instrumentation/metrics-collector.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: 500, 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 /** Why the fallback was used, when isFallback is true */\n fallbackReason?: 'clientless' | 'billing_error';\n}\n\n/**\n * Scope for workspace-scoped resource caps. The cap is derived from the plan of\n * the workspace's owning organization, not from whichever actor happens to be\n * making the request, so two members of the same workspace always see the same\n * limit (BOFF-4808).\n */\nexport interface WorkspaceResourceCapScope {\n /** Stable workspace identifier; used as the cache key. */\n workspaceId: string;\n /**\n * Organization that owns the workspace and whose billing plan determines the\n * cap. This is the `organizationId` from global-tenant-svc's Workspace type\n * (or `orgId` from wspace-workspace-svc's Workspace type).\n */\n organizationId: string;\n}\n\n/**\n * Layer-aware transport for the `userBillingPlan` lookup. Given the GraphQL\n * document + variables, returns the response `data` object (or null). Preferred\n * over `client`: the SDK bootstrap wires this to a same-layer (global) or\n * cross-layer (wspace→global) gateway call so billing is reachable from every\n * service without any per-call-site wiring (BOFF-4805).\n */\nexport type BillingPlanTransport = (query: string, variables: Record<string, unknown>) => Promise<unknown>;\n\nexport interface ResourceCapsConfig {\n client?: BurdenoffGraphQLClient;\n /** Preferred billing lookup path; see {@link BillingPlanTransport}. */\n transport?: BillingPlanTransport;\n /** Optional in-memory TTL for plan lookups; defaults to 60s */\n cacheTtlMs?: number;\n /**\n * Plan tier used when the module has no billing source (clientless). This is\n * the misconfiguration path: services should always bootstrap with a billing\n * transport. Defaults to `free` so a clientless service is visibly broken in\n * tests, while still not hard-blocking local unit tests.\n */\n clientlessTier?: PlanTier;\n /**\n * Plan tier used when a billing source is configured but the lookup fails\n * (network error, billing service down, etc.). Defaults to `paid` so a\n * transient billing outage does not silently downgrade paid customers to the\n * tightest free-tier caps (fail-open with alerting; BOFF-4805/4809).\n */\n billingFailureTier?: PlanTier;\n /**\n * Telemetry hook invoked when the module cannot reach billing (no transport /\n * client) and collapses to the clientless fallback. Wired to a metric counter\n * by observability so an unwired/broken integration is visible (BOFF-4809).\n */\n onFallback?: () => void;\n /**\n * Telemetry hook invoked when a billing source is configured but the lookup\n * fails (billing error fallback to the paid default).\n */\n onBillingFailure?: () => void;\n}\n\ninterface CachedPlan {\n planName: string;\n planTier: PlanTier;\n overrides: Partial<Record<ResourceName, number>>;\n fetchedAt: number;\n isFallback: boolean;\n fallbackReason?: 'clientless' | 'billing_error';\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 userMessage: \"You've reached your plan limit. Upgrade your plan to continue.\",\n technicalMessage: `Resource cap exceeded for ${detail.resource}: ${detail.currentCount}/${detail.limit} on plan '${detail.planName}'.`,\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\ntype PlanNode = NonNullable<PlanQueryResponse['userBillingPlan']>;\n\nexport class ResourceCapsModule {\n private client?: BurdenoffGraphQLClient;\n private transport?: BillingPlanTransport;\n private cacheTtlMs: number;\n private clientlessTier: PlanTier;\n private billingFailureTier: PlanTier;\n private onFallback?: () => void;\n private onBillingFailure?: () => void;\n private warnedClientless = false;\n private readonly planCache = new Map<string, CachedPlan>();\n private readonly planResolutionCounter;\n private readonly enforcementCounter;\n\n constructor(config: ResourceCapsConfig = {}) {\n this.client = config.client;\n this.transport = config.transport;\n this.cacheTtlMs = config.cacheTtlMs ?? 60_000;\n this.clientlessTier = config.clientlessTier ?? 'free';\n this.billingFailureTier = config.billingFailureTier ?? 'paid';\n this.onFallback = config.onFallback;\n this.onBillingFailure = config.onBillingFailure;\n this.planResolutionCounter = createCounter(\n 'resource_caps.plan_resolution_total',\n 'Total number of billing plan resolutions for resource caps'\n );\n this.enforcementCounter = createCounter(\n 'resource_caps.enforcement_total',\n 'Total number of resource cap enforcement checks'\n );\n }\n\n /**\n * (Re)bind runtime dependencies on an already-constructed module. The SDK\n * bootstrap uses this to prime the process-wide singleton with a billing\n * transport even if an early caller already lazily created it clientless\n * (a bare `getResourceCaps(config)` ignores config once the instance exists).\n * Clears the plan cache so stale fallback entries don't linger.\n */\n configure(config: ResourceCapsConfig): void {\n if (config.client !== undefined) this.client = config.client;\n if (config.transport !== undefined) this.transport = config.transport;\n if (config.onFallback !== undefined) this.onFallback = config.onFallback;\n if (config.onBillingFailure !== undefined) this.onBillingFailure = config.onBillingFailure;\n if (typeof config.cacheTtlMs === 'number') this.cacheTtlMs = config.cacheTtlMs;\n if (config.clientlessTier) this.clientlessTier = config.clientlessTier;\n if (config.billingFailureTier) this.billingFailureTier = config.billingFailureTier;\n if (this.hasBillingSource()) this.warnedClientless = false;\n this.planCache.clear();\n }\n\n /** True when the module can actually reach billing (has a transport or client). */\n hasBillingSource(): boolean {\n return Boolean(this.transport || this.client);\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: plan.isFallback,\n fallbackReason: plan.fallbackReason,\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.isFallback,\n fallbackReason: plan.fallbackReason,\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 this.enforcementCounter.inc({\n resource,\n plan_tier: cap.planTier,\n result: 'denied',\n });\n throw new ResourceCapExceededError({\n resource,\n currentCount,\n limit: cap.limit,\n planName: cap.planName,\n });\n }\n this.enforcementCounter.inc({\n resource,\n plan_tier: cap.planTier,\n result: 'allowed',\n });\n return cap;\n }\n\n /**\n * Resolve a workspace-scoped resource cap from the workspace's owner\n * organization plan, regardless of which actor triggered the action.\n *\n * @example\n * ```ts\n * const { limit } = await caps.getWorkspaceResourceCap(\n * { workspaceId: 'ws-1', organizationId: 'org-1' },\n * 'group'\n * );\n * ```\n */\n async getWorkspaceResourceCap(scope: WorkspaceResourceCapScope, resource: ResourceName): Promise<ResourceCap> {\n const { workspaceId, organizationId } = scope;\n if (!workspaceId) {\n throw new Error('getWorkspaceResourceCap: workspaceId is required');\n }\n if (!organizationId) {\n throw new Error('getWorkspaceResourceCap: organizationId is required');\n }\n const defaults = RESOURCE_CAP_DEFAULTS[resource];\n if (!defaults) {\n throw new Error(`getWorkspaceResourceCap: unknown resource '${resource}'`);\n }\n\n const plan = await this.resolvePlan(organizationId, this.workspaceCacheKey(workspaceId));\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: plan.isFallback,\n fallbackReason: plan.fallbackReason,\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.isFallback,\n fallbackReason: plan.fallbackReason,\n };\n }\n\n /**\n * Throws ResourceCapExceededError when currentCount >= limit for a\n * workspace-scoped resource. The cap is resolved from the workspace owner's\n * organization plan, so the answer is stable across members.\n */\n async enforceWorkspaceResourceCap(\n scope: WorkspaceResourceCapScope,\n resource: ResourceName,\n currentCount: number\n ): Promise<ResourceCap> {\n const cap = await this.getWorkspaceResourceCap(scope, resource);\n if (currentCount >= cap.limit) {\n this.enforcementCounter.inc({\n resource,\n plan_tier: cap.planTier,\n result: 'denied',\n });\n throw new ResourceCapExceededError({\n resource,\n currentCount,\n limit: cap.limit,\n planName: cap.planName,\n });\n }\n this.enforcementCounter.inc({\n resource,\n plan_tier: cap.planTier,\n result: 'allowed',\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 /** Invalidate cache for a workspace (e.g. after workspace plan change) */\n invalidateWorkspace(workspaceId: string): void {\n this.planCache.delete(this.workspaceCacheKey(workspaceId));\n }\n\n /** Clear entire plan cache */\n invalidateAll(): void {\n this.planCache.clear();\n }\n\n // -------------------------------------------------------------------------\n // Internals\n // -------------------------------------------------------------------------\n\n private workspaceCacheKey(workspaceId: string): string {\n return `workspace:${workspaceId}`;\n }\n\n private async resolvePlan(userId: string, cacheKey?: string): Promise<CachedPlan> {\n const key = cacheKey ?? userId;\n const cached = this.planCache.get(key);\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(key, plan);\n return plan;\n }\n\n private async fetchPlan(userId: string): Promise<CachedPlan> {\n // No billing source wired → every cap silently collapses to the configured\n // clientless tier. Warn loudly (once) so an unwired/broken integration\n // can't hide in prod (BOFF-4805/4809).\n if (!this.hasBillingSource()) {\n this.warnClientlessOnce();\n this.planResolutionCounter.inc({ result: 'fallback', reason: 'clientless' });\n return this.fallbackPlan(this.clientlessTier, 'clientless');\n }\n try {\n const planNode = await this.fetchPlanNode(userId);\n if (!planNode) {\n logger.warn('resource-caps: plan lookup returned null; falling back to paid defaults', {\n userId,\n });\n this.onBillingFailure?.();\n this.planResolutionCounter.inc({ result: 'fallback', reason: 'billing_error' });\n return this.fallbackPlan(this.billingFailureTier, 'billing_error');\n }\n const tier = normaliseTier(planNode.tier, this.billingFailureTier);\n const overrides = sanitizeOverrides(planNode.resourceCaps);\n this.planResolutionCounter.inc({ result: 'billing' });\n return {\n planName: planNode.planName ?? (tier === 'paid' ? 'paid' : 'free'),\n planTier: tier,\n overrides,\n fetchedAt: Date.now(),\n isFallback: false,\n };\n } catch (err) {\n logger.warn('resource-caps: plan lookup failed; falling back to paid defaults', {\n err,\n userId,\n });\n this.onBillingFailure?.();\n this.planResolutionCounter.inc({ result: 'fallback', reason: 'billing_error' });\n return this.fallbackPlan(this.billingFailureTier, 'billing_error');\n }\n }\n\n /**\n * Resolve the raw `userBillingPlan` node via the injected transport (preferred)\n * or the legacy direct client. Guarded by {@link hasBillingSource} in the\n * caller, so at least one path is present here.\n */\n private async fetchPlanNode(userId: string): Promise<PlanNode | null> {\n if (this.transport) {\n const data = (await this.transport(PLAN_QUERY, { userId })) as PlanQueryResponse | null | undefined;\n return data?.userBillingPlan ?? null;\n }\n if (this.client) {\n const res = await this.client.query<PlanQueryResponse>(PLAN_QUERY, { userId });\n return res?.userBillingPlan ?? null;\n }\n return null;\n }\n\n private warnClientlessOnce(): void {\n if (this.warnedClientless) return;\n this.warnedClientless = true;\n logger.warn(\n 'resource-caps: running WITHOUT a billing transport/client — every resource cap will use the ' +\n 'free-tier fallback regardless of the customer plan. Ensure initializeBurdenoffSDK() ran with a ' +\n 'layer/gateway configured (BOFF-4805).'\n );\n this.onFallback?.();\n }\n\n private fallbackPlan(tier: PlanTier, reason: 'clientless' | 'billing_error'): CachedPlan {\n return {\n planName: tier === 'paid' ? 'fallback-paid' : 'fallback-free',\n planTier: tier,\n overrides: {},\n fetchedAt: Date.now(),\n isFallback: true,\n fallbackReason: reason,\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 } else if (config) {\n // Previously the config was silently dropped once the singleton existed —\n // the root cause of BOFF-4805 where the billing client could never be bound\n // after a lazy first construction. Apply it instead.\n defaultInstance.configure(config);\n }\n return defaultInstance;\n}\n\n/**\n * Prime/rebind the process-wide singleton with billing connectivity. Call once\n * during SDK bootstrap. Unlike a bare `getResourceCaps(config)` on a fresh\n * process, this is explicit about the intent to (re)wire and is safe to call\n * after the singleton was already lazily created clientless.\n */\nexport function configureResourceCaps(config: ResourceCapsConfig): ResourceCapsModule {\n const instance = getResourceCaps();\n instance.configure(config);\n return instance;\n}\n\n/** Reset the singleton (for tests) */\nexport function __resetResourceCapsForTests(): void {\n defaultInstance = null;\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { RBACModule } from './chunk-S32JOMMW.js';
1
+ import { RBACModule } from './chunk-N3YN6ZII.js';
2
2
  import { NotificationsModule, NotificationType } from './chunk-TORD32GS.js';
3
3
  import { ValidationError, QuotaExceededError, PermissionDeniedError } from './chunk-QGBXVNRM.js';
4
4
  import { logger } from './chunk-CBZNP6RF.js';
@@ -1617,5 +1617,5 @@ var TrackerModule = class {
1617
1617
  };
1618
1618
 
1619
1619
  export { ActivityModule, TrackerModule, UNIFIED_TRACKER_CONSTANTS, UnifiedTrackerStub, UsageMetricType, UsageModule, createProductTracker, trackOperation, trackerStub };
1620
- //# sourceMappingURL=chunk-VOJROV6C.js.map
1621
- //# sourceMappingURL=chunk-VOJROV6C.js.map
1620
+ //# sourceMappingURL=chunk-VDYBUTFI.js.map
1621
+ //# sourceMappingURL=chunk-VDYBUTFI.js.map