@happyvertical/smrt-subscriptions 0.37.2 → 0.37.4
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 +1024 -1177
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +2 -2
- package/dist/services/usage-meter.d.ts +1 -0
- package/dist/services/usage-meter.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +10 -10
- package/dist/types.js +0 -2
- package/package.json +13 -13
- package/dist/types.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,710 +1,618 @@
|
|
|
1
|
-
import { ObjectRegistry,
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
var THRESHOLD_ENFORCEMENTS = [
|
|
15
|
+
"observe",
|
|
16
|
+
"warn",
|
|
17
|
+
"block"
|
|
17
18
|
];
|
|
18
19
|
function parseJsonArray(value, fallback = []) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
38
|
+
return JSON.stringify(value ?? null);
|
|
42
39
|
}
|
|
43
40
|
function normalizeFeatureGrants(grants) {
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
103
|
+
return `${window.start.toISOString()}..${window.end.toISOString()}`;
|
|
143
104
|
}
|
|
144
105
|
function isValidThreshold(threshold) {
|
|
145
|
-
|
|
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
|
-
|
|
109
|
+
return typeof value === "string" && THRESHOLD_WINDOWS.includes(value);
|
|
149
110
|
}
|
|
150
111
|
function isThresholdEnforcement(value) {
|
|
151
|
-
|
|
112
|
+
return typeof value === "string" && THRESHOLD_ENFORCEMENTS.includes(value);
|
|
152
113
|
}
|
|
153
114
|
function utcWindow(now, unit) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
], SubscriptionPlan);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
788
|
-
return rows[0] ?? {};
|
|
683
|
+
return resultRows(result)[0] ?? {};
|
|
789
684
|
}
|
|
790
685
|
function resultRows(result) {
|
|
791
|
-
|
|
686
|
+
return Array.isArray(result) ? result : result?.rows ?? [];
|
|
792
687
|
}
|
|
793
688
|
function numberFromRow(row, key) {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1060
|
+
if (typeof input === "string") return {
|
|
1061
|
+
kind: "tenant",
|
|
1062
|
+
tenantId: input
|
|
1063
|
+
};
|
|
1064
|
+
return input;
|
|
1184
1065
|
}
|
|
1185
1066
|
function emptyResolution(subscriber) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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
|
-
|
|
1081
|
+
return context?.[key] !== void 0;
|
|
1201
1082
|
}
|
|
1202
1083
|
function assertSubscriptionMatchesSubscriber(subscription, subscriber) {
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
1098
|
+
return Array.from(new Set(metricKeys));
|
|
1234
1099
|
}
|
|
1235
1100
|
function emptyUsageSummary(subscriber, metricKey, window) {
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
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
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|