@happyvertical/smrt-subscriptions 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/AGENTS.md +26 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/collections/SubscriptionPlanCollection.d.ts +9 -0
  8. package/dist/collections/SubscriptionPlanCollection.d.ts.map +1 -0
  9. package/dist/collections/TenantSubscriptionCollection.d.ts +29 -0
  10. package/dist/collections/TenantSubscriptionCollection.d.ts.map +1 -0
  11. package/dist/collections/TenantUsageMetricCollection.d.ts +28 -0
  12. package/dist/collections/TenantUsageMetricCollection.d.ts.map +1 -0
  13. package/dist/collections/index.d.ts +4 -0
  14. package/dist/collections/index.d.ts.map +1 -0
  15. package/dist/index.d.ts +6 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +1271 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/manifest.json +1291 -0
  20. package/dist/models/SubscriptionPlan.d.ts +30 -0
  21. package/dist/models/SubscriptionPlan.d.ts.map +1 -0
  22. package/dist/models/TenantSubscription.d.ts +59 -0
  23. package/dist/models/TenantSubscription.d.ts.map +1 -0
  24. package/dist/models/TenantUsageMetric.d.ts +35 -0
  25. package/dist/models/TenantUsageMetric.d.ts.map +1 -0
  26. package/dist/models/index.d.ts +4 -0
  27. package/dist/models/index.d.ts.map +1 -0
  28. package/dist/services/index.d.ts +5 -0
  29. package/dist/services/index.d.ts.map +1 -0
  30. package/dist/services/subscription-resolver.d.ts +96 -0
  31. package/dist/services/subscription-resolver.d.ts.map +1 -0
  32. package/dist/services/threshold-evaluator.d.ts +4 -0
  33. package/dist/services/threshold-evaluator.d.ts.map +1 -0
  34. package/dist/services/usage-meter.d.ts +32 -0
  35. package/dist/services/usage-meter.d.ts.map +1 -0
  36. package/dist/smrt-knowledge.json +883 -0
  37. package/dist/svelte/PlanPicker.svelte +82 -0
  38. package/dist/svelte/PlanPicker.svelte.d.ts +10 -0
  39. package/dist/svelte/PlanPicker.svelte.d.ts.map +1 -0
  40. package/dist/svelte/SubscriptionSummary.svelte +65 -0
  41. package/dist/svelte/SubscriptionSummary.svelte.d.ts +8 -0
  42. package/dist/svelte/SubscriptionSummary.svelte.d.ts.map +1 -0
  43. package/dist/svelte/UsageThresholds.svelte +71 -0
  44. package/dist/svelte/UsageThresholds.svelte.d.ts +8 -0
  45. package/dist/svelte/UsageThresholds.svelte.d.ts.map +1 -0
  46. package/dist/svelte/i18n.d.ts +5 -0
  47. package/dist/svelte/i18n.d.ts.map +1 -0
  48. package/dist/svelte/i18n.js +9 -0
  49. package/dist/svelte/index.d.ts +4 -0
  50. package/dist/svelte/index.d.ts.map +1 -0
  51. package/dist/svelte/index.js +3 -0
  52. package/dist/types.d.ts +175 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/types.js +2 -0
  55. package/dist/types.js.map +1 -0
  56. package/dist/utils.d.ts +59 -0
  57. package/dist/utils.d.ts.map +1 -0
  58. package/package.json +80 -0
  59. package/scripts/migrate-1454-drop-legacy-conflict-index.ts +110 -0
package/dist/index.js ADDED
@@ -0,0 +1,1271 @@
1
+ import { ObjectRegistry, smrt, SmrtObject, SmrtCollection, foreignKey } from "@happyvertical/smrt-core";
2
+ import { tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
3
+ ObjectRegistry.registerPackageManifest(
4
+ new URL("./manifest.json", import.meta.url)
5
+ );
6
+ const THRESHOLD_WINDOWS = [
7
+ "day",
8
+ "week",
9
+ "month",
10
+ "year",
11
+ "rolling"
12
+ ];
13
+ const THRESHOLD_ENFORCEMENTS = [
14
+ "observe",
15
+ "warn",
16
+ "block"
17
+ ];
18
+ function parseJsonArray(value, fallback = []) {
19
+ if (!value) {
20
+ return fallback;
21
+ }
22
+ try {
23
+ const parsed = JSON.parse(value);
24
+ return Array.isArray(parsed) ? parsed : fallback;
25
+ } catch {
26
+ return fallback;
27
+ }
28
+ }
29
+ function parseJsonObject(value, fallback) {
30
+ if (!value) {
31
+ return fallback;
32
+ }
33
+ try {
34
+ const parsed = JSON.parse(value);
35
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : fallback;
36
+ } catch {
37
+ return fallback;
38
+ }
39
+ }
40
+ function stringifyJson(value) {
41
+ return JSON.stringify(value ?? null);
42
+ }
43
+ function normalizeFeatureGrants(grants) {
44
+ return grants.map(
45
+ (grant) => typeof grant === "string" ? { featureKey: grant, enabled: true } : grant
46
+ );
47
+ }
48
+ function normalizeSubscriber(input) {
49
+ const rawExternalId = input.subscriberExternalId;
50
+ const externalIdAbsent = rawExternalId === null || rawExternalId === void 0;
51
+ const externalId = externalIdAbsent ? "" : String(rawExternalId);
52
+ const rawKind = input.subscriberKind;
53
+ const kind = rawKind === null || rawKind === void 0 ? "tenant" : rawKind;
54
+ if (kind !== "tenant" && kind !== "external") {
55
+ throw new Error(
56
+ `subscriberKind must be "tenant" or "external" (got ${JSON.stringify(rawKind)})`
57
+ );
58
+ }
59
+ if (kind === "tenant") {
60
+ if (externalId !== "") {
61
+ throw new Error(
62
+ 'subscriberExternalId is set but subscriberKind is "tenant"; set subscriberKind="external" explicitly or omit subscriberExternalId'
63
+ );
64
+ }
65
+ return { kind: "tenant", tenantId: input.tenantId };
66
+ }
67
+ if (externalId === "") {
68
+ throw new Error(
69
+ 'subscriberKind="external" requires a non-empty subscriberExternalId'
70
+ );
71
+ }
72
+ return {
73
+ kind: "external",
74
+ tenantId: input.tenantId,
75
+ externalId
76
+ };
77
+ }
78
+ function subscriberToColumns(subscriber) {
79
+ if (subscriber.kind === "tenant") {
80
+ return {
81
+ tenantId: subscriber.tenantId,
82
+ subscriberKind: "tenant",
83
+ subscriberExternalId: ""
84
+ };
85
+ }
86
+ return {
87
+ tenantId: subscriber.tenantId,
88
+ subscriberKind: "external",
89
+ subscriberExternalId: subscriber.externalId
90
+ };
91
+ }
92
+ function assertSubscriberInvariant(modelName, fields) {
93
+ if (fields.subscriberKind !== "tenant" && fields.subscriberKind !== "external") {
94
+ throw new Error(
95
+ `${modelName}: subscriberKind must be "tenant" or "external" (got ${describe(
96
+ fields.subscriberKind
97
+ )})`
98
+ );
99
+ }
100
+ if (typeof fields.subscriberExternalId !== "string") {
101
+ throw new Error(
102
+ `${modelName}: subscriberExternalId must be a string (got ${describe(
103
+ fields.subscriberExternalId
104
+ )})`
105
+ );
106
+ }
107
+ if (fields.subscriberKind === "tenant" && fields.subscriberExternalId !== "") {
108
+ throw new Error(
109
+ `${modelName}: subscriberExternalId must be empty when subscriberKind is "tenant"`
110
+ );
111
+ }
112
+ if (fields.subscriberKind === "external" && fields.subscriberExternalId === "") {
113
+ throw new Error(
114
+ `${modelName}: subscriberKind="external" requires a non-empty subscriberExternalId`
115
+ );
116
+ }
117
+ }
118
+ function describe(value) {
119
+ if (value === null) return "null";
120
+ if (value === void 0) return "undefined";
121
+ if (typeof value === "string") return `"${value}"`;
122
+ return String(value);
123
+ }
124
+ function getWindowForThreshold(thresholdWindow, now = /* @__PURE__ */ new Date()) {
125
+ switch (thresholdWindow) {
126
+ case "day":
127
+ return utcWindow(now, "day");
128
+ case "week":
129
+ return utcWindow(now, "week");
130
+ case "month":
131
+ return utcWindow(now, "month");
132
+ case "year":
133
+ return utcWindow(now, "year");
134
+ case "rolling":
135
+ return {
136
+ start: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1e3),
137
+ end: now
138
+ };
139
+ }
140
+ }
141
+ function getWindowKey(window) {
142
+ return `${window.start.toISOString()}..${window.end.toISOString()}`;
143
+ }
144
+ function isValidThreshold(threshold) {
145
+ return typeof threshold.metricKey === "string" && threshold.metricKey.length > 0 && Number.isFinite(threshold.limit) && threshold.limit >= 0 && isThresholdWindow(threshold.window) && isThresholdEnforcement(threshold.enforcement) && (threshold.warningRatio === void 0 || Number.isFinite(threshold.warningRatio) && threshold.warningRatio >= 0 && threshold.warningRatio <= 1);
146
+ }
147
+ function isThresholdWindow(value) {
148
+ return typeof value === "string" && THRESHOLD_WINDOWS.includes(value);
149
+ }
150
+ function isThresholdEnforcement(value) {
151
+ return typeof value === "string" && THRESHOLD_ENFORCEMENTS.includes(value);
152
+ }
153
+ function utcWindow(now, unit) {
154
+ const start = new Date(
155
+ Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
156
+ );
157
+ if (unit === "week") {
158
+ const day = start.getUTCDay();
159
+ const mondayOffset = day === 0 ? -6 : 1 - day;
160
+ start.setUTCDate(start.getUTCDate() + mondayOffset);
161
+ }
162
+ if (unit === "month") {
163
+ start.setUTCDate(1);
164
+ }
165
+ if (unit === "year") {
166
+ start.setUTCMonth(0, 1);
167
+ }
168
+ const end = new Date(start);
169
+ switch (unit) {
170
+ case "day":
171
+ end.setUTCDate(end.getUTCDate() + 1);
172
+ break;
173
+ case "week":
174
+ end.setUTCDate(end.getUTCDate() + 7);
175
+ break;
176
+ case "month":
177
+ end.setUTCMonth(end.getUTCMonth() + 1);
178
+ break;
179
+ case "year":
180
+ end.setUTCFullYear(end.getUTCFullYear() + 1);
181
+ break;
182
+ }
183
+ return { start, end };
184
+ }
185
+ var __defProp$2 = Object.defineProperty;
186
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
187
+ var __decorateClass$2 = (decorators, target, key, kind) => {
188
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
189
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
190
+ if (decorator = decorators[i])
191
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
192
+ if (kind && result) __defProp$2(target, key, result);
193
+ return result;
194
+ };
195
+ let SubscriptionPlan = class extends SmrtObject {
196
+ tenantId = null;
197
+ planKey = "";
198
+ name = "";
199
+ description = "";
200
+ status = "active";
201
+ sortOrder = 0;
202
+ priceAmount = 0;
203
+ currency = "USD";
204
+ billingInterval = "month";
205
+ externalProvider = "stripe";
206
+ stripeProductId = "";
207
+ stripePriceId = "";
208
+ features = "[]";
209
+ thresholds = "[]";
210
+ metadata = "{}";
211
+ constructor(options = {}) {
212
+ super(options);
213
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
214
+ if (options.planKey !== void 0) this.planKey = options.planKey;
215
+ if (options.name !== void 0) this.name = options.name;
216
+ if (options.description !== void 0)
217
+ this.description = options.description;
218
+ if (options.status !== void 0) this.status = options.status;
219
+ if (options.sortOrder !== void 0) this.sortOrder = options.sortOrder;
220
+ if (options.priceAmount !== void 0)
221
+ this.priceAmount = options.priceAmount;
222
+ if (options.currency !== void 0) this.currency = options.currency;
223
+ if (options.billingInterval !== void 0) {
224
+ this.billingInterval = options.billingInterval;
225
+ }
226
+ if (options.externalProvider !== void 0) {
227
+ this.externalProvider = options.externalProvider;
228
+ }
229
+ if (options.stripeProductId !== void 0) {
230
+ this.stripeProductId = options.stripeProductId;
231
+ }
232
+ if (options.stripePriceId !== void 0) {
233
+ this.stripePriceId = options.stripePriceId;
234
+ }
235
+ if (options.features !== void 0) this.features = options.features;
236
+ if (options.thresholds !== void 0) this.thresholds = options.thresholds;
237
+ if (options.metadata !== void 0) this.metadata = options.metadata;
238
+ }
239
+ isActive() {
240
+ return this.status === "active";
241
+ }
242
+ getFeatureGrants() {
243
+ return normalizeFeatureGrants(
244
+ parseJsonArray(this.features)
245
+ );
246
+ }
247
+ setFeatureGrants(grants) {
248
+ this.features = stringifyJson(normalizeFeatureGrants(grants));
249
+ }
250
+ getFeatureKeys() {
251
+ return this.getFeatureGrants().filter((grant) => grant.enabled !== false).map((grant) => grant.featureKey);
252
+ }
253
+ getThresholds() {
254
+ return parseJsonArray(this.thresholds).filter(
255
+ (threshold) => threshold.metricKey
256
+ );
257
+ }
258
+ setThresholds(thresholds) {
259
+ this.thresholds = stringifyJson(thresholds);
260
+ }
261
+ getMetadata() {
262
+ return parseJsonObject(this.metadata, {});
263
+ }
264
+ setMetadata(metadata) {
265
+ this.metadata = stringifyJson(metadata);
266
+ }
267
+ };
268
+ __decorateClass$2([
269
+ tenantId({ nullable: true })
270
+ ], SubscriptionPlan.prototype, "tenantId", 2);
271
+ SubscriptionPlan = __decorateClass$2([
272
+ TenantScoped({ mode: "optional" }),
273
+ smrt({
274
+ tableName: "_smrt_subscription_plans",
275
+ api: { include: ["list", "get", "create", "update"] },
276
+ cli: true,
277
+ mcp: { include: ["list", "get"] },
278
+ conflictColumns: ["plan_key"]
279
+ })
280
+ ], SubscriptionPlan);
281
+ class SubscriptionPlanCollection extends SmrtCollection {
282
+ static _itemClass = SubscriptionPlan;
283
+ async findByPlanKey(planKey) {
284
+ const plans = await this.list({ where: { planKey }, limit: 1 });
285
+ return plans[0] ?? null;
286
+ }
287
+ async findActive() {
288
+ return this.list({
289
+ where: { status: "active" },
290
+ orderBy: "sortOrder ASC"
291
+ });
292
+ }
293
+ async findByStripePriceId(stripePriceId) {
294
+ const plans = await this.list({ where: { stripePriceId }, limit: 1 });
295
+ return plans[0] ?? null;
296
+ }
297
+ }
298
+ var __defProp$1 = Object.defineProperty;
299
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
300
+ var __decorateClass$1 = (decorators, target, key, kind) => {
301
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
302
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
303
+ if (decorator = decorators[i])
304
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
305
+ if (kind && result) __defProp$1(target, key, result);
306
+ return result;
307
+ };
308
+ let TenantSubscription = class extends SmrtObject {
309
+ tenantId;
310
+ /**
311
+ * Discriminator for the subscriber identity.
312
+ *
313
+ * - `'tenant'` (default): the subscriber IS the owning `tenantId` — the
314
+ * pre-polymorphic shape. All existing rows continue to behave this way.
315
+ * - `'external'`: the subscriber is `subscriberExternalId`, scoped under the
316
+ * issuing `tenantId`. Used for B2C buyers, anonymous-email subscribers,
317
+ * agent identities, and any other caller-defined identity.
318
+ */
319
+ subscriberKind = "tenant";
320
+ /**
321
+ * Caller-namespaced opaque identifier for the subscriber when
322
+ * `subscriberKind === 'external'`. Empty string when kind is `'tenant'`.
323
+ *
324
+ * The package treats this as opaque — no FK, no inferred semantics. Callers
325
+ * are expected to namespace (e.g. `buyer-contact:abc123`,
326
+ * `agent:hermes-7`, `email:foo@example.com`).
327
+ */
328
+ subscriberExternalId = "";
329
+ planId = "";
330
+ status = "incomplete";
331
+ startedAt = /* @__PURE__ */ new Date();
332
+ currentPeriodStart = null;
333
+ currentPeriodEnd = null;
334
+ trialEndsAt = null;
335
+ cancelAtPeriodEnd = false;
336
+ canceledAt = null;
337
+ externalProvider = "stripe";
338
+ stripeCustomerId = "";
339
+ stripeSubscriptionId = "";
340
+ stripeCheckoutSessionId = "";
341
+ metadata = "{}";
342
+ constructor(options = {}) {
343
+ super(options);
344
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
345
+ if (options.subscriberKind !== void 0)
346
+ this.subscriberKind = options.subscriberKind;
347
+ if (options.subscriberExternalId !== void 0)
348
+ this.subscriberExternalId = options.subscriberExternalId;
349
+ assertSubscriberInvariant("TenantSubscription", {
350
+ subscriberKind: this.subscriberKind,
351
+ subscriberExternalId: this.subscriberExternalId
352
+ });
353
+ if (options.planId !== void 0) this.planId = options.planId;
354
+ if (options.status !== void 0) this.status = options.status;
355
+ if (options.startedAt !== void 0) this.startedAt = options.startedAt;
356
+ if (options.currentPeriodStart !== void 0) {
357
+ this.currentPeriodStart = options.currentPeriodStart;
358
+ }
359
+ if (options.currentPeriodEnd !== void 0) {
360
+ this.currentPeriodEnd = options.currentPeriodEnd;
361
+ }
362
+ if (options.trialEndsAt !== void 0)
363
+ this.trialEndsAt = options.trialEndsAt;
364
+ if (options.cancelAtPeriodEnd !== void 0) {
365
+ this.cancelAtPeriodEnd = options.cancelAtPeriodEnd;
366
+ }
367
+ if (options.canceledAt !== void 0) this.canceledAt = options.canceledAt;
368
+ if (options.externalProvider !== void 0) {
369
+ this.externalProvider = options.externalProvider;
370
+ }
371
+ if (options.stripeCustomerId !== void 0) {
372
+ this.stripeCustomerId = options.stripeCustomerId;
373
+ }
374
+ if (options.stripeSubscriptionId !== void 0) {
375
+ this.stripeSubscriptionId = options.stripeSubscriptionId;
376
+ }
377
+ if (options.stripeCheckoutSessionId !== void 0) {
378
+ this.stripeCheckoutSessionId = options.stripeCheckoutSessionId;
379
+ }
380
+ if (options.metadata !== void 0) this.metadata = options.metadata;
381
+ }
382
+ /**
383
+ * Validates the subscriber XOR invariant before every save. Critical for the
384
+ * generated update path: `SmrtCollection.update()` mutates the existing
385
+ * instance via `Object.assign()` and calls `save()` directly — the
386
+ * constructor never re-runs, so without this hook a partial update like
387
+ * `{ subscriberExternalId: 'buyer:alice' }` on a tenant-kind row would
388
+ * persist a malformed conflict key and let two tenant-kind rows coexist
389
+ * under the same tenant.
390
+ */
391
+ async validateBeforeSave() {
392
+ await super.validateBeforeSave();
393
+ assertSubscriberInvariant("TenantSubscription", {
394
+ subscriberKind: this.subscriberKind,
395
+ subscriberExternalId: this.subscriberExternalId
396
+ });
397
+ }
398
+ isEntitled(now = /* @__PURE__ */ new Date()) {
399
+ if (this.status !== "active" && this.status !== "trialing") {
400
+ return false;
401
+ }
402
+ if (this.currentPeriodEnd && this.currentPeriodEnd.getTime() < now.getTime()) {
403
+ return false;
404
+ }
405
+ return true;
406
+ }
407
+ /**
408
+ * Project this row's polymorphic subscriber columns onto the
409
+ * {@link Subscriber} discriminated union. Returns `null` when the owning
410
+ * tenant is absent — that's an invalid row that should not be acted on.
411
+ */
412
+ getSubscriber() {
413
+ if (!this.tenantId) {
414
+ return null;
415
+ }
416
+ if (this.subscriberKind === "external") {
417
+ if (!this.subscriberExternalId) {
418
+ return null;
419
+ }
420
+ return {
421
+ kind: "external",
422
+ tenantId: this.tenantId,
423
+ externalId: this.subscriberExternalId
424
+ };
425
+ }
426
+ return { kind: "tenant", tenantId: this.tenantId };
427
+ }
428
+ getMetadata() {
429
+ return parseJsonObject(this.metadata, {});
430
+ }
431
+ setMetadata(metadata) {
432
+ this.metadata = stringifyJson(metadata);
433
+ }
434
+ };
435
+ __decorateClass$1([
436
+ tenantId()
437
+ ], TenantSubscription.prototype, "tenantId", 2);
438
+ __decorateClass$1([
439
+ foreignKey("SubscriptionPlan")
440
+ ], TenantSubscription.prototype, "planId", 2);
441
+ TenantSubscription = __decorateClass$1([
442
+ TenantScoped({ mode: "required" }),
443
+ smrt({
444
+ tableName: "_smrt_tenant_subscriptions",
445
+ api: { include: ["list", "get", "create", "update"] },
446
+ cli: true,
447
+ mcp: { include: ["list", "get"] },
448
+ // The conflict key includes the subscriber discriminator so an issuing tenant
449
+ // can host many distinct external subscribers without collisions on the
450
+ // unique index:
451
+ // - tenant-kind rows always have subscriber_external_id = '' → uniqueness
452
+ // reduces to (tenant_id, 'tenant', '') = at most one tenant-kind row per
453
+ // tenant (preserves the pre-polymorphic invariant).
454
+ // - external-kind rows are unique on (tenant_id, 'external', externalId) =
455
+ // at most one active subscription per (issuer, external subscriber).
456
+ //
457
+ // UPGRADE NOTE: databases created against the pre-#1454 conflict key
458
+ // ['tenant_id'] still carry the legacy `_smrt_tenant_subscriptions_tenant_id_idx`.
459
+ // `smrt db:migrate` does not sweep orphan indexes by default, so the legacy
460
+ // index persists and continues to reject external subscriptions until it's
461
+ // dropped. Run scripts/migrate-1454-drop-legacy-conflict-index.ts once on
462
+ // upgrade, or pass `--drop-indexes` to `db:migrate`. Fresh databases need
463
+ // neither — the generated schema already reflects the new key.
464
+ conflictColumns: ["tenant_id", "subscriber_kind", "subscriber_external_id"]
465
+ })
466
+ ], TenantSubscription);
467
+ class TenantSubscriptionCollection extends SmrtCollection {
468
+ static _itemClass = TenantSubscription;
469
+ /**
470
+ * List subscriptions whose owning tenant scope is `tenantId`. Includes both
471
+ * `'tenant'`-kind (subscriber == owner) and `'external'`-kind (subscriber is
472
+ * an external identity scoped under this tenant) rows.
473
+ */
474
+ async findByTenant(tenantId2) {
475
+ return this.list({
476
+ where: { tenantId: tenantId2 },
477
+ orderBy: "created_at DESC"
478
+ });
479
+ }
480
+ /**
481
+ * Legacy single-tenant accessor: returns the current subscription where the
482
+ * tenant IS the subscriber (`subscriberKind = 'tenant'`). External-kind rows
483
+ * scoped under this tenant are intentionally excluded so existing callers
484
+ * are not surprised by buyer/agent subscriptions.
485
+ */
486
+ async findCurrentForTenant(tenantId2, now = /* @__PURE__ */ new Date()) {
487
+ return this.findCurrentForSubscriber({ kind: "tenant", tenantId: tenantId2 }, now);
488
+ }
489
+ /**
490
+ * Polymorphic current-subscription accessor.
491
+ *
492
+ * For `'tenant'` subscribers we match rows with `subscriberKind = 'tenant'`
493
+ * under the same `tenantId`. For `'external'` subscribers we match by
494
+ * `(tenantId, subscriberExternalId)` with `subscriberKind = 'external'`.
495
+ */
496
+ async findCurrentForSubscriber(subscriber, now = /* @__PURE__ */ new Date()) {
497
+ const where = {
498
+ tenantId: subscriber.tenantId,
499
+ subscriberKind: subscriber.kind
500
+ };
501
+ if (subscriber.kind === "external") {
502
+ where.subscriberExternalId = subscriber.externalId;
503
+ }
504
+ const subscriptions = await this.list({
505
+ where,
506
+ orderBy: "created_at DESC"
507
+ });
508
+ return subscriptions.find((subscription) => subscription.isEntitled(now)) ?? subscriptions[0] ?? null;
509
+ }
510
+ async findByStripeSubscriptionId(stripeSubscriptionId) {
511
+ const subscriptions = await this.list({
512
+ where: { stripeSubscriptionId },
513
+ limit: 1
514
+ });
515
+ return subscriptions[0] ?? null;
516
+ }
517
+ }
518
+ var __defProp = Object.defineProperty;
519
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
520
+ var __decorateClass = (decorators, target, key, kind) => {
521
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
522
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
523
+ if (decorator = decorators[i])
524
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
525
+ if (kind && result) __defProp(target, key, result);
526
+ return result;
527
+ };
528
+ let TenantUsageMetric = class extends SmrtObject {
529
+ tenantId;
530
+ /**
531
+ * Discriminator for which subscriber identity recorded this usage.
532
+ * `'tenant'` (default) preserves the legacy shape; `'external'` indicates
533
+ * the subscriber is `subscriberExternalId` under the issuing `tenantId`.
534
+ */
535
+ subscriberKind = "tenant";
536
+ /**
537
+ * Opaque caller-namespaced subscriber id when `subscriberKind === 'external'`.
538
+ * Empty string when kind is `'tenant'`.
539
+ */
540
+ subscriberExternalId = "";
541
+ metricKey = "";
542
+ quantity = 0;
543
+ windowStart = /* @__PURE__ */ new Date();
544
+ windowEnd = /* @__PURE__ */ new Date();
545
+ source = "";
546
+ sourceId = "";
547
+ dimensions = "{}";
548
+ constructor(options = {}) {
549
+ super(options);
550
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
551
+ if (options.subscriberKind !== void 0)
552
+ this.subscriberKind = options.subscriberKind;
553
+ if (options.subscriberExternalId !== void 0)
554
+ this.subscriberExternalId = options.subscriberExternalId;
555
+ assertSubscriberInvariant("TenantUsageMetric", {
556
+ subscriberKind: this.subscriberKind,
557
+ subscriberExternalId: this.subscriberExternalId
558
+ });
559
+ if (options.metricKey !== void 0) this.metricKey = options.metricKey;
560
+ if (options.quantity !== void 0) this.quantity = options.quantity;
561
+ if (options.windowStart !== void 0)
562
+ this.windowStart = options.windowStart;
563
+ if (options.windowEnd !== void 0) this.windowEnd = options.windowEnd;
564
+ if (options.source !== void 0) this.source = options.source;
565
+ if (options.sourceId !== void 0) this.sourceId = options.sourceId;
566
+ if (options.dimensions !== void 0) this.dimensions = options.dimensions;
567
+ }
568
+ /** See `TenantSubscription.validateBeforeSave` for the rationale. */
569
+ async validateBeforeSave() {
570
+ await super.validateBeforeSave();
571
+ assertSubscriberInvariant("TenantUsageMetric", {
572
+ subscriberKind: this.subscriberKind,
573
+ subscriberExternalId: this.subscriberExternalId
574
+ });
575
+ }
576
+ /**
577
+ * Project this row's polymorphic subscriber columns onto the
578
+ * {@link Subscriber} discriminated union.
579
+ */
580
+ getSubscriber() {
581
+ if (!this.tenantId) {
582
+ return null;
583
+ }
584
+ if (this.subscriberKind === "external") {
585
+ if (!this.subscriberExternalId) {
586
+ return null;
587
+ }
588
+ return {
589
+ kind: "external",
590
+ tenantId: this.tenantId,
591
+ externalId: this.subscriberExternalId
592
+ };
593
+ }
594
+ return { kind: "tenant", tenantId: this.tenantId };
595
+ }
596
+ getDimensions() {
597
+ return parseJsonObject(this.dimensions, {});
598
+ }
599
+ setDimensions(dimensions) {
600
+ this.dimensions = stringifyJson(dimensions);
601
+ }
602
+ };
603
+ __decorateClass([
604
+ tenantId()
605
+ ], TenantUsageMetric.prototype, "tenantId", 2);
606
+ TenantUsageMetric = __decorateClass([
607
+ TenantScoped({ mode: "required" }),
608
+ smrt({
609
+ tableName: "_smrt_tenant_usage_metrics",
610
+ api: { include: ["list", "get", "create"] },
611
+ cli: true,
612
+ mcp: { include: ["list", "get"] }
613
+ })
614
+ ], TenantUsageMetric);
615
+ class TenantUsageMetricCollection extends SmrtCollection {
616
+ static _itemClass = TenantUsageMetric;
617
+ async recordUsage(options) {
618
+ const subscriber = normalizeSubscriber({
619
+ tenantId: options.tenantId,
620
+ subscriberKind: options.subscriberKind,
621
+ subscriberExternalId: options.subscriberExternalId
622
+ });
623
+ const columns = subscriberToColumns(subscriber);
624
+ const metric = await this.create({
625
+ tenantId: columns.tenantId,
626
+ subscriberKind: columns.subscriberKind,
627
+ subscriberExternalId: columns.subscriberExternalId,
628
+ metricKey: options.metricKey,
629
+ quantity: options.quantity,
630
+ windowStart: options.windowStart,
631
+ windowEnd: options.windowEnd,
632
+ source: options.source ?? "",
633
+ sourceId: options.sourceId ?? "",
634
+ dimensions: stringifyJson(options.dimensions ?? {})
635
+ });
636
+ return metric;
637
+ }
638
+ /**
639
+ * Legacy accessor — finds metrics for `subscriberKind = 'tenant'` under the
640
+ * given tenant. External-kind rows scoped to the same tenant are excluded.
641
+ */
642
+ async findByTenantAndMetric(tenantId2, metricKey) {
643
+ return this.list({
644
+ where: { tenantId: tenantId2, metricKey, subscriberKind: "tenant" },
645
+ orderBy: "windowStart DESC"
646
+ });
647
+ }
648
+ async summarizeUsage(options) {
649
+ const subscriber = normalizeSubscriber({
650
+ tenantId: options.tenantId,
651
+ subscriberKind: options.subscriberKind,
652
+ subscriberExternalId: options.subscriberExternalId
653
+ });
654
+ const columns = subscriberToColumns(subscriber);
655
+ const where = {
656
+ tenantId: columns.tenantId,
657
+ subscriberKind: columns.subscriberKind,
658
+ metricKey: options.metricKey,
659
+ "windowStart <": options.window.end.toISOString(),
660
+ "windowEnd >": options.window.start.toISOString()
661
+ };
662
+ if (subscriber.kind === "external") {
663
+ where.subscriberExternalId = columns.subscriberExternalId;
664
+ }
665
+ const metrics = await this.list({ where });
666
+ const baseSummary = {
667
+ tenantId: columns.tenantId,
668
+ metricKey: options.metricKey,
669
+ quantity: metrics.reduce((sum, metric) => sum + metric.quantity, 0),
670
+ windowStart: options.window.start,
671
+ windowEnd: options.window.end
672
+ };
673
+ if (subscriber.kind === "external") {
674
+ return {
675
+ ...baseSummary,
676
+ subscriberKind: "external",
677
+ subscriberExternalId: subscriber.externalId
678
+ };
679
+ }
680
+ return baseSummary;
681
+ }
682
+ async summarizeUsageBatch(options) {
683
+ const metricKeys = Array.from(new Set(options.metricKeys));
684
+ if (metricKeys.length === 0) {
685
+ return [];
686
+ }
687
+ const subscriber = normalizeSubscriber({
688
+ tenantId: options.tenantId,
689
+ subscriberKind: options.subscriberKind,
690
+ subscriberExternalId: options.subscriberExternalId
691
+ });
692
+ const columns = subscriberToColumns(subscriber);
693
+ const metricPlaceholders = metricKeys.map(() => "?").join(", ");
694
+ const params = [
695
+ columns.tenantId,
696
+ columns.subscriberKind,
697
+ ...metricKeys,
698
+ options.window.end.toISOString(),
699
+ options.window.start.toISOString()
700
+ ];
701
+ let subscriberSql = "";
702
+ if (subscriber.kind === "external") {
703
+ subscriberSql = "AND subscriber_external_id = ?";
704
+ params.splice(2, 0, columns.subscriberExternalId);
705
+ }
706
+ const result = await this.db.query(
707
+ `SELECT
708
+ metric_key,
709
+ COALESCE(SUM(quantity), 0) AS quantity
710
+ FROM _smrt_tenant_usage_metrics
711
+ WHERE tenant_id = ?
712
+ AND subscriber_kind = ?
713
+ ${subscriberSql}
714
+ AND metric_key IN (${metricPlaceholders})
715
+ AND window_start < ?
716
+ AND window_end > ?
717
+ GROUP BY metric_key`,
718
+ ...params
719
+ );
720
+ const quantityByMetric = /* @__PURE__ */ new Map();
721
+ for (const row of resultRows(result)) {
722
+ const metricKey = String(row.metric_key ?? row.metricKey ?? "");
723
+ if (metricKey) {
724
+ quantityByMetric.set(metricKey, numberFromRow(row, "quantity"));
725
+ }
726
+ }
727
+ return metricKeys.map((metricKey) => {
728
+ const baseSummary = {
729
+ tenantId: columns.tenantId,
730
+ metricKey,
731
+ quantity: quantityByMetric.get(metricKey) ?? 0,
732
+ windowStart: options.window.start,
733
+ windowEnd: options.window.end
734
+ };
735
+ if (subscriber.kind === "external") {
736
+ return {
737
+ ...baseSummary,
738
+ subscriberKind: "external",
739
+ subscriberExternalId: subscriber.externalId
740
+ };
741
+ }
742
+ return baseSummary;
743
+ });
744
+ }
745
+ /**
746
+ * Summarize persisted AI usage for a tenant within a window.
747
+ *
748
+ * Reads the framework `_smrt_ai_usage` system table via a raw aggregate
749
+ * query. This deliberately uses `this.db.query` rather than the inherited
750
+ * `query()`: those rows are framework usage records, not `TenantUsageMetric`
751
+ * instances, so model hydration would discard the aggregate columns. Tenant
752
+ * scoping is applied explicitly through the `tenant_id` filter.
753
+ *
754
+ * Named distinctly from the inherited `SmrtClass.summarizeAiUsage` (which
755
+ * returns grouped stats across all tenants) to avoid overriding it.
756
+ */
757
+ async summarizeTenantAiUsage(options) {
758
+ const result = await this.db.query(
759
+ `SELECT
760
+ COALESCE(SUM(prompt_tokens), 0) AS prompt_tokens,
761
+ COALESCE(SUM(completion_tokens), 0) AS completion_tokens,
762
+ COALESCE(SUM(total_tokens), 0) AS total_tokens,
763
+ COALESCE(SUM(estimated_cost), 0) AS estimated_cost,
764
+ COUNT(*) AS request_count
765
+ FROM _smrt_ai_usage
766
+ WHERE tenant_id = ?
767
+ AND created_at >= ?
768
+ AND created_at < ?`,
769
+ options.tenantId,
770
+ options.window.start.toISOString(),
771
+ options.window.end.toISOString()
772
+ );
773
+ const row = firstRow(result);
774
+ return {
775
+ tenantId: options.tenantId,
776
+ promptTokens: numberFromRow(row, "prompt_tokens"),
777
+ completionTokens: numberFromRow(row, "completion_tokens"),
778
+ totalTokens: numberFromRow(row, "total_tokens"),
779
+ estimatedCost: numberFromRow(row, "estimated_cost"),
780
+ requestCount: numberFromRow(row, "request_count"),
781
+ windowStart: options.window.start,
782
+ windowEnd: options.window.end
783
+ };
784
+ }
785
+ }
786
+ function firstRow(result) {
787
+ const rows = resultRows(result);
788
+ return rows[0] ?? {};
789
+ }
790
+ function resultRows(result) {
791
+ return Array.isArray(result) ? result : result?.rows ?? [];
792
+ }
793
+ function numberFromRow(row, key) {
794
+ const value = row[key];
795
+ if (typeof value === "number") {
796
+ return value;
797
+ }
798
+ if (typeof value === "bigint") {
799
+ return Number(value);
800
+ }
801
+ if (typeof value === "string") {
802
+ return Number.parseFloat(value) || 0;
803
+ }
804
+ return 0;
805
+ }
806
+ function evaluateThreshold(threshold, usage) {
807
+ const ratio = threshold.limit === 0 ? usage.quantity > 0 ? Infinity : 0 : usage.quantity / threshold.limit;
808
+ const warningRatio = threshold.warningRatio ?? 0.8;
809
+ const blocked = threshold.enforcement === "block" && (threshold.limit === 0 ? usage.quantity > 0 : usage.quantity >= threshold.limit);
810
+ const warned = !blocked && ratio >= warningRatio;
811
+ return {
812
+ threshold,
813
+ usage,
814
+ ratio,
815
+ state: blocked ? "blocked" : warned ? "warn" : "ok",
816
+ allowed: !blocked,
817
+ remaining: Math.max(0, threshold.limit - usage.quantity)
818
+ };
819
+ }
820
+ function evaluateThresholds(thresholds, usage) {
821
+ const usageByMetric = new Map(
822
+ usage.map((summary) => [summary.metricKey, summary])
823
+ );
824
+ return thresholds.map((threshold) => {
825
+ const summary = usageByMetric.get(threshold.metricKey) ?? emptyUsageSummary$2(threshold.metricKey);
826
+ return evaluateThreshold(threshold, summary);
827
+ });
828
+ }
829
+ function emptyUsageSummary$2(metricKey) {
830
+ const now = /* @__PURE__ */ new Date();
831
+ return {
832
+ tenantId: "",
833
+ metricKey,
834
+ quantity: 0,
835
+ windowStart: now,
836
+ windowEnd: now
837
+ };
838
+ }
839
+ class TenantUsageMeter {
840
+ constructor(metrics, classOptions = {}) {
841
+ this.metrics = metrics;
842
+ this.classOptions = classOptions;
843
+ }
844
+ metrics;
845
+ classOptions;
846
+ static async create(classOptions = {}) {
847
+ const metrics = await TenantUsageMetricCollection.create(classOptions);
848
+ return new TenantUsageMeter(metrics, classOptions);
849
+ }
850
+ /**
851
+ * Record one usage row.
852
+ *
853
+ * Accepts the polymorphic subscriber fields directly on the options object
854
+ * (`subscriberKind`/`subscriberExternalId`); when omitted defaults to a
855
+ * `'tenant'`-kind row keyed off `tenantId`. The collection layer enforces
856
+ * the XOR invariant — passing `subscriberKind: 'external'` without a
857
+ * non-empty `subscriberExternalId` throws.
858
+ */
859
+ async record(options) {
860
+ await this.metrics.recordUsage(options);
861
+ }
862
+ /**
863
+ * Summarize usage over a window for a given subscriber.
864
+ *
865
+ * The `ai.*` short-circuit only fires for `'tenant'`-kind subscribers since
866
+ * the `_smrt_ai_usage` system table is tenant-scoped; external subscribers
867
+ * fall through to the normal `_smrt_tenant_usage_metrics` aggregation.
868
+ */
869
+ async summarize(options) {
870
+ normalizeSubscriber({
871
+ tenantId: options.tenantId,
872
+ subscriberKind: options.subscriberKind,
873
+ subscriberExternalId: options.subscriberExternalId
874
+ });
875
+ const aiSummary = await this.trySummarizeAiMetric(options);
876
+ if (aiSummary) {
877
+ return aiSummary;
878
+ }
879
+ return this.metrics.summarizeUsage(options);
880
+ }
881
+ async summarizeBatch(options) {
882
+ const subscriber = normalizeSubscriber({
883
+ tenantId: options.tenantId,
884
+ subscriberKind: options.subscriberKind,
885
+ subscriberExternalId: options.subscriberExternalId
886
+ });
887
+ const metricKeys = Array.from(new Set(options.metricKeys));
888
+ if (metricKeys.length === 0) {
889
+ return [];
890
+ }
891
+ const useTenantAiSummary = subscriber.kind === "tenant";
892
+ const aiMetricKeys = useTenantAiSummary ? metricKeys.filter((metricKey) => metricKey.startsWith("ai.")) : [];
893
+ const persistedMetricKeys = useTenantAiSummary ? metricKeys.filter((metricKey) => !metricKey.startsWith("ai.")) : metricKeys;
894
+ const summaries = /* @__PURE__ */ new Map();
895
+ if (aiMetricKeys.length > 0) {
896
+ const aiSummary = await this.summarizeAiUsage({
897
+ tenantId: options.tenantId,
898
+ window: options.window
899
+ });
900
+ const quantityByMetric = aiQuantityByMetric(aiSummary);
901
+ for (const metricKey of aiMetricKeys) {
902
+ summaries.set(metricKey, {
903
+ tenantId: options.tenantId,
904
+ metricKey,
905
+ quantity: quantityByMetric[metricKey] ?? 0,
906
+ windowStart: options.window.start,
907
+ windowEnd: options.window.end
908
+ });
909
+ }
910
+ }
911
+ const persistedSummaries = await this.metrics.summarizeUsageBatch({
912
+ ...options,
913
+ metricKeys: persistedMetricKeys
914
+ });
915
+ for (const summary of persistedSummaries) {
916
+ summaries.set(summary.metricKey, summary);
917
+ }
918
+ return metricKeys.map(
919
+ (metricKey) => summaries.get(metricKey) ?? emptyUsageSummary$1(subscriber, metricKey, options.window)
920
+ );
921
+ }
922
+ async summarizeAiUsage(options) {
923
+ return this.metrics.summarizeTenantAiUsage(options);
924
+ }
925
+ getOptions() {
926
+ return this.classOptions;
927
+ }
928
+ async trySummarizeAiMetric(options) {
929
+ if (!options.metricKey.startsWith("ai.")) {
930
+ return null;
931
+ }
932
+ const kind = options.subscriberKind ?? "tenant";
933
+ if (kind !== "tenant") {
934
+ return null;
935
+ }
936
+ const summary = await this.summarizeAiUsage({
937
+ tenantId: options.tenantId,
938
+ window: options.window
939
+ });
940
+ const quantityByMetric = {
941
+ ...aiQuantityByMetric(summary)
942
+ };
943
+ return {
944
+ tenantId: options.tenantId,
945
+ metricKey: options.metricKey,
946
+ quantity: quantityByMetric[options.metricKey] ?? 0,
947
+ windowStart: options.window.start,
948
+ windowEnd: options.window.end
949
+ };
950
+ }
951
+ }
952
+ function aiQuantityByMetric(summary) {
953
+ return {
954
+ "ai.tokens.prompt": summary.promptTokens,
955
+ "ai.tokens.completion": summary.completionTokens,
956
+ "ai.tokens.total": summary.totalTokens,
957
+ "ai.cost.estimated": summary.estimatedCost,
958
+ "ai.requests": summary.requestCount
959
+ };
960
+ }
961
+ function emptyUsageSummary$1(subscriber, metricKey, window) {
962
+ const summary = {
963
+ tenantId: subscriber.tenantId,
964
+ metricKey,
965
+ quantity: 0,
966
+ windowStart: window.start,
967
+ windowEnd: window.end
968
+ };
969
+ if (subscriber.kind === "external") {
970
+ return {
971
+ ...summary,
972
+ subscriberKind: "external",
973
+ subscriberExternalId: subscriber.externalId
974
+ };
975
+ }
976
+ return summary;
977
+ }
978
+ class SubscriptionResolver {
979
+ constructor(readers) {
980
+ this.readers = readers;
981
+ }
982
+ readers;
983
+ /**
984
+ * Build the default resolver readers once for a request or app lifecycle and
985
+ * reuse the returned resolver across entitlement checks.
986
+ */
987
+ static async create(classOptions = {}) {
988
+ const [plans, subscriptions, usage] = await Promise.all([
989
+ SubscriptionPlanCollection.create(classOptions),
990
+ TenantSubscriptionCollection.create(classOptions),
991
+ TenantUsageMeter.create(classOptions)
992
+ ]);
993
+ return new SubscriptionResolver({ plans, subscriptions, usage });
994
+ }
995
+ /**
996
+ * Load the subscription/plan pair used by entitlement resolution. Callers
997
+ * that need both the entitlement snapshot and the backing records can load
998
+ * this once, then pass it back via `options.context`.
999
+ */
1000
+ async loadEntitlementContext(subscriberOrTenantId, options = {}) {
1001
+ const subscriber = toSubscriber(subscriberOrTenantId);
1002
+ const now = options.now ?? /* @__PURE__ */ new Date();
1003
+ const subscription = await this.resolveSubscription(
1004
+ subscriber,
1005
+ now,
1006
+ options.context
1007
+ );
1008
+ const plan = await this.resolvePlan(subscription, options.context);
1009
+ return { subscription, plan };
1010
+ }
1011
+ /**
1012
+ * Polymorphic entitlement resolution. Works for both `'tenant'`-kind and
1013
+ * `'external'`-kind subscribers and is the preferred surface — the
1014
+ * `resolveTenantEntitlements(tenantId)` method below is a thin wrapper.
1015
+ */
1016
+ async resolveEntitlements(subscriber, options = {}) {
1017
+ const now = options.now ?? /* @__PURE__ */ new Date();
1018
+ const { subscription, plan } = await this.loadEntitlementContext(
1019
+ subscriber,
1020
+ { ...options, now }
1021
+ );
1022
+ if (!subscription) {
1023
+ return emptyResolution(subscriber);
1024
+ }
1025
+ if (!plan || !plan.isActive() || !subscription.isEntitled(now)) {
1026
+ return {
1027
+ ...emptyResolution(subscriber),
1028
+ planId: plan?.id ?? subscription.planId ?? null,
1029
+ planKey: plan?.planKey ?? null,
1030
+ subscriptionId: subscription.id ?? null,
1031
+ status: subscription.status
1032
+ };
1033
+ }
1034
+ const thresholds = plan.getThresholds().filter(isValidThreshold);
1035
+ const thresholdEvaluations = await this.resolveThresholdEvaluations(
1036
+ subscriber,
1037
+ thresholds,
1038
+ now,
1039
+ options
1040
+ );
1041
+ return {
1042
+ tenantId: subscriber.tenantId,
1043
+ subscriber,
1044
+ planId: plan.id ?? null,
1045
+ planKey: plan.planKey,
1046
+ subscriptionId: subscription.id ?? null,
1047
+ status: subscription.status,
1048
+ featureKeys: plan.getFeatureKeys(),
1049
+ thresholds,
1050
+ thresholdEvaluations,
1051
+ allowed: thresholdEvaluations.every((evaluation) => evaluation.allowed)
1052
+ };
1053
+ }
1054
+ /**
1055
+ * Legacy single-tenant wrapper around {@link resolveEntitlements}. Kept so
1056
+ * existing tenant-only callers don't need to update their call sites.
1057
+ */
1058
+ async resolveTenantEntitlements(tenantId2, options = {}) {
1059
+ return this.resolveEntitlements({ kind: "tenant", tenantId: tenantId2 }, options);
1060
+ }
1061
+ async isFeatureEnabled(subscriberOrTenantId, featureKey, options = {}) {
1062
+ const resolution = await this.resolveEntitlements(
1063
+ toSubscriber(subscriberOrTenantId),
1064
+ options
1065
+ );
1066
+ return resolution.featureKeys.includes(featureKey);
1067
+ }
1068
+ async assertWithinThresholds(subscriberOrTenantId, options = {}) {
1069
+ const subscriber = toSubscriber(subscriberOrTenantId);
1070
+ const resolution = await this.resolveEntitlements(subscriber, options);
1071
+ const blocked = resolution.thresholdEvaluations.find(
1072
+ (evaluation) => !evaluation.allowed
1073
+ );
1074
+ if (blocked) {
1075
+ const subject = subscriber.kind === "external" ? `external:${subscriber.externalId}` : `Tenant ${subscriber.tenantId}`;
1076
+ throw new Error(
1077
+ `${subject} exceeded subscription threshold ${blocked.threshold.metricKey}`
1078
+ );
1079
+ }
1080
+ }
1081
+ async resolveSubscription(subscriber, now, context) {
1082
+ if (hasContextValue(context, "subscription")) {
1083
+ const subscription = context?.subscription ?? null;
1084
+ assertSubscriptionMatchesSubscriber(subscription, subscriber);
1085
+ return subscription;
1086
+ }
1087
+ return this.findCurrentSubscription(subscriber, now);
1088
+ }
1089
+ async resolvePlan(subscription, context) {
1090
+ if (!subscription?.planId) {
1091
+ return null;
1092
+ }
1093
+ if (hasContextValue(context, "plan")) {
1094
+ const plan = context?.plan ?? null;
1095
+ assertPlanMatchesSubscription(plan, subscription);
1096
+ return plan;
1097
+ }
1098
+ return this.readers.plans.get({ id: subscription.planId });
1099
+ }
1100
+ /**
1101
+ * Bridge the legacy and polymorphic reader contracts.
1102
+ *
1103
+ * Prefers `findCurrentForSubscriber` when the reader provides it (the
1104
+ * preferred shape). For `'tenant'` subscribers we fall back to the legacy
1105
+ * `findCurrentForTenant`. For `'external'` subscribers the reader MUST
1106
+ * implement `findCurrentForSubscriber` — otherwise we have no way to scope
1107
+ * the lookup and we throw rather than silently returning the tenant's
1108
+ * primary subscription.
1109
+ */
1110
+ async findCurrentSubscription(subscriber, now) {
1111
+ if (this.readers.subscriptions.findCurrentForSubscriber) {
1112
+ return this.readers.subscriptions.findCurrentForSubscriber(
1113
+ subscriber,
1114
+ now
1115
+ );
1116
+ }
1117
+ if (subscriber.kind === "tenant") {
1118
+ return this.readers.subscriptions.findCurrentForTenant(
1119
+ subscriber.tenantId,
1120
+ now
1121
+ );
1122
+ }
1123
+ throw new Error(
1124
+ "External-subscriber resolution requires a TenantSubscriptionReader that implements findCurrentForSubscriber()"
1125
+ );
1126
+ }
1127
+ async resolveThresholdEvaluations(subscriber, thresholds, now, options) {
1128
+ if (thresholds.length === 0) {
1129
+ return [];
1130
+ }
1131
+ if (!this.readers.usage.summarizeBatch) {
1132
+ const evaluations2 = [];
1133
+ for (const threshold of thresholds) {
1134
+ const window = options.usageWindows?.[threshold.window] ?? getWindowForThreshold(threshold.window, now);
1135
+ const usage = await this.readers.usage.summarize({
1136
+ tenantId: subscriber.tenantId,
1137
+ subscriberKind: subscriber.kind,
1138
+ subscriberExternalId: subscriber.kind === "external" ? subscriber.externalId : void 0,
1139
+ metricKey: threshold.metricKey,
1140
+ window
1141
+ });
1142
+ evaluations2.push(evaluateThreshold(threshold, usage));
1143
+ }
1144
+ return evaluations2;
1145
+ }
1146
+ const groups = /* @__PURE__ */ new Map();
1147
+ thresholds.forEach((threshold, index) => {
1148
+ const window = options.usageWindows?.[threshold.window] ?? getWindowForThreshold(threshold.window, now);
1149
+ const key = getWindowKey(window);
1150
+ const group = groups.get(key) ?? { window, entries: [] };
1151
+ group.entries.push({ index, threshold });
1152
+ groups.set(key, group);
1153
+ });
1154
+ const evaluations = new Array(thresholds.length);
1155
+ await Promise.all(
1156
+ Array.from(groups.values()).map(async ({ window, entries }) => {
1157
+ const metricKeys = uniqueMetricKeys(
1158
+ entries.map((entry) => entry.threshold.metricKey)
1159
+ );
1160
+ const summaries = await this.readers.usage.summarizeBatch?.({
1161
+ tenantId: subscriber.tenantId,
1162
+ subscriberKind: subscriber.kind,
1163
+ subscriberExternalId: subscriber.kind === "external" ? subscriber.externalId : void 0,
1164
+ metricKeys,
1165
+ window
1166
+ });
1167
+ const summaryByMetric = new Map(
1168
+ (summaries ?? []).map((summary) => [summary.metricKey, summary])
1169
+ );
1170
+ for (const { index, threshold } of entries) {
1171
+ const usage = summaryByMetric.get(threshold.metricKey) ?? emptyUsageSummary(subscriber, threshold.metricKey, window);
1172
+ evaluations[index] = evaluateThreshold(threshold, usage);
1173
+ }
1174
+ })
1175
+ );
1176
+ return evaluations;
1177
+ }
1178
+ }
1179
+ function toSubscriber(input) {
1180
+ if (typeof input === "string") {
1181
+ return { kind: "tenant", tenantId: input };
1182
+ }
1183
+ return input;
1184
+ }
1185
+ function emptyResolution(subscriber) {
1186
+ return {
1187
+ tenantId: subscriber.tenantId,
1188
+ subscriber,
1189
+ planId: null,
1190
+ planKey: null,
1191
+ subscriptionId: null,
1192
+ status: "none",
1193
+ featureKeys: [],
1194
+ thresholds: [],
1195
+ thresholdEvaluations: [],
1196
+ allowed: false
1197
+ };
1198
+ }
1199
+ function hasContextValue(context, key) {
1200
+ return context?.[key] !== void 0;
1201
+ }
1202
+ function assertSubscriptionMatchesSubscriber(subscription, subscriber) {
1203
+ if (!subscription) {
1204
+ return;
1205
+ }
1206
+ const subscriptionSubscriber = subscription.getSubscriber();
1207
+ if (!subscriptionSubscriber || !sameSubscriber(subscriptionSubscriber, subscriber)) {
1208
+ throw new Error(
1209
+ "Provided entitlement context subscription does not match requested subscriber"
1210
+ );
1211
+ }
1212
+ }
1213
+ function assertPlanMatchesSubscription(plan, subscription) {
1214
+ if (!plan) {
1215
+ return;
1216
+ }
1217
+ if (!plan.id || plan.id !== subscription.planId) {
1218
+ throw new Error(
1219
+ "Provided entitlement context plan does not match subscription.planId"
1220
+ );
1221
+ }
1222
+ }
1223
+ function sameSubscriber(left, right) {
1224
+ if (left.kind !== right.kind || left.tenantId !== right.tenantId) {
1225
+ return false;
1226
+ }
1227
+ if (left.kind === "tenant") {
1228
+ return true;
1229
+ }
1230
+ return right.kind === "external" && left.externalId === right.externalId;
1231
+ }
1232
+ function uniqueMetricKeys(metricKeys) {
1233
+ return Array.from(new Set(metricKeys));
1234
+ }
1235
+ function emptyUsageSummary(subscriber, metricKey, window) {
1236
+ const summary = {
1237
+ tenantId: subscriber.tenantId,
1238
+ metricKey,
1239
+ quantity: 0,
1240
+ windowStart: window.start,
1241
+ windowEnd: window.end
1242
+ };
1243
+ if (subscriber.kind === "external") {
1244
+ return {
1245
+ ...summary,
1246
+ subscriberKind: "external",
1247
+ subscriberExternalId: subscriber.externalId
1248
+ };
1249
+ }
1250
+ return summary;
1251
+ }
1252
+ export {
1253
+ SubscriptionPlan,
1254
+ SubscriptionPlanCollection,
1255
+ SubscriptionResolver,
1256
+ TenantSubscription,
1257
+ TenantSubscriptionCollection,
1258
+ TenantUsageMeter,
1259
+ TenantUsageMetric,
1260
+ TenantUsageMetricCollection,
1261
+ assertSubscriberInvariant,
1262
+ evaluateThreshold,
1263
+ evaluateThresholds,
1264
+ getWindowForThreshold,
1265
+ getWindowKey,
1266
+ isValidThreshold,
1267
+ normalizeFeatureGrants,
1268
+ normalizeSubscriber,
1269
+ subscriberToColumns
1270
+ };
1271
+ //# sourceMappingURL=index.js.map