@vertz/server 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +962 -191
  2. package/dist/index.js +3273 -422
  3. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
2
  import {
3
- BadRequestException,
3
+ BadRequestException as BadRequestException2,
4
4
  ConflictException,
5
5
  createEnv,
6
6
  createImmutableProxy,
@@ -20,6 +20,8 @@ import {
20
20
  // src/auth/index.ts
21
21
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
22
  import { join } from "node:path";
23
+ import { BadRequestException } from "@vertz/core";
24
+ import { parseBody } from "@vertz/core/internals";
23
25
  import {
24
26
  createAuthRateLimitedError,
25
27
  createAuthValidationError as createAuthValidationError2,
@@ -40,7 +42,13 @@ import {
40
42
  // src/auth/billing-period.ts
41
43
  function calculateBillingPeriod(startedAt, per, now = new Date) {
42
44
  if (per === "month") {
43
- return calculateMonthlyPeriod(startedAt, now);
45
+ return calculateMultiMonthPeriod(startedAt, now, 1);
46
+ }
47
+ if (per === "quarter") {
48
+ return calculateMultiMonthPeriod(startedAt, now, 3);
49
+ }
50
+ if (per === "year") {
51
+ return calculateMultiMonthPeriod(startedAt, now, 12);
44
52
  }
45
53
  const durationMs = per === "day" ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000;
46
54
  const elapsed = now.getTime() - startedAt.getTime();
@@ -49,19 +57,21 @@ function calculateBillingPeriod(startedAt, per, now = new Date) {
49
57
  const periodEnd = new Date(periodStart.getTime() + durationMs);
50
58
  return { periodStart, periodEnd };
51
59
  }
52
- function calculateMonthlyPeriod(startedAt, now) {
60
+ function calculateMultiMonthPeriod(startedAt, now, monthInterval) {
53
61
  if (now.getTime() < startedAt.getTime()) {
54
- return { periodStart: new Date(startedAt), periodEnd: addMonths(startedAt, 1) };
62
+ return { periodStart: new Date(startedAt), periodEnd: addMonths(startedAt, monthInterval) };
55
63
  }
56
- let periodStart = new Date(startedAt);
57
64
  const approxMonths = (now.getUTCFullYear() - startedAt.getUTCFullYear()) * 12 + (now.getUTCMonth() - startedAt.getUTCMonth());
58
- if (approxMonths > 0) {
59
- periodStart = addMonths(startedAt, approxMonths);
60
- if (periodStart.getTime() > now.getTime()) {
61
- periodStart = addMonths(startedAt, approxMonths - 1);
62
- }
65
+ const periodsElapsed = Math.floor(approxMonths / monthInterval);
66
+ let periodStart = addMonths(startedAt, periodsElapsed * monthInterval);
67
+ if (periodStart.getTime() > now.getTime()) {
68
+ periodStart = addMonths(startedAt, (periodsElapsed - 1) * monthInterval);
69
+ }
70
+ const nextStart = addMonths(startedAt, (periodsElapsed + 1) * monthInterval);
71
+ if (nextStart.getTime() <= now.getTime()) {
72
+ periodStart = nextStart;
63
73
  }
64
- const periodEnd = addMonths(startedAt, getMonthOffset(startedAt, periodStart) + 1);
74
+ const periodEnd = addMonths(startedAt, getMonthOffset(startedAt, periodStart) + monthInterval);
65
75
  return { periodStart, periodEnd };
66
76
  }
67
77
  function addMonths(base, months) {
@@ -78,45 +88,95 @@ function getMonthOffset(base, target) {
78
88
  return (target.getUTCFullYear() - base.getUTCFullYear()) * 12 + (target.getUTCMonth() - base.getUTCMonth());
79
89
  }
80
90
 
81
- // src/auth/plan-store.ts
82
- function resolveEffectivePlan(orgPlan, plans, defaultPlan = "free", now) {
83
- if (!orgPlan)
91
+ // src/auth/resolve-inherited-role.ts
92
+ function resolveInheritedRole(sourceType, sourceRole, targetType, accessDef) {
93
+ const hierarchy = accessDef.hierarchy;
94
+ const sourceIdx = hierarchy.indexOf(sourceType);
95
+ const targetIdx = hierarchy.indexOf(targetType);
96
+ if (sourceIdx === -1 || targetIdx === -1 || sourceIdx >= targetIdx)
97
+ return null;
98
+ let currentRole = sourceRole;
99
+ for (let i = sourceIdx;i < targetIdx; i++) {
100
+ const currentType = hierarchy[i];
101
+ const inheritanceMap = accessDef.inheritance[currentType];
102
+ if (!inheritanceMap || !(currentRole in inheritanceMap))
103
+ return null;
104
+ currentRole = inheritanceMap[currentRole];
105
+ }
106
+ return currentRole;
107
+ }
108
+
109
+ // src/auth/subscription-store.ts
110
+ function resolveEffectivePlan(subscription, plans, defaultPlan = "free", now) {
111
+ if (!subscription)
84
112
  return null;
85
- if (orgPlan.expiresAt && orgPlan.expiresAt.getTime() < (now ?? Date.now())) {
113
+ if (subscription.expiresAt && subscription.expiresAt.getTime() < (now ?? Date.now())) {
86
114
  return plans?.[defaultPlan] ? defaultPlan : null;
87
115
  }
88
- if (!plans?.[orgPlan.planId])
116
+ if (!plans?.[subscription.planId])
89
117
  return null;
90
- return orgPlan.planId;
118
+ return subscription.planId;
91
119
  }
92
120
 
93
- class InMemoryPlanStore {
94
- plans = new Map;
95
- async assignPlan(orgId, planId, startedAt = new Date, expiresAt = null) {
96
- this.plans.set(orgId, {
97
- orgId,
121
+ class InMemorySubscriptionStore {
122
+ subscriptions = new Map;
123
+ addOns = new Map;
124
+ async assign(tenantId, planId, startedAt = new Date, expiresAt = null) {
125
+ this.subscriptions.set(tenantId, {
126
+ tenantId,
98
127
  planId,
99
128
  startedAt,
100
129
  expiresAt,
101
130
  overrides: {}
102
131
  });
103
132
  }
104
- async getPlan(orgId) {
105
- return this.plans.get(orgId) ?? null;
133
+ async get(tenantId) {
134
+ return this.subscriptions.get(tenantId) ?? null;
106
135
  }
107
- async updateOverrides(orgId, overrides) {
108
- const plan = this.plans.get(orgId);
109
- if (!plan)
136
+ async updateOverrides(tenantId, overrides) {
137
+ const sub = this.subscriptions.get(tenantId);
138
+ if (!sub)
110
139
  return;
111
- plan.overrides = { ...plan.overrides, ...overrides };
140
+ sub.overrides = { ...sub.overrides, ...overrides };
141
+ }
142
+ async remove(tenantId) {
143
+ this.subscriptions.delete(tenantId);
144
+ }
145
+ async attachAddOn(tenantId, addOnId) {
146
+ if (!this.addOns.has(tenantId)) {
147
+ this.addOns.set(tenantId, new Set);
148
+ }
149
+ this.addOns.get(tenantId).add(addOnId);
150
+ }
151
+ async detachAddOn(tenantId, addOnId) {
152
+ this.addOns.get(tenantId)?.delete(addOnId);
112
153
  }
113
- async removePlan(orgId) {
114
- this.plans.delete(orgId);
154
+ async getAddOns(tenantId) {
155
+ return [...this.addOns.get(tenantId) ?? []];
156
+ }
157
+ async listByPlan(planId) {
158
+ const result = [];
159
+ for (const [tenantId, sub] of this.subscriptions.entries()) {
160
+ if (sub.planId === planId) {
161
+ result.push(tenantId);
162
+ }
163
+ }
164
+ return result;
115
165
  }
116
166
  dispose() {
117
- this.plans.clear();
167
+ this.subscriptions.clear();
168
+ this.addOns.clear();
118
169
  }
119
170
  }
171
+ function checkAddOnCompatibility(accessDef, addOnId, currentPlanId) {
172
+ const addOnDef = accessDef.plans?.[addOnId];
173
+ if (!addOnDef?.requires)
174
+ return true;
175
+ return addOnDef.requires.plans.includes(currentPlanId);
176
+ }
177
+ function getIncompatibleAddOns(accessDef, activeAddOnIds, targetPlanId) {
178
+ return activeAddOnIds.filter((id) => !checkAddOnCompatibility(accessDef, id, targetPlanId));
179
+ }
120
180
 
121
181
  // src/auth/access-set.ts
122
182
  async function computeAccessSet(config) {
@@ -125,14 +185,13 @@ async function computeAccessSet(config) {
125
185
  accessDef,
126
186
  roleStore,
127
187
  closureStore,
128
- plan,
129
188
  flagStore,
130
- planStore,
189
+ subscriptionStore,
131
190
  walletStore,
132
- orgId
191
+ tenantId
133
192
  } = config;
134
193
  const entitlements = {};
135
- let resolvedPlan = plan ?? null;
194
+ let resolvedPlan = null;
136
195
  if (!userId) {
137
196
  for (const name of Object.keys(accessDef.entitlements)) {
138
197
  entitlements[name] = {
@@ -144,7 +203,7 @@ async function computeAccessSet(config) {
144
203
  return {
145
204
  entitlements,
146
205
  flags: {},
147
- plan: plan ?? null,
206
+ plan: null,
148
207
  computedAt: new Date().toISOString()
149
208
  };
150
209
  }
@@ -186,14 +245,14 @@ async function computeAccessSet(config) {
186
245
  }
187
246
  }
188
247
  const resolvedFlags = {};
189
- if (flagStore && orgId) {
190
- const orgFlags = flagStore.getFlags(orgId);
248
+ if (flagStore && tenantId) {
249
+ const orgFlags = flagStore.getFlags(tenantId);
191
250
  Object.assign(resolvedFlags, orgFlags);
192
251
  for (const [name, entDef] of Object.entries(accessDef.entitlements)) {
193
252
  if (entDef.flags?.length) {
194
253
  const disabledFlags = [];
195
254
  for (const flag of entDef.flags) {
196
- if (!flagStore.getFlag(orgId, flag)) {
255
+ if (!flagStore.getFlag(tenantId, flag)) {
197
256
  disabledFlags.push(flag);
198
257
  }
199
258
  }
@@ -213,58 +272,105 @@ async function computeAccessSet(config) {
213
272
  }
214
273
  }
215
274
  }
216
- if (planStore && orgId) {
217
- const orgPlan = await planStore.getPlan(orgId);
218
- if (orgPlan) {
219
- const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
275
+ if (subscriptionStore && tenantId) {
276
+ const subscription = await subscriptionStore.get(tenantId);
277
+ if (subscription) {
278
+ const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
220
279
  resolvedPlan = effectivePlanId;
221
280
  if (effectivePlanId) {
222
281
  const planDef = accessDef.plans?.[effectivePlanId];
223
282
  if (planDef) {
224
- for (const [name, entDef] of Object.entries(accessDef.entitlements)) {
225
- if (entDef.plans?.length && !planDef.entitlements.includes(name)) {
283
+ const effectiveFeatures = new Set(planDef.features ?? []);
284
+ const addOns = await subscriptionStore.getAddOns?.(tenantId);
285
+ if (addOns) {
286
+ for (const addOnId of addOns) {
287
+ const addOnDef = accessDef.plans?.[addOnId];
288
+ if (addOnDef?.features) {
289
+ for (const f of addOnDef.features)
290
+ effectiveFeatures.add(f);
291
+ }
292
+ }
293
+ }
294
+ for (const name of Object.keys(accessDef.entitlements)) {
295
+ if (accessDef._planGatedEntitlements.has(name) && !effectiveFeatures.has(name)) {
226
296
  const entry = entitlements[name];
227
297
  const reasons = [...entry.reasons];
228
298
  if (!reasons.includes("plan_required"))
229
299
  reasons.push("plan_required");
300
+ const requiredPlans = [];
301
+ for (const [pName, pDef] of Object.entries(accessDef.plans ?? {})) {
302
+ if (pDef.features?.includes(name))
303
+ requiredPlans.push(pName);
304
+ }
230
305
  entitlements[name] = {
231
306
  ...entry,
232
307
  allowed: false,
233
308
  reasons,
234
309
  reason: reasons[0],
235
- meta: { ...entry.meta, requiredPlans: [...entDef.plans] }
310
+ meta: { ...entry.meta, requiredPlans }
236
311
  };
237
312
  }
238
- if (walletStore && planDef.limits?.[name]) {
239
- const limitDef = planDef.limits[name];
240
- const override = orgPlan.overrides[name];
241
- const effectiveLimit = override ? Math.max(override.max, limitDef.max) : limitDef.max;
242
- const { periodStart, periodEnd } = calculateBillingPeriod(orgPlan.startedAt, limitDef.per);
243
- const consumed = await walletStore.getConsumption(orgId, name, periodStart, periodEnd);
244
- const remaining = Math.max(0, effectiveLimit - consumed);
245
- const entry = entitlements[name];
246
- if (consumed >= effectiveLimit) {
247
- const reasons = [...entry.reasons];
248
- if (!reasons.includes("limit_reached"))
249
- reasons.push("limit_reached");
250
- entitlements[name] = {
251
- ...entry,
252
- allowed: false,
253
- reasons,
254
- reason: reasons[0],
255
- meta: {
256
- ...entry.meta,
257
- limit: { max: effectiveLimit, consumed, remaining }
313
+ const limitKeys = accessDef._entitlementToLimitKeys[name];
314
+ if (walletStore && limitKeys?.length) {
315
+ const limitKey = limitKeys[0];
316
+ const limitDef = planDef.limits?.[limitKey];
317
+ if (limitDef) {
318
+ let effectiveMax = limitDef.max;
319
+ if (addOns) {
320
+ for (const addOnId of addOns) {
321
+ const addOnDef = accessDef.plans?.[addOnId];
322
+ const addOnLimit = addOnDef?.limits?.[limitKey];
323
+ if (addOnLimit) {
324
+ if (effectiveMax === -1)
325
+ break;
326
+ effectiveMax += addOnLimit.max;
327
+ }
258
328
  }
259
- };
260
- } else {
261
- entitlements[name] = {
262
- ...entry,
263
- meta: {
264
- ...entry.meta,
265
- limit: { max: effectiveLimit, consumed, remaining }
329
+ }
330
+ const override = subscription.overrides[limitKey];
331
+ if (override)
332
+ effectiveMax = Math.max(effectiveMax, override.max);
333
+ if (effectiveMax === -1) {
334
+ const entry = entitlements[name];
335
+ entitlements[name] = {
336
+ ...entry,
337
+ meta: {
338
+ ...entry.meta,
339
+ limit: { key: limitKey, max: -1, consumed: 0, remaining: -1 }
340
+ }
341
+ };
342
+ } else {
343
+ const period = limitDef.per ? calculateBillingPeriod(subscription.startedAt, limitDef.per) : {
344
+ periodStart: subscription.startedAt,
345
+ periodEnd: new Date("9999-12-31T23:59:59Z")
346
+ };
347
+ const consumed = await walletStore.getConsumption(tenantId, limitKey, period.periodStart, period.periodEnd);
348
+ const remaining = Math.max(0, effectiveMax - consumed);
349
+ const entry = entitlements[name];
350
+ if (consumed >= effectiveMax) {
351
+ const reasons = [...entry.reasons];
352
+ if (!reasons.includes("limit_reached"))
353
+ reasons.push("limit_reached");
354
+ entitlements[name] = {
355
+ ...entry,
356
+ allowed: false,
357
+ reasons,
358
+ reason: reasons[0],
359
+ meta: {
360
+ ...entry.meta,
361
+ limit: { key: limitKey, max: effectiveMax, consumed, remaining }
362
+ }
363
+ };
364
+ } else {
365
+ entitlements[name] = {
366
+ ...entry,
367
+ meta: {
368
+ ...entry.meta,
369
+ limit: { key: limitKey, max: effectiveMax, consumed, remaining }
370
+ }
371
+ };
266
372
  }
267
- };
373
+ }
268
374
  }
269
375
  }
270
376
  }
@@ -342,22 +448,6 @@ function addRole(map, resourceType, role) {
342
448
  }
343
449
  roles.add(role);
344
450
  }
345
- function resolveInheritedRole(sourceType, sourceRole, targetType, accessDef) {
346
- const hierarchy = accessDef.hierarchy;
347
- const sourceIdx = hierarchy.indexOf(sourceType);
348
- const targetIdx = hierarchy.indexOf(targetType);
349
- if (sourceIdx === -1 || targetIdx === -1 || sourceIdx >= targetIdx)
350
- return null;
351
- let currentRole = sourceRole;
352
- for (let i = sourceIdx;i < targetIdx; i++) {
353
- const currentType = hierarchy[i];
354
- const inheritanceMap = accessDef.inheritance[currentType];
355
- if (!inheritanceMap || !(currentRole in inheritanceMap))
356
- return null;
357
- currentRole = inheritanceMap[currentRole];
358
- }
359
- return currentRole;
360
- }
361
451
 
362
452
  // src/auth/cookies.ts
363
453
  var DEFAULT_COOKIE_CONFIG = {
@@ -703,7 +793,7 @@ var DEFAULT_PASSWORD_REQUIREMENTS = {
703
793
  requireNumbers: false,
704
794
  requireSymbols: false
705
795
  };
706
- var BCRYPT_ROUNDS = 12;
796
+ var BCRYPT_ROUNDS = process.env.VERTZ_TEST === "1" ? 4 : 12;
707
797
  async function hashPassword(password) {
708
798
  return bcrypt.hash(password, BCRYPT_ROUNDS);
709
799
  }
@@ -806,6 +896,60 @@ class InMemoryRateLimitStore {
806
896
  }
807
897
  }
808
898
 
899
+ // src/auth/resolve-session-for-ssr.ts
900
+ function extractAccessSet(acl) {
901
+ if (acl.overflow)
902
+ return null;
903
+ if (!acl.set)
904
+ return null;
905
+ return {
906
+ entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => {
907
+ const data = {
908
+ allowed: check.allowed,
909
+ reasons: check.reasons ?? [],
910
+ ...check.reason ? { reason: check.reason } : {},
911
+ ...check.meta ? { meta: check.meta } : {}
912
+ };
913
+ return [name, data];
914
+ })),
915
+ flags: acl.set.flags,
916
+ plan: acl.set.plan,
917
+ computedAt: acl.set.computedAt
918
+ };
919
+ }
920
+ function resolveSessionForSSR(config) {
921
+ const { jwtSecret, jwtAlgorithm, cookieName } = config;
922
+ return async (request) => {
923
+ const cookieHeader = request.headers.get("cookie");
924
+ if (!cookieHeader)
925
+ return null;
926
+ const cookieEntry = cookieHeader.split(";").find((c) => c.trim().startsWith(`${cookieName}=`));
927
+ if (!cookieEntry)
928
+ return null;
929
+ const token = cookieEntry.trim().slice(`${cookieName}=`.length);
930
+ if (!token)
931
+ return null;
932
+ const payload = await verifyJWT(token, jwtSecret, jwtAlgorithm);
933
+ if (!payload)
934
+ return null;
935
+ const user = {
936
+ id: payload.sub,
937
+ email: payload.email,
938
+ role: payload.role
939
+ };
940
+ if (payload.tenantId) {
941
+ user.tenantId = payload.tenantId;
942
+ }
943
+ const session = {
944
+ user,
945
+ expiresAt: payload.exp * 1000
946
+ };
947
+ const acl = payload.acl;
948
+ const accessSet = acl !== undefined ? extractAccessSet(acl) : undefined;
949
+ return { session, accessSet };
950
+ };
951
+ }
952
+
809
953
  // src/auth/session-store.ts
810
954
  var DEFAULT_MAX_SESSIONS_PER_USER = 50;
811
955
  var CLEANUP_INTERVAL_MS2 = 60000;
@@ -880,6 +1024,13 @@ class InMemorySessionStore {
880
1024
  }
881
1025
  return null;
882
1026
  }
1027
+ async findActiveSessionById(id) {
1028
+ const session = this.sessions.get(id);
1029
+ if (!session || session.revokedAt || session.expiresAt <= new Date) {
1030
+ return null;
1031
+ }
1032
+ return session;
1033
+ }
883
1034
  async findByPreviousRefreshHash(hash) {
884
1035
  for (const session of this.sessions.values()) {
885
1036
  if (session.previousRefreshHash !== null && timingSafeEqual(session.previousRefreshHash, hash) && !session.revokedAt && session.expiresAt > new Date) {
@@ -1050,6 +1201,40 @@ function timingSafeEqual2(a, b) {
1050
1201
  return equal && a.length === b.length;
1051
1202
  }
1052
1203
 
1204
+ // src/auth/types.ts
1205
+ import {
1206
+ s
1207
+ } from "@vertz/schema";
1208
+ var authEmailFieldSchema = s.string().min(1).trim();
1209
+ var authPasswordFieldSchema = s.string().min(1);
1210
+ var signUpInputSchema = s.object({
1211
+ email: authEmailFieldSchema,
1212
+ password: authPasswordFieldSchema
1213
+ }).passthrough();
1214
+ var signInInputSchema = s.object({
1215
+ email: authEmailFieldSchema,
1216
+ password: authPasswordFieldSchema
1217
+ });
1218
+ var codeInputSchema = s.object({
1219
+ code: s.string().min(1)
1220
+ });
1221
+ var passwordInputSchema = s.object({
1222
+ password: s.string().min(1)
1223
+ });
1224
+ var tokenInputSchema = s.object({
1225
+ token: s.string().min(1).optional()
1226
+ });
1227
+ var forgotPasswordInputSchema = s.object({
1228
+ email: s.string().min(1).trim()
1229
+ });
1230
+ var resetPasswordInputSchema = s.object({
1231
+ token: s.string().min(1).optional(),
1232
+ password: s.string().min(1)
1233
+ });
1234
+ var switchTenantInputSchema = s.object({
1235
+ tenantId: s.string().min(1)
1236
+ });
1237
+
1053
1238
  // src/auth/user-store.ts
1054
1239
  class InMemoryUserStore {
1055
1240
  byEmail = new Map;
@@ -1079,6 +1264,13 @@ class InMemoryUserStore {
1079
1264
  user.emailVerified = verified;
1080
1265
  }
1081
1266
  }
1267
+ async deleteUser(id) {
1268
+ const user = this.byId.get(id);
1269
+ if (user) {
1270
+ this.byEmail.delete(user.email.toLowerCase());
1271
+ this.byId.delete(id);
1272
+ }
1273
+ }
1082
1274
  }
1083
1275
  // src/auth/access.ts
1084
1276
  class AuthorizationError extends Error {
@@ -1214,11 +1406,13 @@ function createAccessContext(config) {
1214
1406
  closureStore,
1215
1407
  roleStore,
1216
1408
  flagStore,
1217
- planStore,
1409
+ subscriptionStore,
1218
1410
  walletStore,
1219
- orgResolver
1411
+ overrideStore,
1412
+ orgResolver,
1413
+ planVersionStore
1220
1414
  } = config;
1221
- async function checkLayers1to4(entitlement, resource, resolvedOrgId) {
1415
+ async function checkLayers1to3(entitlement, resource, resolvedOrgId, overrides) {
1222
1416
  if (!userId)
1223
1417
  return false;
1224
1418
  const entDef = accessDef.entitlements[entitlement];
@@ -1241,29 +1435,44 @@ function createAccessContext(config) {
1241
1435
  } else if (entDef.roles.length > 0 && !resource) {
1242
1436
  return false;
1243
1437
  }
1244
- if (entDef.plans?.length && planStore && orgResolver) {
1438
+ if (accessDef._planGatedEntitlements.has(entitlement) && subscriptionStore && orgResolver) {
1245
1439
  if (!resolvedOrgId)
1246
1440
  return false;
1247
- const orgPlan = await planStore.getPlan(resolvedOrgId);
1248
- const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
1441
+ const subscription = await subscriptionStore.get(resolvedOrgId);
1442
+ const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
1249
1443
  if (!effectivePlanId)
1250
1444
  return false;
1251
- const planDef = accessDef.plans?.[effectivePlanId];
1252
- if (!planDef || !planDef.entitlements.includes(entitlement)) {
1445
+ const hasFeature = await resolveEffectiveFeatures(resolvedOrgId, entitlement, effectivePlanId, accessDef, subscriptionStore, planVersionStore, overrides);
1446
+ if (!hasFeature)
1253
1447
  return false;
1254
- }
1255
1448
  }
1256
1449
  return true;
1257
1450
  }
1258
1451
  async function can(entitlement, resource) {
1259
1452
  const resolvedOrgId = orgResolver ? await orgResolver(resource) : null;
1260
- if (!await checkLayers1to4(entitlement, resource, resolvedOrgId))
1453
+ const overrides = resolvedOrgId && overrideStore ? await overrideStore.get(resolvedOrgId) : null;
1454
+ if (!await checkLayers1to3(entitlement, resource, resolvedOrgId, overrides))
1261
1455
  return false;
1262
- const entDef = accessDef.entitlements[entitlement];
1263
- if (entDef?.plans?.length && walletStore && planStore && resolvedOrgId) {
1264
- const walletState = await resolveWalletStateFromOrgId(entitlement, resolvedOrgId, accessDef, planStore, walletStore);
1265
- if (walletState && walletState.consumed >= walletState.limit) {
1266
- return false;
1456
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1457
+ if (limitKeys?.length && walletStore && subscriptionStore && resolvedOrgId) {
1458
+ const walletStates = await resolveAllLimitStates(entitlement, resolvedOrgId, accessDef, subscriptionStore, walletStore, planVersionStore, overrides);
1459
+ for (const ws of walletStates) {
1460
+ if (ws.max === 0)
1461
+ return false;
1462
+ if (ws.max === -1)
1463
+ continue;
1464
+ if (ws.consumed >= ws.max) {
1465
+ if (ws.hasOverage) {
1466
+ if (ws.overageCap !== undefined) {
1467
+ const overageUnits = ws.consumed - ws.max;
1468
+ const overageCost = overageUnits * (ws.overageAmount ?? 0) / (ws.overagePer ?? 1);
1469
+ if (overageCost >= ws.overageCap)
1470
+ return false;
1471
+ }
1472
+ continue;
1473
+ }
1474
+ return false;
1475
+ }
1267
1476
  }
1268
1477
  }
1269
1478
  return true;
@@ -1287,6 +1496,7 @@ function createAccessContext(config) {
1287
1496
  };
1288
1497
  }
1289
1498
  const resolvedOrgId = orgResolver ? await orgResolver(resource) : null;
1499
+ const overrides = resolvedOrgId && overrideStore ? await overrideStore.get(resolvedOrgId) : null;
1290
1500
  if (entDef.flags?.length && flagStore && orgResolver) {
1291
1501
  if (resolvedOrgId) {
1292
1502
  const disabledFlags = [];
@@ -1315,37 +1525,85 @@ function createAccessContext(config) {
1315
1525
  meta.requiredRoles = [...entDef.roles];
1316
1526
  }
1317
1527
  }
1318
- if (entDef.plans?.length && planStore && orgResolver) {
1528
+ if (accessDef._planGatedEntitlements.has(entitlement) && subscriptionStore && orgResolver) {
1319
1529
  let planDenied = false;
1320
1530
  if (!resolvedOrgId) {
1321
1531
  planDenied = true;
1322
1532
  } else {
1323
- const orgPlan = await planStore.getPlan(resolvedOrgId);
1324
- const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
1533
+ const subscription = await subscriptionStore.get(resolvedOrgId);
1534
+ const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
1325
1535
  if (!effectivePlanId) {
1326
1536
  planDenied = true;
1327
1537
  } else {
1328
- const planDef = accessDef.plans?.[effectivePlanId];
1329
- if (!planDef || !planDef.entitlements.includes(entitlement)) {
1538
+ const hasFeature = await resolveEffectiveFeatures(resolvedOrgId, entitlement, effectivePlanId, accessDef, subscriptionStore, planVersionStore, overrides);
1539
+ if (!hasFeature) {
1330
1540
  planDenied = true;
1331
1541
  }
1332
1542
  }
1333
1543
  }
1334
1544
  if (planDenied) {
1335
1545
  reasons.push("plan_required");
1336
- meta.requiredPlans = [...entDef.plans];
1546
+ const plansWithFeature = [];
1547
+ if (accessDef.plans) {
1548
+ for (const [pName, pDef] of Object.entries(accessDef.plans)) {
1549
+ if (pDef.features?.includes(entitlement)) {
1550
+ plansWithFeature.push(pName);
1551
+ }
1552
+ }
1553
+ }
1554
+ meta.requiredPlans = plansWithFeature;
1337
1555
  }
1338
1556
  }
1339
- if (entDef.plans?.length && walletStore && planStore && resolvedOrgId) {
1340
- const walletState = await resolveWalletStateFromOrgId(entitlement, resolvedOrgId, accessDef, planStore, walletStore);
1341
- if (walletState) {
1342
- meta.limit = {
1343
- max: walletState.limit,
1344
- consumed: walletState.consumed,
1345
- remaining: Math.max(0, walletState.limit - walletState.consumed)
1346
- };
1347
- if (walletState.consumed >= walletState.limit) {
1557
+ const checkLimitKeys = accessDef._entitlementToLimitKeys[entitlement];
1558
+ if (checkLimitKeys?.length && walletStore && subscriptionStore && resolvedOrgId) {
1559
+ const walletStates = await resolveAllLimitStates(entitlement, resolvedOrgId, accessDef, subscriptionStore, walletStore, planVersionStore, overrides);
1560
+ for (const ws of walletStates) {
1561
+ const exceeded = ws.max === 0 || ws.max !== -1 && ws.consumed >= ws.max;
1562
+ if (exceeded) {
1563
+ if (ws.hasOverage && ws.max !== 0) {
1564
+ let capHit = false;
1565
+ if (ws.overageCap !== undefined) {
1566
+ const overageUnits = ws.consumed - ws.max;
1567
+ const overageCost = overageUnits * (ws.overageAmount ?? 0) / (ws.overagePer ?? 1);
1568
+ if (overageCost >= ws.overageCap)
1569
+ capHit = true;
1570
+ }
1571
+ if (capHit) {
1572
+ reasons.push("limit_reached");
1573
+ meta.limit = {
1574
+ key: ws.key,
1575
+ max: ws.max,
1576
+ consumed: ws.consumed,
1577
+ remaining: 0,
1578
+ overage: true
1579
+ };
1580
+ break;
1581
+ }
1582
+ meta.limit = {
1583
+ key: ws.key,
1584
+ max: ws.max,
1585
+ consumed: ws.consumed,
1586
+ remaining: 0,
1587
+ overage: true
1588
+ };
1589
+ continue;
1590
+ }
1348
1591
  reasons.push("limit_reached");
1592
+ meta.limit = {
1593
+ key: ws.key,
1594
+ max: ws.max,
1595
+ consumed: ws.consumed,
1596
+ remaining: Math.max(0, ws.max === -1 ? Infinity : ws.max - ws.consumed)
1597
+ };
1598
+ break;
1599
+ }
1600
+ if (!meta.limit) {
1601
+ meta.limit = {
1602
+ key: ws.key,
1603
+ max: ws.max,
1604
+ consumed: ws.consumed,
1605
+ remaining: ws.max === -1 ? Infinity : Math.max(0, ws.max - ws.consumed)
1606
+ };
1349
1607
  }
1350
1608
  }
1351
1609
  }
@@ -1375,57 +1633,96 @@ function createAccessContext(config) {
1375
1633
  }
1376
1634
  return results;
1377
1635
  }
1636
+ async function canBatch(entitlement, resources) {
1637
+ if (resources.length > MAX_BULK_CHECKS) {
1638
+ throw new Error(`canBatch() is limited to ${MAX_BULK_CHECKS} resources per call`);
1639
+ }
1640
+ const results = new Map;
1641
+ for (const resource of resources) {
1642
+ const allowed = await can(entitlement, resource);
1643
+ results.set(resource.id, allowed);
1644
+ }
1645
+ return results;
1646
+ }
1378
1647
  async function canAndConsume(entitlement, resource, amount = 1) {
1379
1648
  const resolvedOrgId = orgResolver ? await orgResolver(resource) : null;
1380
- if (!await checkLayers1to4(entitlement, resource, resolvedOrgId))
1649
+ const overrides = resolvedOrgId && overrideStore ? await overrideStore.get(resolvedOrgId) : null;
1650
+ if (!await checkLayers1to3(entitlement, resource, resolvedOrgId, overrides))
1381
1651
  return false;
1382
- if (!walletStore || !planStore || !orgResolver)
1652
+ if (!walletStore || !subscriptionStore || !orgResolver)
1383
1653
  return true;
1384
- const entDef = accessDef.entitlements[entitlement];
1385
- if (!entDef?.plans?.length)
1654
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1655
+ if (!limitKeys?.length)
1386
1656
  return true;
1387
1657
  if (!resolvedOrgId)
1388
1658
  return false;
1389
- const orgPlan = await planStore.getPlan(resolvedOrgId);
1390
- if (!orgPlan)
1659
+ const subscription = await subscriptionStore.get(resolvedOrgId);
1660
+ if (!subscription)
1391
1661
  return false;
1392
- const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
1662
+ const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
1393
1663
  if (!effectivePlanId)
1394
1664
  return false;
1395
- const planDef = accessDef.plans?.[effectivePlanId];
1396
- if (!planDef)
1397
- return false;
1398
- const limitDef = planDef.limits?.[entitlement];
1399
- if (!limitDef)
1665
+ const limitsToConsume = await resolveAllLimitConsumptions(resolvedOrgId, entitlement, effectivePlanId, accessDef, subscriptionStore, walletStore, subscription, planVersionStore, overrides);
1666
+ if (!limitsToConsume.length)
1400
1667
  return true;
1401
- const effectiveLimit = resolveEffectiveLimit(orgPlan, entitlement, limitDef);
1402
- const { periodStart, periodEnd } = calculateBillingPeriod(orgPlan.startedAt, limitDef.per);
1403
- const result = await walletStore.consume(resolvedOrgId, entitlement, periodStart, periodEnd, effectiveLimit, amount);
1404
- return result.success;
1668
+ const consumed = [];
1669
+ for (const lc of limitsToConsume) {
1670
+ if (lc.effectiveMax === -1)
1671
+ continue;
1672
+ if (lc.effectiveMax === 0) {
1673
+ await rollbackConsumptions(resolvedOrgId, consumed, walletStore, amount);
1674
+ return false;
1675
+ }
1676
+ let consumeMax = lc.effectiveMax;
1677
+ if (lc.hasOverage) {
1678
+ if (lc.overageCap !== undefined) {
1679
+ const currentConsumed = await walletStore.getConsumption(resolvedOrgId, lc.walletKey, lc.periodStart, lc.periodEnd);
1680
+ const overageUnits = Math.max(0, currentConsumed + amount - lc.effectiveMax);
1681
+ const overageCost = overageUnits * (lc.overageAmount ?? 0) / (lc.overagePer ?? 1);
1682
+ if (overageCost > lc.overageCap) {
1683
+ await rollbackConsumptions(resolvedOrgId, consumed, walletStore, amount);
1684
+ return false;
1685
+ }
1686
+ }
1687
+ consumeMax = Number.MAX_SAFE_INTEGER;
1688
+ }
1689
+ const result = await walletStore.consume(resolvedOrgId, lc.walletKey, lc.periodStart, lc.periodEnd, consumeMax, amount);
1690
+ if (!result.success) {
1691
+ await rollbackConsumptions(resolvedOrgId, consumed, walletStore, amount);
1692
+ return false;
1693
+ }
1694
+ consumed.push({
1695
+ key: lc.walletKey,
1696
+ periodStart: lc.periodStart,
1697
+ periodEnd: lc.periodEnd
1698
+ });
1699
+ }
1700
+ return true;
1405
1701
  }
1406
1702
  async function unconsume(entitlement, resource, amount = 1) {
1407
- if (!walletStore || !planStore || !orgResolver)
1703
+ if (!walletStore || !subscriptionStore || !orgResolver)
1408
1704
  return;
1409
- const entDef = accessDef.entitlements[entitlement];
1410
- if (!entDef?.plans?.length)
1705
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1706
+ if (!limitKeys?.length)
1411
1707
  return;
1412
1708
  const orgId = await orgResolver(resource);
1413
1709
  if (!orgId)
1414
1710
  return;
1415
- const orgPlan = await planStore.getPlan(orgId);
1416
- if (!orgPlan)
1711
+ const overrides = overrideStore ? await overrideStore.get(orgId) : null;
1712
+ const subscription = await subscriptionStore.get(orgId);
1713
+ if (!subscription)
1417
1714
  return;
1418
- const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
1715
+ const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
1419
1716
  if (!effectivePlanId)
1420
1717
  return;
1421
- const planDef = accessDef.plans?.[effectivePlanId];
1422
- const limitDef = planDef?.limits?.[entitlement];
1423
- if (!limitDef)
1424
- return;
1425
- const { periodStart, periodEnd } = calculateBillingPeriod(orgPlan.startedAt, limitDef.per);
1426
- await walletStore.unconsume(orgId, entitlement, periodStart, periodEnd, amount);
1718
+ const limitsToUnconsume = await resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId, accessDef, subscriptionStore, walletStore, subscription, planVersionStore, overrides);
1719
+ for (const lc of limitsToUnconsume) {
1720
+ if (lc.effectiveMax === -1)
1721
+ continue;
1722
+ await walletStore.unconsume(orgId, lc.walletKey, lc.periodStart, lc.periodEnd, amount);
1723
+ }
1427
1724
  }
1428
- return { can, check, authorize, canAll, canAndConsume, unconsume };
1725
+ return { can, check, authorize, canAll, canBatch, canAndConsume, unconsume };
1429
1726
  }
1430
1727
  var DENIAL_ORDER = [
1431
1728
  "plan_required",
@@ -1439,25 +1736,174 @@ var DENIAL_ORDER = [
1439
1736
  function orderDenialReasons(reasons) {
1440
1737
  return [...reasons].sort((a, b) => DENIAL_ORDER.indexOf(a) - DENIAL_ORDER.indexOf(b));
1441
1738
  }
1442
- async function resolveWalletStateFromOrgId(entitlement, orgId, accessDef, planStore, walletStore) {
1443
- const orgPlan = await planStore.getPlan(orgId);
1444
- const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
1445
- if (!effectivePlanId || !orgPlan)
1446
- return null;
1739
+ async function resolveEffectiveFeatures(orgId, entitlement, effectivePlanId, accessDef, subscriptionStore, planVersionStore, overrides) {
1740
+ if (planVersionStore) {
1741
+ const tenantVersion = await planVersionStore.getTenantVersion(orgId, effectivePlanId);
1742
+ if (tenantVersion !== null) {
1743
+ const versionInfo = await planVersionStore.getVersion(effectivePlanId, tenantVersion);
1744
+ if (versionInfo) {
1745
+ const snapshotFeatures = versionInfo.snapshot.features;
1746
+ if (snapshotFeatures.includes(entitlement))
1747
+ return true;
1748
+ const addOns2 = await subscriptionStore.getAddOns?.(orgId);
1749
+ if (addOns2) {
1750
+ for (const addOnId of addOns2) {
1751
+ const addOnDef = accessDef.plans?.[addOnId];
1752
+ if (addOnDef?.features?.includes(entitlement))
1753
+ return true;
1754
+ }
1755
+ }
1756
+ if (overrides?.features?.includes(entitlement))
1757
+ return true;
1758
+ return false;
1759
+ }
1760
+ }
1761
+ }
1447
1762
  const planDef = accessDef.plans?.[effectivePlanId];
1448
- const limitDef = planDef?.limits?.[entitlement];
1449
- if (!limitDef)
1450
- return null;
1451
- const effectiveLimit = resolveEffectiveLimit(orgPlan, entitlement, limitDef);
1452
- const { periodStart, periodEnd } = calculateBillingPeriod(orgPlan.startedAt, limitDef.per);
1453
- const consumed = await walletStore.getConsumption(orgId, entitlement, periodStart, periodEnd);
1454
- return { limit: effectiveLimit, consumed };
1763
+ if (planDef?.features?.includes(entitlement))
1764
+ return true;
1765
+ const addOns = await subscriptionStore.getAddOns?.(orgId);
1766
+ if (addOns) {
1767
+ for (const addOnId of addOns) {
1768
+ const addOnDef = accessDef.plans?.[addOnId];
1769
+ if (addOnDef?.features?.includes(entitlement))
1770
+ return true;
1771
+ }
1772
+ }
1773
+ if (overrides?.features?.includes(entitlement))
1774
+ return true;
1775
+ return false;
1776
+ }
1777
+ async function resolveAllLimitStates(entitlement, orgId, accessDef, subscriptionStore, walletStore, planVersionStore, overrides) {
1778
+ const subscription = await subscriptionStore.get(orgId);
1779
+ const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
1780
+ if (!effectivePlanId || !subscription)
1781
+ return [];
1782
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1783
+ if (!limitKeys?.length)
1784
+ return [];
1785
+ let versionedLimits = null;
1786
+ if (planVersionStore) {
1787
+ const tenantVersion = await planVersionStore.getTenantVersion(orgId, effectivePlanId);
1788
+ if (tenantVersion !== null) {
1789
+ const versionInfo = await planVersionStore.getVersion(effectivePlanId, tenantVersion);
1790
+ if (versionInfo) {
1791
+ versionedLimits = versionInfo.snapshot.limits;
1792
+ }
1793
+ }
1794
+ }
1795
+ const states = [];
1796
+ for (const limitKey of limitKeys) {
1797
+ let limitDef;
1798
+ if (versionedLimits && limitKey in versionedLimits) {
1799
+ limitDef = versionedLimits[limitKey];
1800
+ } else {
1801
+ const planDef = accessDef.plans?.[effectivePlanId];
1802
+ limitDef = planDef?.limits?.[limitKey];
1803
+ }
1804
+ if (!limitDef)
1805
+ continue;
1806
+ const effectiveMax = computeEffectiveLimit(limitDef.max, limitKey, orgId, subscription, accessDef, subscriptionStore, overrides);
1807
+ const resolvedMax = await effectiveMax;
1808
+ const hasOverage = !!limitDef.overage;
1809
+ if (resolvedMax === -1) {
1810
+ states.push({ key: limitKey, max: -1, consumed: 0, hasOverage });
1811
+ continue;
1812
+ }
1813
+ const walletKey = limitKey;
1814
+ const period = limitDef.per ? calculateBillingPeriod(subscription.startedAt, limitDef.per) : { periodStart: subscription.startedAt, periodEnd: new Date("9999-12-31T23:59:59Z") };
1815
+ const consumed = await walletStore.getConsumption(orgId, walletKey, period.periodStart, period.periodEnd);
1816
+ states.push({
1817
+ key: limitKey,
1818
+ max: resolvedMax,
1819
+ consumed,
1820
+ hasOverage,
1821
+ ...limitDef.overage ? {
1822
+ overageCap: limitDef.overage.cap,
1823
+ overageAmount: limitDef.overage.amount,
1824
+ overagePer: limitDef.overage.per
1825
+ } : {}
1826
+ });
1827
+ }
1828
+ return states;
1829
+ }
1830
+ async function resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId, accessDef, subscriptionStore, _walletStore, subscription, planVersionStore, overrides) {
1831
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1832
+ if (!limitKeys?.length)
1833
+ return [];
1834
+ let versionedLimits = null;
1835
+ if (planVersionStore) {
1836
+ const tenantVersion = await planVersionStore.getTenantVersion(orgId, effectivePlanId);
1837
+ if (tenantVersion !== null) {
1838
+ const versionInfo = await planVersionStore.getVersion(effectivePlanId, tenantVersion);
1839
+ if (versionInfo) {
1840
+ versionedLimits = versionInfo.snapshot.limits;
1841
+ }
1842
+ }
1843
+ }
1844
+ const consumptions = [];
1845
+ for (const limitKey of limitKeys) {
1846
+ let limitDef;
1847
+ if (versionedLimits && limitKey in versionedLimits) {
1848
+ limitDef = versionedLimits[limitKey];
1849
+ } else {
1850
+ const planDef = accessDef.plans?.[effectivePlanId];
1851
+ limitDef = planDef?.limits?.[limitKey];
1852
+ }
1853
+ if (!limitDef)
1854
+ continue;
1855
+ const effectiveMax = await computeEffectiveLimit(limitDef.max, limitKey, orgId, subscription, accessDef, subscriptionStore, overrides);
1856
+ const walletKey = limitKey;
1857
+ const period = limitDef.per ? calculateBillingPeriod(subscription.startedAt, limitDef.per) : { periodStart: subscription.startedAt, periodEnd: new Date("9999-12-31T23:59:59Z") };
1858
+ consumptions.push({
1859
+ walletKey,
1860
+ periodStart: period.periodStart,
1861
+ periodEnd: period.periodEnd,
1862
+ effectiveMax,
1863
+ hasOverage: !!limitDef.overage,
1864
+ ...limitDef.overage ? {
1865
+ overageCap: limitDef.overage.cap,
1866
+ overageAmount: limitDef.overage.amount,
1867
+ overagePer: limitDef.overage.per
1868
+ } : {}
1869
+ });
1870
+ }
1871
+ return consumptions;
1872
+ }
1873
+ async function computeEffectiveLimit(basePlanMax, limitKey, orgId, subscription, accessDef, subscriptionStore, overrides) {
1874
+ let effectiveMax = basePlanMax;
1875
+ const addOns = await subscriptionStore.getAddOns?.(orgId);
1876
+ if (addOns) {
1877
+ for (const addOnId of addOns) {
1878
+ const addOnDef = accessDef.plans?.[addOnId];
1879
+ const addOnLimit = addOnDef?.limits?.[limitKey];
1880
+ if (addOnLimit) {
1881
+ if (effectiveMax === -1)
1882
+ break;
1883
+ effectiveMax += addOnLimit.max;
1884
+ }
1885
+ }
1886
+ }
1887
+ const oldOverride = subscription.overrides[limitKey];
1888
+ if (oldOverride) {
1889
+ effectiveMax = Math.max(effectiveMax, oldOverride.max);
1890
+ }
1891
+ const limitOverride = overrides?.limits?.[limitKey];
1892
+ if (limitOverride) {
1893
+ if (limitOverride.max !== undefined) {
1894
+ effectiveMax = limitOverride.max;
1895
+ } else if (limitOverride.add !== undefined) {
1896
+ if (effectiveMax !== -1) {
1897
+ effectiveMax = Math.max(0, effectiveMax + limitOverride.add);
1898
+ }
1899
+ }
1900
+ }
1901
+ return effectiveMax;
1455
1902
  }
1456
- function resolveEffectiveLimit(orgPlan, entitlement, planLimit) {
1457
- const override = orgPlan.overrides[entitlement];
1458
- if (!override)
1459
- return planLimit.max;
1460
- return Math.max(override.max, planLimit.max);
1903
+ async function rollbackConsumptions(orgId, consumed, walletStore, amount) {
1904
+ for (const c of consumed) {
1905
+ await walletStore.unconsume(orgId, c.key, c.periodStart, c.periodEnd, amount);
1906
+ }
1461
1907
  }
1462
1908
  // src/auth/access-event-broadcaster.ts
1463
1909
  function createAccessEventBroadcaster(config) {
@@ -1605,6 +2051,22 @@ function createAccessEventBroadcaster(config) {
1605
2051
  const event = { type: "access:plan_changed", orgId };
1606
2052
  broadcastToOrg(orgId, JSON.stringify(event));
1607
2053
  }
2054
+ function broadcastPlanAssigned(orgId, planId) {
2055
+ const event = { type: "access:plan_assigned", orgId, planId };
2056
+ broadcastToOrg(orgId, JSON.stringify(event));
2057
+ }
2058
+ function broadcastAddonAttached(orgId, addonId) {
2059
+ const event = { type: "access:addon_attached", orgId, addonId };
2060
+ broadcastToOrg(orgId, JSON.stringify(event));
2061
+ }
2062
+ function broadcastAddonDetached(orgId, addonId) {
2063
+ const event = { type: "access:addon_detached", orgId, addonId };
2064
+ broadcastToOrg(orgId, JSON.stringify(event));
2065
+ }
2066
+ function broadcastLimitReset(orgId, entitlement, max) {
2067
+ const event = { type: "access:limit_reset", orgId, entitlement, max };
2068
+ broadcastToOrg(orgId, JSON.stringify(event));
2069
+ }
1608
2070
  return {
1609
2071
  handleUpgrade,
1610
2072
  websocket,
@@ -1612,148 +2074,1286 @@ function createAccessEventBroadcaster(config) {
1612
2074
  broadcastLimitUpdate,
1613
2075
  broadcastRoleChange,
1614
2076
  broadcastPlanChange,
2077
+ broadcastPlanAssigned,
2078
+ broadcastAddonAttached,
2079
+ broadcastAddonDetached,
2080
+ broadcastLimitReset,
1615
2081
  get getConnectionCount() {
1616
2082
  return connectionCount;
1617
2083
  }
1618
2084
  };
1619
2085
  }
1620
- // src/auth/closure-store.ts
1621
- var MAX_DEPTH = 4;
2086
+ // src/auth/auth-models.ts
2087
+ import { d } from "@vertz/db";
2088
+ var authUsersTable = d.table("auth_users", {
2089
+ id: d.text().primary(),
2090
+ email: d.text(),
2091
+ passwordHash: d.text().nullable(),
2092
+ role: d.text().default("user"),
2093
+ plan: d.text().nullable(),
2094
+ emailVerified: d.boolean().default(false),
2095
+ createdAt: d.timestamp().default("now"),
2096
+ updatedAt: d.timestamp().default("now")
2097
+ });
2098
+ var authSessionsTable = d.table("auth_sessions", {
2099
+ id: d.text().primary(),
2100
+ userId: d.text(),
2101
+ refreshTokenHash: d.text(),
2102
+ previousRefreshHash: d.text().nullable(),
2103
+ currentTokens: d.text().nullable(),
2104
+ ipAddress: d.text(),
2105
+ userAgent: d.text(),
2106
+ createdAt: d.timestamp().default("now"),
2107
+ lastActiveAt: d.timestamp().default("now"),
2108
+ expiresAt: d.timestamp(),
2109
+ revokedAt: d.timestamp().nullable()
2110
+ });
2111
+ var authOauthAccountsTable = d.table("auth_oauth_accounts", {
2112
+ id: d.text().primary(),
2113
+ userId: d.text(),
2114
+ provider: d.text(),
2115
+ providerId: d.text(),
2116
+ email: d.text().nullable(),
2117
+ createdAt: d.timestamp().default("now")
2118
+ });
2119
+ var authRoleAssignmentsTable = d.table("auth_role_assignments", {
2120
+ id: d.text().primary(),
2121
+ userId: d.text(),
2122
+ resourceType: d.text(),
2123
+ resourceId: d.text(),
2124
+ role: d.text(),
2125
+ createdAt: d.timestamp().default("now")
2126
+ });
2127
+ var authClosureTable = d.table("auth_closure", {
2128
+ id: d.text().primary(),
2129
+ ancestorType: d.text(),
2130
+ ancestorId: d.text(),
2131
+ descendantType: d.text(),
2132
+ descendantId: d.text(),
2133
+ depth: d.integer()
2134
+ });
2135
+ var authPlansTable = d.table("auth_plans", {
2136
+ id: d.text().primary(),
2137
+ tenantId: d.text(),
2138
+ planId: d.text(),
2139
+ startedAt: d.timestamp().default("now"),
2140
+ expiresAt: d.timestamp().nullable()
2141
+ });
2142
+ var authPlanAddonsTable = d.table("auth_plan_addons", {
2143
+ id: d.text().primary(),
2144
+ tenantId: d.text(),
2145
+ addonId: d.text(),
2146
+ isOneOff: d.boolean().default(false),
2147
+ quantity: d.integer().default(1),
2148
+ createdAt: d.timestamp().default("now")
2149
+ });
2150
+ var authFlagsTable = d.table("auth_flags", {
2151
+ id: d.text().primary(),
2152
+ tenantId: d.text(),
2153
+ flag: d.text(),
2154
+ enabled: d.boolean().default(false)
2155
+ });
2156
+ var authOverridesTable = d.table("auth_overrides", {
2157
+ id: d.text().primary(),
2158
+ tenantId: d.text(),
2159
+ overrides: d.text(),
2160
+ updatedAt: d.timestamp().default("now")
2161
+ });
2162
+ var authModels = {
2163
+ auth_users: d.model(authUsersTable),
2164
+ auth_sessions: d.model(authSessionsTable),
2165
+ auth_oauth_accounts: d.model(authOauthAccountsTable),
2166
+ auth_role_assignments: d.model(authRoleAssignmentsTable),
2167
+ auth_closure: d.model(authClosureTable),
2168
+ auth_plans: d.model(authPlansTable),
2169
+ auth_plan_addons: d.model(authPlanAddonsTable),
2170
+ auth_flags: d.model(authFlagsTable),
2171
+ auth_overrides: d.model(authOverridesTable)
2172
+ };
2173
+ // src/auth/auth-tables.ts
2174
+ import { sql } from "@vertz/db/sql";
1622
2175
 
1623
- class InMemoryClosureStore {
1624
- rows = [];
1625
- async addResource(type, id, parent) {
1626
- this.rows.push({
1627
- ancestorType: type,
1628
- ancestorId: id,
1629
- descendantType: type,
1630
- descendantId: id,
1631
- depth: 0
1632
- });
1633
- if (parent) {
1634
- const parentAncestors = await this.getAncestors(parent.parentType, parent.parentId);
1635
- const maxParentDepth = Math.max(...parentAncestors.map((a) => a.depth), 0);
1636
- if (maxParentDepth + 1 >= MAX_DEPTH) {
1637
- this.rows.pop();
1638
- throw new Error("Hierarchy depth exceeds maximum of 4 levels");
1639
- }
1640
- for (const ancestor of parentAncestors) {
1641
- this.rows.push({
1642
- ancestorType: ancestor.type,
1643
- ancestorId: ancestor.id,
1644
- descendantType: type,
1645
- descendantId: id,
1646
- depth: ancestor.depth + 1
1647
- });
1648
- }
1649
- }
2176
+ // src/auth/dialect-ddl.ts
2177
+ function dialectDDL(dialect) {
2178
+ return {
2179
+ boolean: (def) => dialect === "sqlite" ? `INTEGER NOT NULL DEFAULT ${def ? 1 : 0}` : `BOOLEAN NOT NULL DEFAULT ${def}`,
2180
+ timestamp: () => dialect === "sqlite" ? "TEXT NOT NULL" : "TIMESTAMPTZ NOT NULL",
2181
+ timestampNullable: () => dialect === "sqlite" ? "TEXT" : "TIMESTAMPTZ",
2182
+ text: () => "TEXT",
2183
+ textPrimary: () => "TEXT PRIMARY KEY",
2184
+ integer: () => "INTEGER"
2185
+ };
2186
+ }
2187
+
2188
+ // src/auth/auth-tables.ts
2189
+ function generateAuthDDL(dialect) {
2190
+ const t = dialectDDL(dialect);
2191
+ const statements = [];
2192
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_users (
2193
+ id ${t.textPrimary()},
2194
+ email ${t.text()} NOT NULL UNIQUE,
2195
+ password_hash ${t.text()},
2196
+ role ${t.text()} NOT NULL DEFAULT 'user',
2197
+ email_verified ${t.boolean(false)},
2198
+ created_at ${t.timestamp()},
2199
+ updated_at ${t.timestamp()}
2200
+ )`);
2201
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_sessions (
2202
+ id ${t.textPrimary()},
2203
+ user_id ${t.text()} NOT NULL,
2204
+ refresh_token_hash ${t.text()} NOT NULL,
2205
+ previous_refresh_hash ${t.text()},
2206
+ current_tokens ${t.text()},
2207
+ ip_address ${t.text()} NOT NULL,
2208
+ user_agent ${t.text()} NOT NULL,
2209
+ created_at ${t.timestamp()},
2210
+ last_active_at ${t.timestamp()},
2211
+ expires_at ${t.timestamp()},
2212
+ revoked_at ${t.timestampNullable()}
2213
+ )`);
2214
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_oauth_accounts (
2215
+ id ${t.textPrimary()},
2216
+ user_id ${t.text()} NOT NULL,
2217
+ provider ${t.text()} NOT NULL,
2218
+ provider_id ${t.text()} NOT NULL,
2219
+ email ${t.text()},
2220
+ created_at ${t.timestamp()},
2221
+ UNIQUE(provider, provider_id)
2222
+ )`);
2223
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_role_assignments (
2224
+ id ${t.textPrimary()},
2225
+ user_id ${t.text()} NOT NULL,
2226
+ resource_type ${t.text()} NOT NULL,
2227
+ resource_id ${t.text()} NOT NULL,
2228
+ role ${t.text()} NOT NULL,
2229
+ created_at ${t.timestamp()},
2230
+ UNIQUE(user_id, resource_type, resource_id, role)
2231
+ )`);
2232
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_closure (
2233
+ id ${t.textPrimary()},
2234
+ ancestor_type ${t.text()} NOT NULL,
2235
+ ancestor_id ${t.text()} NOT NULL,
2236
+ descendant_type ${t.text()} NOT NULL,
2237
+ descendant_id ${t.text()} NOT NULL,
2238
+ depth ${t.integer()} NOT NULL,
2239
+ UNIQUE(ancestor_type, ancestor_id, descendant_type, descendant_id)
2240
+ )`);
2241
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_plans (
2242
+ id ${t.textPrimary()},
2243
+ tenant_id ${t.text()} NOT NULL UNIQUE,
2244
+ plan_id ${t.text()} NOT NULL,
2245
+ started_at ${t.timestamp()},
2246
+ expires_at ${t.timestampNullable()}
2247
+ )`);
2248
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_plan_addons (
2249
+ id ${t.textPrimary()},
2250
+ tenant_id ${t.text()} NOT NULL,
2251
+ addon_id ${t.text()} NOT NULL,
2252
+ is_one_off ${t.boolean(false)},
2253
+ quantity ${t.integer()} NOT NULL DEFAULT 1,
2254
+ created_at ${t.timestamp()},
2255
+ UNIQUE(tenant_id, addon_id)
2256
+ )`);
2257
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_flags (
2258
+ id ${t.textPrimary()},
2259
+ tenant_id ${t.text()} NOT NULL,
2260
+ flag ${t.text()} NOT NULL,
2261
+ enabled ${t.boolean(false)},
2262
+ UNIQUE(tenant_id, flag)
2263
+ )`);
2264
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_overrides (
2265
+ id ${t.textPrimary()},
2266
+ tenant_id ${t.text()} NOT NULL UNIQUE,
2267
+ overrides ${t.text()} NOT NULL,
2268
+ updated_at ${t.timestamp()}
2269
+ )`);
2270
+ return statements;
2271
+ }
2272
+ var AUTH_TABLE_NAMES = [
2273
+ "auth_users",
2274
+ "auth_sessions",
2275
+ "auth_oauth_accounts",
2276
+ "auth_role_assignments",
2277
+ "auth_closure",
2278
+ "auth_plans",
2279
+ "auth_plan_addons",
2280
+ "auth_flags",
2281
+ "auth_overrides"
2282
+ ];
2283
+ function validateAuthModels(db) {
2284
+ const dbModels = db._internals.models;
2285
+ const missing = AUTH_TABLE_NAMES.filter((m) => !(m in dbModels));
2286
+ if (missing.length > 0) {
2287
+ throw new Error(`Auth requires models ${missing.map((m) => `"${m}"`).join(", ")} in createDb(). ` + "Add authModels to your createDb() call: createDb({ models: { ...authModels, ...yourModels } })");
1650
2288
  }
1651
- async removeResource(type, id) {
1652
- const descendants = await this.getDescendants(type, id);
1653
- const descendantKeys = new Set(descendants.map((d) => `${d.type}:${d.id}`));
1654
- this.rows = this.rows.filter((row) => {
1655
- const ancestorKey = `${row.ancestorType}:${row.ancestorId}`;
1656
- const descendantKey = `${row.descendantType}:${row.descendantId}`;
1657
- return !descendantKeys.has(ancestorKey) && !descendantKeys.has(descendantKey);
1658
- });
2289
+ }
2290
+ async function initializeAuthTables(db) {
2291
+ const dialectName = db._internals.dialect.name;
2292
+ const statements = generateAuthDDL(dialectName);
2293
+ for (const ddl of statements) {
2294
+ await db.query(sql.raw(ddl));
1659
2295
  }
1660
- async getAncestors(type, id) {
2296
+ }
2297
+ // src/auth/billing/event-emitter.ts
2298
+ function createBillingEventEmitter() {
2299
+ const handlers = new Map;
2300
+ return {
2301
+ on(eventType, handler) {
2302
+ const list = handlers.get(eventType) ?? [];
2303
+ list.push(handler);
2304
+ handlers.set(eventType, list);
2305
+ },
2306
+ off(eventType, handler) {
2307
+ const list = handlers.get(eventType);
2308
+ if (!list)
2309
+ return;
2310
+ const idx = list.indexOf(handler);
2311
+ if (idx >= 0)
2312
+ list.splice(idx, 1);
2313
+ },
2314
+ emit(eventType, event) {
2315
+ const list = handlers.get(eventType);
2316
+ if (!list)
2317
+ return;
2318
+ for (const handler of list) {
2319
+ try {
2320
+ handler(event);
2321
+ } catch {}
2322
+ }
2323
+ }
2324
+ };
2325
+ }
2326
+ // src/auth/billing/overage.ts
2327
+ function computeOverage(input) {
2328
+ const { consumed, max, rate } = input;
2329
+ if (max === -1)
2330
+ return 0;
2331
+ if (consumed <= max)
2332
+ return 0;
2333
+ const overageUnits = consumed - max;
2334
+ const charge = overageUnits * rate;
2335
+ if (input.cap !== undefined && charge > input.cap) {
2336
+ return input.cap;
2337
+ }
2338
+ return charge;
2339
+ }
2340
+ // src/auth/billing/stripe-adapter.ts
2341
+ var INTERVAL_MAP = {
2342
+ month: "month",
2343
+ quarter: "month",
2344
+ year: "year"
2345
+ };
2346
+ function mapInterval(interval) {
2347
+ if (interval === "quarter") {
2348
+ return { interval: "month", interval_count: 3 };
2349
+ }
2350
+ return { interval: INTERVAL_MAP[interval] ?? "month" };
2351
+ }
2352
+ function createStripeBillingAdapter(config) {
2353
+ const { stripe, currency = "usd" } = config;
2354
+ async function syncPlans(plans) {
2355
+ const existing = await stripe.products.list();
2356
+ const existingByPlanId = new Map;
2357
+ for (const product of existing.data) {
2358
+ if (product.metadata.vertzPlanId) {
2359
+ existingByPlanId.set(product.metadata.vertzPlanId, product);
2360
+ }
2361
+ }
2362
+ for (const [planId, planDef] of Object.entries(plans)) {
2363
+ const metadata = {
2364
+ vertzPlanId: planId
2365
+ };
2366
+ if (planDef.group)
2367
+ metadata.group = planDef.group;
2368
+ if (planDef.addOn)
2369
+ metadata.addOn = "true";
2370
+ let product = existingByPlanId.get(planId);
2371
+ if (!product) {
2372
+ product = await stripe.products.create({
2373
+ name: planDef.title ?? planId,
2374
+ metadata,
2375
+ description: planDef.description
2376
+ });
2377
+ } else {
2378
+ await stripe.products.update(product.id, {
2379
+ name: planDef.title ?? planId,
2380
+ metadata
2381
+ });
2382
+ }
2383
+ const amount = planDef.price?.amount ?? 0;
2384
+ const interval = planDef.price?.interval;
2385
+ const unitAmount = Math.round(amount * 100);
2386
+ const existingPrices = await stripe.prices.list({ product: product.id });
2387
+ const activePrices = existingPrices.data.filter((p) => p.active);
2388
+ const matchingPrice = activePrices.find((p) => p.unit_amount === unitAmount);
2389
+ if (!matchingPrice) {
2390
+ for (const oldPrice of activePrices) {
2391
+ await stripe.prices.update(oldPrice.id, { active: false });
2392
+ }
2393
+ const priceParams = {
2394
+ product: product.id,
2395
+ unit_amount: unitAmount,
2396
+ currency,
2397
+ metadata: { vertzPlanId: planId }
2398
+ };
2399
+ if (interval && interval !== "one_off") {
2400
+ const mapped = mapInterval(interval);
2401
+ priceParams.recurring = { interval: mapped.interval };
2402
+ }
2403
+ await stripe.prices.create(priceParams);
2404
+ }
2405
+ }
2406
+ }
2407
+ async function createSubscription(_tenantId, _planId) {
2408
+ return "sub_placeholder";
2409
+ }
2410
+ async function cancelSubscription(_tenantId) {}
2411
+ async function attachAddOn(_tenantId, _addOnId) {}
2412
+ async function reportOverage(_tenantId, _limitKey, _amount) {}
2413
+ async function getPortalUrl(_tenantId) {
2414
+ return "https://billing.stripe.com/portal/placeholder";
2415
+ }
2416
+ return {
2417
+ syncPlans,
2418
+ createSubscription,
2419
+ cancelSubscription,
2420
+ attachAddOn,
2421
+ reportOverage,
2422
+ getPortalUrl
2423
+ };
2424
+ }
2425
+ // src/auth/billing/webhook-handler.ts
2426
+ function extractTenantId(obj) {
2427
+ const meta = obj.metadata;
2428
+ if (meta?.tenantId)
2429
+ return meta.tenantId;
2430
+ const subDetails = obj.subscription_details;
2431
+ if (subDetails?.metadata) {
2432
+ const subMeta = subDetails.metadata;
2433
+ if (subMeta.tenantId)
2434
+ return subMeta.tenantId;
2435
+ }
2436
+ return null;
2437
+ }
2438
+ function extractPlanId(obj) {
2439
+ const meta = obj.metadata;
2440
+ if (meta?.vertzPlanId)
2441
+ return meta.vertzPlanId;
2442
+ const items = obj.items;
2443
+ if (items?.data?.[0]?.price?.metadata?.vertzPlanId) {
2444
+ return items.data[0].price.metadata.vertzPlanId;
2445
+ }
2446
+ return null;
2447
+ }
2448
+ function createWebhookHandler(config) {
2449
+ const { subscriptionStore, emitter, defaultPlan, webhookSecret } = config;
2450
+ return async (request) => {
2451
+ const signature = request.headers.get("stripe-signature");
2452
+ if (signature !== webhookSecret) {
2453
+ return new Response(JSON.stringify({ error: "Invalid signature" }), {
2454
+ status: 400,
2455
+ headers: { "content-type": "application/json" }
2456
+ });
2457
+ }
2458
+ let event;
2459
+ try {
2460
+ const body = await request.text();
2461
+ event = JSON.parse(body);
2462
+ } catch {
2463
+ return new Response(JSON.stringify({ error: "Invalid JSON" }), {
2464
+ status: 400,
2465
+ headers: { "content-type": "application/json" }
2466
+ });
2467
+ }
2468
+ const obj = event.data.object;
2469
+ try {
2470
+ switch (event.type) {
2471
+ case "customer.subscription.created": {
2472
+ const tenantId = extractTenantId(obj);
2473
+ const planId = extractPlanId(obj);
2474
+ if (tenantId && planId) {
2475
+ await subscriptionStore.assign(tenantId, planId);
2476
+ emitter.emit("subscription:created", { tenantId, planId });
2477
+ }
2478
+ break;
2479
+ }
2480
+ case "customer.subscription.updated": {
2481
+ const tenantId = extractTenantId(obj);
2482
+ const planId = extractPlanId(obj);
2483
+ if (tenantId && planId) {
2484
+ await subscriptionStore.assign(tenantId, planId);
2485
+ }
2486
+ break;
2487
+ }
2488
+ case "customer.subscription.deleted": {
2489
+ const tenantId = extractTenantId(obj);
2490
+ const planId = extractPlanId(obj);
2491
+ if (tenantId) {
2492
+ await subscriptionStore.assign(tenantId, defaultPlan);
2493
+ emitter.emit("subscription:canceled", {
2494
+ tenantId,
2495
+ planId: planId ?? defaultPlan
2496
+ });
2497
+ }
2498
+ break;
2499
+ }
2500
+ case "invoice.payment_failed": {
2501
+ const tenantId = extractTenantId(obj);
2502
+ const planId = extractPlanId(obj);
2503
+ const attemptCount = obj.attempt_count ?? 1;
2504
+ if (tenantId) {
2505
+ emitter.emit("billing:payment_failed", {
2506
+ tenantId,
2507
+ planId: planId ?? "",
2508
+ attempt: attemptCount
2509
+ });
2510
+ }
2511
+ break;
2512
+ }
2513
+ case "checkout.session.completed": {
2514
+ const tenantId = extractTenantId(obj);
2515
+ const planId = extractPlanId(obj);
2516
+ const meta = obj.metadata;
2517
+ const isAddOn = meta?.isAddOn === "true";
2518
+ if (tenantId && planId) {
2519
+ if (isAddOn && subscriptionStore.attachAddOn) {
2520
+ await subscriptionStore.attachAddOn(tenantId, planId);
2521
+ } else {
2522
+ await subscriptionStore.assign(tenantId, planId);
2523
+ }
2524
+ }
2525
+ break;
2526
+ }
2527
+ default:
2528
+ break;
2529
+ }
2530
+ } catch {
2531
+ return new Response(JSON.stringify({ error: "Processing failed" }), {
2532
+ status: 500,
2533
+ headers: { "content-type": "application/json" }
2534
+ });
2535
+ }
2536
+ return new Response(JSON.stringify({ received: true }), {
2537
+ status: 200,
2538
+ headers: { "content-type": "application/json" }
2539
+ });
2540
+ };
2541
+ }
2542
+ // src/auth/closure-store.ts
2543
+ var MAX_DEPTH = 4;
2544
+
2545
+ class InMemoryClosureStore {
2546
+ rows = [];
2547
+ async addResource(type, id, parent) {
2548
+ this.rows.push({
2549
+ ancestorType: type,
2550
+ ancestorId: id,
2551
+ descendantType: type,
2552
+ descendantId: id,
2553
+ depth: 0
2554
+ });
2555
+ if (parent) {
2556
+ const parentAncestors = await this.getAncestors(parent.parentType, parent.parentId);
2557
+ const maxParentDepth = Math.max(...parentAncestors.map((a) => a.depth), 0);
2558
+ if (maxParentDepth + 1 >= MAX_DEPTH) {
2559
+ this.rows.pop();
2560
+ throw new Error("Hierarchy depth exceeds maximum of 4 levels");
2561
+ }
2562
+ for (const ancestor of parentAncestors) {
2563
+ this.rows.push({
2564
+ ancestorType: ancestor.type,
2565
+ ancestorId: ancestor.id,
2566
+ descendantType: type,
2567
+ descendantId: id,
2568
+ depth: ancestor.depth + 1
2569
+ });
2570
+ }
2571
+ }
2572
+ }
2573
+ async removeResource(type, id) {
2574
+ const descendants = await this.getDescendants(type, id);
2575
+ const descendantKeys = new Set(descendants.map((d2) => `${d2.type}:${d2.id}`));
2576
+ this.rows = this.rows.filter((row) => {
2577
+ const ancestorKey = `${row.ancestorType}:${row.ancestorId}`;
2578
+ const descendantKey = `${row.descendantType}:${row.descendantId}`;
2579
+ return !descendantKeys.has(ancestorKey) && !descendantKeys.has(descendantKey);
2580
+ });
2581
+ }
2582
+ async getAncestors(type, id) {
1661
2583
  return this.rows.filter((row) => row.descendantType === type && row.descendantId === id).map((row) => ({
1662
2584
  type: row.ancestorType,
1663
2585
  id: row.ancestorId,
1664
2586
  depth: row.depth
1665
2587
  }));
1666
2588
  }
1667
- async getDescendants(type, id) {
1668
- return this.rows.filter((row) => row.ancestorType === type && row.ancestorId === id).map((row) => ({
1669
- type: row.descendantType,
1670
- id: row.descendantId,
1671
- depth: row.depth
1672
- }));
2589
+ async getDescendants(type, id) {
2590
+ return this.rows.filter((row) => row.ancestorType === type && row.ancestorId === id).map((row) => ({
2591
+ type: row.descendantType,
2592
+ id: row.descendantId,
2593
+ depth: row.depth
2594
+ }));
2595
+ }
2596
+ async hasPath(ancestorType, ancestorId, descendantType, descendantId) {
2597
+ return this.rows.some((row) => row.ancestorType === ancestorType && row.ancestorId === ancestorId && row.descendantType === descendantType && row.descendantId === descendantId);
2598
+ }
2599
+ dispose() {
2600
+ this.rows = [];
2601
+ }
2602
+ }
2603
+ // src/auth/db-closure-store.ts
2604
+ import { sql as sql2 } from "@vertz/db/sql";
2605
+
2606
+ // src/auth/db-types.ts
2607
+ function boolVal(db, value) {
2608
+ return db._internals.dialect.name === "sqlite" ? value ? 1 : 0 : value;
2609
+ }
2610
+ function assertWrite(result, context) {
2611
+ if (!result.ok) {
2612
+ throw new Error(`Auth DB write failed (${context}): ${result.error.message}`);
2613
+ }
2614
+ }
2615
+
2616
+ // src/auth/db-closure-store.ts
2617
+ var MAX_DEPTH2 = 4;
2618
+
2619
+ class DbClosureStore {
2620
+ db;
2621
+ constructor(db) {
2622
+ this.db = db;
2623
+ }
2624
+ async addResource(type, id, parent) {
2625
+ const selfId = crypto.randomUUID();
2626
+ const selfResult = await this.db.query(sql2`INSERT INTO auth_closure (id, ancestor_type, ancestor_id, descendant_type, descendant_id, depth)
2627
+ VALUES (${selfId}, ${type}, ${id}, ${type}, ${id}, ${0})
2628
+ ON CONFLICT DO NOTHING`);
2629
+ assertWrite(selfResult, "addResource/self");
2630
+ if (parent) {
2631
+ const parentAncestors = await this.getAncestors(parent.parentType, parent.parentId);
2632
+ const maxParentDepth = Math.max(...parentAncestors.map((a) => a.depth), 0);
2633
+ if (maxParentDepth + 1 >= MAX_DEPTH2) {
2634
+ throw new Error("Hierarchy depth exceeds maximum of 4 levels");
2635
+ }
2636
+ for (const ancestor of parentAncestors) {
2637
+ const rowId = crypto.randomUUID();
2638
+ const ancestorResult = await this.db.query(sql2`INSERT INTO auth_closure (id, ancestor_type, ancestor_id, descendant_type, descendant_id, depth)
2639
+ VALUES (${rowId}, ${ancestor.type}, ${ancestor.id}, ${type}, ${id}, ${ancestor.depth + 1})
2640
+ ON CONFLICT DO NOTHING`);
2641
+ assertWrite(ancestorResult, "addResource/ancestor");
2642
+ }
2643
+ }
2644
+ }
2645
+ async removeResource(type, id) {
2646
+ const descendants = await this.getDescendants(type, id);
2647
+ for (const desc of descendants) {
2648
+ const delResult = await this.db.query(sql2`DELETE FROM auth_closure WHERE (descendant_type = ${desc.type} AND descendant_id = ${desc.id}) OR (ancestor_type = ${desc.type} AND ancestor_id = ${desc.id})`);
2649
+ assertWrite(delResult, "removeResource");
2650
+ }
2651
+ }
2652
+ async getAncestors(type, id) {
2653
+ const result = await this.db.query(sql2`SELECT ancestor_type, ancestor_id, depth FROM auth_closure WHERE descendant_type = ${type} AND descendant_id = ${id}`);
2654
+ if (!result.ok)
2655
+ return [];
2656
+ return result.data.rows.map((r) => ({
2657
+ type: r.ancestor_type,
2658
+ id: r.ancestor_id,
2659
+ depth: r.depth
2660
+ }));
2661
+ }
2662
+ async getDescendants(type, id) {
2663
+ const result = await this.db.query(sql2`SELECT descendant_type, descendant_id, depth FROM auth_closure WHERE ancestor_type = ${type} AND ancestor_id = ${id}`);
2664
+ if (!result.ok)
2665
+ return [];
2666
+ return result.data.rows.map((r) => ({
2667
+ type: r.descendant_type,
2668
+ id: r.descendant_id,
2669
+ depth: r.depth
2670
+ }));
2671
+ }
2672
+ async hasPath(ancestorType, ancestorId, descendantType, descendantId) {
2673
+ const result = await this.db.query(sql2`SELECT COUNT(*) as cnt FROM auth_closure WHERE ancestor_type = ${ancestorType} AND ancestor_id = ${ancestorId} AND descendant_type = ${descendantType} AND descendant_id = ${descendantId}`);
2674
+ if (!result.ok)
2675
+ return false;
2676
+ return (result.data.rows[0]?.cnt ?? 0) > 0;
2677
+ }
2678
+ dispose() {}
2679
+ }
2680
+ // src/auth/db-flag-store.ts
2681
+ import { sql as sql3 } from "@vertz/db/sql";
2682
+ class DbFlagStore {
2683
+ db;
2684
+ cache = new Map;
2685
+ constructor(db) {
2686
+ this.db = db;
2687
+ }
2688
+ async loadFlags() {
2689
+ const result = await this.db.query(sql3`SELECT tenant_id, flag, enabled FROM auth_flags`);
2690
+ if (!result.ok)
2691
+ return;
2692
+ this.cache.clear();
2693
+ for (const row of result.data.rows) {
2694
+ let orgFlags = this.cache.get(row.tenant_id);
2695
+ if (!orgFlags) {
2696
+ orgFlags = new Map;
2697
+ this.cache.set(row.tenant_id, orgFlags);
2698
+ }
2699
+ orgFlags.set(row.flag, row.enabled === 1 || row.enabled === true);
2700
+ }
2701
+ }
2702
+ setFlag(orgId, flag, enabled) {
2703
+ let orgFlags = this.cache.get(orgId);
2704
+ if (!orgFlags) {
2705
+ orgFlags = new Map;
2706
+ this.cache.set(orgId, orgFlags);
2707
+ }
2708
+ orgFlags.set(flag, enabled);
2709
+ const id = crypto.randomUUID();
2710
+ const val = boolVal(this.db, enabled);
2711
+ this.db.query(sql3`INSERT INTO auth_flags (id, tenant_id, flag, enabled) VALUES (${id}, ${orgId}, ${flag}, ${val})
2712
+ ON CONFLICT(tenant_id, flag) DO UPDATE SET enabled = ${val}`).then((result) => {
2713
+ if (!result.ok) {
2714
+ console.error("[DbFlagStore] Failed to persist flag:", result.error);
2715
+ }
2716
+ });
2717
+ }
2718
+ getFlag(orgId, flag) {
2719
+ return this.cache.get(orgId)?.get(flag) ?? false;
2720
+ }
2721
+ getFlags(orgId) {
2722
+ const orgFlags = this.cache.get(orgId);
2723
+ if (!orgFlags)
2724
+ return {};
2725
+ const result = {};
2726
+ for (const [key, value] of orgFlags) {
2727
+ result[key] = value;
2728
+ }
2729
+ return result;
2730
+ }
2731
+ }
2732
+ // src/auth/db-oauth-account-store.ts
2733
+ import { sql as sql4 } from "@vertz/db/sql";
2734
+ class DbOAuthAccountStore {
2735
+ db;
2736
+ constructor(db) {
2737
+ this.db = db;
2738
+ }
2739
+ async linkAccount(userId, provider, providerId, email) {
2740
+ const id = crypto.randomUUID();
2741
+ const now = new Date().toISOString();
2742
+ const emailValue = email ?? null;
2743
+ const result = await this.db.query(sql4`INSERT INTO auth_oauth_accounts (id, user_id, provider, provider_id, email, created_at)
2744
+ VALUES (${id}, ${userId}, ${provider}, ${providerId}, ${emailValue}, ${now})
2745
+ ON CONFLICT DO NOTHING`);
2746
+ assertWrite(result, "linkAccount");
2747
+ }
2748
+ async findByProviderAccount(provider, providerId) {
2749
+ const result = await this.db.query(sql4`SELECT user_id FROM auth_oauth_accounts WHERE provider = ${provider} AND provider_id = ${providerId}`);
2750
+ if (!result.ok || result.data.rows.length === 0)
2751
+ return null;
2752
+ return result.data.rows[0]?.user_id ?? null;
2753
+ }
2754
+ async findByUserId(userId) {
2755
+ const result = await this.db.query(sql4`SELECT provider, provider_id FROM auth_oauth_accounts WHERE user_id = ${userId}`);
2756
+ if (!result.ok)
2757
+ return [];
2758
+ return result.data.rows.map((r) => ({
2759
+ provider: r.provider,
2760
+ providerId: r.provider_id
2761
+ }));
2762
+ }
2763
+ async unlinkAccount(userId, provider) {
2764
+ const result = await this.db.query(sql4`DELETE FROM auth_oauth_accounts WHERE user_id = ${userId} AND provider = ${provider}`);
2765
+ assertWrite(result, "unlinkAccount");
2766
+ }
2767
+ dispose() {}
2768
+ }
2769
+ // src/auth/db-role-assignment-store.ts
2770
+ import { sql as sql5 } from "@vertz/db/sql";
2771
+ class DbRoleAssignmentStore {
2772
+ db;
2773
+ constructor(db) {
2774
+ this.db = db;
2775
+ }
2776
+ async assign(userId, resourceType, resourceId, role) {
2777
+ const id = crypto.randomUUID();
2778
+ const now = new Date().toISOString();
2779
+ const result = await this.db.query(sql5`INSERT INTO auth_role_assignments (id, user_id, resource_type, resource_id, role, created_at)
2780
+ VALUES (${id}, ${userId}, ${resourceType}, ${resourceId}, ${role}, ${now})
2781
+ ON CONFLICT DO NOTHING`);
2782
+ assertWrite(result, "assign");
2783
+ }
2784
+ async revoke(userId, resourceType, resourceId, role) {
2785
+ const result = await this.db.query(sql5`DELETE FROM auth_role_assignments WHERE user_id = ${userId} AND resource_type = ${resourceType} AND resource_id = ${resourceId} AND role = ${role}`);
2786
+ assertWrite(result, "revoke");
2787
+ }
2788
+ async getRoles(userId, resourceType, resourceId) {
2789
+ const result = await this.db.query(sql5`SELECT role FROM auth_role_assignments WHERE user_id = ${userId} AND resource_type = ${resourceType} AND resource_id = ${resourceId}`);
2790
+ if (!result.ok)
2791
+ return [];
2792
+ return result.data.rows.map((r) => r.role);
2793
+ }
2794
+ async getRolesForUser(userId) {
2795
+ const result = await this.db.query(sql5`SELECT user_id, resource_type, resource_id, role FROM auth_role_assignments WHERE user_id = ${userId}`);
2796
+ if (!result.ok)
2797
+ return [];
2798
+ return result.data.rows.map((r) => ({
2799
+ userId: r.user_id,
2800
+ resourceType: r.resource_type,
2801
+ resourceId: r.resource_id,
2802
+ role: r.role
2803
+ }));
2804
+ }
2805
+ async getEffectiveRole(userId, resourceType, resourceId, accessDef, closureStore) {
2806
+ const rolesForType = accessDef.roles[resourceType];
2807
+ if (!rolesForType || rolesForType.length === 0)
2808
+ return null;
2809
+ const candidateRoles = [];
2810
+ const directRoles = await this.getRoles(userId, resourceType, resourceId);
2811
+ candidateRoles.push(...directRoles);
2812
+ const ancestors = await closureStore.getAncestors(resourceType, resourceId);
2813
+ for (const ancestor of ancestors) {
2814
+ if (ancestor.depth === 0)
2815
+ continue;
2816
+ const ancestorRoles = await this.getRoles(userId, ancestor.type, ancestor.id);
2817
+ for (const ancestorRole of ancestorRoles) {
2818
+ const inheritedRole = resolveInheritedRole(ancestor.type, ancestorRole, resourceType, accessDef);
2819
+ if (inheritedRole) {
2820
+ candidateRoles.push(inheritedRole);
2821
+ }
2822
+ }
2823
+ }
2824
+ if (candidateRoles.length === 0)
2825
+ return null;
2826
+ const roleOrder = [...rolesForType];
2827
+ let bestIndex = roleOrder.length;
2828
+ for (const role of candidateRoles) {
2829
+ const idx = roleOrder.indexOf(role);
2830
+ if (idx !== -1 && idx < bestIndex) {
2831
+ bestIndex = idx;
2832
+ }
2833
+ }
2834
+ return bestIndex < roleOrder.length ? roleOrder[bestIndex] : null;
2835
+ }
2836
+ dispose() {}
2837
+ }
2838
+ // src/auth/db-session-store.ts
2839
+ import { sql as sql6 } from "@vertz/db/sql";
2840
+ class DbSessionStore {
2841
+ db;
2842
+ constructor(db) {
2843
+ this.db = db;
2844
+ }
2845
+ async createSessionWithId(id, data) {
2846
+ const now = new Date;
2847
+ const tokensJson = data.currentTokens ? JSON.stringify(data.currentTokens) : null;
2848
+ const result = await this.db.query(sql6`INSERT INTO auth_sessions (id, user_id, refresh_token_hash, previous_refresh_hash, current_tokens, ip_address, user_agent, created_at, last_active_at, expires_at, revoked_at)
2849
+ VALUES (${id}, ${data.userId}, ${data.refreshTokenHash}, ${null}, ${tokensJson}, ${data.ipAddress}, ${data.userAgent}, ${now.toISOString()}, ${now.toISOString()}, ${data.expiresAt.toISOString()}, ${null})`);
2850
+ assertWrite(result, "createSessionWithId");
2851
+ return {
2852
+ id,
2853
+ userId: data.userId,
2854
+ refreshTokenHash: data.refreshTokenHash,
2855
+ previousRefreshHash: null,
2856
+ ipAddress: data.ipAddress,
2857
+ userAgent: data.userAgent,
2858
+ createdAt: now,
2859
+ lastActiveAt: now,
2860
+ expiresAt: data.expiresAt,
2861
+ revokedAt: null
2862
+ };
2863
+ }
2864
+ async findByRefreshHash(hash) {
2865
+ const nowStr = new Date().toISOString();
2866
+ const result = await this.db.auth_sessions.get({
2867
+ where: {
2868
+ refreshTokenHash: hash,
2869
+ revokedAt: null,
2870
+ expiresAt: { gt: nowStr }
2871
+ }
2872
+ });
2873
+ if (!result.ok)
2874
+ return null;
2875
+ const row = result.data;
2876
+ if (!row)
2877
+ return null;
2878
+ return this.recordToSession(row);
2879
+ }
2880
+ async findActiveSessionById(id) {
2881
+ const nowStr = new Date().toISOString();
2882
+ const result = await this.db.auth_sessions.get({
2883
+ where: {
2884
+ id,
2885
+ revokedAt: null,
2886
+ expiresAt: { gt: nowStr }
2887
+ }
2888
+ });
2889
+ if (!result.ok)
2890
+ return null;
2891
+ const row = result.data;
2892
+ if (!row)
2893
+ return null;
2894
+ return this.recordToSession(row);
2895
+ }
2896
+ async findByPreviousRefreshHash(hash) {
2897
+ const nowStr = new Date().toISOString();
2898
+ const result = await this.db.auth_sessions.get({
2899
+ where: {
2900
+ previousRefreshHash: hash,
2901
+ revokedAt: null,
2902
+ expiresAt: { gt: nowStr }
2903
+ }
2904
+ });
2905
+ if (!result.ok)
2906
+ return null;
2907
+ const row = result.data;
2908
+ if (!row)
2909
+ return null;
2910
+ return this.recordToSession(row);
2911
+ }
2912
+ async revokeSession(id) {
2913
+ const nowStr = new Date().toISOString();
2914
+ const result = await this.db.query(sql6`UPDATE auth_sessions SET revoked_at = ${nowStr}, current_tokens = ${null} WHERE id = ${id}`);
2915
+ assertWrite(result, "revokeSession");
2916
+ }
2917
+ async listActiveSessions(userId) {
2918
+ const nowStr = new Date().toISOString();
2919
+ const result = await this.db.query(sql6`SELECT * FROM auth_sessions WHERE user_id = ${userId} AND revoked_at IS NULL AND expires_at > ${nowStr}`);
2920
+ if (!result.ok)
2921
+ return [];
2922
+ return result.data.rows.map((row) => this.rowToSession(row));
2923
+ }
2924
+ async countActiveSessions(userId) {
2925
+ const nowStr = new Date().toISOString();
2926
+ const result = await this.db.query(sql6`SELECT COUNT(*) as cnt FROM auth_sessions WHERE user_id = ${userId} AND revoked_at IS NULL AND expires_at > ${nowStr}`);
2927
+ if (!result.ok)
2928
+ return 0;
2929
+ return result.data.rows[0]?.cnt ?? 0;
2930
+ }
2931
+ async getCurrentTokens(sessionId) {
2932
+ const result = await this.db.query(sql6`SELECT current_tokens FROM auth_sessions WHERE id = ${sessionId} LIMIT 1`);
2933
+ if (!result.ok)
2934
+ return null;
2935
+ const row = result.data.rows[0];
2936
+ if (!row?.current_tokens)
2937
+ return null;
2938
+ try {
2939
+ return JSON.parse(row.current_tokens);
2940
+ } catch {
2941
+ return null;
2942
+ }
2943
+ }
2944
+ async updateSession(id, data) {
2945
+ const tokensJson = data.currentTokens ? JSON.stringify(data.currentTokens) : null;
2946
+ const result = await this.db.query(sql6`UPDATE auth_sessions SET refresh_token_hash = ${data.refreshTokenHash}, previous_refresh_hash = ${data.previousRefreshHash}, last_active_at = ${data.lastActiveAt.toISOString()}, current_tokens = ${tokensJson} WHERE id = ${id}`);
2947
+ assertWrite(result, "updateSession");
2948
+ }
2949
+ dispose() {}
2950
+ rowToSession(row) {
2951
+ return {
2952
+ id: row.id,
2953
+ userId: row.user_id,
2954
+ refreshTokenHash: row.refresh_token_hash,
2955
+ previousRefreshHash: row.previous_refresh_hash,
2956
+ ipAddress: row.ip_address,
2957
+ userAgent: row.user_agent,
2958
+ createdAt: new Date(row.created_at),
2959
+ lastActiveAt: new Date(row.last_active_at),
2960
+ expiresAt: new Date(row.expires_at),
2961
+ revokedAt: row.revoked_at ? new Date(row.revoked_at) : null
2962
+ };
1673
2963
  }
1674
- async hasPath(ancestorType, ancestorId, descendantType, descendantId) {
1675
- return this.rows.some((row) => row.ancestorType === ancestorType && row.ancestorId === ancestorId && row.descendantType === descendantType && row.descendantId === descendantId);
2964
+ recordToSession(rec) {
2965
+ return {
2966
+ id: rec.id,
2967
+ userId: rec.userId,
2968
+ refreshTokenHash: rec.refreshTokenHash,
2969
+ previousRefreshHash: rec.previousRefreshHash,
2970
+ ipAddress: rec.ipAddress,
2971
+ userAgent: rec.userAgent,
2972
+ createdAt: new Date(rec.createdAt),
2973
+ lastActiveAt: new Date(rec.lastActiveAt),
2974
+ expiresAt: new Date(rec.expiresAt),
2975
+ revokedAt: rec.revokedAt ? new Date(rec.revokedAt) : null
2976
+ };
1676
2977
  }
1677
- dispose() {
1678
- this.rows = [];
2978
+ }
2979
+ // src/auth/db-subscription-store.ts
2980
+ import { sql as sql7 } from "@vertz/db/sql";
2981
+ class DbSubscriptionStore {
2982
+ db;
2983
+ constructor(db) {
2984
+ this.db = db;
2985
+ }
2986
+ async assign(tenantId, planId, startedAt = new Date, expiresAt = null) {
2987
+ await this.db.transaction(async (tx) => {
2988
+ const id = crypto.randomUUID();
2989
+ const startedAtIso = startedAt.toISOString();
2990
+ const expiresAtIso = expiresAt ? expiresAt.toISOString() : null;
2991
+ const upsertResult = await tx.query(sql7`INSERT INTO auth_plans (id, tenant_id, plan_id, started_at, expires_at)
2992
+ VALUES (${id}, ${tenantId}, ${planId}, ${startedAtIso}, ${expiresAtIso})
2993
+ ON CONFLICT(tenant_id) DO UPDATE SET plan_id = ${planId}, started_at = ${startedAtIso}, expires_at = ${expiresAtIso}`);
2994
+ assertWrite(upsertResult, "assign/upsert");
2995
+ const overrideResult = await tx.query(sql7`DELETE FROM auth_overrides WHERE tenant_id = ${tenantId}`);
2996
+ assertWrite(overrideResult, "assign/clearOverrides");
2997
+ });
2998
+ }
2999
+ async get(tenantId) {
3000
+ const result = await this.db.query(sql7`SELECT tenant_id, plan_id, started_at, expires_at FROM auth_plans WHERE tenant_id = ${tenantId}`);
3001
+ if (!result.ok)
3002
+ return null;
3003
+ const row = result.data.rows[0];
3004
+ if (!row)
3005
+ return null;
3006
+ const overrides = await this.loadOverrides(tenantId);
3007
+ return {
3008
+ tenantId: row.tenant_id,
3009
+ planId: row.plan_id,
3010
+ startedAt: new Date(row.started_at),
3011
+ expiresAt: row.expires_at ? new Date(row.expires_at) : null,
3012
+ overrides
3013
+ };
3014
+ }
3015
+ async updateOverrides(tenantId, overrides) {
3016
+ const planResult = await this.db.query(sql7`SELECT tenant_id FROM auth_plans WHERE tenant_id = ${tenantId}`);
3017
+ if (!planResult.ok || planResult.data.rows.length === 0)
3018
+ return;
3019
+ await this.db.transaction(async (tx) => {
3020
+ const existing = await this.loadOverrides(tenantId);
3021
+ const merged = { ...existing, ...overrides };
3022
+ const overridesJson = JSON.stringify(merged);
3023
+ const now = new Date().toISOString();
3024
+ const overrideResult = await tx.query(sql7`SELECT tenant_id FROM auth_overrides WHERE tenant_id = ${tenantId}`);
3025
+ if (overrideResult.ok && overrideResult.data.rows.length > 0) {
3026
+ const updateResult = await tx.query(sql7`UPDATE auth_overrides SET overrides = ${overridesJson}, updated_at = ${now} WHERE tenant_id = ${tenantId}`);
3027
+ assertWrite(updateResult, "updateOverrides/update");
3028
+ } else {
3029
+ const id = crypto.randomUUID();
3030
+ const insertResult = await tx.query(sql7`INSERT INTO auth_overrides (id, tenant_id, overrides, updated_at)
3031
+ VALUES (${id}, ${tenantId}, ${overridesJson}, ${now})`);
3032
+ assertWrite(insertResult, "updateOverrides/insert");
3033
+ }
3034
+ });
3035
+ }
3036
+ async remove(tenantId) {
3037
+ await this.db.transaction(async (tx) => {
3038
+ const planResult = await tx.query(sql7`DELETE FROM auth_plans WHERE tenant_id = ${tenantId}`);
3039
+ assertWrite(planResult, "remove/plans");
3040
+ const overrideResult = await tx.query(sql7`DELETE FROM auth_overrides WHERE tenant_id = ${tenantId}`);
3041
+ assertWrite(overrideResult, "remove/overrides");
3042
+ });
3043
+ }
3044
+ async attachAddOn(tenantId, addOnId) {
3045
+ const id = crypto.randomUUID();
3046
+ const now = new Date().toISOString();
3047
+ const result = await this.db.query(sql7`INSERT INTO auth_plan_addons (id, tenant_id, addon_id, quantity, created_at)
3048
+ VALUES (${id}, ${tenantId}, ${addOnId}, ${1}, ${now})
3049
+ ON CONFLICT(tenant_id, addon_id) DO NOTHING`);
3050
+ assertWrite(result, "attachAddOn");
3051
+ }
3052
+ async detachAddOn(tenantId, addOnId) {
3053
+ const result = await this.db.query(sql7`DELETE FROM auth_plan_addons WHERE tenant_id = ${tenantId} AND addon_id = ${addOnId}`);
3054
+ assertWrite(result, "detachAddOn");
3055
+ }
3056
+ async getAddOns(tenantId) {
3057
+ const result = await this.db.query(sql7`SELECT addon_id FROM auth_plan_addons WHERE tenant_id = ${tenantId}`);
3058
+ if (!result.ok)
3059
+ return [];
3060
+ return result.data.rows.map((r) => r.addon_id);
3061
+ }
3062
+ dispose() {}
3063
+ async loadOverrides(tenantId) {
3064
+ const result = await this.db.query(sql7`SELECT overrides FROM auth_overrides WHERE tenant_id = ${tenantId}`);
3065
+ if (!result.ok || result.data.rows.length === 0)
3066
+ return {};
3067
+ try {
3068
+ const overridesStr = result.data.rows[0]?.overrides;
3069
+ if (!overridesStr)
3070
+ return {};
3071
+ return JSON.parse(overridesStr);
3072
+ } catch {
3073
+ return {};
3074
+ }
3075
+ }
3076
+ }
3077
+ // src/auth/db-user-store.ts
3078
+ import { sql as sql8 } from "@vertz/db/sql";
3079
+ class DbUserStore {
3080
+ db;
3081
+ constructor(db) {
3082
+ this.db = db;
3083
+ }
3084
+ async createUser(user, passwordHash) {
3085
+ const result = await this.db.query(sql8`INSERT INTO auth_users (id, email, password_hash, role, email_verified, created_at, updated_at)
3086
+ VALUES (${user.id}, ${user.email.toLowerCase()}, ${passwordHash}, ${user.role}, ${boolVal(this.db, user.emailVerified ?? false)}, ${user.createdAt.toISOString()}, ${user.updatedAt.toISOString()})`);
3087
+ assertWrite(result, "createUser");
3088
+ }
3089
+ async findByEmail(email) {
3090
+ const result = await this.db.query(sql8`SELECT * FROM auth_users WHERE email = ${email.toLowerCase()} LIMIT 1`);
3091
+ if (!result.ok)
3092
+ return null;
3093
+ const row = result.data.rows[0];
3094
+ if (!row)
3095
+ return null;
3096
+ return {
3097
+ user: this.rowToUser(row),
3098
+ passwordHash: row.password_hash
3099
+ };
3100
+ }
3101
+ async findById(id) {
3102
+ const result = await this.db.query(sql8`SELECT * FROM auth_users WHERE id = ${id} LIMIT 1`);
3103
+ if (!result.ok)
3104
+ return null;
3105
+ const row = result.data.rows[0];
3106
+ if (!row)
3107
+ return null;
3108
+ return this.rowToUser(row);
3109
+ }
3110
+ async updatePasswordHash(userId, passwordHash) {
3111
+ const result = await this.db.query(sql8`UPDATE auth_users SET password_hash = ${passwordHash}, updated_at = ${new Date().toISOString()} WHERE id = ${userId}`);
3112
+ assertWrite(result, "updatePasswordHash");
3113
+ }
3114
+ async updateEmailVerified(userId, verified) {
3115
+ const val = boolVal(this.db, verified);
3116
+ const result = await this.db.query(sql8`UPDATE auth_users SET email_verified = ${val}, updated_at = ${new Date().toISOString()} WHERE id = ${userId}`);
3117
+ assertWrite(result, "updateEmailVerified");
3118
+ }
3119
+ async deleteUser(id) {
3120
+ const result = await this.db.query(sql8`DELETE FROM auth_users WHERE id = ${id}`);
3121
+ assertWrite(result, "deleteUser");
3122
+ }
3123
+ rowToUser(row) {
3124
+ return {
3125
+ id: row.id,
3126
+ email: row.email,
3127
+ role: row.role,
3128
+ emailVerified: row.email_verified === 1 || row.email_verified === true,
3129
+ createdAt: new Date(row.created_at),
3130
+ updatedAt: new Date(row.updated_at)
3131
+ };
1679
3132
  }
1680
3133
  }
1681
3134
  // src/auth/define-access.ts
1682
3135
  function defineAccess(input) {
1683
- if (input.hierarchy.length === 0) {
1684
- throw new Error("Hierarchy must have at least one resource type");
3136
+ const { entities, entitlements: rawEntitlements } = input;
3137
+ for (const [entityName, entityDef] of Object.entries(entities)) {
3138
+ const roleSet = new Set;
3139
+ for (const role of entityDef.roles) {
3140
+ if (roleSet.has(role)) {
3141
+ throw new Error(`Duplicate role '${role}' in entity '${entityName}'`);
3142
+ }
3143
+ roleSet.add(role);
3144
+ }
3145
+ }
3146
+ const parentToChildren = new Map;
3147
+ const childToParent = new Map;
3148
+ const inheritanceMap = {};
3149
+ for (const [entityName, entityDef] of Object.entries(entities)) {
3150
+ if (!entityDef.inherits)
3151
+ continue;
3152
+ const parentEntities = new Set;
3153
+ for (const [inheritKey, localRole] of Object.entries(entityDef.inherits)) {
3154
+ const colonIdx = inheritKey.indexOf(":");
3155
+ if (colonIdx === -1) {
3156
+ throw new Error(`Invalid inherits key '${inheritKey}' in entity '${entityName}' — expected format 'entity:role'`);
3157
+ }
3158
+ const parentEntityName2 = inheritKey.substring(0, colonIdx);
3159
+ const parentRoleName = inheritKey.substring(colonIdx + 1);
3160
+ if (parentEntityName2 === entityName) {
3161
+ throw new Error(`Entity '${entityName}' cannot inherit from itself`);
3162
+ }
3163
+ if (!(parentEntityName2 in entities)) {
3164
+ throw new Error(`Entity '${parentEntityName2}' in ${entityName}.inherits is not defined`);
3165
+ }
3166
+ const parentEntity = entities[parentEntityName2];
3167
+ if (!parentEntity.roles.includes(parentRoleName)) {
3168
+ throw new Error(`Role '${parentRoleName}' does not exist on entity '${parentEntityName2}'`);
3169
+ }
3170
+ if (!entityDef.roles.includes(localRole)) {
3171
+ throw new Error(`Role '${localRole}' does not exist on entity '${entityName}'`);
3172
+ }
3173
+ parentEntities.add(parentEntityName2);
3174
+ if (!inheritanceMap[parentEntityName2]) {
3175
+ inheritanceMap[parentEntityName2] = {};
3176
+ }
3177
+ inheritanceMap[parentEntityName2][parentRoleName] = localRole;
3178
+ }
3179
+ if (parentEntities.size > 1) {
3180
+ throw new Error(`Entity '${entityName}' inherits from multiple parents (${[...parentEntities].join(", ")}). Each entity can only inherit from one parent.`);
3181
+ }
3182
+ const parentEntityName = [...parentEntities][0];
3183
+ if (parentEntityName) {
3184
+ if (!parentToChildren.has(parentEntityName)) {
3185
+ parentToChildren.set(parentEntityName, new Set);
3186
+ }
3187
+ parentToChildren.get(parentEntityName).add(entityName);
3188
+ if (childToParent.has(entityName)) {
3189
+ throw new Error(`Entity '${entityName}' inherits from multiple parents`);
3190
+ }
3191
+ childToParent.set(entityName, parentEntityName);
3192
+ }
1685
3193
  }
1686
- if (input.hierarchy.length > 4) {
3194
+ detectCycles(childToParent);
3195
+ const hierarchy = inferHierarchy(entities, childToParent, parentToChildren);
3196
+ if (hierarchy.length > 4) {
1687
3197
  throw new Error("Hierarchy depth must not exceed 4 levels");
1688
3198
  }
1689
- const hierarchySet = new Set(input.hierarchy);
1690
- for (const resourceType of Object.keys(input.roles)) {
1691
- if (!hierarchySet.has(resourceType)) {
1692
- throw new Error(`Roles reference unknown resource type: ${resourceType}`);
3199
+ const resolvedEntitlements = {};
3200
+ const ruleContext = createRuleContext();
3201
+ for (const [entName, entValue] of Object.entries(rawEntitlements)) {
3202
+ const colonIdx = entName.indexOf(":");
3203
+ if (colonIdx === -1) {
3204
+ throw new Error(`Entitlement '${entName}' must use format 'entity:action'`);
1693
3205
  }
3206
+ const entityPrefix = entName.substring(0, colonIdx);
3207
+ if (!(entityPrefix in entities)) {
3208
+ throw new Error(`Entitlement '${entName}' references undefined entity '${entityPrefix}'`);
3209
+ }
3210
+ let entDef;
3211
+ if (typeof entValue === "function") {
3212
+ entDef = entValue(ruleContext);
3213
+ } else {
3214
+ entDef = entValue;
3215
+ }
3216
+ const entityRoles = entities[entityPrefix].roles;
3217
+ for (const role of entDef.roles) {
3218
+ if (!entityRoles.includes(role)) {
3219
+ throw new Error(`Role '${role}' in '${entName}' does not exist on entity '${entityPrefix}'`);
3220
+ }
3221
+ }
3222
+ resolvedEntitlements[entName] = entDef;
1694
3223
  }
1695
- const inheritance = input.inheritance ?? {};
1696
- for (const [resourceType, mapping] of Object.entries(inheritance)) {
1697
- if (!hierarchySet.has(resourceType)) {
1698
- throw new Error(`Inheritance references unknown resource type: ${resourceType}`);
3224
+ const entitlementSet = new Set(Object.keys(resolvedEntitlements));
3225
+ const entityNameSet = new Set(Object.keys(entities));
3226
+ if (input.plans) {
3227
+ for (const [planName, planDef] of Object.entries(input.plans)) {
3228
+ if (planDef.features) {
3229
+ for (const feature of planDef.features) {
3230
+ if (!entitlementSet.has(feature)) {
3231
+ throw new Error(`Plan '${planName}' feature '${feature}' is not a defined entitlement`);
3232
+ }
3233
+ }
3234
+ }
3235
+ if (planDef.limits) {
3236
+ for (const [limitKey, limitDef] of Object.entries(planDef.limits)) {
3237
+ if (!entitlementSet.has(limitDef.gates)) {
3238
+ throw new Error(`Limit '${limitKey}' gates '${limitDef.gates}' which is not defined`);
3239
+ }
3240
+ if (limitDef.scope && !entityNameSet.has(limitDef.scope)) {
3241
+ throw new Error(`Limit '${limitKey}' scope '${limitDef.scope}' is not a defined entity`);
3242
+ }
3243
+ if (limitDef.max < -1 || limitDef.max < 0 && limitDef.max !== -1) {
3244
+ throw new Error(`Limit '${limitKey}' max must be -1 (unlimited), 0 (disabled), or a positive integer, got ${limitDef.max}`);
3245
+ }
3246
+ if (!Number.isInteger(limitDef.max)) {
3247
+ throw new Error(`Limit '${limitKey}' max must be -1 (unlimited), 0 (disabled), or a positive integer, got ${limitDef.max}`);
3248
+ }
3249
+ }
3250
+ }
3251
+ if (planDef.addOn) {
3252
+ if (planDef.group) {
3253
+ throw new Error(`Add-on '${planName}' must not have a group`);
3254
+ }
3255
+ } else {
3256
+ if (!planDef.group) {
3257
+ throw new Error(`Base plan '${planName}' must have a group`);
3258
+ }
3259
+ if (planDef.requires) {
3260
+ throw new Error(`Plan '${planName}': 'requires' is only valid on add-on plans`);
3261
+ }
3262
+ }
3263
+ if (planDef.addOn && planDef.requires) {
3264
+ for (const reqPlan of planDef.requires.plans) {
3265
+ if (!input.plans[reqPlan]) {
3266
+ throw new Error(`Add-on '${planName}' requires plan '${reqPlan}' is not defined`);
3267
+ }
3268
+ }
3269
+ }
1699
3270
  }
1700
- const parentRoles = new Set(input.roles[resourceType] ?? []);
1701
- const hierarchyIdx = input.hierarchy.indexOf(resourceType);
1702
- const childType = input.hierarchy[hierarchyIdx + 1];
1703
- const childRoles = childType ? new Set(input.roles[childType] ?? []) : new Set;
1704
- for (const [parentRole, childRole] of Object.entries(mapping)) {
1705
- if (!parentRoles.has(parentRole)) {
1706
- throw new Error(`Inheritance for ${resourceType} references undefined role: ${parentRole}`);
3271
+ if (input.defaultPlan) {
3272
+ const defaultPlanDef = input.plans[input.defaultPlan];
3273
+ if (defaultPlanDef?.addOn) {
3274
+ throw new Error(`defaultPlan '${input.defaultPlan}' is an add-on, not a base plan`);
1707
3275
  }
1708
- if (!childRoles.has(childRole)) {
1709
- throw new Error(`Inheritance for ${resourceType} maps to undefined child role: ${childRole}`);
3276
+ }
3277
+ const basePlanLimitKeys = new Set;
3278
+ for (const [, planDef] of Object.entries(input.plans)) {
3279
+ if (!planDef.addOn && planDef.limits) {
3280
+ for (const limitKey of Object.keys(planDef.limits)) {
3281
+ basePlanLimitKeys.add(limitKey);
3282
+ }
1710
3283
  }
1711
3284
  }
1712
- }
1713
- const planNameSet = new Set(Object.keys(input.plans ?? {}));
1714
- for (const [entName, entDef] of Object.entries(input.entitlements)) {
1715
- if (entDef.plans) {
1716
- for (const planRef of entDef.plans) {
1717
- if (!planNameSet.has(planRef)) {
1718
- throw new Error(`Entitlement "${entName}" references unknown plan: ${planRef}`);
3285
+ for (const [, planDef] of Object.entries(input.plans)) {
3286
+ if (planDef.addOn && planDef.limits) {
3287
+ for (const limitKey of Object.keys(planDef.limits)) {
3288
+ if (!basePlanLimitKeys.has(limitKey)) {
3289
+ throw new Error(`Add-on limit '${limitKey}' not defined in any base plan`);
3290
+ }
1719
3291
  }
1720
3292
  }
1721
3293
  }
1722
3294
  }
1723
- const entitlementSet = new Set(Object.keys(input.entitlements));
3295
+ const planGatedEntitlements = new Set;
3296
+ const entitlementToLimitKeys = {};
1724
3297
  if (input.plans) {
1725
- for (const [planName, planDef] of Object.entries(input.plans)) {
1726
- const planEntitlementSet = new Set(planDef.entitlements);
1727
- for (const ent of planDef.entitlements) {
1728
- if (!entitlementSet.has(ent)) {
1729
- throw new Error(`Plan "${planName}" references unknown entitlement: ${ent}`);
3298
+ for (const [, planDef] of Object.entries(input.plans)) {
3299
+ if (planDef.features) {
3300
+ for (const feature of planDef.features) {
3301
+ planGatedEntitlements.add(feature);
1730
3302
  }
1731
3303
  }
1732
3304
  if (planDef.limits) {
1733
- for (const limitKey of Object.keys(planDef.limits)) {
1734
- if (!planEntitlementSet.has(limitKey)) {
1735
- throw new Error(`Plan "${planName}" has limit for "${limitKey}" which is not in the plan's entitlements`);
3305
+ for (const [limitKey, limitDef] of Object.entries(planDef.limits)) {
3306
+ if (!entitlementToLimitKeys[limitDef.gates]) {
3307
+ entitlementToLimitKeys[limitDef.gates] = [];
3308
+ }
3309
+ if (!entitlementToLimitKeys[limitDef.gates].includes(limitKey)) {
3310
+ entitlementToLimitKeys[limitDef.gates].push(limitKey);
1736
3311
  }
1737
3312
  }
1738
3313
  }
1739
3314
  }
1740
3315
  }
3316
+ const rolesMap = {};
3317
+ for (const [entityName, entityDef] of Object.entries(entities)) {
3318
+ rolesMap[entityName] = Object.freeze([...entityDef.roles]);
3319
+ }
1741
3320
  const config = {
1742
- hierarchy: Object.freeze([...input.hierarchy]),
1743
- roles: Object.freeze(Object.fromEntries(Object.entries(input.roles).map(([k, v]) => [k, Object.freeze([...v])]))),
1744
- inheritance: Object.freeze(Object.fromEntries(Object.entries(input.inheritance ?? {}).map(([k, v]) => [k, Object.freeze({ ...v })]))),
1745
- entitlements: Object.freeze(Object.fromEntries(Object.entries(input.entitlements).map(([k, v]) => [k, Object.freeze({ ...v })]))),
3321
+ hierarchy: Object.freeze([...hierarchy]),
3322
+ entities: Object.freeze(Object.fromEntries(Object.entries(entities).map(([k, v]) => [
3323
+ k,
3324
+ Object.freeze({
3325
+ roles: Object.freeze([...v.roles]),
3326
+ ...v.inherits ? { inherits: Object.freeze({ ...v.inherits }) } : {}
3327
+ })
3328
+ ]))),
3329
+ roles: Object.freeze(rolesMap),
3330
+ inheritance: Object.freeze(Object.fromEntries(Object.entries(inheritanceMap).map(([k, v]) => [k, Object.freeze({ ...v })]))),
3331
+ entitlements: Object.freeze(Object.fromEntries(Object.entries(resolvedEntitlements).map(([k, v]) => [k, Object.freeze({ ...v })]))),
3332
+ _planGatedEntitlements: Object.freeze(planGatedEntitlements),
3333
+ _entitlementToLimitKeys: Object.freeze(Object.fromEntries(Object.entries(entitlementToLimitKeys).map(([k, v]) => [k, Object.freeze([...v])]))),
1746
3334
  ...input.defaultPlan ? { defaultPlan: input.defaultPlan } : {},
1747
3335
  ...input.plans ? {
1748
3336
  plans: Object.freeze(Object.fromEntries(Object.entries(input.plans).map(([planName, planDef]) => [
1749
3337
  planName,
1750
3338
  Object.freeze({
1751
- entitlements: Object.freeze([...planDef.entitlements]),
3339
+ ...planDef.title ? { title: planDef.title } : {},
3340
+ ...planDef.description ? { description: planDef.description } : {},
3341
+ ...planDef.group ? { group: planDef.group } : {},
3342
+ ...planDef.addOn ? { addOn: planDef.addOn } : {},
3343
+ ...planDef.price ? { price: Object.freeze({ ...planDef.price }) } : {},
3344
+ ...planDef.features ? { features: Object.freeze([...planDef.features]) } : {},
1752
3345
  ...planDef.limits ? {
1753
3346
  limits: Object.freeze(Object.fromEntries(Object.entries(planDef.limits).map(([k, v]) => [
1754
3347
  k,
1755
3348
  Object.freeze({ ...v })
1756
3349
  ])))
3350
+ } : {},
3351
+ ...planDef.grandfathering ? { grandfathering: Object.freeze({ ...planDef.grandfathering }) } : {},
3352
+ ...planDef.requires ? {
3353
+ requires: Object.freeze({
3354
+ group: planDef.requires.group,
3355
+ plans: Object.freeze([...planDef.requires.plans])
3356
+ })
1757
3357
  } : {}
1758
3358
  })
1759
3359
  ])))
@@ -1761,6 +3361,79 @@ function defineAccess(input) {
1761
3361
  };
1762
3362
  return Object.freeze(config);
1763
3363
  }
3364
+ function inferHierarchy(entities, childToParent, parentToChildren) {
3365
+ const inInheritance = new Set;
3366
+ for (const [child, parent] of childToParent.entries()) {
3367
+ inInheritance.add(child);
3368
+ inInheritance.add(parent);
3369
+ }
3370
+ if (inInheritance.size === 0) {
3371
+ return Object.keys(entities);
3372
+ }
3373
+ const roots = [];
3374
+ for (const entity of inInheritance) {
3375
+ if (!childToParent.has(entity)) {
3376
+ roots.push(entity);
3377
+ }
3378
+ }
3379
+ const hierarchy = [];
3380
+ const visited = new Set;
3381
+ function walkChain(entity) {
3382
+ if (visited.has(entity))
3383
+ return;
3384
+ visited.add(entity);
3385
+ hierarchy.push(entity);
3386
+ const children = parentToChildren.get(entity);
3387
+ if (children) {
3388
+ for (const child of children) {
3389
+ walkChain(child);
3390
+ }
3391
+ }
3392
+ }
3393
+ for (const root of roots) {
3394
+ walkChain(root);
3395
+ }
3396
+ for (const entityName of Object.keys(entities)) {
3397
+ if (!visited.has(entityName)) {
3398
+ hierarchy.push(entityName);
3399
+ }
3400
+ }
3401
+ return hierarchy;
3402
+ }
3403
+ function detectCycles(childToParent) {
3404
+ const visited = new Set;
3405
+ const inPath = new Set;
3406
+ for (const entity of childToParent.keys()) {
3407
+ if (!visited.has(entity)) {
3408
+ walkForCycles(entity, childToParent, visited, inPath);
3409
+ }
3410
+ }
3411
+ }
3412
+ function walkForCycles(entity, childToParent, visited, inPath) {
3413
+ if (inPath.has(entity)) {
3414
+ throw new Error("Circular inheritance detected");
3415
+ }
3416
+ if (visited.has(entity))
3417
+ return;
3418
+ inPath.add(entity);
3419
+ const parent = childToParent.get(entity);
3420
+ if (parent) {
3421
+ walkForCycles(parent, childToParent, visited, inPath);
3422
+ }
3423
+ inPath.delete(entity);
3424
+ visited.add(entity);
3425
+ }
3426
+ function createRuleContext() {
3427
+ return {
3428
+ where(conditions) {
3429
+ return { type: "where", conditions: Object.freeze({ ...conditions }) };
3430
+ },
3431
+ user: {
3432
+ id: Object.freeze({ __marker: "user.id" }),
3433
+ tenantId: Object.freeze({ __marker: "user.tenantId" })
3434
+ }
3435
+ };
3436
+ }
1764
3437
  // src/auth/entity-access.ts
1765
3438
  async function computeEntityAccess(entitlements, entity, accessContext) {
1766
3439
  const results = {};
@@ -1775,23 +3448,23 @@ async function computeEntityAccess(entitlements, entity, accessContext) {
1775
3448
  // src/auth/flag-store.ts
1776
3449
  class InMemoryFlagStore {
1777
3450
  flags = new Map;
1778
- setFlag(orgId, flag, enabled) {
1779
- let orgFlags = this.flags.get(orgId);
1780
- if (!orgFlags) {
1781
- orgFlags = new Map;
1782
- this.flags.set(orgId, orgFlags);
3451
+ setFlag(tenantId, flag, enabled) {
3452
+ let tenantFlags = this.flags.get(tenantId);
3453
+ if (!tenantFlags) {
3454
+ tenantFlags = new Map;
3455
+ this.flags.set(tenantId, tenantFlags);
1783
3456
  }
1784
- orgFlags.set(flag, enabled);
3457
+ tenantFlags.set(flag, enabled);
1785
3458
  }
1786
- getFlag(orgId, flag) {
1787
- return this.flags.get(orgId)?.get(flag) ?? false;
3459
+ getFlag(tenantId, flag) {
3460
+ return this.flags.get(tenantId)?.get(flag) ?? false;
1788
3461
  }
1789
- getFlags(orgId) {
1790
- const orgFlags = this.flags.get(orgId);
1791
- if (!orgFlags)
3462
+ getFlags(tenantId) {
3463
+ const tenantFlags = this.flags.get(tenantId);
3464
+ if (!tenantFlags)
1792
3465
  return {};
1793
3466
  const result = {};
1794
- for (const [key, value] of orgFlags) {
3467
+ for (const [key, value] of tenantFlags) {
1795
3468
  result[key] = value;
1796
3469
  }
1797
3470
  return result;
@@ -1804,6 +3477,385 @@ function checkFva(payload, maxAgeSeconds) {
1804
3477
  const now = Math.floor(Date.now() / 1000);
1805
3478
  return now - payload.fva < maxAgeSeconds;
1806
3479
  }
3480
+ // src/auth/grandfathering-store.ts
3481
+ class InMemoryGrandfatheringStore {
3482
+ states = new Map;
3483
+ async setGrandfathered(tenantId, planId, version, graceEnds) {
3484
+ this.states.set(`${tenantId}:${planId}`, { tenantId, planId, version, graceEnds });
3485
+ }
3486
+ async getGrandfathered(tenantId, planId) {
3487
+ return this.states.get(`${tenantId}:${planId}`) ?? null;
3488
+ }
3489
+ async listGrandfathered(planId) {
3490
+ const result = [];
3491
+ for (const state of this.states.values()) {
3492
+ if (state.planId === planId) {
3493
+ result.push(state);
3494
+ }
3495
+ }
3496
+ return result;
3497
+ }
3498
+ async removeGrandfathered(tenantId, planId) {
3499
+ this.states.delete(`${tenantId}:${planId}`);
3500
+ }
3501
+ dispose() {
3502
+ this.states.clear();
3503
+ }
3504
+ }
3505
+ // src/auth/override-store.ts
3506
+ class InMemoryOverrideStore {
3507
+ overrides = new Map;
3508
+ async set(tenantId, overrides) {
3509
+ const existing = this.overrides.get(tenantId) ?? {};
3510
+ if (overrides.features) {
3511
+ const featureSet = new Set(existing.features ?? []);
3512
+ for (const f of overrides.features) {
3513
+ featureSet.add(f);
3514
+ }
3515
+ existing.features = [...featureSet];
3516
+ }
3517
+ if (overrides.limits) {
3518
+ existing.limits = { ...existing.limits, ...overrides.limits };
3519
+ }
3520
+ this.overrides.set(tenantId, existing);
3521
+ }
3522
+ async remove(tenantId, keys) {
3523
+ const existing = this.overrides.get(tenantId);
3524
+ if (!existing)
3525
+ return;
3526
+ if (keys.features && existing.features) {
3527
+ const removeSet = new Set(keys.features);
3528
+ existing.features = existing.features.filter((f) => !removeSet.has(f));
3529
+ if (existing.features.length === 0) {
3530
+ delete existing.features;
3531
+ }
3532
+ }
3533
+ if (keys.limits && existing.limits) {
3534
+ for (const key of keys.limits) {
3535
+ delete existing.limits[key];
3536
+ }
3537
+ if (Object.keys(existing.limits).length === 0) {
3538
+ delete existing.limits;
3539
+ }
3540
+ }
3541
+ if (!existing.features && !existing.limits) {
3542
+ this.overrides.delete(tenantId);
3543
+ }
3544
+ }
3545
+ async get(tenantId) {
3546
+ return this.overrides.get(tenantId) ?? null;
3547
+ }
3548
+ dispose() {
3549
+ this.overrides.clear();
3550
+ }
3551
+ }
3552
+ function validateOverrides(accessDef, overrides) {
3553
+ const allLimitKeys = new Set;
3554
+ if (accessDef.plans) {
3555
+ for (const planDef of Object.values(accessDef.plans)) {
3556
+ if (planDef.limits) {
3557
+ for (const key of Object.keys(planDef.limits)) {
3558
+ allLimitKeys.add(key);
3559
+ }
3560
+ }
3561
+ }
3562
+ }
3563
+ if (overrides.limits) {
3564
+ for (const [key, limitDef] of Object.entries(overrides.limits)) {
3565
+ if (!allLimitKeys.has(key)) {
3566
+ throw new Error(`Override limit key '${key}' is not defined in any plan`);
3567
+ }
3568
+ if (limitDef.max !== undefined) {
3569
+ if (limitDef.max < -1) {
3570
+ throw new Error(`Override limit '${key}' max must be -1 (unlimited), 0 (disabled), or a positive integer, got ${limitDef.max}`);
3571
+ }
3572
+ }
3573
+ if (limitDef.add !== undefined) {
3574
+ if (!Number.isFinite(limitDef.add)) {
3575
+ throw new Error(`Override limit '${key}' add must be a finite integer, got ${limitDef.add}`);
3576
+ }
3577
+ if (!Number.isInteger(limitDef.add)) {
3578
+ throw new Error(`Override limit '${key}' add must be an integer, got ${limitDef.add}`);
3579
+ }
3580
+ }
3581
+ }
3582
+ }
3583
+ if (overrides.features) {
3584
+ for (const feature of overrides.features) {
3585
+ if (!accessDef.entitlements[feature]) {
3586
+ throw new Error(`Override feature '${feature}' is not a defined entitlement`);
3587
+ }
3588
+ }
3589
+ }
3590
+ }
3591
+ // src/auth/plan-hash.ts
3592
+ async function computePlanHash(config) {
3593
+ const canonical = {
3594
+ features: config.features ?? [],
3595
+ limits: config.limits ?? {},
3596
+ price: config.price ?? null
3597
+ };
3598
+ const json = JSON.stringify(canonical, sortedReplacer);
3599
+ return sha256Hex(json);
3600
+ }
3601
+ function sortedReplacer(_key, value) {
3602
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
3603
+ const sorted = {};
3604
+ for (const k of Object.keys(value).sort()) {
3605
+ sorted[k] = value[k];
3606
+ }
3607
+ return sorted;
3608
+ }
3609
+ return value;
3610
+ }
3611
+ // src/auth/plan-manager.ts
3612
+ var GRACE_DURATION_MS = {
3613
+ "1m": 30 * 24 * 60 * 60 * 1000,
3614
+ "3m": 90 * 24 * 60 * 60 * 1000,
3615
+ "6m": 180 * 24 * 60 * 60 * 1000,
3616
+ "12m": 365 * 24 * 60 * 60 * 1000
3617
+ };
3618
+ function resolveGraceEnd(planDef, now) {
3619
+ const grace = planDef.grandfathering?.grace;
3620
+ if (grace === "indefinite")
3621
+ return null;
3622
+ if (grace && GRACE_DURATION_MS[grace]) {
3623
+ return new Date(now.getTime() + GRACE_DURATION_MS[grace]);
3624
+ }
3625
+ const interval = planDef.price?.interval;
3626
+ if (interval === "year") {
3627
+ return new Date(now.getTime() + GRACE_DURATION_MS["3m"]);
3628
+ }
3629
+ return new Date(now.getTime() + GRACE_DURATION_MS["1m"]);
3630
+ }
3631
+ function createPlanManager(config) {
3632
+ const { plans, versionStore, grandfatheringStore, subscriptionStore } = config;
3633
+ const clock = config.clock ?? (() => new Date);
3634
+ const handlers = [];
3635
+ function emit(event) {
3636
+ for (const handler of handlers) {
3637
+ handler(event);
3638
+ }
3639
+ }
3640
+ function extractSnapshot(planDef) {
3641
+ return {
3642
+ features: planDef.features ? [...planDef.features] : [],
3643
+ limits: planDef.limits ? { ...planDef.limits } : {},
3644
+ price: planDef.price ? { ...planDef.price } : null
3645
+ };
3646
+ }
3647
+ async function initialize() {
3648
+ const now = clock();
3649
+ for (const [planId, planDef] of Object.entries(plans)) {
3650
+ if (planDef.addOn)
3651
+ continue;
3652
+ const snapshot = extractSnapshot(planDef);
3653
+ const hash = await computePlanHash(snapshot);
3654
+ const currentHash = await versionStore.getCurrentHash(planId);
3655
+ if (currentHash === hash)
3656
+ continue;
3657
+ const version = await versionStore.createVersion(planId, hash, snapshot);
3658
+ const currentVersion = version;
3659
+ emit({
3660
+ type: "plan:version_created",
3661
+ planId,
3662
+ version,
3663
+ currentVersion,
3664
+ timestamp: now
3665
+ });
3666
+ if (version > 1) {
3667
+ const grandfatheredTenants = await listTenantsOnPlan(planId);
3668
+ const graceEnds = resolveGraceEnd(planDef, now);
3669
+ for (const tenantId of grandfatheredTenants) {
3670
+ const tenantVersion = await versionStore.getTenantVersion(tenantId, planId);
3671
+ if (tenantVersion !== null && tenantVersion < version) {
3672
+ await grandfatheringStore.setGrandfathered(tenantId, planId, tenantVersion, graceEnds);
3673
+ }
3674
+ }
3675
+ }
3676
+ }
3677
+ }
3678
+ async function listTenantsOnPlan(planId) {
3679
+ if (subscriptionStore.listByPlan) {
3680
+ return subscriptionStore.listByPlan(planId);
3681
+ }
3682
+ return [];
3683
+ }
3684
+ async function migrate(planId, opts) {
3685
+ const now = clock();
3686
+ const currentVersion = await versionStore.getCurrentVersion(planId);
3687
+ if (currentVersion === null)
3688
+ return;
3689
+ if (opts?.tenantId) {
3690
+ await migrateTenant(opts.tenantId, planId, currentVersion, now);
3691
+ return;
3692
+ }
3693
+ const grandfatheredList = await grandfatheringStore.listGrandfathered(planId);
3694
+ for (const state of grandfatheredList) {
3695
+ if (state.graceEnds === null)
3696
+ continue;
3697
+ if (state.graceEnds.getTime() <= now.getTime()) {
3698
+ await migrateTenant(state.tenantId, planId, currentVersion, now);
3699
+ }
3700
+ }
3701
+ }
3702
+ async function migrateTenant(tenantId, planId, targetVersion, now) {
3703
+ const previousVersion = await versionStore.getTenantVersion(tenantId, planId);
3704
+ if (previousVersion !== null) {
3705
+ const prevInfo = await versionStore.getVersion(planId, previousVersion);
3706
+ const targetInfo = await versionStore.getVersion(planId, targetVersion);
3707
+ if (prevInfo && targetInfo) {
3708
+ const prevFeatures = new Set(prevInfo.snapshot.features);
3709
+ const targetFeatures = new Set(targetInfo.snapshot.features);
3710
+ for (const f of prevFeatures) {
3711
+ if (!targetFeatures.has(f)) {
3712
+ break;
3713
+ }
3714
+ }
3715
+ }
3716
+ }
3717
+ await versionStore.setTenantVersion(tenantId, planId, targetVersion);
3718
+ await grandfatheringStore.removeGrandfathered(tenantId, planId);
3719
+ emit({
3720
+ type: "plan:migrated",
3721
+ planId,
3722
+ tenantId,
3723
+ version: targetVersion,
3724
+ previousVersion: previousVersion ?? undefined,
3725
+ timestamp: now
3726
+ });
3727
+ }
3728
+ async function schedule(planId, opts) {
3729
+ const at = typeof opts.at === "string" ? new Date(opts.at) : opts.at;
3730
+ const grandfatheredList = await grandfatheringStore.listGrandfathered(planId);
3731
+ for (const state of grandfatheredList) {
3732
+ await grandfatheringStore.setGrandfathered(state.tenantId, planId, state.version, at);
3733
+ }
3734
+ }
3735
+ async function resolve(tenantId) {
3736
+ const subscription = await subscriptionStore.get(tenantId);
3737
+ if (!subscription)
3738
+ return null;
3739
+ const planId = subscription.planId;
3740
+ const currentVersion = await versionStore.getCurrentVersion(planId);
3741
+ if (currentVersion === null)
3742
+ return null;
3743
+ const tenantVersion = await versionStore.getTenantVersion(tenantId, planId);
3744
+ const effectiveVersion = tenantVersion ?? currentVersion;
3745
+ const grandfatheringState = await grandfatheringStore.getGrandfathered(tenantId, planId);
3746
+ const versionInfo = await versionStore.getVersion(planId, effectiveVersion);
3747
+ if (!versionInfo)
3748
+ return null;
3749
+ return {
3750
+ planId,
3751
+ version: effectiveVersion,
3752
+ currentVersion,
3753
+ grandfathered: grandfatheringState !== null,
3754
+ graceEnds: grandfatheringState?.graceEnds ?? null,
3755
+ snapshot: versionInfo.snapshot
3756
+ };
3757
+ }
3758
+ async function grandfatheredFn(planId) {
3759
+ return grandfatheringStore.listGrandfathered(planId);
3760
+ }
3761
+ const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
3762
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
3763
+ async function checkGraceEvents() {
3764
+ const now = clock();
3765
+ for (const planId of Object.keys(plans)) {
3766
+ const grandfatheredList = await grandfatheringStore.listGrandfathered(planId);
3767
+ for (const state of grandfatheredList) {
3768
+ if (state.graceEnds === null)
3769
+ continue;
3770
+ const timeUntilGraceEnd = state.graceEnds.getTime() - now.getTime();
3771
+ if (timeUntilGraceEnd <= SEVEN_DAYS_MS && timeUntilGraceEnd > 0) {
3772
+ emit({
3773
+ type: "plan:grace_expiring",
3774
+ planId,
3775
+ tenantId: state.tenantId,
3776
+ version: state.version,
3777
+ graceEnds: state.graceEnds,
3778
+ timestamp: now
3779
+ });
3780
+ } else if (timeUntilGraceEnd <= THIRTY_DAYS_MS && timeUntilGraceEnd > SEVEN_DAYS_MS) {
3781
+ emit({
3782
+ type: "plan:grace_approaching",
3783
+ planId,
3784
+ tenantId: state.tenantId,
3785
+ version: state.version,
3786
+ graceEnds: state.graceEnds,
3787
+ timestamp: now
3788
+ });
3789
+ }
3790
+ }
3791
+ }
3792
+ }
3793
+ function on(handler) {
3794
+ handlers.push(handler);
3795
+ }
3796
+ function off(handler) {
3797
+ const idx = handlers.indexOf(handler);
3798
+ if (idx >= 0)
3799
+ handlers.splice(idx, 1);
3800
+ }
3801
+ return {
3802
+ initialize,
3803
+ migrate,
3804
+ schedule,
3805
+ resolve,
3806
+ grandfathered: grandfatheredFn,
3807
+ checkGraceEvents,
3808
+ on,
3809
+ off
3810
+ };
3811
+ }
3812
+ // src/auth/plan-version-store.ts
3813
+ class InMemoryPlanVersionStore {
3814
+ versions = new Map;
3815
+ tenantVersions = new Map;
3816
+ async createVersion(planId, hash, snapshot) {
3817
+ const planVersions = this.versions.get(planId) ?? [];
3818
+ const version = planVersions.length + 1;
3819
+ const info = {
3820
+ planId,
3821
+ version,
3822
+ hash,
3823
+ snapshot,
3824
+ createdAt: new Date
3825
+ };
3826
+ planVersions.push(info);
3827
+ this.versions.set(planId, planVersions);
3828
+ return version;
3829
+ }
3830
+ async getCurrentVersion(planId) {
3831
+ const planVersions = this.versions.get(planId);
3832
+ if (!planVersions || planVersions.length === 0)
3833
+ return null;
3834
+ return planVersions.length;
3835
+ }
3836
+ async getVersion(planId, version) {
3837
+ const planVersions = this.versions.get(planId);
3838
+ if (!planVersions)
3839
+ return null;
3840
+ return planVersions[version - 1] ?? null;
3841
+ }
3842
+ async getTenantVersion(tenantId, planId) {
3843
+ return this.tenantVersions.get(`${tenantId}:${planId}`) ?? null;
3844
+ }
3845
+ async setTenantVersion(tenantId, planId, version) {
3846
+ this.tenantVersions.set(`${tenantId}:${planId}`, version);
3847
+ }
3848
+ async getCurrentHash(planId) {
3849
+ const planVersions = this.versions.get(planId);
3850
+ if (!planVersions || planVersions.length === 0)
3851
+ return null;
3852
+ return planVersions[planVersions.length - 1].hash;
3853
+ }
3854
+ dispose() {
3855
+ this.versions.clear();
3856
+ this.tenantVersions.clear();
3857
+ }
3858
+ }
1807
3859
  // src/auth/providers/discord.ts
1808
3860
  var AUTHORIZATION_URL = "https://discord.com/api/oauth2/authorize";
1809
3861
  var TOKEN_URL = "https://discord.com/api/oauth2/token";
@@ -1862,8 +3914,7 @@ function discord(config) {
1862
3914
  providerId: data.id,
1863
3915
  email: data.email ?? "",
1864
3916
  emailVerified: data.verified ?? false,
1865
- name: data.global_name ?? data.username,
1866
- avatarUrl: data.avatar ? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png` : undefined
3917
+ raw: data
1867
3918
  };
1868
3919
  }
1869
3920
  };
@@ -1905,6 +3956,9 @@ function github(config) {
1905
3956
  })
1906
3957
  });
1907
3958
  const data = await response.json();
3959
+ if (data.error) {
3960
+ throw new Error(`GitHub token exchange failed: ${data.error} — ${data.error_description}`);
3961
+ }
1908
3962
  return {
1909
3963
  accessToken: data.access_token
1910
3964
  };
@@ -1933,8 +3987,7 @@ function github(config) {
1933
3987
  providerId: String(userData.id),
1934
3988
  email: email?.email ?? "",
1935
3989
  emailVerified: email?.verified ?? false,
1936
- name: userData.name,
1937
- avatarUrl: userData.avatar_url
3990
+ raw: userData
1938
3991
  };
1939
3992
  }
1940
3993
  };
@@ -2006,8 +4059,7 @@ function google(config) {
2006
4059
  providerId: payload.sub,
2007
4060
  email: payload.email,
2008
4061
  emailVerified: payload.email_verified,
2009
- name: payload.name,
2010
- avatarUrl: payload.picture
4062
+ raw: payload
2011
4063
  };
2012
4064
  }
2013
4065
  };
@@ -2043,7 +4095,7 @@ class InMemoryRoleAssignmentStore {
2043
4095
  continue;
2044
4096
  const ancestorRoles = await this.getRoles(userId, ancestor.type, ancestor.id);
2045
4097
  for (const ancestorRole of ancestorRoles) {
2046
- const inheritedRole = resolveInheritedRole2(ancestor.type, ancestorRole, resourceType, accessDef);
4098
+ const inheritedRole = resolveInheritedRole(ancestor.type, ancestorRole, resourceType, accessDef);
2047
4099
  if (inheritedRole) {
2048
4100
  candidateRoles.push(inheritedRole);
2049
4101
  }
@@ -2065,28 +4117,13 @@ class InMemoryRoleAssignmentStore {
2065
4117
  this.assignments = [];
2066
4118
  }
2067
4119
  }
2068
- function resolveInheritedRole2(sourceType, sourceRole, targetType, accessDef) {
2069
- const hierarchy = accessDef.hierarchy;
2070
- const sourceIdx = hierarchy.indexOf(sourceType);
2071
- const targetIdx = hierarchy.indexOf(targetType);
2072
- if (sourceIdx === -1 || targetIdx === -1 || sourceIdx >= targetIdx)
2073
- return null;
2074
- let currentRole = sourceRole;
2075
- for (let i = sourceIdx;i < targetIdx; i++) {
2076
- const currentType = hierarchy[i];
2077
- const inheritanceMap = accessDef.inheritance[currentType];
2078
- if (!inheritanceMap || !(currentRole in inheritanceMap))
2079
- return null;
2080
- currentRole = inheritanceMap[currentRole];
2081
- }
2082
- return currentRole;
2083
- }
2084
4120
  // src/auth/rules.ts
2085
4121
  var userMarkers = {
2086
4122
  id: Object.freeze({ __marker: "user.id" }),
2087
4123
  tenantId: Object.freeze({ __marker: "user.tenantId" })
2088
4124
  };
2089
4125
  var rules = {
4126
+ public: Object.freeze({ type: "public" }),
2090
4127
  role(...roleNames) {
2091
4128
  return { type: "role", roles: Object.freeze([...roleNames]) };
2092
4129
  },
@@ -2113,13 +4150,13 @@ var rules = {
2113
4150
  // src/auth/wallet-store.ts
2114
4151
  class InMemoryWalletStore {
2115
4152
  entries = new Map;
2116
- key(orgId, entitlement, periodStart) {
2117
- return `${orgId}:${entitlement}:${periodStart.getTime()}`;
4153
+ key(tenantId, entitlement, periodStart) {
4154
+ return `${tenantId}:${entitlement}:${periodStart.getTime()}`;
2118
4155
  }
2119
- async consume(orgId, entitlement, periodStart, periodEnd, limit, amount = 1) {
2120
- const k = this.key(orgId, entitlement, periodStart);
4156
+ async consume(tenantId, entitlement, periodStart, periodEnd, limit, amount = 1) {
4157
+ const k = this.key(tenantId, entitlement, periodStart);
2121
4158
  if (!this.entries.has(k)) {
2122
- this.entries.set(k, { orgId, entitlement, periodStart, periodEnd, consumed: 0 });
4159
+ this.entries.set(k, { tenantId, entitlement, periodStart, periodEnd, consumed: 0 });
2123
4160
  }
2124
4161
  const entry = this.entries.get(k);
2125
4162
  if (entry.consumed + amount > limit) {
@@ -2138,15 +4175,15 @@ class InMemoryWalletStore {
2138
4175
  remaining: limit - entry.consumed
2139
4176
  };
2140
4177
  }
2141
- async unconsume(orgId, entitlement, periodStart, _periodEnd, amount = 1) {
2142
- const k = this.key(orgId, entitlement, periodStart);
4178
+ async unconsume(tenantId, entitlement, periodStart, _periodEnd, amount = 1) {
4179
+ const k = this.key(tenantId, entitlement, periodStart);
2143
4180
  const entry = this.entries.get(k);
2144
4181
  if (!entry)
2145
4182
  return;
2146
4183
  entry.consumed = Math.max(0, entry.consumed - amount);
2147
4184
  }
2148
- async getConsumption(orgId, entitlement, periodStart, _periodEnd) {
2149
- const k = this.key(orgId, entitlement, periodStart);
4185
+ async getConsumption(tenantId, entitlement, periodStart, _periodEnd) {
4186
+ const k = this.key(tenantId, entitlement, periodStart);
2150
4187
  const entry = this.entries.get(k);
2151
4188
  return entry?.consumed ?? 0;
2152
4189
  }
@@ -2185,7 +4222,12 @@ function createAuth(config) {
2185
4222
  if (session.strategy !== "jwt") {
2186
4223
  throw new Error(`Session strategy "${session.strategy}" is not yet supported. Use "jwt".`);
2187
4224
  }
2188
- const cookieConfig = { ...DEFAULT_COOKIE_CONFIG, ...session.cookie };
4225
+ const ttlSeconds = Math.floor(parseDuration(session.ttl) / 1000);
4226
+ const cookieConfig = {
4227
+ ...DEFAULT_COOKIE_CONFIG,
4228
+ maxAge: ttlSeconds,
4229
+ ...session.cookie
4230
+ };
2189
4231
  const refreshName = session.refreshName ?? "vertz.ref";
2190
4232
  const refreshTtlMs = parseDuration(session.refreshTtl ?? "7d");
2191
4233
  const refreshMaxAge = Math.floor(refreshTtlMs / 1000);
@@ -2198,7 +4240,7 @@ function createAuth(config) {
2198
4240
  if (!isProduction && cookieConfig.secure === false) {
2199
4241
  console.warn("Cookie 'secure' flag is disabled. This is allowed in development but must be enabled in production.");
2200
4242
  }
2201
- const ttlMs = parseDuration(session.ttl);
4243
+ const ttlMs = ttlSeconds * 1000;
2202
4244
  const DUMMY_HASH = "$2a$12$000000000000000000000uGWDREoC/y2KhZ5l2QkI4j0LpDjWcaq";
2203
4245
  const sessionStore = config.sessionStore ?? new InMemorySessionStore;
2204
4246
  const userStore = config.userStore ?? new InMemoryUserStore;
@@ -2228,6 +4270,9 @@ function createAuth(config) {
2228
4270
  const passwordResetTtlMs = parseDuration(passwordResetConfig?.tokenTtl ?? "1h");
2229
4271
  const revokeSessionsOnReset = passwordResetConfig?.revokeSessionsOnReset ?? true;
2230
4272
  const passwordResetStore = config.passwordResetStore ?? (passwordResetEnabled ? new InMemoryPasswordResetStore : undefined);
4273
+ const tenantConfig = config.tenant;
4274
+ const onUserCreated = config.onUserCreated;
4275
+ const entityProxy = config._entityProxy ?? {};
2231
4276
  const rateLimitStore = config.rateLimitStore ?? new InMemoryRateLimitStore;
2232
4277
  const signInWindowMs = parseDuration(emailPassword?.rateLimit?.window || "15m");
2233
4278
  const signUpWindowMs = parseDuration("1h");
@@ -2237,6 +4282,7 @@ function createAuth(config) {
2237
4282
  const expiresAt = new Date(Date.now() + ttlMs);
2238
4283
  const userClaims = claims ? claims(user) : {};
2239
4284
  const fvaClaim = options?.fva !== undefined ? { fva: options.fva } : {};
4285
+ const tenantClaim = options?.tenantId ? { tenantId: options.tenantId } : {};
2240
4286
  let aclClaim = {};
2241
4287
  if (config.access) {
2242
4288
  const accessSet = await computeAccessSet({
@@ -2245,7 +4291,8 @@ function createAuth(config) {
2245
4291
  roleStore: config.access.roleStore,
2246
4292
  closureStore: config.access.closureStore,
2247
4293
  flagStore: config.access.flagStore,
2248
- plan: user.plan ?? null
4294
+ subscriptionStore: config.access?.subscriptionStore,
4295
+ tenantId: null
2249
4296
  });
2250
4297
  const encoded = encodeAccessSet(accessSet);
2251
4298
  const canonicalJson = JSON.stringify(encoded);
@@ -2265,6 +4312,7 @@ function createAuth(config) {
2265
4312
  const jwt = await createJWT(user, jwtSecret, ttlMs, jwtAlgorithm, () => ({
2266
4313
  ...userClaims,
2267
4314
  ...fvaClaim,
4315
+ ...tenantClaim,
2268
4316
  ...aclClaim,
2269
4317
  jti,
2270
4318
  sid: sessionId
@@ -2280,16 +4328,24 @@ function createAuth(config) {
2280
4328
  sid: sessionId,
2281
4329
  claims: userClaims || undefined,
2282
4330
  ...options?.fva !== undefined ? { fva: options.fva } : {},
4331
+ ...options?.tenantId ? { tenantId: options.tenantId } : {},
2283
4332
  ...aclClaim
2284
4333
  };
2285
4334
  return { jwt, refreshToken, payload, expiresAt };
2286
4335
  }
4336
+ const FORGOT_PASSWORD_MIN_RESPONSE_MS = 15;
2287
4337
  function generateToken() {
2288
4338
  const bytes = crypto.getRandomValues(new Uint8Array(32));
2289
4339
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2290
4340
  }
4341
+ async function waitForMinimumDuration(startedAt, minDurationMs) {
4342
+ const remaining = minDurationMs - (Date.now() - startedAt);
4343
+ if (remaining > 0) {
4344
+ await new Promise((resolve) => setTimeout(resolve, remaining));
4345
+ }
4346
+ }
2291
4347
  async function signUp(data, ctx) {
2292
- const { email, password, role = "user", ...additionalFields } = data;
4348
+ const { email, password, ...additionalFields } = data;
2293
4349
  if (!email || !email.includes("@")) {
2294
4350
  return err(createAuthValidationError2("Invalid email format", "email", "INVALID_FORMAT"));
2295
4351
  }
@@ -2311,18 +4367,37 @@ function createAuth(config) {
2311
4367
  id: _id,
2312
4368
  createdAt: _c,
2313
4369
  updatedAt: _u,
4370
+ role: _role,
4371
+ emailVerified: _emailVerified,
2314
4372
  ...safeFields
2315
4373
  } = additionalFields;
2316
4374
  const user = {
2317
- ...safeFields,
2318
4375
  id: crypto.randomUUID(),
2319
4376
  email: email.toLowerCase(),
2320
- role,
4377
+ role: "user",
2321
4378
  emailVerified: !emailVerificationEnabled,
2322
4379
  createdAt: now,
2323
4380
  updatedAt: now
2324
4381
  };
2325
4382
  await userStore.createUser(user, passwordHash);
4383
+ if (onUserCreated) {
4384
+ const callbackCtx = { entities: entityProxy };
4385
+ const payload = {
4386
+ user,
4387
+ provider: null,
4388
+ signUpData: { ...safeFields }
4389
+ };
4390
+ try {
4391
+ await onUserCreated(payload, callbackCtx);
4392
+ } catch (callbackErr) {
4393
+ try {
4394
+ await userStore.deleteUser(user.id);
4395
+ } catch (rollbackErr) {
4396
+ console.error("[Auth] Failed to rollback user after onUserCreated failure:", rollbackErr);
4397
+ }
4398
+ return err(createAuthValidationError2("User setup failed", "general", "CALLBACK_FAILED"));
4399
+ }
4400
+ }
2326
4401
  if (emailVerificationEnabled && emailVerificationStore && emailVerificationConfig?.onSend) {
2327
4402
  const token = generateToken();
2328
4403
  const tokenHash = await sha256Hex(token);
@@ -2414,6 +4489,10 @@ function createAuth(config) {
2414
4489
  if (!payload) {
2415
4490
  return ok(null);
2416
4491
  }
4492
+ const storedSession = await sessionStore.findActiveSessionById(payload.sid);
4493
+ if (!storedSession || storedSession.userId !== payload.sub) {
4494
+ return ok(null);
4495
+ }
2417
4496
  const user = await userStore.findById(payload.sub);
2418
4497
  if (!user) {
2419
4498
  return ok(null);
@@ -2496,16 +4575,16 @@ function createAuth(config) {
2496
4575
  }
2497
4576
  const currentSid = sessionResult.data.payload.sid;
2498
4577
  const sessions = await sessionStore.listActiveSessions(sessionResult.data.user.id);
2499
- const infos = sessions.map((s) => ({
2500
- id: s.id,
2501
- userId: s.userId,
2502
- ipAddress: s.ipAddress,
2503
- userAgent: s.userAgent,
2504
- deviceName: parseDeviceName(s.userAgent),
2505
- createdAt: s.createdAt,
2506
- lastActiveAt: s.lastActiveAt,
2507
- expiresAt: s.expiresAt,
2508
- isCurrent: s.id === currentSid
4578
+ const infos = sessions.map((s2) => ({
4579
+ id: s2.id,
4580
+ userId: s2.userId,
4581
+ ipAddress: s2.ipAddress,
4582
+ userAgent: s2.userAgent,
4583
+ deviceName: parseDeviceName(s2.userAgent),
4584
+ createdAt: s2.createdAt,
4585
+ lastActiveAt: s2.lastActiveAt,
4586
+ expiresAt: s2.expiresAt,
4587
+ isCurrent: s2.id === currentSid
2509
4588
  }));
2510
4589
  return ok(infos);
2511
4590
  }
@@ -2517,7 +4596,7 @@ function createAuth(config) {
2517
4596
  return err(createSessionExpiredError("Not authenticated"));
2518
4597
  }
2519
4598
  const targetSessions = await sessionStore.listActiveSessions(sessionResult.data.user.id);
2520
- const target = targetSessions.find((s) => s.id === sessionId);
4599
+ const target = targetSessions.find((s2) => s2.id === sessionId);
2521
4600
  if (!target) {
2522
4601
  return err(createSessionNotFoundError("Session not found"));
2523
4602
  }
@@ -2533,9 +4612,9 @@ function createAuth(config) {
2533
4612
  }
2534
4613
  const currentSid = sessionResult.data.payload.sid;
2535
4614
  const sessions = await sessionStore.listActiveSessions(sessionResult.data.user.id);
2536
- for (const s of sessions) {
2537
- if (s.id !== currentSid) {
2538
- await sessionStore.revokeSession(s.id);
4615
+ for (const s2 of sessions) {
4616
+ if (s2.id !== currentSid) {
4617
+ await sessionStore.revokeSession(s2.id);
2539
4618
  }
2540
4619
  }
2541
4620
  return ok(undefined);
@@ -2568,6 +4647,32 @@ function createAuth(config) {
2568
4647
  "Referrer-Policy": "strict-origin-when-cross-origin"
2569
4648
  };
2570
4649
  }
4650
+ function authValidationResponse(message) {
4651
+ return new Response(JSON.stringify({
4652
+ error: {
4653
+ code: "AUTH_VALIDATION_ERROR",
4654
+ message
4655
+ }
4656
+ }), {
4657
+ status: 400,
4658
+ headers: { "Content-Type": "application/json", ...securityHeaders() }
4659
+ });
4660
+ }
4661
+ async function parseJsonAuthBody(request, schema) {
4662
+ try {
4663
+ const body = await parseBody(request);
4664
+ const result = schema.safeParse(body);
4665
+ if (!result.ok) {
4666
+ return { ok: false, response: authValidationResponse(result.error.message) };
4667
+ }
4668
+ return { ok: true, data: result.data };
4669
+ } catch (error) {
4670
+ if (error instanceof BadRequestException) {
4671
+ return { ok: false, response: authValidationResponse(error.message) };
4672
+ }
4673
+ throw error;
4674
+ }
4675
+ }
2571
4676
  async function handleAuthRequest(request) {
2572
4677
  const url = new URL(request.url);
2573
4678
  const path = url.pathname.replace("/api/auth", "") || "/";
@@ -2611,7 +4716,11 @@ function createAuth(config) {
2611
4716
  }
2612
4717
  try {
2613
4718
  if (method === "POST" && path === "/signup") {
2614
- const body = await request.json();
4719
+ const bodyResult = await parseJsonAuthBody(request, signUpInputSchema);
4720
+ if (!bodyResult.ok) {
4721
+ return bodyResult.response;
4722
+ }
4723
+ const body = bodyResult.data;
2615
4724
  const result = await signUp(body, { headers: request.headers });
2616
4725
  if (!result.ok) {
2617
4726
  return new Response(JSON.stringify({ error: result.error }), {
@@ -2636,7 +4745,11 @@ function createAuth(config) {
2636
4745
  });
2637
4746
  }
2638
4747
  if (method === "POST" && path === "/signin") {
2639
- const body = await request.json();
4748
+ const bodyResult = await parseJsonAuthBody(request, signInInputSchema);
4749
+ if (!bodyResult.ok) {
4750
+ return bodyResult.response;
4751
+ }
4752
+ const body = bodyResult.data;
2640
4753
  const result = await signIn(body, { headers: request.headers });
2641
4754
  if (!result.ok) {
2642
4755
  if (result.error.code === "MFA_REQUIRED" && oauthEncryptionKey) {
@@ -2725,7 +4838,8 @@ function createAuth(config) {
2725
4838
  roleStore: config.access.roleStore,
2726
4839
  closureStore: config.access.closureStore,
2727
4840
  flagStore: config.access.flagStore,
2728
- plan: sessionResult.data.user.plan ?? null
4841
+ subscriptionStore: config.access?.subscriptionStore,
4842
+ tenantId: sessionResult.data.payload?.tenantId ?? null
2729
4843
  });
2730
4844
  const encoded = encodeAccessSet(accessSet);
2731
4845
  const hashPayload = {
@@ -2741,8 +4855,9 @@ function createAuth(config) {
2741
4855
  status: 304,
2742
4856
  headers: {
2743
4857
  ETag: quotedEtag,
4858
+ Vary: "Cookie",
2744
4859
  ...securityHeaders(),
2745
- "Cache-Control": "no-cache"
4860
+ "Cache-Control": "private, no-cache"
2746
4861
  }
2747
4862
  });
2748
4863
  }
@@ -2751,8 +4866,9 @@ function createAuth(config) {
2751
4866
  headers: {
2752
4867
  "Content-Type": "application/json",
2753
4868
  ETag: quotedEtag,
4869
+ Vary: "Cookie",
2754
4870
  ...securityHeaders(),
2755
- "Cache-Control": "no-cache"
4871
+ "Cache-Control": "private, no-cache"
2756
4872
  }
2757
4873
  });
2758
4874
  }
@@ -2826,6 +4942,17 @@ function createAuth(config) {
2826
4942
  headers: { "Content-Type": "application/json", ...securityHeaders() }
2827
4943
  });
2828
4944
  }
4945
+ if (method === "GET" && path === "/providers") {
4946
+ const providerList = Array.from(providers.values()).map((p) => ({
4947
+ id: p.id,
4948
+ name: p.name,
4949
+ authUrl: `/api/auth/oauth/${p.id}`
4950
+ }));
4951
+ return new Response(JSON.stringify(providerList), {
4952
+ status: 200,
4953
+ headers: { "Content-Type": "application/json", ...securityHeaders() }
4954
+ });
4955
+ }
2829
4956
  if (method === "GET" && path.startsWith("/oauth/") && !path.includes("/callback")) {
2830
4957
  const providerId = path.replace("/oauth/", "");
2831
4958
  const provider = providers.get(providerId);
@@ -2872,12 +4999,17 @@ function createAuth(config) {
2872
4999
  if (method === "GET" && path.includes("/oauth/") && path.endsWith("/callback")) {
2873
5000
  const providerId = path.replace("/oauth/", "").replace("/callback", "");
2874
5001
  const provider = providers.get(providerId);
2875
- const errorRedirect = oauthErrorRedirect;
5002
+ const isAbsoluteUrl = /^https?:\/\//.test(oauthErrorRedirect);
5003
+ const errorUrl = (error) => {
5004
+ const url2 = new URL(oauthErrorRedirect, "http://localhost");
5005
+ url2.searchParams.set("error", error);
5006
+ return isAbsoluteUrl ? url2.toString() : url2.pathname + url2.search + url2.hash;
5007
+ };
2876
5008
  if (!provider || !oauthEncryptionKey || !oauthAccountStore) {
2877
5009
  return new Response(null, {
2878
5010
  status: 302,
2879
5011
  headers: {
2880
- Location: `${errorRedirect}?error=provider_not_configured`,
5012
+ Location: errorUrl("provider_not_configured"),
2881
5013
  ...securityHeaders()
2882
5014
  }
2883
5015
  });
@@ -2888,7 +5020,7 @@ function createAuth(config) {
2888
5020
  return new Response(null, {
2889
5021
  status: 302,
2890
5022
  headers: {
2891
- Location: `${errorRedirect}?error=invalid_state`,
5023
+ Location: errorUrl("invalid_state"),
2892
5024
  ...securityHeaders()
2893
5025
  }
2894
5026
  });
@@ -2898,7 +5030,7 @@ function createAuth(config) {
2898
5030
  return new Response(null, {
2899
5031
  status: 302,
2900
5032
  headers: {
2901
- Location: `${errorRedirect}?error=invalid_state`,
5033
+ Location: errorUrl("invalid_state"),
2902
5034
  ...securityHeaders()
2903
5035
  }
2904
5036
  });
@@ -2909,7 +5041,7 @@ function createAuth(config) {
2909
5041
  const providerError = url.searchParams.get("error");
2910
5042
  if (providerError) {
2911
5043
  const headers = new Headers({
2912
- Location: `${errorRedirect}?error=${encodeURIComponent(providerError)}`,
5044
+ Location: errorUrl(providerError),
2913
5045
  ...securityHeaders()
2914
5046
  });
2915
5047
  headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
@@ -2917,7 +5049,7 @@ function createAuth(config) {
2917
5049
  }
2918
5050
  if (stateData.state !== queryState || stateData.provider !== providerId) {
2919
5051
  const headers = new Headers({
2920
- Location: `${errorRedirect}?error=invalid_state`,
5052
+ Location: errorUrl("invalid_state"),
2921
5053
  ...securityHeaders()
2922
5054
  });
2923
5055
  headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
@@ -2925,7 +5057,7 @@ function createAuth(config) {
2925
5057
  }
2926
5058
  if (stateData.expiresAt < Date.now()) {
2927
5059
  const headers = new Headers({
2928
- Location: `${errorRedirect}?error=invalid_state`,
5060
+ Location: errorUrl("invalid_state"),
2929
5061
  ...securityHeaders()
2930
5062
  });
2931
5063
  headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
@@ -2952,7 +5084,7 @@ function createAuth(config) {
2952
5084
  if (!userId) {
2953
5085
  if (!userInfo.email || !userInfo.email.includes("@")) {
2954
5086
  const headers = new Headers({
2955
- Location: `${errorRedirect}?error=email_required`,
5087
+ Location: errorUrl("email_required"),
2956
5088
  ...securityHeaders()
2957
5089
  });
2958
5090
  headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
@@ -2962,6 +5094,7 @@ function createAuth(config) {
2962
5094
  const newUser = {
2963
5095
  id: crypto.randomUUID(),
2964
5096
  email: userInfo.email.toLowerCase(),
5097
+ emailVerified: userInfo.emailVerified,
2965
5098
  role: "user",
2966
5099
  createdAt: now,
2967
5100
  updatedAt: now
@@ -2969,6 +5102,34 @@ function createAuth(config) {
2969
5102
  await userStore.createUser(newUser, null);
2970
5103
  userId = newUser.id;
2971
5104
  await oauthAccountStore.linkAccount(userId, provider.id, userInfo.providerId, userInfo.email);
5105
+ if (onUserCreated) {
5106
+ const callbackCtx = { entities: entityProxy };
5107
+ const payload = {
5108
+ user: newUser,
5109
+ provider: { id: provider.id, name: provider.name },
5110
+ profile: userInfo.raw
5111
+ };
5112
+ try {
5113
+ await onUserCreated(payload, callbackCtx);
5114
+ } catch {
5115
+ try {
5116
+ await oauthAccountStore.unlinkAccount(userId, provider.id);
5117
+ } catch (rollbackErr) {
5118
+ console.error("[Auth] Failed to unlink OAuth account during rollback:", rollbackErr);
5119
+ }
5120
+ try {
5121
+ await userStore.deleteUser(userId);
5122
+ } catch (rollbackErr) {
5123
+ console.error("[Auth] Failed to delete user during rollback:", rollbackErr);
5124
+ }
5125
+ const headers = new Headers({
5126
+ Location: errorUrl("user_setup_failed"),
5127
+ ...securityHeaders()
5128
+ });
5129
+ headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
5130
+ return new Response(null, { status: 302, headers });
5131
+ }
5132
+ }
2972
5133
  }
2973
5134
  }
2974
5135
  const user = await userStore.findById(userId);
@@ -2976,7 +5137,7 @@ function createAuth(config) {
2976
5137
  return new Response(null, {
2977
5138
  status: 302,
2978
5139
  headers: {
2979
- Location: `${errorRedirect}?error=user_info_failed`,
5140
+ Location: errorUrl("user_info_failed"),
2980
5141
  ...securityHeaders()
2981
5142
  }
2982
5143
  });
@@ -2997,9 +5158,10 @@ function createAuth(config) {
2997
5158
  responseHeaders.append("Set-Cookie", buildSessionCookie(sessionTokens.jwt, cookieConfig));
2998
5159
  responseHeaders.append("Set-Cookie", buildRefreshCookie(sessionTokens.refreshToken, cookieConfig, refreshName, refreshMaxAge));
2999
5160
  return new Response(null, { status: 302, headers: responseHeaders });
3000
- } catch {
5161
+ } catch (oauthErr) {
5162
+ console.error("[Auth] OAuth callback error:", oauthErr);
3001
5163
  const headers = new Headers({
3002
- Location: `${errorRedirect}?error=token_exchange_failed`,
5164
+ Location: errorUrl("token_exchange_failed"),
3003
5165
  ...securityHeaders()
3004
5166
  });
3005
5167
  headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
@@ -3049,7 +5211,11 @@ function createAuth(config) {
3049
5211
  headers: { "Content-Type": "application/json", ...securityHeaders() }
3050
5212
  });
3051
5213
  }
3052
- const body = await request.json();
5214
+ const bodyResult = await parseJsonAuthBody(request, codeInputSchema);
5215
+ if (!bodyResult.ok) {
5216
+ return bodyResult.response;
5217
+ }
5218
+ const body = bodyResult.data;
3053
5219
  const userId = challengeData.userId;
3054
5220
  const encryptedSecret = await mfaStore.getSecret(userId);
3055
5221
  if (!encryptedSecret) {
@@ -3169,7 +5335,11 @@ function createAuth(config) {
3169
5335
  headers: { "Content-Type": "application/json", ...securityHeaders() }
3170
5336
  });
3171
5337
  }
3172
- const body = await request.json();
5338
+ const bodyResult = await parseJsonAuthBody(request, codeInputSchema);
5339
+ if (!bodyResult.ok) {
5340
+ return bodyResult.response;
5341
+ }
5342
+ const body = bodyResult.data;
3173
5343
  const valid = await verifyTotpCode(pendingEntry.secret, body.code);
3174
5344
  if (!valid) {
3175
5345
  return new Response(JSON.stringify({ error: createMfaInvalidCodeError() }), {
@@ -3203,7 +5373,11 @@ function createAuth(config) {
3203
5373
  });
3204
5374
  }
3205
5375
  const userId = sessionResult.data.user.id;
3206
- const body = await request.json();
5376
+ const bodyResult = await parseJsonAuthBody(request, passwordInputSchema);
5377
+ if (!bodyResult.ok) {
5378
+ return bodyResult.response;
5379
+ }
5380
+ const body = bodyResult.data;
3207
5381
  const stored = await userStore.findByEmail(sessionResult.data.user.email);
3208
5382
  if (!stored || !stored.passwordHash) {
3209
5383
  return new Response(JSON.stringify({ error: createInvalidCredentialsError() }), {
@@ -3238,7 +5412,11 @@ function createAuth(config) {
3238
5412
  headers: { "Content-Type": "application/json", ...securityHeaders() }
3239
5413
  });
3240
5414
  }
3241
- const body = await request.json();
5415
+ const bodyResult = await parseJsonAuthBody(request, passwordInputSchema);
5416
+ if (!bodyResult.ok) {
5417
+ return bodyResult.response;
5418
+ }
5419
+ const body = bodyResult.data;
3242
5420
  const stored = await userStore.findByEmail(sessionResult.data.user.email);
3243
5421
  if (!stored || !stored.passwordHash) {
3244
5422
  return new Response(JSON.stringify({ error: createInvalidCredentialsError() }), {
@@ -3325,7 +5503,11 @@ function createAuth(config) {
3325
5503
  headers: { "Content-Type": "application/json", ...securityHeaders() }
3326
5504
  });
3327
5505
  }
3328
- const body = await request.json();
5506
+ const bodyResult = await parseJsonAuthBody(request, codeInputSchema);
5507
+ if (!bodyResult.ok) {
5508
+ return bodyResult.response;
5509
+ }
5510
+ const body = bodyResult.data;
3329
5511
  const valid = await verifyTotpCode(totpSecret, body.code);
3330
5512
  if (!valid) {
3331
5513
  return new Response(JSON.stringify({ error: createMfaInvalidCodeError() }), {
@@ -3369,7 +5551,11 @@ function createAuth(config) {
3369
5551
  headers: { "Content-Type": "application/json", ...securityHeaders() }
3370
5552
  });
3371
5553
  }
3372
- const body = await request.json();
5554
+ const bodyResult = await parseJsonAuthBody(request, tokenInputSchema);
5555
+ if (!bodyResult.ok) {
5556
+ return bodyResult.response;
5557
+ }
5558
+ const body = bodyResult.data;
3373
5559
  if (!body.token) {
3374
5560
  return new Response(JSON.stringify({ error: createTokenInvalidError("Token is required") }), {
3375
5561
  status: 400,
@@ -3448,25 +5634,32 @@ function createAuth(config) {
3448
5634
  headers: { "Content-Type": "application/json", ...securityHeaders() }
3449
5635
  });
3450
5636
  }
3451
- const body = await request.json();
5637
+ const bodyResult = await parseJsonAuthBody(request, forgotPasswordInputSchema);
5638
+ if (!bodyResult.ok) {
5639
+ return bodyResult.response;
5640
+ }
5641
+ const body = bodyResult.data;
5642
+ const startedAt = Date.now();
3452
5643
  const forgotRateLimit = await rateLimitStore.check(`forgot-password:${(body.email ?? "").toLowerCase()}`, 3, parseDuration("1h"));
3453
5644
  if (!forgotRateLimit.allowed) {
5645
+ await waitForMinimumDuration(startedAt, FORGOT_PASSWORD_MIN_RESPONSE_MS);
3454
5646
  return new Response(JSON.stringify({ ok: true }), {
3455
5647
  status: 200,
3456
5648
  headers: { "Content-Type": "application/json", ...securityHeaders() }
3457
5649
  });
3458
5650
  }
5651
+ const token = generateToken();
5652
+ const tokenHash = await sha256Hex(token);
3459
5653
  const stored = await userStore.findByEmail((body.email ?? "").toLowerCase());
3460
5654
  if (stored) {
3461
- const token = generateToken();
3462
- const tokenHash = await sha256Hex(token);
3463
5655
  await passwordResetStore.createReset({
3464
5656
  userId: stored.user.id,
3465
5657
  tokenHash,
3466
5658
  expiresAt: new Date(Date.now() + passwordResetTtlMs)
3467
5659
  });
3468
- await passwordResetConfig.onSend(stored.user, token);
5660
+ passwordResetConfig.onSend(stored.user, token).catch((error) => console.error("[Auth] Failed to send password reset email", error));
3469
5661
  }
5662
+ await waitForMinimumDuration(startedAt, FORGOT_PASSWORD_MIN_RESPONSE_MS);
3470
5663
  return new Response(JSON.stringify({ ok: true }), {
3471
5664
  status: 200,
3472
5665
  headers: { "Content-Type": "application/json", ...securityHeaders() }
@@ -3481,7 +5674,11 @@ function createAuth(config) {
3481
5674
  headers: { "Content-Type": "application/json", ...securityHeaders() }
3482
5675
  });
3483
5676
  }
3484
- const body = await request.json();
5677
+ const bodyResult = await parseJsonAuthBody(request, resetPasswordInputSchema);
5678
+ if (!bodyResult.ok) {
5679
+ return bodyResult.response;
5680
+ }
5681
+ const body = bodyResult.data;
3485
5682
  if (!body.token) {
3486
5683
  return new Response(JSON.stringify({ error: createTokenInvalidError("Token is required") }), {
3487
5684
  status: 400,
@@ -3515,8 +5712,8 @@ function createAuth(config) {
3515
5712
  await passwordResetStore.deleteByUserId(resetRecord.userId);
3516
5713
  if (revokeSessionsOnReset) {
3517
5714
  const activeSessions = await sessionStore.listActiveSessions(resetRecord.userId);
3518
- for (const s of activeSessions) {
3519
- await sessionStore.revokeSession(s.id);
5715
+ for (const s2 of activeSessions) {
5716
+ await sessionStore.revokeSession(s2.id);
3520
5717
  }
3521
5718
  }
3522
5719
  return new Response(JSON.stringify({ ok: true }), {
@@ -3524,6 +5721,63 @@ function createAuth(config) {
3524
5721
  headers: { "Content-Type": "application/json", ...securityHeaders() }
3525
5722
  });
3526
5723
  }
5724
+ if (method === "POST" && path === "/switch-tenant") {
5725
+ if (!tenantConfig) {
5726
+ return new Response(JSON.stringify({ error: "Not found" }), {
5727
+ status: 404,
5728
+ headers: { "Content-Type": "application/json", ...securityHeaders() }
5729
+ });
5730
+ }
5731
+ const sessionResult = await getSession(request.headers);
5732
+ if (!sessionResult.ok || !sessionResult.data) {
5733
+ return new Response(JSON.stringify({ error: { code: "SESSION_EXPIRED", message: "Not authenticated" } }), {
5734
+ status: 401,
5735
+ headers: { "Content-Type": "application/json", ...securityHeaders() }
5736
+ });
5737
+ }
5738
+ const bodyResult = await parseJsonAuthBody(request, switchTenantInputSchema);
5739
+ if (!bodyResult.ok) {
5740
+ return bodyResult.response;
5741
+ }
5742
+ const { tenantId } = bodyResult.data;
5743
+ const userId = sessionResult.data.user.id;
5744
+ const hasMembership = await tenantConfig.verifyMembership(userId, tenantId);
5745
+ if (!hasMembership) {
5746
+ return new Response(JSON.stringify({
5747
+ error: { code: "AUTH_FORBIDDEN", message: "Not a member of this tenant" }
5748
+ }), {
5749
+ status: 403,
5750
+ headers: { "Content-Type": "application/json", ...securityHeaders() }
5751
+ });
5752
+ }
5753
+ const currentPayload = sessionResult.data.payload;
5754
+ const tokens = await createSessionTokens(sessionResult.data.user, currentPayload.sid, {
5755
+ fva: currentPayload.fva,
5756
+ tenantId
5757
+ });
5758
+ const refreshTokenHash = await sha256Hex(tokens.refreshToken);
5759
+ const previousRefreshHash = (await sessionStore.findActiveSessionById(currentPayload.sid))?.refreshTokenHash ?? "";
5760
+ await sessionStore.updateSession(currentPayload.sid, {
5761
+ refreshTokenHash,
5762
+ previousRefreshHash,
5763
+ lastActiveAt: new Date,
5764
+ currentTokens: { jwt: tokens.jwt, refreshToken: tokens.refreshToken }
5765
+ });
5766
+ const headers = new Headers({
5767
+ "Content-Type": "application/json",
5768
+ ...securityHeaders()
5769
+ });
5770
+ headers.append("Set-Cookie", buildSessionCookie(tokens.jwt, cookieConfig));
5771
+ headers.append("Set-Cookie", buildRefreshCookie(tokens.refreshToken, cookieConfig, refreshName, refreshMaxAge));
5772
+ return new Response(JSON.stringify({
5773
+ tenantId,
5774
+ user: sessionResult.data.user,
5775
+ expiresAt: tokens.expiresAt.getTime()
5776
+ }), {
5777
+ status: 200,
5778
+ headers
5779
+ });
5780
+ }
3527
5781
  return new Response(JSON.stringify({ error: "Not found" }), {
3528
5782
  status: 404,
3529
5783
  headers: { "Content-Type": "application/json" }
@@ -3574,9 +5828,49 @@ function createAuth(config) {
3574
5828
  emailVerificationStore?.dispose();
3575
5829
  passwordResetStore?.dispose();
3576
5830
  pendingMfaSecrets.clear();
5831
+ },
5832
+ resolveSessionForSSR: resolveSessionForSSR({
5833
+ jwtSecret,
5834
+ jwtAlgorithm,
5835
+ cookieName: cookieConfig.name || "vertz.sid"
5836
+ })
5837
+ };
5838
+ }
5839
+ // src/content/builders.ts
5840
+ function stringDescriptor(contentType) {
5841
+ return {
5842
+ _kind: "content",
5843
+ _contentType: contentType,
5844
+ parse(value) {
5845
+ if (typeof value === "string") {
5846
+ return { ok: true, data: value };
5847
+ }
5848
+ return { ok: false, error: new Error(`Expected string, got ${typeof value}`) };
3577
5849
  }
3578
5850
  };
3579
5851
  }
5852
+ var content = {
5853
+ xml: () => stringDescriptor("application/xml"),
5854
+ html: () => stringDescriptor("text/html"),
5855
+ text: () => stringDescriptor("text/plain"),
5856
+ binary: () => ({
5857
+ _kind: "content",
5858
+ _contentType: "application/octet-stream",
5859
+ parse(value) {
5860
+ if (value instanceof Uint8Array) {
5861
+ return { ok: true, data: value };
5862
+ }
5863
+ return {
5864
+ ok: false,
5865
+ error: new Error(`Expected Uint8Array, got ${typeof value}`)
5866
+ };
5867
+ }
5868
+ })
5869
+ };
5870
+ // src/content/content-descriptor.ts
5871
+ function isContentDescriptor(value) {
5872
+ return value != null && typeof value === "object" && "_kind" in value && value._kind === "content";
5873
+ }
3580
5874
  // src/create-server.ts
3581
5875
  import { createServer as coreCreateServer } from "@vertz/core";
3582
5876
  import {
@@ -3657,10 +5951,15 @@ function narrowRelationFields(relationsConfig, data) {
3657
5951
  if (config === undefined || config === true) {
3658
5952
  result[key] = value;
3659
5953
  } else if (config === false) {} else if (typeof config === "object" && value !== null && typeof value === "object") {
5954
+ const selectMap = config.select;
5955
+ if (!selectMap) {
5956
+ result[key] = value;
5957
+ continue;
5958
+ }
3660
5959
  if (Array.isArray(value)) {
3661
5960
  result[key] = value.map((item) => {
3662
5961
  const narrowed = {};
3663
- for (const field of Object.keys(config)) {
5962
+ for (const field of Object.keys(selectMap)) {
3664
5963
  if (field in item) {
3665
5964
  narrowed[field] = item[field];
3666
5965
  }
@@ -3669,7 +5968,7 @@ function narrowRelationFields(relationsConfig, data) {
3669
5968
  });
3670
5969
  } else {
3671
5970
  const narrowed = {};
3672
- for (const field of Object.keys(config)) {
5971
+ for (const field of Object.keys(selectMap)) {
3673
5972
  if (field in value) {
3674
5973
  narrowed[field] = value[field];
3675
5974
  }
@@ -3722,7 +6021,138 @@ import {
3722
6021
 
3723
6022
  // src/entity/access-enforcer.ts
3724
6023
  import { EntityForbiddenError, err as err2, ok as ok2 } from "@vertz/errors";
3725
- async function enforceAccess(operation, accessRules, ctx, row) {
6024
+ function deny(operation) {
6025
+ return err2(new EntityForbiddenError(`Access denied for operation "${operation}"`));
6026
+ }
6027
+ function isUserMarker(value) {
6028
+ return typeof value === "object" && value !== null && "__marker" in value;
6029
+ }
6030
+ function resolveMarker(marker, ctx) {
6031
+ switch (marker.__marker) {
6032
+ case "user.id":
6033
+ return ctx.userId;
6034
+ case "user.tenantId":
6035
+ return ctx.tenantId;
6036
+ default:
6037
+ return;
6038
+ }
6039
+ }
6040
+ async function evaluateRule(rule, operation, ctx, row, options) {
6041
+ switch (rule.type) {
6042
+ case "public":
6043
+ return ok2(undefined);
6044
+ case "authenticated":
6045
+ return ctx.authenticated() ? ok2(undefined) : deny(operation);
6046
+ case "role":
6047
+ return ctx.role(...rule.roles) ? ok2(undefined) : deny(operation);
6048
+ case "entitlement": {
6049
+ if (!options.can) {
6050
+ return deny(operation);
6051
+ }
6052
+ const allowed = await options.can(rule.entitlement);
6053
+ return allowed ? ok2(undefined) : deny(operation);
6054
+ }
6055
+ case "where": {
6056
+ for (const [key, expected] of Object.entries(rule.conditions)) {
6057
+ const resolved = isUserMarker(expected) ? resolveMarker(expected, ctx) : expected;
6058
+ if (row[key] !== resolved) {
6059
+ return deny(operation);
6060
+ }
6061
+ }
6062
+ return ok2(undefined);
6063
+ }
6064
+ case "all": {
6065
+ for (const sub of rule.rules) {
6066
+ const result = await evaluateDescriptor(sub, operation, ctx, row, options);
6067
+ if (!result.ok)
6068
+ return result;
6069
+ }
6070
+ return ok2(undefined);
6071
+ }
6072
+ case "any": {
6073
+ for (const sub of rule.rules) {
6074
+ const result = await evaluateDescriptor(sub, operation, ctx, row, options);
6075
+ if (result.ok)
6076
+ return result;
6077
+ }
6078
+ return deny(operation);
6079
+ }
6080
+ case "fva": {
6081
+ if (options.fvaAge === undefined) {
6082
+ return deny(operation);
6083
+ }
6084
+ return options.fvaAge <= rule.maxAge ? ok2(undefined) : deny(operation);
6085
+ }
6086
+ }
6087
+ }
6088
+ async function evaluateDescriptor(rule, operation, ctx, row, options) {
6089
+ if (typeof rule === "function") {
6090
+ const allowed = await rule(ctx, row);
6091
+ return allowed ? ok2(undefined) : deny(operation);
6092
+ }
6093
+ return evaluateRule(rule, operation, ctx, row, options);
6094
+ }
6095
+ async function evaluateRuleSkipWhere(rule, operation, ctx, row, options) {
6096
+ if (rule.type === "where") {
6097
+ return ok2(undefined);
6098
+ }
6099
+ if (rule.type === "all") {
6100
+ for (const sub of rule.rules) {
6101
+ const result = await evaluateDescriptorSkipWhere(sub, operation, ctx, row, options);
6102
+ if (!result.ok)
6103
+ return result;
6104
+ }
6105
+ return ok2(undefined);
6106
+ }
6107
+ if (rule.type === "any") {
6108
+ for (const sub of rule.rules) {
6109
+ const result = await evaluateDescriptorSkipWhere(sub, operation, ctx, row, options);
6110
+ if (result.ok)
6111
+ return result;
6112
+ }
6113
+ return deny(operation);
6114
+ }
6115
+ return evaluateRule(rule, operation, ctx, row, options);
6116
+ }
6117
+ async function evaluateDescriptorSkipWhere(rule, operation, ctx, row, options) {
6118
+ if (typeof rule === "function") {
6119
+ const allowed = await rule(ctx, row);
6120
+ return allowed ? ok2(undefined) : deny(operation);
6121
+ }
6122
+ return evaluateRuleSkipWhere(rule, operation, ctx, row, options);
6123
+ }
6124
+ function extractFromDescriptor(rule, ctx) {
6125
+ if (typeof rule === "function")
6126
+ return null;
6127
+ switch (rule.type) {
6128
+ case "where": {
6129
+ const resolved = {};
6130
+ for (const [key, value] of Object.entries(rule.conditions)) {
6131
+ resolved[key] = isUserMarker(value) ? resolveMarker(value, ctx) : value;
6132
+ }
6133
+ return resolved;
6134
+ }
6135
+ case "all": {
6136
+ let merged = null;
6137
+ for (const sub of rule.rules) {
6138
+ const extracted = extractFromDescriptor(sub, ctx);
6139
+ if (extracted) {
6140
+ merged = { ...merged ?? {}, ...extracted };
6141
+ }
6142
+ }
6143
+ return merged;
6144
+ }
6145
+ default:
6146
+ return null;
6147
+ }
6148
+ }
6149
+ function extractWhereConditions(operation, accessRules, ctx) {
6150
+ const rule = accessRules[operation];
6151
+ if (rule === undefined || rule === false)
6152
+ return null;
6153
+ return extractFromDescriptor(rule, ctx);
6154
+ }
6155
+ async function enforceAccess(operation, accessRules, ctx, row, options) {
3726
6156
  const rule = accessRules[operation];
3727
6157
  if (rule === undefined) {
3728
6158
  return err2(new EntityForbiddenError(`Access denied: no access rule for operation "${operation}"`));
@@ -3730,11 +6160,10 @@ async function enforceAccess(operation, accessRules, ctx, row) {
3730
6160
  if (rule === false) {
3731
6161
  return err2(new EntityForbiddenError(`Operation "${operation}" is disabled`));
3732
6162
  }
3733
- const allowed = await rule(ctx, row ?? {});
3734
- if (!allowed) {
3735
- return err2(new EntityForbiddenError(`Access denied for operation "${operation}"`));
6163
+ if (options?.skipWhere) {
6164
+ return evaluateDescriptorSkipWhere(rule, operation, ctx, row ?? {}, options);
3736
6165
  }
3737
- return ok2(undefined);
6166
+ return evaluateDescriptor(rule, operation, ctx, row ?? {}, options ?? {});
3738
6167
  }
3739
6168
 
3740
6169
  // src/entity/action-pipeline.ts
@@ -3776,6 +6205,7 @@ function createEntityContext(request, entityOps, registryProxy) {
3776
6205
  const tenantId = request.tenantId ?? null;
3777
6206
  return {
3778
6207
  userId,
6208
+ tenantId,
3779
6209
  authenticated() {
3780
6210
  return userId !== null;
3781
6211
  },
@@ -3791,7 +6221,12 @@ function createEntityContext(request, entityOps, registryProxy) {
3791
6221
  }
3792
6222
 
3793
6223
  // src/entity/crud-pipeline.ts
3794
- import { EntityNotFoundError as EntityNotFoundError2, err as err4, ok as ok4 } from "@vertz/errors";
6224
+ import {
6225
+ EntityForbiddenError as EntityForbiddenError2,
6226
+ EntityNotFoundError as EntityNotFoundError2,
6227
+ err as err4,
6228
+ ok as ok4
6229
+ } from "@vertz/errors";
3795
6230
  function resolvePrimaryKeyColumn(table) {
3796
6231
  for (const key of Object.keys(table._columns)) {
3797
6232
  const col = table._columns[key];
@@ -3800,20 +6235,82 @@ function resolvePrimaryKeyColumn(table) {
3800
6235
  }
3801
6236
  return "id";
3802
6237
  }
3803
- function createCrudHandlers(def, db) {
6238
+ function createCrudHandlers(def, db, options) {
3804
6239
  const table = def.model.table;
6240
+ const isTenantScoped = def.tenantScoped;
6241
+ const tenantChain = options?.tenantChain ?? def.tenantChain ?? null;
6242
+ const isIndirectlyScoped = tenantChain !== null;
6243
+ const queryParentIds = options?.queryParentIds ?? null;
6244
+ function notFound(id) {
6245
+ return err4(new EntityNotFoundError2(`${def.name} with id "${id}" not found`));
6246
+ }
6247
+ function isSameTenant(ctx, row) {
6248
+ if (!isTenantScoped)
6249
+ return true;
6250
+ if (isIndirectlyScoped)
6251
+ return true;
6252
+ return row.tenantId === ctx.tenantId;
6253
+ }
6254
+ function withTenantFilter(ctx, where) {
6255
+ if (!isTenantScoped)
6256
+ return where;
6257
+ if (isIndirectlyScoped)
6258
+ return where;
6259
+ return { ...where, tenantId: ctx.tenantId };
6260
+ }
6261
+ async function resolveIndirectTenantWhere(ctx) {
6262
+ if (!isIndirectlyScoped || !tenantChain || !queryParentIds)
6263
+ return null;
6264
+ if (!ctx.tenantId) {
6265
+ return { [tenantChain.hops[0].foreignKey]: { in: [] } };
6266
+ }
6267
+ const { hops, tenantColumn } = tenantChain;
6268
+ const lastHop = hops[hops.length - 1];
6269
+ let currentIds = await queryParentIds(lastHop.tableName, { [tenantColumn]: ctx.tenantId });
6270
+ for (let i = hops.length - 2;i >= 0; i--) {
6271
+ if (currentIds.length === 0)
6272
+ break;
6273
+ const hop = hops[i];
6274
+ const nextHop = hops[i + 1];
6275
+ currentIds = await queryParentIds(hop.tableName, {
6276
+ [nextHop.foreignKey]: { in: currentIds }
6277
+ });
6278
+ }
6279
+ return { [hops[0].foreignKey]: { in: currentIds } };
6280
+ }
6281
+ async function verifyIndirectTenantOwnership(ctx, row) {
6282
+ if (!isIndirectlyScoped || !tenantChain || !queryParentIds)
6283
+ return true;
6284
+ if (!ctx.tenantId)
6285
+ return false;
6286
+ const indirectWhere = await resolveIndirectTenantWhere(ctx);
6287
+ if (!indirectWhere)
6288
+ return false;
6289
+ const firstHopFK = tenantChain.hops[0].foreignKey;
6290
+ const allowed = indirectWhere[firstHopFK];
6291
+ if (!allowed)
6292
+ return false;
6293
+ return allowed.in.includes(row[firstHopFK]);
6294
+ }
3805
6295
  return {
3806
- async list(ctx, options) {
3807
- const accessResult = await enforceAccess("list", def.access, ctx);
6296
+ async list(ctx, options2) {
6297
+ const accessWhere = extractWhereConditions("list", def.access, ctx);
6298
+ const accessResult = await enforceAccess("list", def.access, ctx, undefined, {
6299
+ skipWhere: accessWhere !== null
6300
+ });
3808
6301
  if (!accessResult.ok)
3809
6302
  return err4(accessResult.error);
3810
- const rawWhere = options?.where;
6303
+ const rawWhere = options2?.where;
3811
6304
  const safeWhere = rawWhere ? stripHiddenFields(table, rawWhere) : undefined;
3812
- const where = safeWhere && Object.keys(safeWhere).length > 0 ? safeWhere : undefined;
3813
- const limit = Math.max(0, options?.limit ?? 20);
3814
- const after = options?.after && options.after.length <= 512 ? options.after : undefined;
3815
- const orderBy = options?.orderBy;
3816
- const { data: rows, total } = await db.list({ where, orderBy, limit, after });
6305
+ const cleanWhere = safeWhere && Object.keys(safeWhere).length > 0 ? safeWhere : undefined;
6306
+ const directWhere = withTenantFilter(ctx, { ...accessWhere, ...cleanWhere });
6307
+ const indirectWhere = await resolveIndirectTenantWhere(ctx);
6308
+ const where = indirectWhere ? { ...directWhere, ...indirectWhere } : directWhere;
6309
+ const limit = Math.max(0, options2?.limit ?? 20);
6310
+ const after = options2?.after && options2.after.length <= 512 ? options2.after : undefined;
6311
+ const orderBy = options2?.orderBy;
6312
+ const include = options2?.include;
6313
+ const { data: rows, total } = await db.list({ where, orderBy, limit, after, include });
3817
6314
  const data = rows.map((row) => narrowRelationFields(def.relations, stripHiddenFields(table, row)));
3818
6315
  const pkColumn = resolvePrimaryKeyColumn(table);
3819
6316
  const lastRow = rows[rows.length - 1];
@@ -3821,10 +6318,15 @@ function createCrudHandlers(def, db) {
3821
6318
  const hasNextPage = nextCursor !== null;
3822
6319
  return ok4({ status: 200, body: { items: data, total, limit, nextCursor, hasNextPage } });
3823
6320
  },
3824
- async get(ctx, id) {
3825
- const row = await db.get(id);
3826
- if (!row) {
3827
- return err4(new EntityNotFoundError2(`${def.name} with id "${id}" not found`));
6321
+ async get(ctx, id, options2) {
6322
+ const getOptions = options2?.include ? { include: options2.include } : undefined;
6323
+ const row = await db.get(id, getOptions);
6324
+ if (!row)
6325
+ return notFound(id);
6326
+ if (!isSameTenant(ctx, row))
6327
+ return notFound(id);
6328
+ if (isIndirectlyScoped && !await verifyIndirectTenantOwnership(ctx, row)) {
6329
+ return notFound(id);
3828
6330
  }
3829
6331
  const accessResult = await enforceAccess("get", def.access, ctx, row);
3830
6332
  if (!accessResult.ok)
@@ -3839,6 +6341,29 @@ function createCrudHandlers(def, db) {
3839
6341
  if (!accessResult.ok)
3840
6342
  return err4(accessResult.error);
3841
6343
  let input = stripReadOnlyFields(table, data);
6344
+ if (isIndirectlyScoped && tenantChain && queryParentIds && ctx.tenantId) {
6345
+ const firstHop = tenantChain.hops[0];
6346
+ const parentId = input[firstHop.foreignKey];
6347
+ if (!parentId) {
6348
+ return err4(new EntityNotFoundError2(`Referenced parent not found: ${firstHop.foreignKey} is required`));
6349
+ }
6350
+ const parentExists = await queryParentIds(firstHop.tableName, {
6351
+ [firstHop.targetColumn]: parentId
6352
+ });
6353
+ if (parentExists.length === 0) {
6354
+ return err4(new EntityNotFoundError2(`Referenced parent not found: ${firstHop.tableName} with ${firstHop.targetColumn} "${parentId}" does not exist`));
6355
+ }
6356
+ const indirectWhere = await resolveIndirectTenantWhere(ctx);
6357
+ if (indirectWhere) {
6358
+ const allowed = indirectWhere[firstHop.foreignKey];
6359
+ if (!allowed || !allowed.in.includes(parentId)) {
6360
+ return err4(new EntityForbiddenError2(`Referenced ${firstHop.tableName} does not belong to your tenant`));
6361
+ }
6362
+ }
6363
+ }
6364
+ if (isTenantScoped && !isIndirectlyScoped && ctx.tenantId) {
6365
+ input = { ...input, tenantId: ctx.tenantId };
6366
+ }
3842
6367
  if (def.before.create) {
3843
6368
  input = await def.before.create(input, ctx);
3844
6369
  }
@@ -3849,12 +6374,19 @@ function createCrudHandlers(def, db) {
3849
6374
  await def.after.create(strippedResult, ctx);
3850
6375
  } catch {}
3851
6376
  }
3852
- return ok4({ status: 201, body: narrowRelationFields(def.relations, strippedResult) });
6377
+ return ok4({
6378
+ status: 201,
6379
+ body: narrowRelationFields(def.relations, strippedResult)
6380
+ });
3853
6381
  },
3854
6382
  async update(ctx, id, data) {
3855
6383
  const existing = await db.get(id);
3856
- if (!existing) {
3857
- return err4(new EntityNotFoundError2(`${def.name} with id "${id}" not found`));
6384
+ if (!existing)
6385
+ return notFound(id);
6386
+ if (!isSameTenant(ctx, existing))
6387
+ return notFound(id);
6388
+ if (isIndirectlyScoped && !await verifyIndirectTenantOwnership(ctx, existing)) {
6389
+ return notFound(id);
3858
6390
  }
3859
6391
  const accessResult = await enforceAccess("update", def.access, ctx, existing);
3860
6392
  if (!accessResult.ok)
@@ -3878,8 +6410,12 @@ function createCrudHandlers(def, db) {
3878
6410
  },
3879
6411
  async delete(ctx, id) {
3880
6412
  const existing = await db.get(id);
3881
- if (!existing) {
3882
- return err4(new EntityNotFoundError2(`${def.name} with id "${id}" not found`));
6413
+ if (!existing)
6414
+ return notFound(id);
6415
+ if (!isSameTenant(ctx, existing))
6416
+ return notFound(id);
6417
+ if (isIndirectlyScoped && !await verifyIndirectTenantOwnership(ctx, existing)) {
6418
+ return notFound(id);
3883
6419
  }
3884
6420
  const accessResult = await enforceAccess("delete", def.access, ctx, existing);
3885
6421
  if (!accessResult.ok)
@@ -3960,14 +6496,7 @@ function entityErrorHandler(error) {
3960
6496
  // src/entity/vertzql-parser.ts
3961
6497
  var MAX_LIMIT = 1000;
3962
6498
  var MAX_Q_BASE64_LENGTH = 10240;
3963
- var ALLOWED_Q_KEYS = new Set([
3964
- "select",
3965
- "include",
3966
- "where",
3967
- "orderBy",
3968
- "limit",
3969
- "offset"
3970
- ]);
6499
+ var ALLOWED_Q_KEYS = new Set(["select", "include", "where", "orderBy", "limit", "offset"]);
3971
6500
  function parseVertzQL(query) {
3972
6501
  const result = {};
3973
6502
  for (const [key, value] of Object.entries(query)) {
@@ -4077,22 +6606,94 @@ function validateVertzQL(options, table, relationsConfig) {
4077
6606
  }
4078
6607
  }
4079
6608
  if (options.include && relationsConfig) {
4080
- for (const [relation, requested] of Object.entries(options.include)) {
4081
- const entityConfig = relationsConfig[relation];
4082
- if (entityConfig === undefined || entityConfig === false) {
4083
- return { ok: false, error: `Relation "${relation}" is not exposed` };
4084
- }
4085
- if (typeof entityConfig === "object" && typeof requested === "object") {
4086
- for (const field of Object.keys(requested)) {
4087
- if (!(field in entityConfig)) {
4088
- return {
4089
- ok: false,
4090
- error: `Field "${field}" is not exposed on relation "${relation}"`
4091
- };
4092
- }
6609
+ const includeResult = validateInclude(options.include, relationsConfig, "");
6610
+ if (!includeResult.ok)
6611
+ return includeResult;
6612
+ }
6613
+ return { ok: true };
6614
+ }
6615
+ function validateInclude(include, relationsConfig, pathPrefix) {
6616
+ for (const [relation, requested] of Object.entries(include)) {
6617
+ const entityConfig = relationsConfig[relation];
6618
+ const relationPath = pathPrefix ? `${pathPrefix}.${relation}` : relation;
6619
+ if (entityConfig === undefined || entityConfig === false) {
6620
+ return { ok: false, error: `Relation "${relationPath}" is not exposed` };
6621
+ }
6622
+ if (requested === true)
6623
+ continue;
6624
+ const configObj = typeof entityConfig === "object" ? entityConfig : undefined;
6625
+ if (requested.where) {
6626
+ if (!configObj || !configObj.allowWhere || configObj.allowWhere.length === 0) {
6627
+ return {
6628
+ ok: false,
6629
+ error: `Filtering is not enabled on relation '${relationPath}'. ` + "Add 'allowWhere' to the entity relations config."
6630
+ };
6631
+ }
6632
+ const allowedSet = new Set(configObj.allowWhere);
6633
+ for (const field of Object.keys(requested.where)) {
6634
+ if (!allowedSet.has(field)) {
6635
+ return {
6636
+ ok: false,
6637
+ error: `Field '${field}' is not filterable on relation '${relationPath}'. ` + `Allowed: ${configObj.allowWhere.join(", ")}`
6638
+ };
6639
+ }
6640
+ }
6641
+ }
6642
+ if (requested.orderBy) {
6643
+ if (!configObj || !configObj.allowOrderBy || configObj.allowOrderBy.length === 0) {
6644
+ return {
6645
+ ok: false,
6646
+ error: `Sorting is not enabled on relation '${relationPath}'. ` + "Add 'allowOrderBy' to the entity relations config."
6647
+ };
6648
+ }
6649
+ const allowedSet = new Set(configObj.allowOrderBy);
6650
+ for (const [field, dir] of Object.entries(requested.orderBy)) {
6651
+ if (!allowedSet.has(field)) {
6652
+ return {
6653
+ ok: false,
6654
+ error: `Field '${field}' is not sortable on relation '${relationPath}'. ` + `Allowed: ${configObj.allowOrderBy.join(", ")}`
6655
+ };
6656
+ }
6657
+ if (dir !== "asc" && dir !== "desc") {
6658
+ return {
6659
+ ok: false,
6660
+ error: `Invalid orderBy direction '${String(dir)}' for field '${field}' on relation '${relationPath}'. Must be 'asc' or 'desc'.`
6661
+ };
6662
+ }
6663
+ }
6664
+ }
6665
+ if (requested.limit !== undefined) {
6666
+ if (typeof requested.limit !== "number" || !Number.isFinite(requested.limit)) {
6667
+ return {
6668
+ ok: false,
6669
+ error: `Invalid limit on relation '${relationPath}': must be a finite number`
6670
+ };
6671
+ }
6672
+ if (requested.limit < 0) {
6673
+ requested.limit = 0;
6674
+ }
6675
+ if (configObj?.maxLimit !== undefined && requested.limit > configObj.maxLimit) {
6676
+ requested.limit = configObj.maxLimit;
6677
+ }
6678
+ }
6679
+ if (requested.select && configObj?.select) {
6680
+ for (const field of Object.keys(requested.select)) {
6681
+ if (!(field in configObj.select)) {
6682
+ return {
6683
+ ok: false,
6684
+ error: `Field "${field}" is not exposed on relation "${relationPath}"`
6685
+ };
4093
6686
  }
4094
6687
  }
4095
6688
  }
6689
+ if (requested.include) {
6690
+ if (entityConfig === true) {
6691
+ return {
6692
+ ok: false,
6693
+ error: `Nested includes are not supported on relation '${relationPath}' ` + "without a structured relations config."
6694
+ };
6695
+ }
6696
+ }
4096
6697
  }
4097
6698
  return { ok: true };
4098
6699
  }
@@ -4120,7 +6721,11 @@ function getParams(ctx) {
4120
6721
  function generateEntityRoutes(def, registry, db, options) {
4121
6722
  const prefix = options?.apiPrefix ?? "/api";
4122
6723
  const basePath = `${prefix}/${def.name}`;
4123
- const crudHandlers = createCrudHandlers(def, db);
6724
+ const tenantChain = options?.tenantChain ?? null;
6725
+ const crudHandlers = createCrudHandlers(def, db, {
6726
+ tenantChain,
6727
+ queryParentIds: options?.queryParentIds
6728
+ });
4124
6729
  const inject = def.inject ?? {};
4125
6730
  const registryProxy = Object.keys(inject).length > 0 ? registry.createScopedProxy(inject) : {};
4126
6731
  const routes = [];
@@ -4159,7 +6764,8 @@ function generateEntityRoutes(def, registry, db, options) {
4159
6764
  where: parsed.where ? parsed.where : undefined,
4160
6765
  orderBy: parsed.orderBy,
4161
6766
  limit: parsed.limit,
4162
- after: parsed.after
6767
+ after: parsed.after,
6768
+ include: parsed.include
4163
6769
  };
4164
6770
  const result = await crudHandlers.list(entityCtx, options2);
4165
6771
  if (!result.ok) {
@@ -4201,7 +6807,8 @@ function generateEntityRoutes(def, registry, db, options) {
4201
6807
  where: parsed.where,
4202
6808
  orderBy: parsed.orderBy,
4203
6809
  limit: parsed.limit,
4204
- after: parsed.after
6810
+ after: parsed.after,
6811
+ include: parsed.include
4205
6812
  };
4206
6813
  const result = await crudHandlers.list(entityCtx, options2);
4207
6814
  if (!result.ok) {
@@ -4246,7 +6853,8 @@ function generateEntityRoutes(def, registry, db, options) {
4246
6853
  if (!validation.ok) {
4247
6854
  return jsonResponse({ error: { code: "BadRequest", message: validation.error } }, 400);
4248
6855
  }
4249
- const result = await crudHandlers.get(entityCtx, id);
6856
+ const getOptions = parsed.include ? { include: parsed.include } : undefined;
6857
+ const result = await crudHandlers.get(entityCtx, id, getOptions);
4250
6858
  if (!result.ok) {
4251
6859
  const { status, body: body2 } = entityErrorHandler(result.error);
4252
6860
  return jsonResponse(body2, status);
@@ -4411,13 +7019,106 @@ function generateEntityRoutes(def, registry, db, options) {
4411
7019
  return routes;
4412
7020
  }
4413
7021
 
7022
+ // src/entity/tenant-chain.ts
7023
+ function resolveTenantChain(entityKey, tenantGraph, registry) {
7024
+ if (!tenantGraph.indirectlyScoped.includes(entityKey)) {
7025
+ return null;
7026
+ }
7027
+ if (tenantGraph.root === null) {
7028
+ return null;
7029
+ }
7030
+ const rootEntry = registry[tenantGraph.root];
7031
+ if (!rootEntry)
7032
+ return null;
7033
+ const rootTableName = rootEntry.table._name;
7034
+ const tableNameToKey = new Map;
7035
+ for (const [key, entry] of Object.entries(registry)) {
7036
+ tableNameToKey.set(entry.table._name, key);
7037
+ }
7038
+ const directlyScopedTableNames = new Set;
7039
+ for (const key of tenantGraph.directlyScoped) {
7040
+ const entry = registry[key];
7041
+ if (entry)
7042
+ directlyScopedTableNames.add(entry.table._name);
7043
+ }
7044
+ directlyScopedTableNames.add(rootTableName);
7045
+ const hops = [];
7046
+ let currentKey = entityKey;
7047
+ const visited = new Set;
7048
+ while (true) {
7049
+ if (visited.has(currentKey)) {
7050
+ return null;
7051
+ }
7052
+ visited.add(currentKey);
7053
+ const currentEntry = registry[currentKey];
7054
+ if (!currentEntry)
7055
+ return null;
7056
+ let foundHop = false;
7057
+ for (const [, rel] of Object.entries(currentEntry.relations)) {
7058
+ if (rel._type !== "one" || !rel._foreignKey)
7059
+ continue;
7060
+ const targetTableName = rel._target()._name;
7061
+ const targetKey = tableNameToKey.get(targetTableName);
7062
+ if (!targetKey)
7063
+ continue;
7064
+ const targetIsDirectlyScoped = directlyScopedTableNames.has(targetTableName);
7065
+ const targetIsIndirectlyScoped = tenantGraph.indirectlyScoped.includes(targetKey);
7066
+ if (!targetIsDirectlyScoped && !targetIsIndirectlyScoped)
7067
+ continue;
7068
+ const targetEntry = registry[targetKey];
7069
+ if (!targetEntry)
7070
+ continue;
7071
+ const targetPk = resolvePrimaryKey(targetEntry.table._columns);
7072
+ hops.push({
7073
+ tableName: targetTableName,
7074
+ foreignKey: rel._foreignKey,
7075
+ targetColumn: targetPk
7076
+ });
7077
+ if (targetIsDirectlyScoped && targetKey !== tenantGraph.root) {
7078
+ const tenantColumn = resolveTenantFk(targetKey, registry);
7079
+ if (!tenantColumn)
7080
+ return null;
7081
+ return { hops, tenantColumn };
7082
+ }
7083
+ if (targetKey === tenantGraph.root) {
7084
+ return { hops, tenantColumn: rel._foreignKey };
7085
+ }
7086
+ currentKey = targetKey;
7087
+ foundHop = true;
7088
+ break;
7089
+ }
7090
+ if (!foundHop)
7091
+ return null;
7092
+ }
7093
+ }
7094
+ function resolvePrimaryKey(columns) {
7095
+ for (const [key, col] of Object.entries(columns)) {
7096
+ if (col && typeof col === "object" && "_meta" in col) {
7097
+ const meta = col._meta;
7098
+ if (meta.primary)
7099
+ return key;
7100
+ }
7101
+ }
7102
+ return "id";
7103
+ }
7104
+ function resolveTenantFk(modelKey, registry) {
7105
+ const entry = registry[modelKey];
7106
+ if (!entry || !entry._tenant)
7107
+ return null;
7108
+ const tenantRel = entry.relations[entry._tenant];
7109
+ if (!tenantRel || !tenantRel._foreignKey)
7110
+ return null;
7111
+ return tenantRel._foreignKey;
7112
+ }
7113
+
4414
7114
  // src/service/context.ts
4415
- function createServiceContext(request, registryProxy) {
7115
+ function createServiceContext(request, registryProxy, rawRequest) {
4416
7116
  const userId = request.userId ?? null;
4417
7117
  const roles = request.roles ?? [];
4418
7118
  const tenantId = request.tenantId ?? null;
4419
7119
  return {
4420
7120
  userId,
7121
+ tenantId,
4421
7122
  authenticated() {
4422
7123
  return userId !== null;
4423
7124
  },
@@ -4427,7 +7128,8 @@ function createServiceContext(request, registryProxy) {
4427
7128
  role(...rolesToCheck) {
4428
7129
  return rolesToCheck.some((r) => roles.includes(r));
4429
7130
  },
4430
- entities: registryProxy
7131
+ entities: registryProxy,
7132
+ request: rawRequest ?? { url: "", method: "", headers: new Headers, body: undefined }
4431
7133
  };
4432
7134
  }
4433
7135
 
@@ -4476,23 +7178,56 @@ function generateServiceRoutes(def, registry, options) {
4476
7178
  handler: async (ctx) => {
4477
7179
  try {
4478
7180
  const requestInfo = extractRequestInfo2(ctx);
4479
- const serviceCtx = createServiceContext(requestInfo, registryProxy);
7181
+ const raw = ctx.raw;
7182
+ const ctxHeaders = ctx.headers;
7183
+ const rawRequest = {
7184
+ url: raw?.url ?? "",
7185
+ method: raw?.method ?? "",
7186
+ headers: new Headers(ctxHeaders ?? {}),
7187
+ body: ctx.body
7188
+ };
7189
+ const serviceCtx = createServiceContext(requestInfo, registryProxy, rawRequest);
4480
7190
  const accessResult = await enforceAccess(handlerName, def.access, serviceCtx);
4481
7191
  if (!accessResult.ok) {
4482
7192
  return jsonResponse2({ error: { code: "Forbidden", message: accessResult.error.message } }, 403);
4483
7193
  }
4484
- const rawBody = ctx.body ?? {};
4485
- const parsed = handlerDef.body.parse(rawBody);
4486
- if (!parsed.ok) {
4487
- const message = parsed.error instanceof Error ? parsed.error.message : "Invalid request body";
4488
- return jsonResponse2({ error: { code: "BadRequest", message } }, 400);
7194
+ if (handlerDef.body && isContentDescriptor(handlerDef.body)) {
7195
+ const reqContentType = rawRequest.headers.get("content-type")?.toLowerCase() ?? "";
7196
+ const expected = handlerDef.body._contentType;
7197
+ const isXml = expected === "application/xml";
7198
+ const matches = isXml ? reqContentType.includes("application/xml") || reqContentType.includes("text/xml") : reqContentType.includes(expected);
7199
+ if (reqContentType && !matches) {
7200
+ return jsonResponse2({
7201
+ error: {
7202
+ code: "UnsupportedMediaType",
7203
+ message: `Expected content-type ${expected}, got ${reqContentType}`
7204
+ }
7205
+ }, 415);
7206
+ }
7207
+ }
7208
+ let input;
7209
+ if (handlerDef.body) {
7210
+ const rawBody = ctx.body ?? {};
7211
+ const parsed = handlerDef.body.parse(rawBody);
7212
+ if (!parsed.ok) {
7213
+ const message = parsed.error instanceof Error ? parsed.error.message : "Invalid request body";
7214
+ return jsonResponse2({ error: { code: "BadRequest", message } }, 400);
7215
+ }
7216
+ input = parsed.data;
4489
7217
  }
4490
- const result = await handlerDef.handler(parsed.data, serviceCtx);
7218
+ const result = await handlerDef.handler(input, serviceCtx);
4491
7219
  const responseParsed = handlerDef.response.parse(result);
4492
7220
  if (!responseParsed.ok) {
4493
7221
  const message = responseParsed.error instanceof Error ? responseParsed.error.message : "Response validation failed";
4494
7222
  console.warn(`[vertz] Service response validation warning: ${message}`);
4495
7223
  }
7224
+ if (isContentDescriptor(handlerDef.response)) {
7225
+ const body = result instanceof Uint8Array ? result : String(result);
7226
+ return new Response(body, {
7227
+ status: 200,
7228
+ headers: { "content-type": handlerDef.response._contentType }
7229
+ });
7230
+ }
4496
7231
  return jsonResponse2(result, 200);
4497
7232
  } catch (error) {
4498
7233
  const message = error instanceof Error ? error.message : "Internal server error";
@@ -4565,23 +7300,62 @@ function createServer(config) {
4565
7300
  const allRoutes = [];
4566
7301
  const registry = new EntityRegistry;
4567
7302
  const apiPrefix = config.apiPrefix === undefined ? "/api" : config.apiPrefix;
7303
+ const { db } = config;
7304
+ const hasDbClient = db && isDatabaseClient(db);
7305
+ if (hasDbClient && config.auth) {
7306
+ validateAuthModels(db);
7307
+ }
4568
7308
  if (config.entities && config.entities.length > 0) {
4569
- const { db } = config;
4570
7309
  let dbFactory;
4571
- if (db && isDatabaseClient(db)) {
7310
+ const tableOf = (e) => e.table ?? e.name;
7311
+ if (hasDbClient) {
4572
7312
  const dbModels = db._internals.models;
4573
- const missing = config.entities.filter((e) => !(e.name in dbModels)).map((e) => `"${e.name}"`);
4574
- if (missing.length > 0) {
7313
+ const missing = config.entities.filter((e) => !(tableOf(e) in dbModels)).map((e) => `"${tableOf(e)}"`);
7314
+ const uniqueMissing = [...new Set(missing)];
7315
+ if (uniqueMissing.length > 0) {
4575
7316
  const registered = Object.keys(dbModels).map((k) => `"${k}"`).join(", ");
4576
- const plural = missing.length > 1;
4577
- throw new Error(`${plural ? "Entities" : "Entity"} ${missing.join(", ")} ${plural ? "are" : "is"} not registered in createDb(). ` + `Add the missing model${plural ? "s" : ""} to the models object in your createDb() call. ` + `Registered models: ${registered || "(none)"}`);
7317
+ const plural = uniqueMissing.length > 1;
7318
+ throw new Error(`${plural ? "Entities" : "Entity"} ${uniqueMissing.join(", ")} ${plural ? "are" : "is"} not registered in createDb(). ` + `Add the missing model${plural ? "s" : ""} to the models object in your createDb() call. ` + `Registered models: ${registered || "(none)"}`);
4578
7319
  }
4579
- dbFactory = (entityDef) => createDatabaseBridgeAdapter(db, entityDef.name);
7320
+ dbFactory = (entityDef) => createDatabaseBridgeAdapter(db, tableOf(entityDef));
4580
7321
  } else if (db) {
4581
7322
  dbFactory = () => db;
4582
7323
  } else {
4583
7324
  dbFactory = config._entityDbFactory ?? createNoopDbAdapter;
4584
7325
  }
7326
+ const tenantChains = new Map;
7327
+ let queryParentIds;
7328
+ if (hasDbClient) {
7329
+ const dbClient = db;
7330
+ const tenantGraph = dbClient._internals.tenantGraph;
7331
+ const dbModelsMap = dbClient._internals.models;
7332
+ for (const entityDef of config.entities) {
7333
+ const eDef = entityDef;
7334
+ if (eDef.tenantScoped === false)
7335
+ continue;
7336
+ const modelKey = tableOf(eDef);
7337
+ const chain = resolveTenantChain(modelKey, tenantGraph, dbModelsMap);
7338
+ if (chain) {
7339
+ tenantChains.set(eDef.name, chain);
7340
+ }
7341
+ }
7342
+ if (tenantChains.size > 0) {
7343
+ queryParentIds = async (tableName, where) => {
7344
+ const delegate = dbClient[tableName];
7345
+ if (!delegate)
7346
+ return [];
7347
+ const result = await delegate.list({ where });
7348
+ if (!result.ok || !result.data)
7349
+ return [];
7350
+ return result.data.map((row) => row.id);
7351
+ };
7352
+ }
7353
+ }
7354
+ if (config._tenantChains) {
7355
+ for (const [name, chain] of config._tenantChains) {
7356
+ tenantChains.set(name, chain);
7357
+ }
7358
+ }
4585
7359
  for (const entityDef of config.entities) {
4586
7360
  const entityDb = dbFactory(entityDef);
4587
7361
  const ops = createEntityOps(entityDef, entityDb);
@@ -4589,8 +7363,11 @@ function createServer(config) {
4589
7363
  }
4590
7364
  for (const entityDef of config.entities) {
4591
7365
  const entityDb = dbFactory(entityDef);
7366
+ const tenantChain = tenantChains.get(entityDef.name) ?? null;
4592
7367
  const routes = generateEntityRoutes(entityDef, registry, entityDb, {
4593
- apiPrefix
7368
+ apiPrefix,
7369
+ tenantChain,
7370
+ queryParentIds: tenantChain ? queryParentIds ?? config._queryParentIds : undefined
4594
7371
  });
4595
7372
  allRoutes.push(...routes);
4596
7373
  }
@@ -4601,10 +7378,53 @@ function createServer(config) {
4601
7378
  allRoutes.push(...routes);
4602
7379
  }
4603
7380
  }
4604
- return coreCreateServer({
7381
+ const app = coreCreateServer({
4605
7382
  ...config,
4606
7383
  _entityRoutes: allRoutes.length > 0 ? allRoutes : undefined
4607
7384
  });
7385
+ if (hasDbClient && config.auth) {
7386
+ const dbClient = db;
7387
+ const authConfig = {
7388
+ ...config.auth,
7389
+ userStore: config.auth.userStore ?? new DbUserStore(dbClient),
7390
+ sessionStore: config.auth.sessionStore ?? new DbSessionStore(dbClient),
7391
+ oauthAccountStore: config.auth.oauthAccountStore ?? (config.auth.providers?.length ? new DbOAuthAccountStore(dbClient) : undefined),
7392
+ _entityProxy: config.auth.onUserCreated ? config.auth._entityProxy ?? registry.createProxy() : undefined
7393
+ };
7394
+ const auth = createAuth(authConfig);
7395
+ if (apiPrefix !== "/api") {
7396
+ throw new Error(`requestHandler requires apiPrefix to be '/api' (got '${apiPrefix}'). ` + "Custom API prefixes are not yet supported with auth.");
7397
+ }
7398
+ const authPrefix = `${apiPrefix}/auth`;
7399
+ const authPrefixSlash = `${authPrefix}/`;
7400
+ const serverInstance = app;
7401
+ serverInstance.auth = auth;
7402
+ serverInstance.initialize = async () => {
7403
+ await initializeAuthTables(dbClient);
7404
+ await auth.initialize();
7405
+ };
7406
+ let cachedRequestHandler = null;
7407
+ Object.defineProperty(serverInstance, "requestHandler", {
7408
+ get() {
7409
+ if (!cachedRequestHandler) {
7410
+ const entityHandler = this.handler;
7411
+ const authHandler = this.auth.handler;
7412
+ cachedRequestHandler = (request) => {
7413
+ const pathname = new URL(request.url).pathname;
7414
+ if (pathname === authPrefix || pathname.startsWith(authPrefixSlash)) {
7415
+ return authHandler(request);
7416
+ }
7417
+ return entityHandler(request);
7418
+ };
7419
+ }
7420
+ return cachedRequestHandler;
7421
+ },
7422
+ enumerable: true,
7423
+ configurable: false
7424
+ });
7425
+ return serverInstance;
7426
+ }
7427
+ return app;
4608
7428
  }
4609
7429
  // src/entity/entity.ts
4610
7430
  import { deepFreeze } from "@vertz/core";
@@ -4616,6 +7436,8 @@ function entity(name, config) {
4616
7436
  if (!config.model) {
4617
7437
  throw new Error("entity() requires a model in the config.");
4618
7438
  }
7439
+ const hasTenantIdColumn = "tenantId" in config.model.table._columns;
7440
+ const tenantScoped = config.tenantScoped ?? hasTenantIdColumn;
4619
7441
  const def = {
4620
7442
  kind: "entity",
4621
7443
  name,
@@ -4625,7 +7447,10 @@ function entity(name, config) {
4625
7447
  before: config.before ?? {},
4626
7448
  after: config.after ?? {},
4627
7449
  actions: config.actions ?? {},
4628
- relations: config.relations ?? {}
7450
+ relations: config.relations ?? {},
7451
+ table: config.table ?? name,
7452
+ tenantScoped,
7453
+ tenantChain: null
4629
7454
  };
4630
7455
  return deepFreeze(def);
4631
7456
  }
@@ -4652,14 +7477,20 @@ export {
4652
7477
  vertz,
4653
7478
  verifyPassword,
4654
7479
  validatePassword,
7480
+ validateOverrides,
7481
+ validateAuthModels,
4655
7482
  stripReadOnlyFields,
4656
7483
  stripHiddenFields,
4657
7484
  service,
4658
7485
  rules,
7486
+ resolveTenantChain,
4659
7487
  makeImmutable,
7488
+ isContentDescriptor,
7489
+ initializeAuthTables,
4660
7490
  hashPassword,
4661
7491
  google,
4662
7492
  github,
7493
+ getIncompatibleAddOns,
4663
7494
  generateEntityRoutes,
4664
7495
  entityErrorHandler,
4665
7496
  entity,
@@ -4670,20 +7501,29 @@ export {
4670
7501
  defaultAccess,
4671
7502
  deepFreeze3 as deepFreeze,
4672
7503
  decodeAccessSet,
7504
+ createWebhookHandler,
7505
+ createStripeBillingAdapter,
4673
7506
  createServer,
7507
+ createPlanManager,
4674
7508
  createMiddleware,
4675
7509
  createImmutableProxy,
4676
7510
  createEnv,
4677
7511
  createEntityContext,
4678
7512
  createCrudHandlers,
7513
+ createBillingEventEmitter,
4679
7514
  createAuth,
4680
7515
  createAccessEventBroadcaster,
4681
7516
  createAccessContext,
4682
7517
  createAccess,
7518
+ content,
7519
+ computePlanHash,
7520
+ computeOverage,
4683
7521
  computeEntityAccess,
4684
7522
  computeAccessSet,
4685
7523
  checkFva,
7524
+ checkAddOnCompatibility,
4686
7525
  calculateBillingPeriod,
7526
+ authModels,
4687
7527
  VertzException2 as VertzException,
4688
7528
  ValidationException2 as ValidationException,
4689
7529
  UnauthorizedException,
@@ -4692,19 +7532,30 @@ export {
4692
7532
  InternalServerErrorException,
4693
7533
  InMemoryWalletStore,
4694
7534
  InMemoryUserStore,
7535
+ InMemorySubscriptionStore,
4695
7536
  InMemorySessionStore,
4696
7537
  InMemoryRoleAssignmentStore,
4697
7538
  InMemoryRateLimitStore,
4698
- InMemoryPlanStore,
7539
+ InMemoryPlanVersionStore,
4699
7540
  InMemoryPasswordResetStore,
7541
+ InMemoryOverrideStore,
4700
7542
  InMemoryOAuthAccountStore,
4701
7543
  InMemoryMFAStore,
7544
+ InMemoryGrandfatheringStore,
4702
7545
  InMemoryFlagStore,
4703
7546
  InMemoryEmailVerificationStore,
4704
7547
  InMemoryClosureStore,
4705
7548
  ForbiddenException,
4706
7549
  EntityRegistry,
7550
+ DbUserStore,
7551
+ DbSubscriptionStore,
7552
+ DbSessionStore,
7553
+ DbRoleAssignmentStore,
7554
+ DbOAuthAccountStore,
7555
+ DbFlagStore,
7556
+ DbClosureStore,
4707
7557
  ConflictException,
4708
- BadRequestException,
4709
- AuthorizationError
7558
+ BadRequestException2 as BadRequestException,
7559
+ AuthorizationError,
7560
+ AUTH_TABLE_NAMES
4710
7561
  };