@vertz/server 0.2.13 → 0.2.15

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 +671 -102
  2. package/dist/index.js +2139 -208
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -40,7 +40,13 @@ import {
40
40
  // src/auth/billing-period.ts
41
41
  function calculateBillingPeriod(startedAt, per, now = new Date) {
42
42
  if (per === "month") {
43
- return calculateMonthlyPeriod(startedAt, now);
43
+ return calculateMultiMonthPeriod(startedAt, now, 1);
44
+ }
45
+ if (per === "quarter") {
46
+ return calculateMultiMonthPeriod(startedAt, now, 3);
47
+ }
48
+ if (per === "year") {
49
+ return calculateMultiMonthPeriod(startedAt, now, 12);
44
50
  }
45
51
  const durationMs = per === "day" ? 24 * 60 * 60 * 1000 : 60 * 60 * 1000;
46
52
  const elapsed = now.getTime() - startedAt.getTime();
@@ -49,19 +55,21 @@ function calculateBillingPeriod(startedAt, per, now = new Date) {
49
55
  const periodEnd = new Date(periodStart.getTime() + durationMs);
50
56
  return { periodStart, periodEnd };
51
57
  }
52
- function calculateMonthlyPeriod(startedAt, now) {
58
+ function calculateMultiMonthPeriod(startedAt, now, monthInterval) {
53
59
  if (now.getTime() < startedAt.getTime()) {
54
- return { periodStart: new Date(startedAt), periodEnd: addMonths(startedAt, 1) };
60
+ return { periodStart: new Date(startedAt), periodEnd: addMonths(startedAt, monthInterval) };
55
61
  }
56
- let periodStart = new Date(startedAt);
57
62
  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
- }
63
+ const periodsElapsed = Math.floor(approxMonths / monthInterval);
64
+ let periodStart = addMonths(startedAt, periodsElapsed * monthInterval);
65
+ if (periodStart.getTime() > now.getTime()) {
66
+ periodStart = addMonths(startedAt, (periodsElapsed - 1) * monthInterval);
67
+ }
68
+ const nextStart = addMonths(startedAt, (periodsElapsed + 1) * monthInterval);
69
+ if (nextStart.getTime() <= now.getTime()) {
70
+ periodStart = nextStart;
63
71
  }
64
- const periodEnd = addMonths(startedAt, getMonthOffset(startedAt, periodStart) + 1);
72
+ const periodEnd = addMonths(startedAt, getMonthOffset(startedAt, periodStart) + monthInterval);
65
73
  return { periodStart, periodEnd };
66
74
  }
67
75
  function addMonths(base, months) {
@@ -92,6 +100,7 @@ function resolveEffectivePlan(orgPlan, plans, defaultPlan = "free", now) {
92
100
 
93
101
  class InMemoryPlanStore {
94
102
  plans = new Map;
103
+ addOns = new Map;
95
104
  async assignPlan(orgId, planId, startedAt = new Date, expiresAt = null) {
96
105
  this.plans.set(orgId, {
97
106
  orgId,
@@ -113,9 +122,58 @@ class InMemoryPlanStore {
113
122
  async removePlan(orgId) {
114
123
  this.plans.delete(orgId);
115
124
  }
125
+ async attachAddOn(orgId, addOnId) {
126
+ if (!this.addOns.has(orgId)) {
127
+ this.addOns.set(orgId, new Set);
128
+ }
129
+ this.addOns.get(orgId).add(addOnId);
130
+ }
131
+ async detachAddOn(orgId, addOnId) {
132
+ this.addOns.get(orgId)?.delete(addOnId);
133
+ }
134
+ async getAddOns(orgId) {
135
+ return [...this.addOns.get(orgId) ?? []];
136
+ }
137
+ async listByPlan(planId) {
138
+ const result = [];
139
+ for (const [orgId, plan] of this.plans.entries()) {
140
+ if (plan.planId === planId) {
141
+ result.push(orgId);
142
+ }
143
+ }
144
+ return result;
145
+ }
116
146
  dispose() {
117
147
  this.plans.clear();
148
+ this.addOns.clear();
149
+ }
150
+ }
151
+ function checkAddOnCompatibility(accessDef, addOnId, currentPlanId) {
152
+ const addOnDef = accessDef.plans?.[addOnId];
153
+ if (!addOnDef?.requires)
154
+ return true;
155
+ return addOnDef.requires.plans.includes(currentPlanId);
156
+ }
157
+ function getIncompatibleAddOns(accessDef, activeAddOnIds, targetPlanId) {
158
+ return activeAddOnIds.filter((id) => !checkAddOnCompatibility(accessDef, id, targetPlanId));
159
+ }
160
+
161
+ // src/auth/resolve-inherited-role.ts
162
+ function resolveInheritedRole(sourceType, sourceRole, targetType, accessDef) {
163
+ const hierarchy = accessDef.hierarchy;
164
+ const sourceIdx = hierarchy.indexOf(sourceType);
165
+ const targetIdx = hierarchy.indexOf(targetType);
166
+ if (sourceIdx === -1 || targetIdx === -1 || sourceIdx >= targetIdx)
167
+ return null;
168
+ let currentRole = sourceRole;
169
+ for (let i = sourceIdx;i < targetIdx; i++) {
170
+ const currentType = hierarchy[i];
171
+ const inheritanceMap = accessDef.inheritance[currentType];
172
+ if (!inheritanceMap || !(currentRole in inheritanceMap))
173
+ return null;
174
+ currentRole = inheritanceMap[currentRole];
118
175
  }
176
+ return currentRole;
119
177
  }
120
178
 
121
179
  // src/auth/access-set.ts
@@ -221,50 +279,97 @@ async function computeAccessSet(config) {
221
279
  if (effectivePlanId) {
222
280
  const planDef = accessDef.plans?.[effectivePlanId];
223
281
  if (planDef) {
224
- for (const [name, entDef] of Object.entries(accessDef.entitlements)) {
225
- if (entDef.plans?.length && !planDef.entitlements.includes(name)) {
282
+ const effectiveFeatures = new Set(planDef.features ?? []);
283
+ const addOns = await planStore.getAddOns?.(orgId);
284
+ if (addOns) {
285
+ for (const addOnId of addOns) {
286
+ const addOnDef = accessDef.plans?.[addOnId];
287
+ if (addOnDef?.features) {
288
+ for (const f of addOnDef.features)
289
+ effectiveFeatures.add(f);
290
+ }
291
+ }
292
+ }
293
+ for (const name of Object.keys(accessDef.entitlements)) {
294
+ if (accessDef._planGatedEntitlements.has(name) && !effectiveFeatures.has(name)) {
226
295
  const entry = entitlements[name];
227
296
  const reasons = [...entry.reasons];
228
297
  if (!reasons.includes("plan_required"))
229
298
  reasons.push("plan_required");
299
+ const requiredPlans = [];
300
+ for (const [pName, pDef] of Object.entries(accessDef.plans ?? {})) {
301
+ if (pDef.features?.includes(name))
302
+ requiredPlans.push(pName);
303
+ }
230
304
  entitlements[name] = {
231
305
  ...entry,
232
306
  allowed: false,
233
307
  reasons,
234
308
  reason: reasons[0],
235
- meta: { ...entry.meta, requiredPlans: [...entDef.plans] }
309
+ meta: { ...entry.meta, requiredPlans }
236
310
  };
237
311
  }
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 }
312
+ const limitKeys = accessDef._entitlementToLimitKeys[name];
313
+ if (walletStore && limitKeys?.length) {
314
+ const limitKey = limitKeys[0];
315
+ const limitDef = planDef.limits?.[limitKey];
316
+ if (limitDef) {
317
+ let effectiveMax = limitDef.max;
318
+ if (addOns) {
319
+ for (const addOnId of addOns) {
320
+ const addOnDef = accessDef.plans?.[addOnId];
321
+ const addOnLimit = addOnDef?.limits?.[limitKey];
322
+ if (addOnLimit) {
323
+ if (effectiveMax === -1)
324
+ break;
325
+ effectiveMax += addOnLimit.max;
326
+ }
258
327
  }
259
- };
260
- } else {
261
- entitlements[name] = {
262
- ...entry,
263
- meta: {
264
- ...entry.meta,
265
- limit: { max: effectiveLimit, consumed, remaining }
328
+ }
329
+ const override = orgPlan.overrides[limitKey];
330
+ if (override)
331
+ effectiveMax = Math.max(effectiveMax, override.max);
332
+ if (effectiveMax === -1) {
333
+ const entry = entitlements[name];
334
+ entitlements[name] = {
335
+ ...entry,
336
+ meta: {
337
+ ...entry.meta,
338
+ limit: { key: limitKey, max: -1, consumed: 0, remaining: -1 }
339
+ }
340
+ };
341
+ } else {
342
+ const period = limitDef.per ? calculateBillingPeriod(orgPlan.startedAt, limitDef.per) : {
343
+ periodStart: orgPlan.startedAt,
344
+ periodEnd: new Date("9999-12-31T23:59:59Z")
345
+ };
346
+ const consumed = await walletStore.getConsumption(orgId, limitKey, period.periodStart, period.periodEnd);
347
+ const remaining = Math.max(0, effectiveMax - consumed);
348
+ const entry = entitlements[name];
349
+ if (consumed >= effectiveMax) {
350
+ const reasons = [...entry.reasons];
351
+ if (!reasons.includes("limit_reached"))
352
+ reasons.push("limit_reached");
353
+ entitlements[name] = {
354
+ ...entry,
355
+ allowed: false,
356
+ reasons,
357
+ reason: reasons[0],
358
+ meta: {
359
+ ...entry.meta,
360
+ limit: { key: limitKey, max: effectiveMax, consumed, remaining }
361
+ }
362
+ };
363
+ } else {
364
+ entitlements[name] = {
365
+ ...entry,
366
+ meta: {
367
+ ...entry.meta,
368
+ limit: { key: limitKey, max: effectiveMax, consumed, remaining }
369
+ }
370
+ };
266
371
  }
267
- };
372
+ }
268
373
  }
269
374
  }
270
375
  }
@@ -342,22 +447,6 @@ function addRole(map, resourceType, role) {
342
447
  }
343
448
  roles.add(role);
344
449
  }
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
450
 
362
451
  // src/auth/cookies.ts
363
452
  var DEFAULT_COOKIE_CONFIG = {
@@ -703,7 +792,7 @@ var DEFAULT_PASSWORD_REQUIREMENTS = {
703
792
  requireNumbers: false,
704
793
  requireSymbols: false
705
794
  };
706
- var BCRYPT_ROUNDS = 12;
795
+ var BCRYPT_ROUNDS = process.env.VERTZ_TEST === "1" ? 4 : 12;
707
796
  async function hashPassword(password) {
708
797
  return bcrypt.hash(password, BCRYPT_ROUNDS);
709
798
  }
@@ -1216,9 +1305,11 @@ function createAccessContext(config) {
1216
1305
  flagStore,
1217
1306
  planStore,
1218
1307
  walletStore,
1219
- orgResolver
1308
+ overrideStore,
1309
+ orgResolver,
1310
+ planVersionStore
1220
1311
  } = config;
1221
- async function checkLayers1to4(entitlement, resource, resolvedOrgId) {
1312
+ async function checkLayers1to3(entitlement, resource, resolvedOrgId, overrides) {
1222
1313
  if (!userId)
1223
1314
  return false;
1224
1315
  const entDef = accessDef.entitlements[entitlement];
@@ -1241,29 +1332,44 @@ function createAccessContext(config) {
1241
1332
  } else if (entDef.roles.length > 0 && !resource) {
1242
1333
  return false;
1243
1334
  }
1244
- if (entDef.plans?.length && planStore && orgResolver) {
1335
+ if (accessDef._planGatedEntitlements.has(entitlement) && planStore && orgResolver) {
1245
1336
  if (!resolvedOrgId)
1246
1337
  return false;
1247
1338
  const orgPlan = await planStore.getPlan(resolvedOrgId);
1248
1339
  const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
1249
1340
  if (!effectivePlanId)
1250
1341
  return false;
1251
- const planDef = accessDef.plans?.[effectivePlanId];
1252
- if (!planDef || !planDef.entitlements.includes(entitlement)) {
1342
+ const hasFeature = await resolveEffectiveFeatures(resolvedOrgId, entitlement, effectivePlanId, accessDef, planStore, planVersionStore, overrides);
1343
+ if (!hasFeature)
1253
1344
  return false;
1254
- }
1255
1345
  }
1256
1346
  return true;
1257
1347
  }
1258
1348
  async function can(entitlement, resource) {
1259
1349
  const resolvedOrgId = orgResolver ? await orgResolver(resource) : null;
1260
- if (!await checkLayers1to4(entitlement, resource, resolvedOrgId))
1350
+ const overrides = resolvedOrgId && overrideStore ? await overrideStore.get(resolvedOrgId) : null;
1351
+ if (!await checkLayers1to3(entitlement, resource, resolvedOrgId, overrides))
1261
1352
  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;
1353
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1354
+ if (limitKeys?.length && walletStore && planStore && resolvedOrgId) {
1355
+ const walletStates = await resolveAllLimitStates(entitlement, resolvedOrgId, accessDef, planStore, walletStore, planVersionStore, overrides);
1356
+ for (const ws of walletStates) {
1357
+ if (ws.max === 0)
1358
+ return false;
1359
+ if (ws.max === -1)
1360
+ continue;
1361
+ if (ws.consumed >= ws.max) {
1362
+ if (ws.hasOverage) {
1363
+ if (ws.overageCap !== undefined) {
1364
+ const overageUnits = ws.consumed - ws.max;
1365
+ const overageCost = overageUnits * (ws.overageAmount ?? 0) / (ws.overagePer ?? 1);
1366
+ if (overageCost >= ws.overageCap)
1367
+ return false;
1368
+ }
1369
+ continue;
1370
+ }
1371
+ return false;
1372
+ }
1267
1373
  }
1268
1374
  }
1269
1375
  return true;
@@ -1287,6 +1393,7 @@ function createAccessContext(config) {
1287
1393
  };
1288
1394
  }
1289
1395
  const resolvedOrgId = orgResolver ? await orgResolver(resource) : null;
1396
+ const overrides = resolvedOrgId && overrideStore ? await overrideStore.get(resolvedOrgId) : null;
1290
1397
  if (entDef.flags?.length && flagStore && orgResolver) {
1291
1398
  if (resolvedOrgId) {
1292
1399
  const disabledFlags = [];
@@ -1315,7 +1422,7 @@ function createAccessContext(config) {
1315
1422
  meta.requiredRoles = [...entDef.roles];
1316
1423
  }
1317
1424
  }
1318
- if (entDef.plans?.length && planStore && orgResolver) {
1425
+ if (accessDef._planGatedEntitlements.has(entitlement) && planStore && orgResolver) {
1319
1426
  let planDenied = false;
1320
1427
  if (!resolvedOrgId) {
1321
1428
  planDenied = true;
@@ -1325,27 +1432,75 @@ function createAccessContext(config) {
1325
1432
  if (!effectivePlanId) {
1326
1433
  planDenied = true;
1327
1434
  } else {
1328
- const planDef = accessDef.plans?.[effectivePlanId];
1329
- if (!planDef || !planDef.entitlements.includes(entitlement)) {
1435
+ const hasFeature = await resolveEffectiveFeatures(resolvedOrgId, entitlement, effectivePlanId, accessDef, planStore, planVersionStore, overrides);
1436
+ if (!hasFeature) {
1330
1437
  planDenied = true;
1331
1438
  }
1332
1439
  }
1333
1440
  }
1334
1441
  if (planDenied) {
1335
1442
  reasons.push("plan_required");
1336
- meta.requiredPlans = [...entDef.plans];
1443
+ const plansWithFeature = [];
1444
+ if (accessDef.plans) {
1445
+ for (const [pName, pDef] of Object.entries(accessDef.plans)) {
1446
+ if (pDef.features?.includes(entitlement)) {
1447
+ plansWithFeature.push(pName);
1448
+ }
1449
+ }
1450
+ }
1451
+ meta.requiredPlans = plansWithFeature;
1337
1452
  }
1338
1453
  }
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) {
1454
+ const checkLimitKeys = accessDef._entitlementToLimitKeys[entitlement];
1455
+ if (checkLimitKeys?.length && walletStore && planStore && resolvedOrgId) {
1456
+ const walletStates = await resolveAllLimitStates(entitlement, resolvedOrgId, accessDef, planStore, walletStore, planVersionStore, overrides);
1457
+ for (const ws of walletStates) {
1458
+ const exceeded = ws.max === 0 || ws.max !== -1 && ws.consumed >= ws.max;
1459
+ if (exceeded) {
1460
+ if (ws.hasOverage && ws.max !== 0) {
1461
+ let capHit = false;
1462
+ if (ws.overageCap !== undefined) {
1463
+ const overageUnits = ws.consumed - ws.max;
1464
+ const overageCost = overageUnits * (ws.overageAmount ?? 0) / (ws.overagePer ?? 1);
1465
+ if (overageCost >= ws.overageCap)
1466
+ capHit = true;
1467
+ }
1468
+ if (capHit) {
1469
+ reasons.push("limit_reached");
1470
+ meta.limit = {
1471
+ key: ws.key,
1472
+ max: ws.max,
1473
+ consumed: ws.consumed,
1474
+ remaining: 0,
1475
+ overage: true
1476
+ };
1477
+ break;
1478
+ }
1479
+ meta.limit = {
1480
+ key: ws.key,
1481
+ max: ws.max,
1482
+ consumed: ws.consumed,
1483
+ remaining: 0,
1484
+ overage: true
1485
+ };
1486
+ continue;
1487
+ }
1348
1488
  reasons.push("limit_reached");
1489
+ meta.limit = {
1490
+ key: ws.key,
1491
+ max: ws.max,
1492
+ consumed: ws.consumed,
1493
+ remaining: Math.max(0, ws.max === -1 ? Infinity : ws.max - ws.consumed)
1494
+ };
1495
+ break;
1496
+ }
1497
+ if (!meta.limit) {
1498
+ meta.limit = {
1499
+ key: ws.key,
1500
+ max: ws.max,
1501
+ consumed: ws.consumed,
1502
+ remaining: ws.max === -1 ? Infinity : Math.max(0, ws.max - ws.consumed)
1503
+ };
1349
1504
  }
1350
1505
  }
1351
1506
  }
@@ -1375,14 +1530,26 @@ function createAccessContext(config) {
1375
1530
  }
1376
1531
  return results;
1377
1532
  }
1533
+ async function canBatch(entitlement, resources) {
1534
+ if (resources.length > MAX_BULK_CHECKS) {
1535
+ throw new Error(`canBatch() is limited to ${MAX_BULK_CHECKS} resources per call`);
1536
+ }
1537
+ const results = new Map;
1538
+ for (const resource of resources) {
1539
+ const allowed = await can(entitlement, resource);
1540
+ results.set(resource.id, allowed);
1541
+ }
1542
+ return results;
1543
+ }
1378
1544
  async function canAndConsume(entitlement, resource, amount = 1) {
1379
1545
  const resolvedOrgId = orgResolver ? await orgResolver(resource) : null;
1380
- if (!await checkLayers1to4(entitlement, resource, resolvedOrgId))
1546
+ const overrides = resolvedOrgId && overrideStore ? await overrideStore.get(resolvedOrgId) : null;
1547
+ if (!await checkLayers1to3(entitlement, resource, resolvedOrgId, overrides))
1381
1548
  return false;
1382
1549
  if (!walletStore || !planStore || !orgResolver)
1383
1550
  return true;
1384
- const entDef = accessDef.entitlements[entitlement];
1385
- if (!entDef?.plans?.length)
1551
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1552
+ if (!limitKeys?.length)
1386
1553
  return true;
1387
1554
  if (!resolvedOrgId)
1388
1555
  return false;
@@ -1392,40 +1559,67 @@ function createAccessContext(config) {
1392
1559
  const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
1393
1560
  if (!effectivePlanId)
1394
1561
  return false;
1395
- const planDef = accessDef.plans?.[effectivePlanId];
1396
- if (!planDef)
1397
- return false;
1398
- const limitDef = planDef.limits?.[entitlement];
1399
- if (!limitDef)
1562
+ const limitsToConsume = await resolveAllLimitConsumptions(resolvedOrgId, entitlement, effectivePlanId, accessDef, planStore, walletStore, orgPlan, planVersionStore, overrides);
1563
+ if (!limitsToConsume.length)
1400
1564
  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;
1565
+ const consumed = [];
1566
+ for (const lc of limitsToConsume) {
1567
+ if (lc.effectiveMax === -1)
1568
+ continue;
1569
+ if (lc.effectiveMax === 0) {
1570
+ await rollbackConsumptions(resolvedOrgId, consumed, walletStore, amount);
1571
+ return false;
1572
+ }
1573
+ let consumeMax = lc.effectiveMax;
1574
+ if (lc.hasOverage) {
1575
+ if (lc.overageCap !== undefined) {
1576
+ const currentConsumed = await walletStore.getConsumption(resolvedOrgId, lc.walletKey, lc.periodStart, lc.periodEnd);
1577
+ const overageUnits = Math.max(0, currentConsumed + amount - lc.effectiveMax);
1578
+ const overageCost = overageUnits * (lc.overageAmount ?? 0) / (lc.overagePer ?? 1);
1579
+ if (overageCost > lc.overageCap) {
1580
+ await rollbackConsumptions(resolvedOrgId, consumed, walletStore, amount);
1581
+ return false;
1582
+ }
1583
+ }
1584
+ consumeMax = Number.MAX_SAFE_INTEGER;
1585
+ }
1586
+ const result = await walletStore.consume(resolvedOrgId, lc.walletKey, lc.periodStart, lc.periodEnd, consumeMax, amount);
1587
+ if (!result.success) {
1588
+ await rollbackConsumptions(resolvedOrgId, consumed, walletStore, amount);
1589
+ return false;
1590
+ }
1591
+ consumed.push({
1592
+ key: lc.walletKey,
1593
+ periodStart: lc.periodStart,
1594
+ periodEnd: lc.periodEnd
1595
+ });
1596
+ }
1597
+ return true;
1405
1598
  }
1406
1599
  async function unconsume(entitlement, resource, amount = 1) {
1407
1600
  if (!walletStore || !planStore || !orgResolver)
1408
1601
  return;
1409
- const entDef = accessDef.entitlements[entitlement];
1410
- if (!entDef?.plans?.length)
1602
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1603
+ if (!limitKeys?.length)
1411
1604
  return;
1412
1605
  const orgId = await orgResolver(resource);
1413
1606
  if (!orgId)
1414
1607
  return;
1608
+ const overrides = overrideStore ? await overrideStore.get(orgId) : null;
1415
1609
  const orgPlan = await planStore.getPlan(orgId);
1416
1610
  if (!orgPlan)
1417
1611
  return;
1418
1612
  const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
1419
1613
  if (!effectivePlanId)
1420
1614
  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);
1615
+ const limitsToUnconsume = await resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId, accessDef, planStore, walletStore, orgPlan, planVersionStore, overrides);
1616
+ for (const lc of limitsToUnconsume) {
1617
+ if (lc.effectiveMax === -1)
1618
+ continue;
1619
+ await walletStore.unconsume(orgId, lc.walletKey, lc.periodStart, lc.periodEnd, amount);
1620
+ }
1427
1621
  }
1428
- return { can, check, authorize, canAll, canAndConsume, unconsume };
1622
+ return { can, check, authorize, canAll, canBatch, canAndConsume, unconsume };
1429
1623
  }
1430
1624
  var DENIAL_ORDER = [
1431
1625
  "plan_required",
@@ -1439,25 +1633,174 @@ var DENIAL_ORDER = [
1439
1633
  function orderDenialReasons(reasons) {
1440
1634
  return [...reasons].sort((a, b) => DENIAL_ORDER.indexOf(a) - DENIAL_ORDER.indexOf(b));
1441
1635
  }
1442
- async function resolveWalletStateFromOrgId(entitlement, orgId, accessDef, planStore, walletStore) {
1636
+ async function resolveEffectiveFeatures(orgId, entitlement, effectivePlanId, accessDef, planStore, planVersionStore, overrides) {
1637
+ if (planVersionStore) {
1638
+ const tenantVersion = await planVersionStore.getTenantVersion(orgId, effectivePlanId);
1639
+ if (tenantVersion !== null) {
1640
+ const versionInfo = await planVersionStore.getVersion(effectivePlanId, tenantVersion);
1641
+ if (versionInfo) {
1642
+ const snapshotFeatures = versionInfo.snapshot.features;
1643
+ if (snapshotFeatures.includes(entitlement))
1644
+ return true;
1645
+ const addOns2 = await planStore.getAddOns?.(orgId);
1646
+ if (addOns2) {
1647
+ for (const addOnId of addOns2) {
1648
+ const addOnDef = accessDef.plans?.[addOnId];
1649
+ if (addOnDef?.features?.includes(entitlement))
1650
+ return true;
1651
+ }
1652
+ }
1653
+ if (overrides?.features?.includes(entitlement))
1654
+ return true;
1655
+ return false;
1656
+ }
1657
+ }
1658
+ }
1659
+ const planDef = accessDef.plans?.[effectivePlanId];
1660
+ if (planDef?.features?.includes(entitlement))
1661
+ return true;
1662
+ const addOns = await planStore.getAddOns?.(orgId);
1663
+ if (addOns) {
1664
+ for (const addOnId of addOns) {
1665
+ const addOnDef = accessDef.plans?.[addOnId];
1666
+ if (addOnDef?.features?.includes(entitlement))
1667
+ return true;
1668
+ }
1669
+ }
1670
+ if (overrides?.features?.includes(entitlement))
1671
+ return true;
1672
+ return false;
1673
+ }
1674
+ async function resolveAllLimitStates(entitlement, orgId, accessDef, planStore, walletStore, planVersionStore, overrides) {
1443
1675
  const orgPlan = await planStore.getPlan(orgId);
1444
1676
  const effectivePlanId = resolveEffectivePlan(orgPlan, accessDef.plans, accessDef.defaultPlan);
1445
1677
  if (!effectivePlanId || !orgPlan)
1446
- return null;
1447
- 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 };
1678
+ return [];
1679
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1680
+ if (!limitKeys?.length)
1681
+ return [];
1682
+ let versionedLimits = null;
1683
+ if (planVersionStore) {
1684
+ const tenantVersion = await planVersionStore.getTenantVersion(orgId, effectivePlanId);
1685
+ if (tenantVersion !== null) {
1686
+ const versionInfo = await planVersionStore.getVersion(effectivePlanId, tenantVersion);
1687
+ if (versionInfo) {
1688
+ versionedLimits = versionInfo.snapshot.limits;
1689
+ }
1690
+ }
1691
+ }
1692
+ const states = [];
1693
+ for (const limitKey of limitKeys) {
1694
+ let limitDef;
1695
+ if (versionedLimits && limitKey in versionedLimits) {
1696
+ limitDef = versionedLimits[limitKey];
1697
+ } else {
1698
+ const planDef = accessDef.plans?.[effectivePlanId];
1699
+ limitDef = planDef?.limits?.[limitKey];
1700
+ }
1701
+ if (!limitDef)
1702
+ continue;
1703
+ const effectiveMax = computeEffectiveLimit(limitDef.max, limitKey, orgId, orgPlan, accessDef, planStore, overrides);
1704
+ const resolvedMax = await effectiveMax;
1705
+ const hasOverage = !!limitDef.overage;
1706
+ if (resolvedMax === -1) {
1707
+ states.push({ key: limitKey, max: -1, consumed: 0, hasOverage });
1708
+ continue;
1709
+ }
1710
+ const walletKey = limitKey;
1711
+ const period = limitDef.per ? calculateBillingPeriod(orgPlan.startedAt, limitDef.per) : { periodStart: orgPlan.startedAt, periodEnd: new Date("9999-12-31T23:59:59Z") };
1712
+ const consumed = await walletStore.getConsumption(orgId, walletKey, period.periodStart, period.periodEnd);
1713
+ states.push({
1714
+ key: limitKey,
1715
+ max: resolvedMax,
1716
+ consumed,
1717
+ hasOverage,
1718
+ ...limitDef.overage ? {
1719
+ overageCap: limitDef.overage.cap,
1720
+ overageAmount: limitDef.overage.amount,
1721
+ overagePer: limitDef.overage.per
1722
+ } : {}
1723
+ });
1724
+ }
1725
+ return states;
1726
+ }
1727
+ async function resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId, accessDef, planStore, _walletStore, orgPlan, planVersionStore, overrides) {
1728
+ const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
1729
+ if (!limitKeys?.length)
1730
+ return [];
1731
+ let versionedLimits = null;
1732
+ if (planVersionStore) {
1733
+ const tenantVersion = await planVersionStore.getTenantVersion(orgId, effectivePlanId);
1734
+ if (tenantVersion !== null) {
1735
+ const versionInfo = await planVersionStore.getVersion(effectivePlanId, tenantVersion);
1736
+ if (versionInfo) {
1737
+ versionedLimits = versionInfo.snapshot.limits;
1738
+ }
1739
+ }
1740
+ }
1741
+ const consumptions = [];
1742
+ for (const limitKey of limitKeys) {
1743
+ let limitDef;
1744
+ if (versionedLimits && limitKey in versionedLimits) {
1745
+ limitDef = versionedLimits[limitKey];
1746
+ } else {
1747
+ const planDef = accessDef.plans?.[effectivePlanId];
1748
+ limitDef = planDef?.limits?.[limitKey];
1749
+ }
1750
+ if (!limitDef)
1751
+ continue;
1752
+ const effectiveMax = await computeEffectiveLimit(limitDef.max, limitKey, orgId, orgPlan, accessDef, planStore, overrides);
1753
+ const walletKey = limitKey;
1754
+ const period = limitDef.per ? calculateBillingPeriod(orgPlan.startedAt, limitDef.per) : { periodStart: orgPlan.startedAt, periodEnd: new Date("9999-12-31T23:59:59Z") };
1755
+ consumptions.push({
1756
+ walletKey,
1757
+ periodStart: period.periodStart,
1758
+ periodEnd: period.periodEnd,
1759
+ effectiveMax,
1760
+ hasOverage: !!limitDef.overage,
1761
+ ...limitDef.overage ? {
1762
+ overageCap: limitDef.overage.cap,
1763
+ overageAmount: limitDef.overage.amount,
1764
+ overagePer: limitDef.overage.per
1765
+ } : {}
1766
+ });
1767
+ }
1768
+ return consumptions;
1769
+ }
1770
+ async function computeEffectiveLimit(basePlanMax, limitKey, orgId, orgPlan, accessDef, planStore, overrides) {
1771
+ let effectiveMax = basePlanMax;
1772
+ const addOns = await planStore.getAddOns?.(orgId);
1773
+ if (addOns) {
1774
+ for (const addOnId of addOns) {
1775
+ const addOnDef = accessDef.plans?.[addOnId];
1776
+ const addOnLimit = addOnDef?.limits?.[limitKey];
1777
+ if (addOnLimit) {
1778
+ if (effectiveMax === -1)
1779
+ break;
1780
+ effectiveMax += addOnLimit.max;
1781
+ }
1782
+ }
1783
+ }
1784
+ const oldOverride = orgPlan.overrides[limitKey];
1785
+ if (oldOverride) {
1786
+ effectiveMax = Math.max(effectiveMax, oldOverride.max);
1787
+ }
1788
+ const limitOverride = overrides?.limits?.[limitKey];
1789
+ if (limitOverride) {
1790
+ if (limitOverride.max !== undefined) {
1791
+ effectiveMax = limitOverride.max;
1792
+ } else if (limitOverride.add !== undefined) {
1793
+ if (effectiveMax !== -1) {
1794
+ effectiveMax = Math.max(0, effectiveMax + limitOverride.add);
1795
+ }
1796
+ }
1797
+ }
1798
+ return effectiveMax;
1455
1799
  }
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);
1800
+ async function rollbackConsumptions(orgId, consumed, walletStore, amount) {
1801
+ for (const c of consumed) {
1802
+ await walletStore.unconsume(orgId, c.key, c.periodStart, c.periodEnd, amount);
1803
+ }
1461
1804
  }
1462
1805
  // src/auth/access-event-broadcaster.ts
1463
1806
  function createAccessEventBroadcaster(config) {
@@ -1605,6 +1948,22 @@ function createAccessEventBroadcaster(config) {
1605
1948
  const event = { type: "access:plan_changed", orgId };
1606
1949
  broadcastToOrg(orgId, JSON.stringify(event));
1607
1950
  }
1951
+ function broadcastPlanAssigned(orgId, planId) {
1952
+ const event = { type: "access:plan_assigned", orgId, planId };
1953
+ broadcastToOrg(orgId, JSON.stringify(event));
1954
+ }
1955
+ function broadcastAddonAttached(orgId, addonId) {
1956
+ const event = { type: "access:addon_attached", orgId, addonId };
1957
+ broadcastToOrg(orgId, JSON.stringify(event));
1958
+ }
1959
+ function broadcastAddonDetached(orgId, addonId) {
1960
+ const event = { type: "access:addon_detached", orgId, addonId };
1961
+ broadcastToOrg(orgId, JSON.stringify(event));
1962
+ }
1963
+ function broadcastLimitReset(orgId, entitlement, max) {
1964
+ const event = { type: "access:limit_reset", orgId, entitlement, max };
1965
+ broadcastToOrg(orgId, JSON.stringify(event));
1966
+ }
1608
1967
  return {
1609
1968
  handleUpgrade,
1610
1969
  websocket,
@@ -1612,11 +1971,472 @@ function createAccessEventBroadcaster(config) {
1612
1971
  broadcastLimitUpdate,
1613
1972
  broadcastRoleChange,
1614
1973
  broadcastPlanChange,
1974
+ broadcastPlanAssigned,
1975
+ broadcastAddonAttached,
1976
+ broadcastAddonDetached,
1977
+ broadcastLimitReset,
1615
1978
  get getConnectionCount() {
1616
1979
  return connectionCount;
1617
1980
  }
1618
1981
  };
1619
1982
  }
1983
+ // src/auth/auth-models.ts
1984
+ import { d } from "@vertz/db";
1985
+ var authUsersTable = d.table("auth_users", {
1986
+ id: d.text().primary(),
1987
+ email: d.text(),
1988
+ passwordHash: d.text().nullable(),
1989
+ role: d.text().default("user"),
1990
+ plan: d.text().nullable(),
1991
+ emailVerified: d.boolean().default(false),
1992
+ createdAt: d.timestamp().default("now"),
1993
+ updatedAt: d.timestamp().default("now")
1994
+ });
1995
+ var authSessionsTable = d.table("auth_sessions", {
1996
+ id: d.text().primary(),
1997
+ userId: d.text(),
1998
+ refreshTokenHash: d.text(),
1999
+ previousRefreshHash: d.text().nullable(),
2000
+ currentTokens: d.text().nullable(),
2001
+ ipAddress: d.text(),
2002
+ userAgent: d.text(),
2003
+ createdAt: d.timestamp().default("now"),
2004
+ lastActiveAt: d.timestamp().default("now"),
2005
+ expiresAt: d.timestamp(),
2006
+ revokedAt: d.timestamp().nullable()
2007
+ });
2008
+ var authOauthAccountsTable = d.table("auth_oauth_accounts", {
2009
+ id: d.text().primary(),
2010
+ userId: d.text(),
2011
+ provider: d.text(),
2012
+ providerId: d.text(),
2013
+ email: d.text().nullable(),
2014
+ createdAt: d.timestamp().default("now")
2015
+ });
2016
+ var authRoleAssignmentsTable = d.table("auth_role_assignments", {
2017
+ id: d.text().primary(),
2018
+ userId: d.text(),
2019
+ resourceType: d.text(),
2020
+ resourceId: d.text(),
2021
+ role: d.text(),
2022
+ createdAt: d.timestamp().default("now")
2023
+ });
2024
+ var authClosureTable = d.table("auth_closure", {
2025
+ id: d.text().primary(),
2026
+ ancestorType: d.text(),
2027
+ ancestorId: d.text(),
2028
+ descendantType: d.text(),
2029
+ descendantId: d.text(),
2030
+ depth: d.integer()
2031
+ });
2032
+ var authPlansTable = d.table("auth_plans", {
2033
+ id: d.text().primary(),
2034
+ tenantId: d.text(),
2035
+ planId: d.text(),
2036
+ startedAt: d.timestamp().default("now"),
2037
+ expiresAt: d.timestamp().nullable()
2038
+ });
2039
+ var authPlanAddonsTable = d.table("auth_plan_addons", {
2040
+ id: d.text().primary(),
2041
+ tenantId: d.text(),
2042
+ addonId: d.text(),
2043
+ isOneOff: d.boolean().default(false),
2044
+ quantity: d.integer().default(1),
2045
+ createdAt: d.timestamp().default("now")
2046
+ });
2047
+ var authFlagsTable = d.table("auth_flags", {
2048
+ id: d.text().primary(),
2049
+ tenantId: d.text(),
2050
+ flag: d.text(),
2051
+ enabled: d.boolean().default(false)
2052
+ });
2053
+ var authOverridesTable = d.table("auth_overrides", {
2054
+ id: d.text().primary(),
2055
+ tenantId: d.text(),
2056
+ overrides: d.text(),
2057
+ updatedAt: d.timestamp().default("now")
2058
+ });
2059
+ var authModels = {
2060
+ auth_users: d.model(authUsersTable),
2061
+ auth_sessions: d.model(authSessionsTable),
2062
+ auth_oauth_accounts: d.model(authOauthAccountsTable),
2063
+ auth_role_assignments: d.model(authRoleAssignmentsTable),
2064
+ auth_closure: d.model(authClosureTable),
2065
+ auth_plans: d.model(authPlansTable),
2066
+ auth_plan_addons: d.model(authPlanAddonsTable),
2067
+ auth_flags: d.model(authFlagsTable),
2068
+ auth_overrides: d.model(authOverridesTable)
2069
+ };
2070
+ // src/auth/auth-tables.ts
2071
+ import { sql } from "@vertz/db/sql";
2072
+
2073
+ // src/auth/dialect-ddl.ts
2074
+ function dialectDDL(dialect) {
2075
+ return {
2076
+ boolean: (def) => dialect === "sqlite" ? `INTEGER NOT NULL DEFAULT ${def ? 1 : 0}` : `BOOLEAN NOT NULL DEFAULT ${def}`,
2077
+ timestamp: () => dialect === "sqlite" ? "TEXT NOT NULL" : "TIMESTAMPTZ NOT NULL",
2078
+ timestampNullable: () => dialect === "sqlite" ? "TEXT" : "TIMESTAMPTZ",
2079
+ text: () => "TEXT",
2080
+ textPrimary: () => "TEXT PRIMARY KEY",
2081
+ integer: () => "INTEGER"
2082
+ };
2083
+ }
2084
+
2085
+ // src/auth/auth-tables.ts
2086
+ function generateAuthDDL(dialect) {
2087
+ const t = dialectDDL(dialect);
2088
+ const statements = [];
2089
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_users (
2090
+ id ${t.textPrimary()},
2091
+ email ${t.text()} NOT NULL UNIQUE,
2092
+ password_hash ${t.text()},
2093
+ role ${t.text()} NOT NULL DEFAULT 'user',
2094
+ plan ${t.text()},
2095
+ email_verified ${t.boolean(false)},
2096
+ created_at ${t.timestamp()},
2097
+ updated_at ${t.timestamp()}
2098
+ )`);
2099
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_sessions (
2100
+ id ${t.textPrimary()},
2101
+ user_id ${t.text()} NOT NULL,
2102
+ refresh_token_hash ${t.text()} NOT NULL,
2103
+ previous_refresh_hash ${t.text()},
2104
+ current_tokens ${t.text()},
2105
+ ip_address ${t.text()} NOT NULL,
2106
+ user_agent ${t.text()} NOT NULL,
2107
+ created_at ${t.timestamp()},
2108
+ last_active_at ${t.timestamp()},
2109
+ expires_at ${t.timestamp()},
2110
+ revoked_at ${t.timestampNullable()}
2111
+ )`);
2112
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_oauth_accounts (
2113
+ id ${t.textPrimary()},
2114
+ user_id ${t.text()} NOT NULL,
2115
+ provider ${t.text()} NOT NULL,
2116
+ provider_id ${t.text()} NOT NULL,
2117
+ email ${t.text()},
2118
+ created_at ${t.timestamp()},
2119
+ UNIQUE(provider, provider_id)
2120
+ )`);
2121
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_role_assignments (
2122
+ id ${t.textPrimary()},
2123
+ user_id ${t.text()} NOT NULL,
2124
+ resource_type ${t.text()} NOT NULL,
2125
+ resource_id ${t.text()} NOT NULL,
2126
+ role ${t.text()} NOT NULL,
2127
+ created_at ${t.timestamp()},
2128
+ UNIQUE(user_id, resource_type, resource_id, role)
2129
+ )`);
2130
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_closure (
2131
+ id ${t.textPrimary()},
2132
+ ancestor_type ${t.text()} NOT NULL,
2133
+ ancestor_id ${t.text()} NOT NULL,
2134
+ descendant_type ${t.text()} NOT NULL,
2135
+ descendant_id ${t.text()} NOT NULL,
2136
+ depth ${t.integer()} NOT NULL,
2137
+ UNIQUE(ancestor_type, ancestor_id, descendant_type, descendant_id)
2138
+ )`);
2139
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_plans (
2140
+ id ${t.textPrimary()},
2141
+ tenant_id ${t.text()} NOT NULL UNIQUE,
2142
+ plan_id ${t.text()} NOT NULL,
2143
+ started_at ${t.timestamp()},
2144
+ expires_at ${t.timestampNullable()}
2145
+ )`);
2146
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_plan_addons (
2147
+ id ${t.textPrimary()},
2148
+ tenant_id ${t.text()} NOT NULL,
2149
+ addon_id ${t.text()} NOT NULL,
2150
+ is_one_off ${t.boolean(false)},
2151
+ quantity ${t.integer()} NOT NULL DEFAULT 1,
2152
+ created_at ${t.timestamp()},
2153
+ UNIQUE(tenant_id, addon_id)
2154
+ )`);
2155
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_flags (
2156
+ id ${t.textPrimary()},
2157
+ tenant_id ${t.text()} NOT NULL,
2158
+ flag ${t.text()} NOT NULL,
2159
+ enabled ${t.boolean(false)},
2160
+ UNIQUE(tenant_id, flag)
2161
+ )`);
2162
+ statements.push(`CREATE TABLE IF NOT EXISTS auth_overrides (
2163
+ id ${t.textPrimary()},
2164
+ tenant_id ${t.text()} NOT NULL UNIQUE,
2165
+ overrides ${t.text()} NOT NULL,
2166
+ updated_at ${t.timestamp()}
2167
+ )`);
2168
+ return statements;
2169
+ }
2170
+ var AUTH_TABLE_NAMES = [
2171
+ "auth_users",
2172
+ "auth_sessions",
2173
+ "auth_oauth_accounts",
2174
+ "auth_role_assignments",
2175
+ "auth_closure",
2176
+ "auth_plans",
2177
+ "auth_plan_addons",
2178
+ "auth_flags",
2179
+ "auth_overrides"
2180
+ ];
2181
+ function validateAuthModels(db) {
2182
+ const dbModels = db._internals.models;
2183
+ const missing = AUTH_TABLE_NAMES.filter((m) => !(m in dbModels));
2184
+ if (missing.length > 0) {
2185
+ throw new Error(`Auth requires models ${missing.map((m) => `"${m}"`).join(", ")} in createDb(). ` + "Add authModels to your createDb() call: createDb({ models: { ...authModels, ...yourModels } })");
2186
+ }
2187
+ }
2188
+ async function initializeAuthTables(db) {
2189
+ const dialectName = db._internals.dialect.name;
2190
+ const statements = generateAuthDDL(dialectName);
2191
+ for (const ddl of statements) {
2192
+ await db.query(sql.raw(ddl));
2193
+ }
2194
+ }
2195
+ // src/auth/billing/event-emitter.ts
2196
+ function createBillingEventEmitter() {
2197
+ const handlers = new Map;
2198
+ return {
2199
+ on(eventType, handler) {
2200
+ const list = handlers.get(eventType) ?? [];
2201
+ list.push(handler);
2202
+ handlers.set(eventType, list);
2203
+ },
2204
+ off(eventType, handler) {
2205
+ const list = handlers.get(eventType);
2206
+ if (!list)
2207
+ return;
2208
+ const idx = list.indexOf(handler);
2209
+ if (idx >= 0)
2210
+ list.splice(idx, 1);
2211
+ },
2212
+ emit(eventType, event) {
2213
+ const list = handlers.get(eventType);
2214
+ if (!list)
2215
+ return;
2216
+ for (const handler of list) {
2217
+ try {
2218
+ handler(event);
2219
+ } catch {}
2220
+ }
2221
+ }
2222
+ };
2223
+ }
2224
+ // src/auth/billing/overage.ts
2225
+ function computeOverage(input) {
2226
+ const { consumed, max, rate } = input;
2227
+ if (max === -1)
2228
+ return 0;
2229
+ if (consumed <= max)
2230
+ return 0;
2231
+ const overageUnits = consumed - max;
2232
+ const charge = overageUnits * rate;
2233
+ if (input.cap !== undefined && charge > input.cap) {
2234
+ return input.cap;
2235
+ }
2236
+ return charge;
2237
+ }
2238
+ // src/auth/billing/stripe-adapter.ts
2239
+ var INTERVAL_MAP = {
2240
+ month: "month",
2241
+ quarter: "month",
2242
+ year: "year"
2243
+ };
2244
+ function mapInterval(interval) {
2245
+ if (interval === "quarter") {
2246
+ return { interval: "month", interval_count: 3 };
2247
+ }
2248
+ return { interval: INTERVAL_MAP[interval] ?? "month" };
2249
+ }
2250
+ function createStripeBillingAdapter(config) {
2251
+ const { stripe, currency = "usd" } = config;
2252
+ async function syncPlans(plans) {
2253
+ const existing = await stripe.products.list();
2254
+ const existingByPlanId = new Map;
2255
+ for (const product of existing.data) {
2256
+ if (product.metadata.vertzPlanId) {
2257
+ existingByPlanId.set(product.metadata.vertzPlanId, product);
2258
+ }
2259
+ }
2260
+ for (const [planId, planDef] of Object.entries(plans)) {
2261
+ const metadata = {
2262
+ vertzPlanId: planId
2263
+ };
2264
+ if (planDef.group)
2265
+ metadata.group = planDef.group;
2266
+ if (planDef.addOn)
2267
+ metadata.addOn = "true";
2268
+ let product = existingByPlanId.get(planId);
2269
+ if (!product) {
2270
+ product = await stripe.products.create({
2271
+ name: planDef.title ?? planId,
2272
+ metadata,
2273
+ description: planDef.description
2274
+ });
2275
+ } else {
2276
+ await stripe.products.update(product.id, {
2277
+ name: planDef.title ?? planId,
2278
+ metadata
2279
+ });
2280
+ }
2281
+ const amount = planDef.price?.amount ?? 0;
2282
+ const interval = planDef.price?.interval;
2283
+ const unitAmount = Math.round(amount * 100);
2284
+ const existingPrices = await stripe.prices.list({ product: product.id });
2285
+ const activePrices = existingPrices.data.filter((p) => p.active);
2286
+ const matchingPrice = activePrices.find((p) => p.unit_amount === unitAmount);
2287
+ if (!matchingPrice) {
2288
+ for (const oldPrice of activePrices) {
2289
+ await stripe.prices.update(oldPrice.id, { active: false });
2290
+ }
2291
+ const priceParams = {
2292
+ product: product.id,
2293
+ unit_amount: unitAmount,
2294
+ currency,
2295
+ metadata: { vertzPlanId: planId }
2296
+ };
2297
+ if (interval && interval !== "one_off") {
2298
+ const mapped = mapInterval(interval);
2299
+ priceParams.recurring = { interval: mapped.interval };
2300
+ }
2301
+ await stripe.prices.create(priceParams);
2302
+ }
2303
+ }
2304
+ }
2305
+ async function createSubscription(_tenantId, _planId) {
2306
+ return "sub_placeholder";
2307
+ }
2308
+ async function cancelSubscription(_tenantId) {}
2309
+ async function attachAddOn(_tenantId, _addOnId) {}
2310
+ async function reportOverage(_tenantId, _limitKey, _amount) {}
2311
+ async function getPortalUrl(_tenantId) {
2312
+ return "https://billing.stripe.com/portal/placeholder";
2313
+ }
2314
+ return {
2315
+ syncPlans,
2316
+ createSubscription,
2317
+ cancelSubscription,
2318
+ attachAddOn,
2319
+ reportOverage,
2320
+ getPortalUrl
2321
+ };
2322
+ }
2323
+ // src/auth/billing/webhook-handler.ts
2324
+ function extractTenantId(obj) {
2325
+ const meta = obj.metadata;
2326
+ if (meta?.tenantId)
2327
+ return meta.tenantId;
2328
+ const subDetails = obj.subscription_details;
2329
+ if (subDetails?.metadata) {
2330
+ const subMeta = subDetails.metadata;
2331
+ if (subMeta.tenantId)
2332
+ return subMeta.tenantId;
2333
+ }
2334
+ return null;
2335
+ }
2336
+ function extractPlanId(obj) {
2337
+ const meta = obj.metadata;
2338
+ if (meta?.vertzPlanId)
2339
+ return meta.vertzPlanId;
2340
+ const items = obj.items;
2341
+ if (items?.data?.[0]?.price?.metadata?.vertzPlanId) {
2342
+ return items.data[0].price.metadata.vertzPlanId;
2343
+ }
2344
+ return null;
2345
+ }
2346
+ function createWebhookHandler(config) {
2347
+ const { planStore, emitter, defaultPlan, webhookSecret } = config;
2348
+ return async (request) => {
2349
+ const signature = request.headers.get("stripe-signature");
2350
+ if (signature !== webhookSecret) {
2351
+ return new Response(JSON.stringify({ error: "Invalid signature" }), {
2352
+ status: 400,
2353
+ headers: { "content-type": "application/json" }
2354
+ });
2355
+ }
2356
+ let event;
2357
+ try {
2358
+ const body = await request.text();
2359
+ event = JSON.parse(body);
2360
+ } catch {
2361
+ return new Response(JSON.stringify({ error: "Invalid JSON" }), {
2362
+ status: 400,
2363
+ headers: { "content-type": "application/json" }
2364
+ });
2365
+ }
2366
+ const obj = event.data.object;
2367
+ try {
2368
+ switch (event.type) {
2369
+ case "customer.subscription.created": {
2370
+ const tenantId = extractTenantId(obj);
2371
+ const planId = extractPlanId(obj);
2372
+ if (tenantId && planId) {
2373
+ await planStore.assignPlan(tenantId, planId);
2374
+ emitter.emit("subscription:created", { tenantId, planId });
2375
+ }
2376
+ break;
2377
+ }
2378
+ case "customer.subscription.updated": {
2379
+ const tenantId = extractTenantId(obj);
2380
+ const planId = extractPlanId(obj);
2381
+ if (tenantId && planId) {
2382
+ await planStore.assignPlan(tenantId, planId);
2383
+ }
2384
+ break;
2385
+ }
2386
+ case "customer.subscription.deleted": {
2387
+ const tenantId = extractTenantId(obj);
2388
+ const planId = extractPlanId(obj);
2389
+ if (tenantId) {
2390
+ await planStore.assignPlan(tenantId, defaultPlan);
2391
+ emitter.emit("subscription:canceled", {
2392
+ tenantId,
2393
+ planId: planId ?? defaultPlan
2394
+ });
2395
+ }
2396
+ break;
2397
+ }
2398
+ case "invoice.payment_failed": {
2399
+ const tenantId = extractTenantId(obj);
2400
+ const planId = extractPlanId(obj);
2401
+ const attemptCount = obj.attempt_count ?? 1;
2402
+ if (tenantId) {
2403
+ emitter.emit("billing:payment_failed", {
2404
+ tenantId,
2405
+ planId: planId ?? "",
2406
+ attempt: attemptCount
2407
+ });
2408
+ }
2409
+ break;
2410
+ }
2411
+ case "checkout.session.completed": {
2412
+ const tenantId = extractTenantId(obj);
2413
+ const planId = extractPlanId(obj);
2414
+ const meta = obj.metadata;
2415
+ const isAddOn = meta?.isAddOn === "true";
2416
+ if (tenantId && planId) {
2417
+ if (isAddOn && planStore.attachAddOn) {
2418
+ await planStore.attachAddOn(tenantId, planId);
2419
+ } else {
2420
+ await planStore.assignPlan(tenantId, planId);
2421
+ }
2422
+ }
2423
+ break;
2424
+ }
2425
+ default:
2426
+ break;
2427
+ }
2428
+ } catch {
2429
+ return new Response(JSON.stringify({ error: "Processing failed" }), {
2430
+ status: 500,
2431
+ headers: { "content-type": "application/json" }
2432
+ });
2433
+ }
2434
+ return new Response(JSON.stringify({ received: true }), {
2435
+ status: 200,
2436
+ headers: { "content-type": "application/json" }
2437
+ });
2438
+ };
2439
+ }
1620
2440
  // src/auth/closure-store.ts
1621
2441
  var MAX_DEPTH = 4;
1622
2442
 
@@ -1648,112 +2468,739 @@ class InMemoryClosureStore {
1648
2468
  }
1649
2469
  }
1650
2470
  }
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
- });
2471
+ async removeResource(type, id) {
2472
+ const descendants = await this.getDescendants(type, id);
2473
+ const descendantKeys = new Set(descendants.map((d2) => `${d2.type}:${d2.id}`));
2474
+ this.rows = this.rows.filter((row) => {
2475
+ const ancestorKey = `${row.ancestorType}:${row.ancestorId}`;
2476
+ const descendantKey = `${row.descendantType}:${row.descendantId}`;
2477
+ return !descendantKeys.has(ancestorKey) && !descendantKeys.has(descendantKey);
2478
+ });
2479
+ }
2480
+ async getAncestors(type, id) {
2481
+ return this.rows.filter((row) => row.descendantType === type && row.descendantId === id).map((row) => ({
2482
+ type: row.ancestorType,
2483
+ id: row.ancestorId,
2484
+ depth: row.depth
2485
+ }));
2486
+ }
2487
+ async getDescendants(type, id) {
2488
+ return this.rows.filter((row) => row.ancestorType === type && row.ancestorId === id).map((row) => ({
2489
+ type: row.descendantType,
2490
+ id: row.descendantId,
2491
+ depth: row.depth
2492
+ }));
2493
+ }
2494
+ async hasPath(ancestorType, ancestorId, descendantType, descendantId) {
2495
+ return this.rows.some((row) => row.ancestorType === ancestorType && row.ancestorId === ancestorId && row.descendantType === descendantType && row.descendantId === descendantId);
2496
+ }
2497
+ dispose() {
2498
+ this.rows = [];
2499
+ }
2500
+ }
2501
+ // src/auth/db-closure-store.ts
2502
+ import { sql as sql2 } from "@vertz/db/sql";
2503
+
2504
+ // src/auth/db-types.ts
2505
+ function boolVal(db, value) {
2506
+ return db._internals.dialect.name === "sqlite" ? value ? 1 : 0 : value;
2507
+ }
2508
+ function assertWrite(result, context) {
2509
+ if (!result.ok) {
2510
+ throw new Error(`Auth DB write failed (${context}): ${result.error.message}`);
2511
+ }
2512
+ }
2513
+
2514
+ // src/auth/db-closure-store.ts
2515
+ var MAX_DEPTH2 = 4;
2516
+
2517
+ class DbClosureStore {
2518
+ db;
2519
+ constructor(db) {
2520
+ this.db = db;
2521
+ }
2522
+ async addResource(type, id, parent) {
2523
+ const selfId = crypto.randomUUID();
2524
+ const selfResult = await this.db.query(sql2`INSERT INTO auth_closure (id, ancestor_type, ancestor_id, descendant_type, descendant_id, depth)
2525
+ VALUES (${selfId}, ${type}, ${id}, ${type}, ${id}, ${0})
2526
+ ON CONFLICT DO NOTHING`);
2527
+ assertWrite(selfResult, "addResource/self");
2528
+ if (parent) {
2529
+ const parentAncestors = await this.getAncestors(parent.parentType, parent.parentId);
2530
+ const maxParentDepth = Math.max(...parentAncestors.map((a) => a.depth), 0);
2531
+ if (maxParentDepth + 1 >= MAX_DEPTH2) {
2532
+ throw new Error("Hierarchy depth exceeds maximum of 4 levels");
2533
+ }
2534
+ for (const ancestor of parentAncestors) {
2535
+ const rowId = crypto.randomUUID();
2536
+ const ancestorResult = await this.db.query(sql2`INSERT INTO auth_closure (id, ancestor_type, ancestor_id, descendant_type, descendant_id, depth)
2537
+ VALUES (${rowId}, ${ancestor.type}, ${ancestor.id}, ${type}, ${id}, ${ancestor.depth + 1})
2538
+ ON CONFLICT DO NOTHING`);
2539
+ assertWrite(ancestorResult, "addResource/ancestor");
2540
+ }
2541
+ }
2542
+ }
2543
+ async removeResource(type, id) {
2544
+ const descendants = await this.getDescendants(type, id);
2545
+ for (const desc of descendants) {
2546
+ 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})`);
2547
+ assertWrite(delResult, "removeResource");
2548
+ }
2549
+ }
2550
+ async getAncestors(type, id) {
2551
+ const result = await this.db.query(sql2`SELECT ancestor_type, ancestor_id, depth FROM auth_closure WHERE descendant_type = ${type} AND descendant_id = ${id}`);
2552
+ if (!result.ok)
2553
+ return [];
2554
+ return result.data.rows.map((r) => ({
2555
+ type: r.ancestor_type,
2556
+ id: r.ancestor_id,
2557
+ depth: r.depth
2558
+ }));
2559
+ }
2560
+ async getDescendants(type, id) {
2561
+ const result = await this.db.query(sql2`SELECT descendant_type, descendant_id, depth FROM auth_closure WHERE ancestor_type = ${type} AND ancestor_id = ${id}`);
2562
+ if (!result.ok)
2563
+ return [];
2564
+ return result.data.rows.map((r) => ({
2565
+ type: r.descendant_type,
2566
+ id: r.descendant_id,
2567
+ depth: r.depth
2568
+ }));
2569
+ }
2570
+ async hasPath(ancestorType, ancestorId, descendantType, descendantId) {
2571
+ 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}`);
2572
+ if (!result.ok)
2573
+ return false;
2574
+ return (result.data.rows[0]?.cnt ?? 0) > 0;
2575
+ }
2576
+ dispose() {}
2577
+ }
2578
+ // src/auth/db-flag-store.ts
2579
+ import { sql as sql3 } from "@vertz/db/sql";
2580
+ class DbFlagStore {
2581
+ db;
2582
+ cache = new Map;
2583
+ constructor(db) {
2584
+ this.db = db;
2585
+ }
2586
+ async loadFlags() {
2587
+ const result = await this.db.query(sql3`SELECT tenant_id, flag, enabled FROM auth_flags`);
2588
+ if (!result.ok)
2589
+ return;
2590
+ this.cache.clear();
2591
+ for (const row of result.data.rows) {
2592
+ let orgFlags = this.cache.get(row.tenant_id);
2593
+ if (!orgFlags) {
2594
+ orgFlags = new Map;
2595
+ this.cache.set(row.tenant_id, orgFlags);
2596
+ }
2597
+ orgFlags.set(row.flag, row.enabled === 1 || row.enabled === true);
2598
+ }
2599
+ }
2600
+ setFlag(orgId, flag, enabled) {
2601
+ let orgFlags = this.cache.get(orgId);
2602
+ if (!orgFlags) {
2603
+ orgFlags = new Map;
2604
+ this.cache.set(orgId, orgFlags);
2605
+ }
2606
+ orgFlags.set(flag, enabled);
2607
+ const id = crypto.randomUUID();
2608
+ const val = boolVal(this.db, enabled);
2609
+ this.db.query(sql3`INSERT INTO auth_flags (id, tenant_id, flag, enabled) VALUES (${id}, ${orgId}, ${flag}, ${val})
2610
+ ON CONFLICT(tenant_id, flag) DO UPDATE SET enabled = ${val}`).then((result) => {
2611
+ if (!result.ok) {
2612
+ console.error("[DbFlagStore] Failed to persist flag:", result.error);
2613
+ }
2614
+ });
2615
+ }
2616
+ getFlag(orgId, flag) {
2617
+ return this.cache.get(orgId)?.get(flag) ?? false;
2618
+ }
2619
+ getFlags(orgId) {
2620
+ const orgFlags = this.cache.get(orgId);
2621
+ if (!orgFlags)
2622
+ return {};
2623
+ const result = {};
2624
+ for (const [key, value] of orgFlags) {
2625
+ result[key] = value;
2626
+ }
2627
+ return result;
2628
+ }
2629
+ }
2630
+ // src/auth/db-oauth-account-store.ts
2631
+ import { sql as sql4 } from "@vertz/db/sql";
2632
+ class DbOAuthAccountStore {
2633
+ db;
2634
+ constructor(db) {
2635
+ this.db = db;
2636
+ }
2637
+ async linkAccount(userId, provider, providerId, email) {
2638
+ const id = crypto.randomUUID();
2639
+ const now = new Date().toISOString();
2640
+ const emailValue = email ?? null;
2641
+ const result = await this.db.query(sql4`INSERT INTO auth_oauth_accounts (id, user_id, provider, provider_id, email, created_at)
2642
+ VALUES (${id}, ${userId}, ${provider}, ${providerId}, ${emailValue}, ${now})
2643
+ ON CONFLICT DO NOTHING`);
2644
+ assertWrite(result, "linkAccount");
2645
+ }
2646
+ async findByProviderAccount(provider, providerId) {
2647
+ const result = await this.db.query(sql4`SELECT user_id FROM auth_oauth_accounts WHERE provider = ${provider} AND provider_id = ${providerId}`);
2648
+ if (!result.ok || result.data.rows.length === 0)
2649
+ return null;
2650
+ return result.data.rows[0]?.user_id ?? null;
2651
+ }
2652
+ async findByUserId(userId) {
2653
+ const result = await this.db.query(sql4`SELECT provider, provider_id FROM auth_oauth_accounts WHERE user_id = ${userId}`);
2654
+ if (!result.ok)
2655
+ return [];
2656
+ return result.data.rows.map((r) => ({
2657
+ provider: r.provider,
2658
+ providerId: r.provider_id
2659
+ }));
2660
+ }
2661
+ async unlinkAccount(userId, provider) {
2662
+ const result = await this.db.query(sql4`DELETE FROM auth_oauth_accounts WHERE user_id = ${userId} AND provider = ${provider}`);
2663
+ assertWrite(result, "unlinkAccount");
2664
+ }
2665
+ dispose() {}
2666
+ }
2667
+ // src/auth/db-plan-store.ts
2668
+ import { sql as sql5 } from "@vertz/db/sql";
2669
+ class DbPlanStore {
2670
+ db;
2671
+ constructor(db) {
2672
+ this.db = db;
2673
+ }
2674
+ async assignPlan(orgId, planId, startedAt = new Date, expiresAt = null) {
2675
+ const id = crypto.randomUUID();
2676
+ const startedAtIso = startedAt.toISOString();
2677
+ const expiresAtIso = expiresAt ? expiresAt.toISOString() : null;
2678
+ const upsertResult = await this.db.query(sql5`INSERT INTO auth_plans (id, tenant_id, plan_id, started_at, expires_at)
2679
+ VALUES (${id}, ${orgId}, ${planId}, ${startedAtIso}, ${expiresAtIso})
2680
+ ON CONFLICT(tenant_id) DO UPDATE SET plan_id = ${planId}, started_at = ${startedAtIso}, expires_at = ${expiresAtIso}`);
2681
+ assertWrite(upsertResult, "assignPlan/upsert");
2682
+ const overrideResult = await this.db.query(sql5`DELETE FROM auth_overrides WHERE tenant_id = ${orgId}`);
2683
+ assertWrite(overrideResult, "assignPlan/clearOverrides");
2684
+ }
2685
+ async getPlan(orgId) {
2686
+ const result = await this.db.query(sql5`SELECT tenant_id, plan_id, started_at, expires_at FROM auth_plans WHERE tenant_id = ${orgId}`);
2687
+ if (!result.ok)
2688
+ return null;
2689
+ const row = result.data.rows[0];
2690
+ if (!row)
2691
+ return null;
2692
+ const overrides = await this.loadOverrides(orgId);
2693
+ return {
2694
+ orgId: row.tenant_id,
2695
+ planId: row.plan_id,
2696
+ startedAt: new Date(row.started_at),
2697
+ expiresAt: row.expires_at ? new Date(row.expires_at) : null,
2698
+ overrides
2699
+ };
2700
+ }
2701
+ async updateOverrides(orgId, overrides) {
2702
+ const planResult = await this.db.query(sql5`SELECT tenant_id FROM auth_plans WHERE tenant_id = ${orgId}`);
2703
+ if (!planResult.ok || planResult.data.rows.length === 0)
2704
+ return;
2705
+ const existing = await this.loadOverrides(orgId);
2706
+ const merged = { ...existing, ...overrides };
2707
+ const overridesJson = JSON.stringify(merged);
2708
+ const now = new Date().toISOString();
2709
+ const overrideResult = await this.db.query(sql5`SELECT tenant_id FROM auth_overrides WHERE tenant_id = ${orgId}`);
2710
+ if (overrideResult.ok && overrideResult.data.rows.length > 0) {
2711
+ const updateResult = await this.db.query(sql5`UPDATE auth_overrides SET overrides = ${overridesJson}, updated_at = ${now} WHERE tenant_id = ${orgId}`);
2712
+ assertWrite(updateResult, "updateOverrides/update");
2713
+ } else {
2714
+ const id = crypto.randomUUID();
2715
+ const insertResult = await this.db.query(sql5`INSERT INTO auth_overrides (id, tenant_id, overrides, updated_at)
2716
+ VALUES (${id}, ${orgId}, ${overridesJson}, ${now})`);
2717
+ assertWrite(insertResult, "updateOverrides/insert");
2718
+ }
2719
+ }
2720
+ async removePlan(orgId) {
2721
+ const planResult = await this.db.query(sql5`DELETE FROM auth_plans WHERE tenant_id = ${orgId}`);
2722
+ assertWrite(planResult, "removePlan/plans");
2723
+ const overrideResult = await this.db.query(sql5`DELETE FROM auth_overrides WHERE tenant_id = ${orgId}`);
2724
+ assertWrite(overrideResult, "removePlan/overrides");
2725
+ }
2726
+ async attachAddOn(orgId, addOnId) {
2727
+ const id = crypto.randomUUID();
2728
+ const now = new Date().toISOString();
2729
+ const result = await this.db.query(sql5`INSERT INTO auth_plan_addons (id, tenant_id, addon_id, quantity, created_at)
2730
+ VALUES (${id}, ${orgId}, ${addOnId}, ${1}, ${now})
2731
+ ON CONFLICT(tenant_id, addon_id) DO NOTHING`);
2732
+ assertWrite(result, "attachAddOn");
2733
+ }
2734
+ async detachAddOn(orgId, addOnId) {
2735
+ const result = await this.db.query(sql5`DELETE FROM auth_plan_addons WHERE tenant_id = ${orgId} AND addon_id = ${addOnId}`);
2736
+ assertWrite(result, "detachAddOn");
2737
+ }
2738
+ async getAddOns(orgId) {
2739
+ const result = await this.db.query(sql5`SELECT addon_id FROM auth_plan_addons WHERE tenant_id = ${orgId}`);
2740
+ if (!result.ok)
2741
+ return [];
2742
+ return result.data.rows.map((r) => r.addon_id);
2743
+ }
2744
+ dispose() {}
2745
+ async loadOverrides(orgId) {
2746
+ const result = await this.db.query(sql5`SELECT overrides FROM auth_overrides WHERE tenant_id = ${orgId}`);
2747
+ if (!result.ok || result.data.rows.length === 0)
2748
+ return {};
2749
+ try {
2750
+ const overridesStr = result.data.rows[0]?.overrides;
2751
+ if (!overridesStr)
2752
+ return {};
2753
+ return JSON.parse(overridesStr);
2754
+ } catch {
2755
+ return {};
2756
+ }
2757
+ }
2758
+ }
2759
+ // src/auth/db-role-assignment-store.ts
2760
+ import { sql as sql6 } from "@vertz/db/sql";
2761
+ class DbRoleAssignmentStore {
2762
+ db;
2763
+ constructor(db) {
2764
+ this.db = db;
2765
+ }
2766
+ async assign(userId, resourceType, resourceId, role) {
2767
+ const id = crypto.randomUUID();
2768
+ const now = new Date().toISOString();
2769
+ const result = await this.db.query(sql6`INSERT INTO auth_role_assignments (id, user_id, resource_type, resource_id, role, created_at)
2770
+ VALUES (${id}, ${userId}, ${resourceType}, ${resourceId}, ${role}, ${now})
2771
+ ON CONFLICT DO NOTHING`);
2772
+ assertWrite(result, "assign");
2773
+ }
2774
+ async revoke(userId, resourceType, resourceId, role) {
2775
+ const result = await this.db.query(sql6`DELETE FROM auth_role_assignments WHERE user_id = ${userId} AND resource_type = ${resourceType} AND resource_id = ${resourceId} AND role = ${role}`);
2776
+ assertWrite(result, "revoke");
2777
+ }
2778
+ async getRoles(userId, resourceType, resourceId) {
2779
+ const result = await this.db.query(sql6`SELECT role FROM auth_role_assignments WHERE user_id = ${userId} AND resource_type = ${resourceType} AND resource_id = ${resourceId}`);
2780
+ if (!result.ok)
2781
+ return [];
2782
+ return result.data.rows.map((r) => r.role);
2783
+ }
2784
+ async getRolesForUser(userId) {
2785
+ const result = await this.db.query(sql6`SELECT user_id, resource_type, resource_id, role FROM auth_role_assignments WHERE user_id = ${userId}`);
2786
+ if (!result.ok)
2787
+ return [];
2788
+ return result.data.rows.map((r) => ({
2789
+ userId: r.user_id,
2790
+ resourceType: r.resource_type,
2791
+ resourceId: r.resource_id,
2792
+ role: r.role
2793
+ }));
2794
+ }
2795
+ async getEffectiveRole(userId, resourceType, resourceId, accessDef, closureStore) {
2796
+ const rolesForType = accessDef.roles[resourceType];
2797
+ if (!rolesForType || rolesForType.length === 0)
2798
+ return null;
2799
+ const candidateRoles = [];
2800
+ const directRoles = await this.getRoles(userId, resourceType, resourceId);
2801
+ candidateRoles.push(...directRoles);
2802
+ const ancestors = await closureStore.getAncestors(resourceType, resourceId);
2803
+ for (const ancestor of ancestors) {
2804
+ if (ancestor.depth === 0)
2805
+ continue;
2806
+ const ancestorRoles = await this.getRoles(userId, ancestor.type, ancestor.id);
2807
+ for (const ancestorRole of ancestorRoles) {
2808
+ const inheritedRole = resolveInheritedRole(ancestor.type, ancestorRole, resourceType, accessDef);
2809
+ if (inheritedRole) {
2810
+ candidateRoles.push(inheritedRole);
2811
+ }
2812
+ }
2813
+ }
2814
+ if (candidateRoles.length === 0)
2815
+ return null;
2816
+ const roleOrder = [...rolesForType];
2817
+ let bestIndex = roleOrder.length;
2818
+ for (const role of candidateRoles) {
2819
+ const idx = roleOrder.indexOf(role);
2820
+ if (idx !== -1 && idx < bestIndex) {
2821
+ bestIndex = idx;
2822
+ }
2823
+ }
2824
+ return bestIndex < roleOrder.length ? roleOrder[bestIndex] : null;
2825
+ }
2826
+ dispose() {}
2827
+ }
2828
+ // src/auth/db-session-store.ts
2829
+ import { sql as sql7 } from "@vertz/db/sql";
2830
+ class DbSessionStore {
2831
+ db;
2832
+ constructor(db) {
2833
+ this.db = db;
2834
+ }
2835
+ async createSessionWithId(id, data) {
2836
+ const now = new Date;
2837
+ const tokensJson = data.currentTokens ? JSON.stringify(data.currentTokens) : null;
2838
+ const result = await this.db.query(sql7`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)
2839
+ VALUES (${id}, ${data.userId}, ${data.refreshTokenHash}, ${null}, ${tokensJson}, ${data.ipAddress}, ${data.userAgent}, ${now.toISOString()}, ${now.toISOString()}, ${data.expiresAt.toISOString()}, ${null})`);
2840
+ assertWrite(result, "createSessionWithId");
2841
+ return {
2842
+ id,
2843
+ userId: data.userId,
2844
+ refreshTokenHash: data.refreshTokenHash,
2845
+ previousRefreshHash: null,
2846
+ ipAddress: data.ipAddress,
2847
+ userAgent: data.userAgent,
2848
+ createdAt: now,
2849
+ lastActiveAt: now,
2850
+ expiresAt: data.expiresAt,
2851
+ revokedAt: null
2852
+ };
2853
+ }
2854
+ async findByRefreshHash(hash) {
2855
+ const nowStr = new Date().toISOString();
2856
+ const result = await this.db.query(sql7`SELECT * FROM auth_sessions WHERE refresh_token_hash = ${hash} AND revoked_at IS NULL AND expires_at > ${nowStr} LIMIT 1`);
2857
+ if (!result.ok)
2858
+ return null;
2859
+ const row = result.data.rows[0];
2860
+ if (!row)
2861
+ return null;
2862
+ return this.rowToSession(row);
2863
+ }
2864
+ async findByPreviousRefreshHash(hash) {
2865
+ const nowStr = new Date().toISOString();
2866
+ const result = await this.db.query(sql7`SELECT * FROM auth_sessions WHERE previous_refresh_hash = ${hash} AND revoked_at IS NULL AND expires_at > ${nowStr} LIMIT 1`);
2867
+ if (!result.ok)
2868
+ return null;
2869
+ const row = result.data.rows[0];
2870
+ if (!row)
2871
+ return null;
2872
+ return this.rowToSession(row);
2873
+ }
2874
+ async revokeSession(id) {
2875
+ const nowStr = new Date().toISOString();
2876
+ const result = await this.db.query(sql7`UPDATE auth_sessions SET revoked_at = ${nowStr}, current_tokens = ${null} WHERE id = ${id}`);
2877
+ assertWrite(result, "revokeSession");
2878
+ }
2879
+ async listActiveSessions(userId) {
2880
+ const nowStr = new Date().toISOString();
2881
+ const result = await this.db.query(sql7`SELECT * FROM auth_sessions WHERE user_id = ${userId} AND revoked_at IS NULL AND expires_at > ${nowStr}`);
2882
+ if (!result.ok)
2883
+ return [];
2884
+ return result.data.rows.map((row) => this.rowToSession(row));
2885
+ }
2886
+ async countActiveSessions(userId) {
2887
+ const nowStr = new Date().toISOString();
2888
+ const result = await this.db.query(sql7`SELECT COUNT(*) as cnt FROM auth_sessions WHERE user_id = ${userId} AND revoked_at IS NULL AND expires_at > ${nowStr}`);
2889
+ if (!result.ok)
2890
+ return 0;
2891
+ return result.data.rows[0]?.cnt ?? 0;
2892
+ }
2893
+ async getCurrentTokens(sessionId) {
2894
+ const result = await this.db.query(sql7`SELECT current_tokens FROM auth_sessions WHERE id = ${sessionId} LIMIT 1`);
2895
+ if (!result.ok)
2896
+ return null;
2897
+ const row = result.data.rows[0];
2898
+ if (!row?.current_tokens)
2899
+ return null;
2900
+ try {
2901
+ return JSON.parse(row.current_tokens);
2902
+ } catch {
2903
+ return null;
2904
+ }
2905
+ }
2906
+ async updateSession(id, data) {
2907
+ const tokensJson = data.currentTokens ? JSON.stringify(data.currentTokens) : null;
2908
+ const result = await this.db.query(sql7`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}`);
2909
+ assertWrite(result, "updateSession");
2910
+ }
2911
+ dispose() {}
2912
+ rowToSession(row) {
2913
+ return {
2914
+ id: row.id,
2915
+ userId: row.user_id,
2916
+ refreshTokenHash: row.refresh_token_hash,
2917
+ previousRefreshHash: row.previous_refresh_hash,
2918
+ ipAddress: row.ip_address,
2919
+ userAgent: row.user_agent,
2920
+ createdAt: new Date(row.created_at),
2921
+ lastActiveAt: new Date(row.last_active_at),
2922
+ expiresAt: new Date(row.expires_at),
2923
+ revokedAt: row.revoked_at ? new Date(row.revoked_at) : null
2924
+ };
1659
2925
  }
1660
- async getAncestors(type, id) {
1661
- return this.rows.filter((row) => row.descendantType === type && row.descendantId === id).map((row) => ({
1662
- type: row.ancestorType,
1663
- id: row.ancestorId,
1664
- depth: row.depth
1665
- }));
2926
+ }
2927
+ // src/auth/db-user-store.ts
2928
+ import { sql as sql8 } from "@vertz/db/sql";
2929
+ class DbUserStore {
2930
+ db;
2931
+ constructor(db) {
2932
+ this.db = db;
1666
2933
  }
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
- }));
2934
+ async createUser(user, passwordHash) {
2935
+ const result = await this.db.query(sql8`INSERT INTO auth_users (id, email, password_hash, role, plan, email_verified, created_at, updated_at)
2936
+ VALUES (${user.id}, ${user.email.toLowerCase()}, ${passwordHash}, ${user.role}, ${user.plan ?? null}, ${boolVal(this.db, user.emailVerified ?? false)}, ${user.createdAt.toISOString()}, ${user.updatedAt.toISOString()})`);
2937
+ assertWrite(result, "createUser");
1673
2938
  }
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);
2939
+ async findByEmail(email) {
2940
+ const result = await this.db.query(sql8`SELECT * FROM auth_users WHERE email = ${email.toLowerCase()} LIMIT 1`);
2941
+ if (!result.ok)
2942
+ return null;
2943
+ const row = result.data.rows[0];
2944
+ if (!row)
2945
+ return null;
2946
+ return {
2947
+ user: this.rowToUser(row),
2948
+ passwordHash: row.password_hash
2949
+ };
1676
2950
  }
1677
- dispose() {
1678
- this.rows = [];
2951
+ async findById(id) {
2952
+ const result = await this.db.query(sql8`SELECT * FROM auth_users WHERE id = ${id} LIMIT 1`);
2953
+ if (!result.ok)
2954
+ return null;
2955
+ const row = result.data.rows[0];
2956
+ if (!row)
2957
+ return null;
2958
+ return this.rowToUser(row);
2959
+ }
2960
+ async updatePasswordHash(userId, passwordHash) {
2961
+ const result = await this.db.query(sql8`UPDATE auth_users SET password_hash = ${passwordHash}, updated_at = ${new Date().toISOString()} WHERE id = ${userId}`);
2962
+ assertWrite(result, "updatePasswordHash");
2963
+ }
2964
+ async updateEmailVerified(userId, verified) {
2965
+ const val = boolVal(this.db, verified);
2966
+ const result = await this.db.query(sql8`UPDATE auth_users SET email_verified = ${val}, updated_at = ${new Date().toISOString()} WHERE id = ${userId}`);
2967
+ assertWrite(result, "updateEmailVerified");
2968
+ }
2969
+ rowToUser(row) {
2970
+ return {
2971
+ id: row.id,
2972
+ email: row.email,
2973
+ role: row.role,
2974
+ plan: row.plan ?? undefined,
2975
+ emailVerified: row.email_verified === 1 || row.email_verified === true,
2976
+ createdAt: new Date(row.created_at),
2977
+ updatedAt: new Date(row.updated_at)
2978
+ };
1679
2979
  }
1680
2980
  }
1681
2981
  // src/auth/define-access.ts
1682
2982
  function defineAccess(input) {
1683
- if (input.hierarchy.length === 0) {
1684
- throw new Error("Hierarchy must have at least one resource type");
2983
+ const { entities, entitlements: rawEntitlements } = input;
2984
+ for (const [entityName, entityDef] of Object.entries(entities)) {
2985
+ const roleSet = new Set;
2986
+ for (const role of entityDef.roles) {
2987
+ if (roleSet.has(role)) {
2988
+ throw new Error(`Duplicate role '${role}' in entity '${entityName}'`);
2989
+ }
2990
+ roleSet.add(role);
2991
+ }
2992
+ }
2993
+ const parentToChildren = new Map;
2994
+ const childToParent = new Map;
2995
+ const inheritanceMap = {};
2996
+ for (const [entityName, entityDef] of Object.entries(entities)) {
2997
+ if (!entityDef.inherits)
2998
+ continue;
2999
+ const parentEntities = new Set;
3000
+ for (const [inheritKey, localRole] of Object.entries(entityDef.inherits)) {
3001
+ const colonIdx = inheritKey.indexOf(":");
3002
+ if (colonIdx === -1) {
3003
+ throw new Error(`Invalid inherits key '${inheritKey}' in entity '${entityName}' — expected format 'entity:role'`);
3004
+ }
3005
+ const parentEntityName2 = inheritKey.substring(0, colonIdx);
3006
+ const parentRoleName = inheritKey.substring(colonIdx + 1);
3007
+ if (parentEntityName2 === entityName) {
3008
+ throw new Error(`Entity '${entityName}' cannot inherit from itself`);
3009
+ }
3010
+ if (!(parentEntityName2 in entities)) {
3011
+ throw new Error(`Entity '${parentEntityName2}' in ${entityName}.inherits is not defined`);
3012
+ }
3013
+ const parentEntity = entities[parentEntityName2];
3014
+ if (!parentEntity.roles.includes(parentRoleName)) {
3015
+ throw new Error(`Role '${parentRoleName}' does not exist on entity '${parentEntityName2}'`);
3016
+ }
3017
+ if (!entityDef.roles.includes(localRole)) {
3018
+ throw new Error(`Role '${localRole}' does not exist on entity '${entityName}'`);
3019
+ }
3020
+ parentEntities.add(parentEntityName2);
3021
+ if (!inheritanceMap[parentEntityName2]) {
3022
+ inheritanceMap[parentEntityName2] = {};
3023
+ }
3024
+ inheritanceMap[parentEntityName2][parentRoleName] = localRole;
3025
+ }
3026
+ if (parentEntities.size > 1) {
3027
+ throw new Error(`Entity '${entityName}' inherits from multiple parents (${[...parentEntities].join(", ")}). Each entity can only inherit from one parent.`);
3028
+ }
3029
+ const parentEntityName = [...parentEntities][0];
3030
+ if (parentEntityName) {
3031
+ if (!parentToChildren.has(parentEntityName)) {
3032
+ parentToChildren.set(parentEntityName, new Set);
3033
+ }
3034
+ parentToChildren.get(parentEntityName).add(entityName);
3035
+ if (childToParent.has(entityName)) {
3036
+ throw new Error(`Entity '${entityName}' inherits from multiple parents`);
3037
+ }
3038
+ childToParent.set(entityName, parentEntityName);
3039
+ }
1685
3040
  }
1686
- if (input.hierarchy.length > 4) {
3041
+ detectCycles(childToParent);
3042
+ const hierarchy = inferHierarchy(entities, childToParent, parentToChildren);
3043
+ if (hierarchy.length > 4) {
1687
3044
  throw new Error("Hierarchy depth must not exceed 4 levels");
1688
3045
  }
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}`);
3046
+ const resolvedEntitlements = {};
3047
+ const ruleContext = createRuleContext();
3048
+ for (const [entName, entValue] of Object.entries(rawEntitlements)) {
3049
+ const colonIdx = entName.indexOf(":");
3050
+ if (colonIdx === -1) {
3051
+ throw new Error(`Entitlement '${entName}' must use format 'entity:action'`);
3052
+ }
3053
+ const entityPrefix = entName.substring(0, colonIdx);
3054
+ if (!(entityPrefix in entities)) {
3055
+ throw new Error(`Entitlement '${entName}' references undefined entity '${entityPrefix}'`);
3056
+ }
3057
+ let entDef;
3058
+ if (typeof entValue === "function") {
3059
+ entDef = entValue(ruleContext);
3060
+ } else {
3061
+ entDef = entValue;
1693
3062
  }
3063
+ const entityRoles = entities[entityPrefix].roles;
3064
+ for (const role of entDef.roles) {
3065
+ if (!entityRoles.includes(role)) {
3066
+ throw new Error(`Role '${role}' in '${entName}' does not exist on entity '${entityPrefix}'`);
3067
+ }
3068
+ }
3069
+ resolvedEntitlements[entName] = entDef;
1694
3070
  }
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}`);
3071
+ const entitlementSet = new Set(Object.keys(resolvedEntitlements));
3072
+ const entityNameSet = new Set(Object.keys(entities));
3073
+ if (input.plans) {
3074
+ for (const [planName, planDef] of Object.entries(input.plans)) {
3075
+ if (planDef.features) {
3076
+ for (const feature of planDef.features) {
3077
+ if (!entitlementSet.has(feature)) {
3078
+ throw new Error(`Plan '${planName}' feature '${feature}' is not a defined entitlement`);
3079
+ }
3080
+ }
3081
+ }
3082
+ if (planDef.limits) {
3083
+ for (const [limitKey, limitDef] of Object.entries(planDef.limits)) {
3084
+ if (!entitlementSet.has(limitDef.gates)) {
3085
+ throw new Error(`Limit '${limitKey}' gates '${limitDef.gates}' which is not defined`);
3086
+ }
3087
+ if (limitDef.scope && !entityNameSet.has(limitDef.scope)) {
3088
+ throw new Error(`Limit '${limitKey}' scope '${limitDef.scope}' is not a defined entity`);
3089
+ }
3090
+ if (limitDef.max < -1 || limitDef.max < 0 && limitDef.max !== -1) {
3091
+ throw new Error(`Limit '${limitKey}' max must be -1 (unlimited), 0 (disabled), or a positive integer, got ${limitDef.max}`);
3092
+ }
3093
+ if (!Number.isInteger(limitDef.max)) {
3094
+ throw new Error(`Limit '${limitKey}' max must be -1 (unlimited), 0 (disabled), or a positive integer, got ${limitDef.max}`);
3095
+ }
3096
+ }
3097
+ }
3098
+ if (planDef.addOn) {
3099
+ if (planDef.group) {
3100
+ throw new Error(`Add-on '${planName}' must not have a group`);
3101
+ }
3102
+ } else {
3103
+ if (!planDef.group) {
3104
+ throw new Error(`Base plan '${planName}' must have a group`);
3105
+ }
3106
+ if (planDef.requires) {
3107
+ throw new Error(`Plan '${planName}': 'requires' is only valid on add-on plans`);
3108
+ }
3109
+ }
3110
+ if (planDef.addOn && planDef.requires) {
3111
+ for (const reqPlan of planDef.requires.plans) {
3112
+ if (!input.plans[reqPlan]) {
3113
+ throw new Error(`Add-on '${planName}' requires plan '${reqPlan}' is not defined`);
3114
+ }
3115
+ }
3116
+ }
1699
3117
  }
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}`);
3118
+ if (input.defaultPlan) {
3119
+ const defaultPlanDef = input.plans[input.defaultPlan];
3120
+ if (defaultPlanDef?.addOn) {
3121
+ throw new Error(`defaultPlan '${input.defaultPlan}' is an add-on, not a base plan`);
1707
3122
  }
1708
- if (!childRoles.has(childRole)) {
1709
- throw new Error(`Inheritance for ${resourceType} maps to undefined child role: ${childRole}`);
3123
+ }
3124
+ const basePlanLimitKeys = new Set;
3125
+ for (const [, planDef] of Object.entries(input.plans)) {
3126
+ if (!planDef.addOn && planDef.limits) {
3127
+ for (const limitKey of Object.keys(planDef.limits)) {
3128
+ basePlanLimitKeys.add(limitKey);
3129
+ }
1710
3130
  }
1711
3131
  }
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}`);
3132
+ for (const [, planDef] of Object.entries(input.plans)) {
3133
+ if (planDef.addOn && planDef.limits) {
3134
+ for (const limitKey of Object.keys(planDef.limits)) {
3135
+ if (!basePlanLimitKeys.has(limitKey)) {
3136
+ throw new Error(`Add-on limit '${limitKey}' not defined in any base plan`);
3137
+ }
1719
3138
  }
1720
3139
  }
1721
3140
  }
1722
3141
  }
1723
- const entitlementSet = new Set(Object.keys(input.entitlements));
3142
+ const planGatedEntitlements = new Set;
3143
+ const entitlementToLimitKeys = {};
1724
3144
  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}`);
3145
+ for (const [, planDef] of Object.entries(input.plans)) {
3146
+ if (planDef.features) {
3147
+ for (const feature of planDef.features) {
3148
+ planGatedEntitlements.add(feature);
1730
3149
  }
1731
3150
  }
1732
3151
  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`);
3152
+ for (const [limitKey, limitDef] of Object.entries(planDef.limits)) {
3153
+ if (!entitlementToLimitKeys[limitDef.gates]) {
3154
+ entitlementToLimitKeys[limitDef.gates] = [];
3155
+ }
3156
+ if (!entitlementToLimitKeys[limitDef.gates].includes(limitKey)) {
3157
+ entitlementToLimitKeys[limitDef.gates].push(limitKey);
1736
3158
  }
1737
3159
  }
1738
3160
  }
1739
3161
  }
1740
3162
  }
3163
+ const rolesMap = {};
3164
+ for (const [entityName, entityDef] of Object.entries(entities)) {
3165
+ rolesMap[entityName] = Object.freeze([...entityDef.roles]);
3166
+ }
1741
3167
  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 })]))),
3168
+ hierarchy: Object.freeze([...hierarchy]),
3169
+ entities: Object.freeze(Object.fromEntries(Object.entries(entities).map(([k, v]) => [
3170
+ k,
3171
+ Object.freeze({
3172
+ roles: Object.freeze([...v.roles]),
3173
+ ...v.inherits ? { inherits: Object.freeze({ ...v.inherits }) } : {}
3174
+ })
3175
+ ]))),
3176
+ roles: Object.freeze(rolesMap),
3177
+ inheritance: Object.freeze(Object.fromEntries(Object.entries(inheritanceMap).map(([k, v]) => [k, Object.freeze({ ...v })]))),
3178
+ entitlements: Object.freeze(Object.fromEntries(Object.entries(resolvedEntitlements).map(([k, v]) => [k, Object.freeze({ ...v })]))),
3179
+ _planGatedEntitlements: Object.freeze(planGatedEntitlements),
3180
+ _entitlementToLimitKeys: Object.freeze(Object.fromEntries(Object.entries(entitlementToLimitKeys).map(([k, v]) => [k, Object.freeze([...v])]))),
1746
3181
  ...input.defaultPlan ? { defaultPlan: input.defaultPlan } : {},
1747
3182
  ...input.plans ? {
1748
3183
  plans: Object.freeze(Object.fromEntries(Object.entries(input.plans).map(([planName, planDef]) => [
1749
3184
  planName,
1750
3185
  Object.freeze({
1751
- entitlements: Object.freeze([...planDef.entitlements]),
3186
+ ...planDef.title ? { title: planDef.title } : {},
3187
+ ...planDef.description ? { description: planDef.description } : {},
3188
+ ...planDef.group ? { group: planDef.group } : {},
3189
+ ...planDef.addOn ? { addOn: planDef.addOn } : {},
3190
+ ...planDef.price ? { price: Object.freeze({ ...planDef.price }) } : {},
3191
+ ...planDef.features ? { features: Object.freeze([...planDef.features]) } : {},
1752
3192
  ...planDef.limits ? {
1753
3193
  limits: Object.freeze(Object.fromEntries(Object.entries(planDef.limits).map(([k, v]) => [
1754
3194
  k,
1755
3195
  Object.freeze({ ...v })
1756
3196
  ])))
3197
+ } : {},
3198
+ ...planDef.grandfathering ? { grandfathering: Object.freeze({ ...planDef.grandfathering }) } : {},
3199
+ ...planDef.requires ? {
3200
+ requires: Object.freeze({
3201
+ group: planDef.requires.group,
3202
+ plans: Object.freeze([...planDef.requires.plans])
3203
+ })
1757
3204
  } : {}
1758
3205
  })
1759
3206
  ])))
@@ -1761,6 +3208,79 @@ function defineAccess(input) {
1761
3208
  };
1762
3209
  return Object.freeze(config);
1763
3210
  }
3211
+ function inferHierarchy(entities, childToParent, parentToChildren) {
3212
+ const inInheritance = new Set;
3213
+ for (const [child, parent] of childToParent.entries()) {
3214
+ inInheritance.add(child);
3215
+ inInheritance.add(parent);
3216
+ }
3217
+ if (inInheritance.size === 0) {
3218
+ return Object.keys(entities);
3219
+ }
3220
+ const roots = [];
3221
+ for (const entity of inInheritance) {
3222
+ if (!childToParent.has(entity)) {
3223
+ roots.push(entity);
3224
+ }
3225
+ }
3226
+ const hierarchy = [];
3227
+ const visited = new Set;
3228
+ function walkChain(entity) {
3229
+ if (visited.has(entity))
3230
+ return;
3231
+ visited.add(entity);
3232
+ hierarchy.push(entity);
3233
+ const children = parentToChildren.get(entity);
3234
+ if (children) {
3235
+ for (const child of children) {
3236
+ walkChain(child);
3237
+ }
3238
+ }
3239
+ }
3240
+ for (const root of roots) {
3241
+ walkChain(root);
3242
+ }
3243
+ for (const entityName of Object.keys(entities)) {
3244
+ if (!visited.has(entityName)) {
3245
+ hierarchy.push(entityName);
3246
+ }
3247
+ }
3248
+ return hierarchy;
3249
+ }
3250
+ function detectCycles(childToParent) {
3251
+ const visited = new Set;
3252
+ const inPath = new Set;
3253
+ for (const entity of childToParent.keys()) {
3254
+ if (!visited.has(entity)) {
3255
+ walkForCycles(entity, childToParent, visited, inPath);
3256
+ }
3257
+ }
3258
+ }
3259
+ function walkForCycles(entity, childToParent, visited, inPath) {
3260
+ if (inPath.has(entity)) {
3261
+ throw new Error("Circular inheritance detected");
3262
+ }
3263
+ if (visited.has(entity))
3264
+ return;
3265
+ inPath.add(entity);
3266
+ const parent = childToParent.get(entity);
3267
+ if (parent) {
3268
+ walkForCycles(parent, childToParent, visited, inPath);
3269
+ }
3270
+ inPath.delete(entity);
3271
+ visited.add(entity);
3272
+ }
3273
+ function createRuleContext() {
3274
+ return {
3275
+ where(conditions) {
3276
+ return { type: "where", conditions: Object.freeze({ ...conditions }) };
3277
+ },
3278
+ user: {
3279
+ id: Object.freeze({ __marker: "user.id" }),
3280
+ tenantId: Object.freeze({ __marker: "user.tenantId" })
3281
+ }
3282
+ };
3283
+ }
1764
3284
  // src/auth/entity-access.ts
1765
3285
  async function computeEntityAccess(entitlements, entity, accessContext) {
1766
3286
  const results = {};
@@ -1804,6 +3324,385 @@ function checkFva(payload, maxAgeSeconds) {
1804
3324
  const now = Math.floor(Date.now() / 1000);
1805
3325
  return now - payload.fva < maxAgeSeconds;
1806
3326
  }
3327
+ // src/auth/grandfathering-store.ts
3328
+ class InMemoryGrandfatheringStore {
3329
+ states = new Map;
3330
+ async setGrandfathered(tenantId, planId, version, graceEnds) {
3331
+ this.states.set(`${tenantId}:${planId}`, { tenantId, planId, version, graceEnds });
3332
+ }
3333
+ async getGrandfathered(tenantId, planId) {
3334
+ return this.states.get(`${tenantId}:${planId}`) ?? null;
3335
+ }
3336
+ async listGrandfathered(planId) {
3337
+ const result = [];
3338
+ for (const state of this.states.values()) {
3339
+ if (state.planId === planId) {
3340
+ result.push(state);
3341
+ }
3342
+ }
3343
+ return result;
3344
+ }
3345
+ async removeGrandfathered(tenantId, planId) {
3346
+ this.states.delete(`${tenantId}:${planId}`);
3347
+ }
3348
+ dispose() {
3349
+ this.states.clear();
3350
+ }
3351
+ }
3352
+ // src/auth/override-store.ts
3353
+ class InMemoryOverrideStore {
3354
+ overrides = new Map;
3355
+ async set(tenantId, overrides) {
3356
+ const existing = this.overrides.get(tenantId) ?? {};
3357
+ if (overrides.features) {
3358
+ const featureSet = new Set(existing.features ?? []);
3359
+ for (const f of overrides.features) {
3360
+ featureSet.add(f);
3361
+ }
3362
+ existing.features = [...featureSet];
3363
+ }
3364
+ if (overrides.limits) {
3365
+ existing.limits = { ...existing.limits, ...overrides.limits };
3366
+ }
3367
+ this.overrides.set(tenantId, existing);
3368
+ }
3369
+ async remove(tenantId, keys) {
3370
+ const existing = this.overrides.get(tenantId);
3371
+ if (!existing)
3372
+ return;
3373
+ if (keys.features && existing.features) {
3374
+ const removeSet = new Set(keys.features);
3375
+ existing.features = existing.features.filter((f) => !removeSet.has(f));
3376
+ if (existing.features.length === 0) {
3377
+ delete existing.features;
3378
+ }
3379
+ }
3380
+ if (keys.limits && existing.limits) {
3381
+ for (const key of keys.limits) {
3382
+ delete existing.limits[key];
3383
+ }
3384
+ if (Object.keys(existing.limits).length === 0) {
3385
+ delete existing.limits;
3386
+ }
3387
+ }
3388
+ if (!existing.features && !existing.limits) {
3389
+ this.overrides.delete(tenantId);
3390
+ }
3391
+ }
3392
+ async get(tenantId) {
3393
+ return this.overrides.get(tenantId) ?? null;
3394
+ }
3395
+ dispose() {
3396
+ this.overrides.clear();
3397
+ }
3398
+ }
3399
+ function validateOverrides(accessDef, overrides) {
3400
+ const allLimitKeys = new Set;
3401
+ if (accessDef.plans) {
3402
+ for (const planDef of Object.values(accessDef.plans)) {
3403
+ if (planDef.limits) {
3404
+ for (const key of Object.keys(planDef.limits)) {
3405
+ allLimitKeys.add(key);
3406
+ }
3407
+ }
3408
+ }
3409
+ }
3410
+ if (overrides.limits) {
3411
+ for (const [key, limitDef] of Object.entries(overrides.limits)) {
3412
+ if (!allLimitKeys.has(key)) {
3413
+ throw new Error(`Override limit key '${key}' is not defined in any plan`);
3414
+ }
3415
+ if (limitDef.max !== undefined) {
3416
+ if (limitDef.max < -1) {
3417
+ throw new Error(`Override limit '${key}' max must be -1 (unlimited), 0 (disabled), or a positive integer, got ${limitDef.max}`);
3418
+ }
3419
+ }
3420
+ if (limitDef.add !== undefined) {
3421
+ if (!Number.isFinite(limitDef.add)) {
3422
+ throw new Error(`Override limit '${key}' add must be a finite integer, got ${limitDef.add}`);
3423
+ }
3424
+ if (!Number.isInteger(limitDef.add)) {
3425
+ throw new Error(`Override limit '${key}' add must be an integer, got ${limitDef.add}`);
3426
+ }
3427
+ }
3428
+ }
3429
+ }
3430
+ if (overrides.features) {
3431
+ for (const feature of overrides.features) {
3432
+ if (!accessDef.entitlements[feature]) {
3433
+ throw new Error(`Override feature '${feature}' is not a defined entitlement`);
3434
+ }
3435
+ }
3436
+ }
3437
+ }
3438
+ // src/auth/plan-hash.ts
3439
+ async function computePlanHash(config) {
3440
+ const canonical = {
3441
+ features: config.features ?? [],
3442
+ limits: config.limits ?? {},
3443
+ price: config.price ?? null
3444
+ };
3445
+ const json = JSON.stringify(canonical, sortedReplacer);
3446
+ return sha256Hex(json);
3447
+ }
3448
+ function sortedReplacer(_key, value) {
3449
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
3450
+ const sorted = {};
3451
+ for (const k of Object.keys(value).sort()) {
3452
+ sorted[k] = value[k];
3453
+ }
3454
+ return sorted;
3455
+ }
3456
+ return value;
3457
+ }
3458
+ // src/auth/plan-manager.ts
3459
+ var GRACE_DURATION_MS = {
3460
+ "1m": 30 * 24 * 60 * 60 * 1000,
3461
+ "3m": 90 * 24 * 60 * 60 * 1000,
3462
+ "6m": 180 * 24 * 60 * 60 * 1000,
3463
+ "12m": 365 * 24 * 60 * 60 * 1000
3464
+ };
3465
+ function resolveGraceEnd(planDef, now) {
3466
+ const grace = planDef.grandfathering?.grace;
3467
+ if (grace === "indefinite")
3468
+ return null;
3469
+ if (grace && GRACE_DURATION_MS[grace]) {
3470
+ return new Date(now.getTime() + GRACE_DURATION_MS[grace]);
3471
+ }
3472
+ const interval = planDef.price?.interval;
3473
+ if (interval === "year") {
3474
+ return new Date(now.getTime() + GRACE_DURATION_MS["3m"]);
3475
+ }
3476
+ return new Date(now.getTime() + GRACE_DURATION_MS["1m"]);
3477
+ }
3478
+ function createPlanManager(config) {
3479
+ const { plans, versionStore, grandfatheringStore, planStore } = config;
3480
+ const clock = config.clock ?? (() => new Date);
3481
+ const handlers = [];
3482
+ function emit(event) {
3483
+ for (const handler of handlers) {
3484
+ handler(event);
3485
+ }
3486
+ }
3487
+ function extractSnapshot(planDef) {
3488
+ return {
3489
+ features: planDef.features ? [...planDef.features] : [],
3490
+ limits: planDef.limits ? { ...planDef.limits } : {},
3491
+ price: planDef.price ? { ...planDef.price } : null
3492
+ };
3493
+ }
3494
+ async function initialize() {
3495
+ const now = clock();
3496
+ for (const [planId, planDef] of Object.entries(plans)) {
3497
+ if (planDef.addOn)
3498
+ continue;
3499
+ const snapshot = extractSnapshot(planDef);
3500
+ const hash = await computePlanHash(snapshot);
3501
+ const currentHash = await versionStore.getCurrentHash(planId);
3502
+ if (currentHash === hash)
3503
+ continue;
3504
+ const version = await versionStore.createVersion(planId, hash, snapshot);
3505
+ const currentVersion = version;
3506
+ emit({
3507
+ type: "plan:version_created",
3508
+ planId,
3509
+ version,
3510
+ currentVersion,
3511
+ timestamp: now
3512
+ });
3513
+ if (version > 1) {
3514
+ const grandfatheredTenants = await listTenantsOnPlan(planId);
3515
+ const graceEnds = resolveGraceEnd(planDef, now);
3516
+ for (const tenantId of grandfatheredTenants) {
3517
+ const tenantVersion = await versionStore.getTenantVersion(tenantId, planId);
3518
+ if (tenantVersion !== null && tenantVersion < version) {
3519
+ await grandfatheringStore.setGrandfathered(tenantId, planId, tenantVersion, graceEnds);
3520
+ }
3521
+ }
3522
+ }
3523
+ }
3524
+ }
3525
+ async function listTenantsOnPlan(planId) {
3526
+ if (planStore.listByPlan) {
3527
+ return planStore.listByPlan(planId);
3528
+ }
3529
+ return [];
3530
+ }
3531
+ async function migrate(planId, opts) {
3532
+ const now = clock();
3533
+ const currentVersion = await versionStore.getCurrentVersion(planId);
3534
+ if (currentVersion === null)
3535
+ return;
3536
+ if (opts?.tenantId) {
3537
+ await migrateTenant(opts.tenantId, planId, currentVersion, now);
3538
+ return;
3539
+ }
3540
+ const grandfatheredList = await grandfatheringStore.listGrandfathered(planId);
3541
+ for (const state of grandfatheredList) {
3542
+ if (state.graceEnds === null)
3543
+ continue;
3544
+ if (state.graceEnds.getTime() <= now.getTime()) {
3545
+ await migrateTenant(state.tenantId, planId, currentVersion, now);
3546
+ }
3547
+ }
3548
+ }
3549
+ async function migrateTenant(tenantId, planId, targetVersion, now) {
3550
+ const previousVersion = await versionStore.getTenantVersion(tenantId, planId);
3551
+ if (previousVersion !== null) {
3552
+ const prevInfo = await versionStore.getVersion(planId, previousVersion);
3553
+ const targetInfo = await versionStore.getVersion(planId, targetVersion);
3554
+ if (prevInfo && targetInfo) {
3555
+ const prevFeatures = new Set(prevInfo.snapshot.features);
3556
+ const targetFeatures = new Set(targetInfo.snapshot.features);
3557
+ for (const f of prevFeatures) {
3558
+ if (!targetFeatures.has(f)) {
3559
+ break;
3560
+ }
3561
+ }
3562
+ }
3563
+ }
3564
+ await versionStore.setTenantVersion(tenantId, planId, targetVersion);
3565
+ await grandfatheringStore.removeGrandfathered(tenantId, planId);
3566
+ emit({
3567
+ type: "plan:migrated",
3568
+ planId,
3569
+ tenantId,
3570
+ version: targetVersion,
3571
+ previousVersion: previousVersion ?? undefined,
3572
+ timestamp: now
3573
+ });
3574
+ }
3575
+ async function schedule(planId, opts) {
3576
+ const at = typeof opts.at === "string" ? new Date(opts.at) : opts.at;
3577
+ const grandfatheredList = await grandfatheringStore.listGrandfathered(planId);
3578
+ for (const state of grandfatheredList) {
3579
+ await grandfatheringStore.setGrandfathered(state.tenantId, planId, state.version, at);
3580
+ }
3581
+ }
3582
+ async function resolve(tenantId) {
3583
+ const orgPlan = await planStore.getPlan(tenantId);
3584
+ if (!orgPlan)
3585
+ return null;
3586
+ const planId = orgPlan.planId;
3587
+ const currentVersion = await versionStore.getCurrentVersion(planId);
3588
+ if (currentVersion === null)
3589
+ return null;
3590
+ const tenantVersion = await versionStore.getTenantVersion(tenantId, planId);
3591
+ const effectiveVersion = tenantVersion ?? currentVersion;
3592
+ const grandfatheringState = await grandfatheringStore.getGrandfathered(tenantId, planId);
3593
+ const versionInfo = await versionStore.getVersion(planId, effectiveVersion);
3594
+ if (!versionInfo)
3595
+ return null;
3596
+ return {
3597
+ planId,
3598
+ version: effectiveVersion,
3599
+ currentVersion,
3600
+ grandfathered: grandfatheringState !== null,
3601
+ graceEnds: grandfatheringState?.graceEnds ?? null,
3602
+ snapshot: versionInfo.snapshot
3603
+ };
3604
+ }
3605
+ async function grandfatheredFn(planId) {
3606
+ return grandfatheringStore.listGrandfathered(planId);
3607
+ }
3608
+ const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
3609
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
3610
+ async function checkGraceEvents() {
3611
+ const now = clock();
3612
+ for (const planId of Object.keys(plans)) {
3613
+ const grandfatheredList = await grandfatheringStore.listGrandfathered(planId);
3614
+ for (const state of grandfatheredList) {
3615
+ if (state.graceEnds === null)
3616
+ continue;
3617
+ const timeUntilGraceEnd = state.graceEnds.getTime() - now.getTime();
3618
+ if (timeUntilGraceEnd <= SEVEN_DAYS_MS && timeUntilGraceEnd > 0) {
3619
+ emit({
3620
+ type: "plan:grace_expiring",
3621
+ planId,
3622
+ tenantId: state.tenantId,
3623
+ version: state.version,
3624
+ graceEnds: state.graceEnds,
3625
+ timestamp: now
3626
+ });
3627
+ } else if (timeUntilGraceEnd <= THIRTY_DAYS_MS && timeUntilGraceEnd > SEVEN_DAYS_MS) {
3628
+ emit({
3629
+ type: "plan:grace_approaching",
3630
+ planId,
3631
+ tenantId: state.tenantId,
3632
+ version: state.version,
3633
+ graceEnds: state.graceEnds,
3634
+ timestamp: now
3635
+ });
3636
+ }
3637
+ }
3638
+ }
3639
+ }
3640
+ function on(handler) {
3641
+ handlers.push(handler);
3642
+ }
3643
+ function off(handler) {
3644
+ const idx = handlers.indexOf(handler);
3645
+ if (idx >= 0)
3646
+ handlers.splice(idx, 1);
3647
+ }
3648
+ return {
3649
+ initialize,
3650
+ migrate,
3651
+ schedule,
3652
+ resolve,
3653
+ grandfathered: grandfatheredFn,
3654
+ checkGraceEvents,
3655
+ on,
3656
+ off
3657
+ };
3658
+ }
3659
+ // src/auth/plan-version-store.ts
3660
+ class InMemoryPlanVersionStore {
3661
+ versions = new Map;
3662
+ tenantVersions = new Map;
3663
+ async createVersion(planId, hash, snapshot) {
3664
+ const planVersions = this.versions.get(planId) ?? [];
3665
+ const version = planVersions.length + 1;
3666
+ const info = {
3667
+ planId,
3668
+ version,
3669
+ hash,
3670
+ snapshot,
3671
+ createdAt: new Date
3672
+ };
3673
+ planVersions.push(info);
3674
+ this.versions.set(planId, planVersions);
3675
+ return version;
3676
+ }
3677
+ async getCurrentVersion(planId) {
3678
+ const planVersions = this.versions.get(planId);
3679
+ if (!planVersions || planVersions.length === 0)
3680
+ return null;
3681
+ return planVersions.length;
3682
+ }
3683
+ async getVersion(planId, version) {
3684
+ const planVersions = this.versions.get(planId);
3685
+ if (!planVersions)
3686
+ return null;
3687
+ return planVersions[version - 1] ?? null;
3688
+ }
3689
+ async getTenantVersion(tenantId, planId) {
3690
+ return this.tenantVersions.get(`${tenantId}:${planId}`) ?? null;
3691
+ }
3692
+ async setTenantVersion(tenantId, planId, version) {
3693
+ this.tenantVersions.set(`${tenantId}:${planId}`, version);
3694
+ }
3695
+ async getCurrentHash(planId) {
3696
+ const planVersions = this.versions.get(planId);
3697
+ if (!planVersions || planVersions.length === 0)
3698
+ return null;
3699
+ return planVersions[planVersions.length - 1].hash;
3700
+ }
3701
+ dispose() {
3702
+ this.versions.clear();
3703
+ this.tenantVersions.clear();
3704
+ }
3705
+ }
1807
3706
  // src/auth/providers/discord.ts
1808
3707
  var AUTHORIZATION_URL = "https://discord.com/api/oauth2/authorize";
1809
3708
  var TOKEN_URL = "https://discord.com/api/oauth2/token";
@@ -2043,7 +3942,7 @@ class InMemoryRoleAssignmentStore {
2043
3942
  continue;
2044
3943
  const ancestorRoles = await this.getRoles(userId, ancestor.type, ancestor.id);
2045
3944
  for (const ancestorRole of ancestorRoles) {
2046
- const inheritedRole = resolveInheritedRole2(ancestor.type, ancestorRole, resourceType, accessDef);
3945
+ const inheritedRole = resolveInheritedRole(ancestor.type, ancestorRole, resourceType, accessDef);
2047
3946
  if (inheritedRole) {
2048
3947
  candidateRoles.push(inheritedRole);
2049
3948
  }
@@ -2065,28 +3964,13 @@ class InMemoryRoleAssignmentStore {
2065
3964
  this.assignments = [];
2066
3965
  }
2067
3966
  }
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
3967
  // src/auth/rules.ts
2085
3968
  var userMarkers = {
2086
3969
  id: Object.freeze({ __marker: "user.id" }),
2087
3970
  tenantId: Object.freeze({ __marker: "user.tenantId" })
2088
3971
  };
2089
3972
  var rules = {
3973
+ public: Object.freeze({ type: "public" }),
2090
3974
  role(...roleNames) {
2091
3975
  return { type: "role", roles: Object.freeze([...roleNames]) };
2092
3976
  },
@@ -3730,9 +5614,12 @@ async function enforceAccess(operation, accessRules, ctx, row) {
3730
5614
  if (rule === false) {
3731
5615
  return err2(new EntityForbiddenError(`Operation "${operation}" is disabled`));
3732
5616
  }
3733
- const allowed = await rule(ctx, row ?? {});
3734
- if (!allowed) {
3735
- return err2(new EntityForbiddenError(`Access denied for operation "${operation}"`));
5617
+ if (typeof rule === "function") {
5618
+ const allowed = await rule(ctx, row ?? {});
5619
+ if (!allowed) {
5620
+ return err2(new EntityForbiddenError(`Access denied for operation "${operation}"`));
5621
+ }
5622
+ return ok2(undefined);
3736
5623
  }
3737
5624
  return ok2(undefined);
3738
5625
  }
@@ -4565,10 +6452,14 @@ function createServer(config) {
4565
6452
  const allRoutes = [];
4566
6453
  const registry = new EntityRegistry;
4567
6454
  const apiPrefix = config.apiPrefix === undefined ? "/api" : config.apiPrefix;
6455
+ const { db } = config;
6456
+ const hasDbClient = db && isDatabaseClient(db);
6457
+ if (hasDbClient && config.auth) {
6458
+ validateAuthModels(db);
6459
+ }
4568
6460
  if (config.entities && config.entities.length > 0) {
4569
- const { db } = config;
4570
6461
  let dbFactory;
4571
- if (db && isDatabaseClient(db)) {
6462
+ if (hasDbClient) {
4572
6463
  const dbModels = db._internals.models;
4573
6464
  const missing = config.entities.filter((e) => !(e.name in dbModels)).map((e) => `"${e.name}"`);
4574
6465
  if (missing.length > 0) {
@@ -4601,10 +6492,27 @@ function createServer(config) {
4601
6492
  allRoutes.push(...routes);
4602
6493
  }
4603
6494
  }
4604
- return coreCreateServer({
6495
+ const app = coreCreateServer({
4605
6496
  ...config,
4606
6497
  _entityRoutes: allRoutes.length > 0 ? allRoutes : undefined
4607
6498
  });
6499
+ if (hasDbClient && config.auth) {
6500
+ const dbClient = db;
6501
+ const authConfig = {
6502
+ ...config.auth,
6503
+ userStore: config.auth.userStore ?? new DbUserStore(dbClient),
6504
+ sessionStore: config.auth.sessionStore ?? new DbSessionStore(dbClient)
6505
+ };
6506
+ const auth = createAuth(authConfig);
6507
+ const serverInstance = app;
6508
+ serverInstance.auth = auth;
6509
+ serverInstance.initialize = async () => {
6510
+ await initializeAuthTables(dbClient);
6511
+ await auth.initialize();
6512
+ };
6513
+ return serverInstance;
6514
+ }
6515
+ return app;
4608
6516
  }
4609
6517
  // src/entity/entity.ts
4610
6518
  import { deepFreeze } from "@vertz/core";
@@ -4652,14 +6560,18 @@ export {
4652
6560
  vertz,
4653
6561
  verifyPassword,
4654
6562
  validatePassword,
6563
+ validateOverrides,
6564
+ validateAuthModels,
4655
6565
  stripReadOnlyFields,
4656
6566
  stripHiddenFields,
4657
6567
  service,
4658
6568
  rules,
4659
6569
  makeImmutable,
6570
+ initializeAuthTables,
4660
6571
  hashPassword,
4661
6572
  google,
4662
6573
  github,
6574
+ getIncompatibleAddOns,
4663
6575
  generateEntityRoutes,
4664
6576
  entityErrorHandler,
4665
6577
  entity,
@@ -4670,20 +6582,28 @@ export {
4670
6582
  defaultAccess,
4671
6583
  deepFreeze3 as deepFreeze,
4672
6584
  decodeAccessSet,
6585
+ createWebhookHandler,
6586
+ createStripeBillingAdapter,
4673
6587
  createServer,
6588
+ createPlanManager,
4674
6589
  createMiddleware,
4675
6590
  createImmutableProxy,
4676
6591
  createEnv,
4677
6592
  createEntityContext,
4678
6593
  createCrudHandlers,
6594
+ createBillingEventEmitter,
4679
6595
  createAuth,
4680
6596
  createAccessEventBroadcaster,
4681
6597
  createAccessContext,
4682
6598
  createAccess,
6599
+ computePlanHash,
6600
+ computeOverage,
4683
6601
  computeEntityAccess,
4684
6602
  computeAccessSet,
4685
6603
  checkFva,
6604
+ checkAddOnCompatibility,
4686
6605
  calculateBillingPeriod,
6606
+ authModels,
4687
6607
  VertzException2 as VertzException,
4688
6608
  ValidationException2 as ValidationException,
4689
6609
  UnauthorizedException,
@@ -4695,16 +6615,27 @@ export {
4695
6615
  InMemorySessionStore,
4696
6616
  InMemoryRoleAssignmentStore,
4697
6617
  InMemoryRateLimitStore,
6618
+ InMemoryPlanVersionStore,
4698
6619
  InMemoryPlanStore,
4699
6620
  InMemoryPasswordResetStore,
6621
+ InMemoryOverrideStore,
4700
6622
  InMemoryOAuthAccountStore,
4701
6623
  InMemoryMFAStore,
6624
+ InMemoryGrandfatheringStore,
4702
6625
  InMemoryFlagStore,
4703
6626
  InMemoryEmailVerificationStore,
4704
6627
  InMemoryClosureStore,
4705
6628
  ForbiddenException,
4706
6629
  EntityRegistry,
6630
+ DbUserStore,
6631
+ DbSessionStore,
6632
+ DbRoleAssignmentStore,
6633
+ DbPlanStore,
6634
+ DbOAuthAccountStore,
6635
+ DbFlagStore,
6636
+ DbClosureStore,
4707
6637
  ConflictException,
4708
6638
  BadRequestException,
4709
- AuthorizationError
6639
+ AuthorizationError,
6640
+ AUTH_TABLE_NAMES
4710
6641
  };