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