@vertz/server 0.2.15 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +401 -199
- package/dist/index.js +1302 -382
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import {
|
|
3
|
-
BadRequestException,
|
|
3
|
+
BadRequestException as BadRequestException2,
|
|
4
4
|
ConflictException,
|
|
5
5
|
createEnv,
|
|
6
6
|
createImmutableProxy,
|
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
// src/auth/index.ts
|
|
21
21
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
+
import { BadRequestException } from "@vertz/core";
|
|
24
|
+
import { parseBody } from "@vertz/core/internals";
|
|
23
25
|
import {
|
|
24
26
|
createAuthRateLimitedError,
|
|
25
27
|
createAuthValidationError as createAuthValidationError2,
|
|
@@ -86,65 +88,83 @@ function getMonthOffset(base, target) {
|
|
|
86
88
|
return (target.getUTCFullYear() - base.getUTCFullYear()) * 12 + (target.getUTCMonth() - base.getUTCMonth());
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
// src/auth/
|
|
90
|
-
function
|
|
91
|
-
|
|
91
|
+
// src/auth/resolve-inherited-role.ts
|
|
92
|
+
function resolveInheritedRole(sourceType, sourceRole, targetType, accessDef) {
|
|
93
|
+
const hierarchy = accessDef.hierarchy;
|
|
94
|
+
const sourceIdx = hierarchy.indexOf(sourceType);
|
|
95
|
+
const targetIdx = hierarchy.indexOf(targetType);
|
|
96
|
+
if (sourceIdx === -1 || targetIdx === -1 || sourceIdx >= targetIdx)
|
|
92
97
|
return null;
|
|
93
|
-
|
|
98
|
+
let currentRole = sourceRole;
|
|
99
|
+
for (let i = sourceIdx;i < targetIdx; i++) {
|
|
100
|
+
const currentType = hierarchy[i];
|
|
101
|
+
const inheritanceMap = accessDef.inheritance[currentType];
|
|
102
|
+
if (!inheritanceMap || !(currentRole in inheritanceMap))
|
|
103
|
+
return null;
|
|
104
|
+
currentRole = inheritanceMap[currentRole];
|
|
105
|
+
}
|
|
106
|
+
return currentRole;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/auth/subscription-store.ts
|
|
110
|
+
function resolveEffectivePlan(subscription, plans, defaultPlan = "free", now) {
|
|
111
|
+
if (!subscription)
|
|
112
|
+
return null;
|
|
113
|
+
if (subscription.expiresAt && subscription.expiresAt.getTime() < (now ?? Date.now())) {
|
|
94
114
|
return plans?.[defaultPlan] ? defaultPlan : null;
|
|
95
115
|
}
|
|
96
|
-
if (!plans?.[
|
|
116
|
+
if (!plans?.[subscription.planId])
|
|
97
117
|
return null;
|
|
98
|
-
return
|
|
118
|
+
return subscription.planId;
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
class
|
|
102
|
-
|
|
121
|
+
class InMemorySubscriptionStore {
|
|
122
|
+
subscriptions = new Map;
|
|
103
123
|
addOns = new Map;
|
|
104
|
-
async
|
|
105
|
-
this.
|
|
106
|
-
|
|
124
|
+
async assign(tenantId, planId, startedAt = new Date, expiresAt = null) {
|
|
125
|
+
this.subscriptions.set(tenantId, {
|
|
126
|
+
tenantId,
|
|
107
127
|
planId,
|
|
108
128
|
startedAt,
|
|
109
129
|
expiresAt,
|
|
110
130
|
overrides: {}
|
|
111
131
|
});
|
|
112
132
|
}
|
|
113
|
-
async
|
|
114
|
-
return this.
|
|
133
|
+
async get(tenantId) {
|
|
134
|
+
return this.subscriptions.get(tenantId) ?? null;
|
|
115
135
|
}
|
|
116
|
-
async updateOverrides(
|
|
117
|
-
const
|
|
118
|
-
if (!
|
|
136
|
+
async updateOverrides(tenantId, overrides) {
|
|
137
|
+
const sub = this.subscriptions.get(tenantId);
|
|
138
|
+
if (!sub)
|
|
119
139
|
return;
|
|
120
|
-
|
|
140
|
+
sub.overrides = { ...sub.overrides, ...overrides };
|
|
121
141
|
}
|
|
122
|
-
async
|
|
123
|
-
this.
|
|
142
|
+
async remove(tenantId) {
|
|
143
|
+
this.subscriptions.delete(tenantId);
|
|
124
144
|
}
|
|
125
|
-
async attachAddOn(
|
|
126
|
-
if (!this.addOns.has(
|
|
127
|
-
this.addOns.set(
|
|
145
|
+
async attachAddOn(tenantId, addOnId) {
|
|
146
|
+
if (!this.addOns.has(tenantId)) {
|
|
147
|
+
this.addOns.set(tenantId, new Set);
|
|
128
148
|
}
|
|
129
|
-
this.addOns.get(
|
|
149
|
+
this.addOns.get(tenantId).add(addOnId);
|
|
130
150
|
}
|
|
131
|
-
async detachAddOn(
|
|
132
|
-
this.addOns.get(
|
|
151
|
+
async detachAddOn(tenantId, addOnId) {
|
|
152
|
+
this.addOns.get(tenantId)?.delete(addOnId);
|
|
133
153
|
}
|
|
134
|
-
async getAddOns(
|
|
135
|
-
return [...this.addOns.get(
|
|
154
|
+
async getAddOns(tenantId) {
|
|
155
|
+
return [...this.addOns.get(tenantId) ?? []];
|
|
136
156
|
}
|
|
137
157
|
async listByPlan(planId) {
|
|
138
158
|
const result = [];
|
|
139
|
-
for (const [
|
|
140
|
-
if (
|
|
141
|
-
result.push(
|
|
159
|
+
for (const [tenantId, sub] of this.subscriptions.entries()) {
|
|
160
|
+
if (sub.planId === planId) {
|
|
161
|
+
result.push(tenantId);
|
|
142
162
|
}
|
|
143
163
|
}
|
|
144
164
|
return result;
|
|
145
165
|
}
|
|
146
166
|
dispose() {
|
|
147
|
-
this.
|
|
167
|
+
this.subscriptions.clear();
|
|
148
168
|
this.addOns.clear();
|
|
149
169
|
}
|
|
150
170
|
}
|
|
@@ -158,24 +178,6 @@ function getIncompatibleAddOns(accessDef, activeAddOnIds, targetPlanId) {
|
|
|
158
178
|
return activeAddOnIds.filter((id) => !checkAddOnCompatibility(accessDef, id, targetPlanId));
|
|
159
179
|
}
|
|
160
180
|
|
|
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];
|
|
175
|
-
}
|
|
176
|
-
return currentRole;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
181
|
// src/auth/access-set.ts
|
|
180
182
|
async function computeAccessSet(config) {
|
|
181
183
|
const {
|
|
@@ -183,14 +185,13 @@ async function computeAccessSet(config) {
|
|
|
183
185
|
accessDef,
|
|
184
186
|
roleStore,
|
|
185
187
|
closureStore,
|
|
186
|
-
plan,
|
|
187
188
|
flagStore,
|
|
188
|
-
|
|
189
|
+
subscriptionStore,
|
|
189
190
|
walletStore,
|
|
190
|
-
|
|
191
|
+
tenantId
|
|
191
192
|
} = config;
|
|
192
193
|
const entitlements = {};
|
|
193
|
-
let resolvedPlan =
|
|
194
|
+
let resolvedPlan = null;
|
|
194
195
|
if (!userId) {
|
|
195
196
|
for (const name of Object.keys(accessDef.entitlements)) {
|
|
196
197
|
entitlements[name] = {
|
|
@@ -202,7 +203,7 @@ async function computeAccessSet(config) {
|
|
|
202
203
|
return {
|
|
203
204
|
entitlements,
|
|
204
205
|
flags: {},
|
|
205
|
-
plan:
|
|
206
|
+
plan: null,
|
|
206
207
|
computedAt: new Date().toISOString()
|
|
207
208
|
};
|
|
208
209
|
}
|
|
@@ -244,14 +245,14 @@ async function computeAccessSet(config) {
|
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
const resolvedFlags = {};
|
|
247
|
-
if (flagStore &&
|
|
248
|
-
const orgFlags = flagStore.getFlags(
|
|
248
|
+
if (flagStore && tenantId) {
|
|
249
|
+
const orgFlags = flagStore.getFlags(tenantId);
|
|
249
250
|
Object.assign(resolvedFlags, orgFlags);
|
|
250
251
|
for (const [name, entDef] of Object.entries(accessDef.entitlements)) {
|
|
251
252
|
if (entDef.flags?.length) {
|
|
252
253
|
const disabledFlags = [];
|
|
253
254
|
for (const flag of entDef.flags) {
|
|
254
|
-
if (!flagStore.getFlag(
|
|
255
|
+
if (!flagStore.getFlag(tenantId, flag)) {
|
|
255
256
|
disabledFlags.push(flag);
|
|
256
257
|
}
|
|
257
258
|
}
|
|
@@ -271,16 +272,16 @@ async function computeAccessSet(config) {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
}
|
|
274
|
-
if (
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
const effectivePlanId = resolveEffectivePlan(
|
|
275
|
+
if (subscriptionStore && tenantId) {
|
|
276
|
+
const subscription = await subscriptionStore.get(tenantId);
|
|
277
|
+
if (subscription) {
|
|
278
|
+
const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
|
|
278
279
|
resolvedPlan = effectivePlanId;
|
|
279
280
|
if (effectivePlanId) {
|
|
280
281
|
const planDef = accessDef.plans?.[effectivePlanId];
|
|
281
282
|
if (planDef) {
|
|
282
283
|
const effectiveFeatures = new Set(planDef.features ?? []);
|
|
283
|
-
const addOns = await
|
|
284
|
+
const addOns = await subscriptionStore.getAddOns?.(tenantId);
|
|
284
285
|
if (addOns) {
|
|
285
286
|
for (const addOnId of addOns) {
|
|
286
287
|
const addOnDef = accessDef.plans?.[addOnId];
|
|
@@ -326,7 +327,7 @@ async function computeAccessSet(config) {
|
|
|
326
327
|
}
|
|
327
328
|
}
|
|
328
329
|
}
|
|
329
|
-
const override =
|
|
330
|
+
const override = subscription.overrides[limitKey];
|
|
330
331
|
if (override)
|
|
331
332
|
effectiveMax = Math.max(effectiveMax, override.max);
|
|
332
333
|
if (effectiveMax === -1) {
|
|
@@ -339,11 +340,11 @@ async function computeAccessSet(config) {
|
|
|
339
340
|
}
|
|
340
341
|
};
|
|
341
342
|
} else {
|
|
342
|
-
const period = limitDef.per ? calculateBillingPeriod(
|
|
343
|
-
periodStart:
|
|
343
|
+
const period = limitDef.per ? calculateBillingPeriod(subscription.startedAt, limitDef.per) : {
|
|
344
|
+
periodStart: subscription.startedAt,
|
|
344
345
|
periodEnd: new Date("9999-12-31T23:59:59Z")
|
|
345
346
|
};
|
|
346
|
-
const consumed = await walletStore.getConsumption(
|
|
347
|
+
const consumed = await walletStore.getConsumption(tenantId, limitKey, period.periodStart, period.periodEnd);
|
|
347
348
|
const remaining = Math.max(0, effectiveMax - consumed);
|
|
348
349
|
const entry = entitlements[name];
|
|
349
350
|
if (consumed >= effectiveMax) {
|
|
@@ -895,6 +896,60 @@ class InMemoryRateLimitStore {
|
|
|
895
896
|
}
|
|
896
897
|
}
|
|
897
898
|
|
|
899
|
+
// src/auth/resolve-session-for-ssr.ts
|
|
900
|
+
function extractAccessSet(acl) {
|
|
901
|
+
if (acl.overflow)
|
|
902
|
+
return null;
|
|
903
|
+
if (!acl.set)
|
|
904
|
+
return null;
|
|
905
|
+
return {
|
|
906
|
+
entitlements: Object.fromEntries(Object.entries(acl.set.entitlements).map(([name, check]) => {
|
|
907
|
+
const data = {
|
|
908
|
+
allowed: check.allowed,
|
|
909
|
+
reasons: check.reasons ?? [],
|
|
910
|
+
...check.reason ? { reason: check.reason } : {},
|
|
911
|
+
...check.meta ? { meta: check.meta } : {}
|
|
912
|
+
};
|
|
913
|
+
return [name, data];
|
|
914
|
+
})),
|
|
915
|
+
flags: acl.set.flags,
|
|
916
|
+
plan: acl.set.plan,
|
|
917
|
+
computedAt: acl.set.computedAt
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function resolveSessionForSSR(config) {
|
|
921
|
+
const { jwtSecret, jwtAlgorithm, cookieName } = config;
|
|
922
|
+
return async (request) => {
|
|
923
|
+
const cookieHeader = request.headers.get("cookie");
|
|
924
|
+
if (!cookieHeader)
|
|
925
|
+
return null;
|
|
926
|
+
const cookieEntry = cookieHeader.split(";").find((c) => c.trim().startsWith(`${cookieName}=`));
|
|
927
|
+
if (!cookieEntry)
|
|
928
|
+
return null;
|
|
929
|
+
const token = cookieEntry.trim().slice(`${cookieName}=`.length);
|
|
930
|
+
if (!token)
|
|
931
|
+
return null;
|
|
932
|
+
const payload = await verifyJWT(token, jwtSecret, jwtAlgorithm);
|
|
933
|
+
if (!payload)
|
|
934
|
+
return null;
|
|
935
|
+
const user = {
|
|
936
|
+
id: payload.sub,
|
|
937
|
+
email: payload.email,
|
|
938
|
+
role: payload.role
|
|
939
|
+
};
|
|
940
|
+
if (payload.tenantId) {
|
|
941
|
+
user.tenantId = payload.tenantId;
|
|
942
|
+
}
|
|
943
|
+
const session = {
|
|
944
|
+
user,
|
|
945
|
+
expiresAt: payload.exp * 1000
|
|
946
|
+
};
|
|
947
|
+
const acl = payload.acl;
|
|
948
|
+
const accessSet = acl !== undefined ? extractAccessSet(acl) : undefined;
|
|
949
|
+
return { session, accessSet };
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
|
|
898
953
|
// src/auth/session-store.ts
|
|
899
954
|
var DEFAULT_MAX_SESSIONS_PER_USER = 50;
|
|
900
955
|
var CLEANUP_INTERVAL_MS2 = 60000;
|
|
@@ -969,6 +1024,13 @@ class InMemorySessionStore {
|
|
|
969
1024
|
}
|
|
970
1025
|
return null;
|
|
971
1026
|
}
|
|
1027
|
+
async findActiveSessionById(id) {
|
|
1028
|
+
const session = this.sessions.get(id);
|
|
1029
|
+
if (!session || session.revokedAt || session.expiresAt <= new Date) {
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
return session;
|
|
1033
|
+
}
|
|
972
1034
|
async findByPreviousRefreshHash(hash) {
|
|
973
1035
|
for (const session of this.sessions.values()) {
|
|
974
1036
|
if (session.previousRefreshHash !== null && timingSafeEqual(session.previousRefreshHash, hash) && !session.revokedAt && session.expiresAt > new Date) {
|
|
@@ -1139,6 +1201,40 @@ function timingSafeEqual2(a, b) {
|
|
|
1139
1201
|
return equal && a.length === b.length;
|
|
1140
1202
|
}
|
|
1141
1203
|
|
|
1204
|
+
// src/auth/types.ts
|
|
1205
|
+
import {
|
|
1206
|
+
s
|
|
1207
|
+
} from "@vertz/schema";
|
|
1208
|
+
var authEmailFieldSchema = s.string().min(1).trim();
|
|
1209
|
+
var authPasswordFieldSchema = s.string().min(1);
|
|
1210
|
+
var signUpInputSchema = s.object({
|
|
1211
|
+
email: authEmailFieldSchema,
|
|
1212
|
+
password: authPasswordFieldSchema
|
|
1213
|
+
}).passthrough();
|
|
1214
|
+
var signInInputSchema = s.object({
|
|
1215
|
+
email: authEmailFieldSchema,
|
|
1216
|
+
password: authPasswordFieldSchema
|
|
1217
|
+
});
|
|
1218
|
+
var codeInputSchema = s.object({
|
|
1219
|
+
code: s.string().min(1)
|
|
1220
|
+
});
|
|
1221
|
+
var passwordInputSchema = s.object({
|
|
1222
|
+
password: s.string().min(1)
|
|
1223
|
+
});
|
|
1224
|
+
var tokenInputSchema = s.object({
|
|
1225
|
+
token: s.string().min(1).optional()
|
|
1226
|
+
});
|
|
1227
|
+
var forgotPasswordInputSchema = s.object({
|
|
1228
|
+
email: s.string().min(1).trim()
|
|
1229
|
+
});
|
|
1230
|
+
var resetPasswordInputSchema = s.object({
|
|
1231
|
+
token: s.string().min(1).optional(),
|
|
1232
|
+
password: s.string().min(1)
|
|
1233
|
+
});
|
|
1234
|
+
var switchTenantInputSchema = s.object({
|
|
1235
|
+
tenantId: s.string().min(1)
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1142
1238
|
// src/auth/user-store.ts
|
|
1143
1239
|
class InMemoryUserStore {
|
|
1144
1240
|
byEmail = new Map;
|
|
@@ -1168,6 +1264,13 @@ class InMemoryUserStore {
|
|
|
1168
1264
|
user.emailVerified = verified;
|
|
1169
1265
|
}
|
|
1170
1266
|
}
|
|
1267
|
+
async deleteUser(id) {
|
|
1268
|
+
const user = this.byId.get(id);
|
|
1269
|
+
if (user) {
|
|
1270
|
+
this.byEmail.delete(user.email.toLowerCase());
|
|
1271
|
+
this.byId.delete(id);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1171
1274
|
}
|
|
1172
1275
|
// src/auth/access.ts
|
|
1173
1276
|
class AuthorizationError extends Error {
|
|
@@ -1303,7 +1406,7 @@ function createAccessContext(config) {
|
|
|
1303
1406
|
closureStore,
|
|
1304
1407
|
roleStore,
|
|
1305
1408
|
flagStore,
|
|
1306
|
-
|
|
1409
|
+
subscriptionStore,
|
|
1307
1410
|
walletStore,
|
|
1308
1411
|
overrideStore,
|
|
1309
1412
|
orgResolver,
|
|
@@ -1332,14 +1435,14 @@ function createAccessContext(config) {
|
|
|
1332
1435
|
} else if (entDef.roles.length > 0 && !resource) {
|
|
1333
1436
|
return false;
|
|
1334
1437
|
}
|
|
1335
|
-
if (accessDef._planGatedEntitlements.has(entitlement) &&
|
|
1438
|
+
if (accessDef._planGatedEntitlements.has(entitlement) && subscriptionStore && orgResolver) {
|
|
1336
1439
|
if (!resolvedOrgId)
|
|
1337
1440
|
return false;
|
|
1338
|
-
const
|
|
1339
|
-
const effectivePlanId = resolveEffectivePlan(
|
|
1441
|
+
const subscription = await subscriptionStore.get(resolvedOrgId);
|
|
1442
|
+
const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
|
|
1340
1443
|
if (!effectivePlanId)
|
|
1341
1444
|
return false;
|
|
1342
|
-
const hasFeature = await resolveEffectiveFeatures(resolvedOrgId, entitlement, effectivePlanId, accessDef,
|
|
1445
|
+
const hasFeature = await resolveEffectiveFeatures(resolvedOrgId, entitlement, effectivePlanId, accessDef, subscriptionStore, planVersionStore, overrides);
|
|
1343
1446
|
if (!hasFeature)
|
|
1344
1447
|
return false;
|
|
1345
1448
|
}
|
|
@@ -1351,8 +1454,8 @@ function createAccessContext(config) {
|
|
|
1351
1454
|
if (!await checkLayers1to3(entitlement, resource, resolvedOrgId, overrides))
|
|
1352
1455
|
return false;
|
|
1353
1456
|
const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
|
|
1354
|
-
if (limitKeys?.length && walletStore &&
|
|
1355
|
-
const walletStates = await resolveAllLimitStates(entitlement, resolvedOrgId, accessDef,
|
|
1457
|
+
if (limitKeys?.length && walletStore && subscriptionStore && resolvedOrgId) {
|
|
1458
|
+
const walletStates = await resolveAllLimitStates(entitlement, resolvedOrgId, accessDef, subscriptionStore, walletStore, planVersionStore, overrides);
|
|
1356
1459
|
for (const ws of walletStates) {
|
|
1357
1460
|
if (ws.max === 0)
|
|
1358
1461
|
return false;
|
|
@@ -1422,17 +1525,17 @@ function createAccessContext(config) {
|
|
|
1422
1525
|
meta.requiredRoles = [...entDef.roles];
|
|
1423
1526
|
}
|
|
1424
1527
|
}
|
|
1425
|
-
if (accessDef._planGatedEntitlements.has(entitlement) &&
|
|
1528
|
+
if (accessDef._planGatedEntitlements.has(entitlement) && subscriptionStore && orgResolver) {
|
|
1426
1529
|
let planDenied = false;
|
|
1427
1530
|
if (!resolvedOrgId) {
|
|
1428
1531
|
planDenied = true;
|
|
1429
1532
|
} else {
|
|
1430
|
-
const
|
|
1431
|
-
const effectivePlanId = resolveEffectivePlan(
|
|
1533
|
+
const subscription = await subscriptionStore.get(resolvedOrgId);
|
|
1534
|
+
const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
|
|
1432
1535
|
if (!effectivePlanId) {
|
|
1433
1536
|
planDenied = true;
|
|
1434
1537
|
} else {
|
|
1435
|
-
const hasFeature = await resolveEffectiveFeatures(resolvedOrgId, entitlement, effectivePlanId, accessDef,
|
|
1538
|
+
const hasFeature = await resolveEffectiveFeatures(resolvedOrgId, entitlement, effectivePlanId, accessDef, subscriptionStore, planVersionStore, overrides);
|
|
1436
1539
|
if (!hasFeature) {
|
|
1437
1540
|
planDenied = true;
|
|
1438
1541
|
}
|
|
@@ -1452,8 +1555,8 @@ function createAccessContext(config) {
|
|
|
1452
1555
|
}
|
|
1453
1556
|
}
|
|
1454
1557
|
const checkLimitKeys = accessDef._entitlementToLimitKeys[entitlement];
|
|
1455
|
-
if (checkLimitKeys?.length && walletStore &&
|
|
1456
|
-
const walletStates = await resolveAllLimitStates(entitlement, resolvedOrgId, accessDef,
|
|
1558
|
+
if (checkLimitKeys?.length && walletStore && subscriptionStore && resolvedOrgId) {
|
|
1559
|
+
const walletStates = await resolveAllLimitStates(entitlement, resolvedOrgId, accessDef, subscriptionStore, walletStore, planVersionStore, overrides);
|
|
1457
1560
|
for (const ws of walletStates) {
|
|
1458
1561
|
const exceeded = ws.max === 0 || ws.max !== -1 && ws.consumed >= ws.max;
|
|
1459
1562
|
if (exceeded) {
|
|
@@ -1546,20 +1649,20 @@ function createAccessContext(config) {
|
|
|
1546
1649
|
const overrides = resolvedOrgId && overrideStore ? await overrideStore.get(resolvedOrgId) : null;
|
|
1547
1650
|
if (!await checkLayers1to3(entitlement, resource, resolvedOrgId, overrides))
|
|
1548
1651
|
return false;
|
|
1549
|
-
if (!walletStore || !
|
|
1652
|
+
if (!walletStore || !subscriptionStore || !orgResolver)
|
|
1550
1653
|
return true;
|
|
1551
1654
|
const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
|
|
1552
1655
|
if (!limitKeys?.length)
|
|
1553
1656
|
return true;
|
|
1554
1657
|
if (!resolvedOrgId)
|
|
1555
1658
|
return false;
|
|
1556
|
-
const
|
|
1557
|
-
if (!
|
|
1659
|
+
const subscription = await subscriptionStore.get(resolvedOrgId);
|
|
1660
|
+
if (!subscription)
|
|
1558
1661
|
return false;
|
|
1559
|
-
const effectivePlanId = resolveEffectivePlan(
|
|
1662
|
+
const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
|
|
1560
1663
|
if (!effectivePlanId)
|
|
1561
1664
|
return false;
|
|
1562
|
-
const limitsToConsume = await resolveAllLimitConsumptions(resolvedOrgId, entitlement, effectivePlanId, accessDef,
|
|
1665
|
+
const limitsToConsume = await resolveAllLimitConsumptions(resolvedOrgId, entitlement, effectivePlanId, accessDef, subscriptionStore, walletStore, subscription, planVersionStore, overrides);
|
|
1563
1666
|
if (!limitsToConsume.length)
|
|
1564
1667
|
return true;
|
|
1565
1668
|
const consumed = [];
|
|
@@ -1597,7 +1700,7 @@ function createAccessContext(config) {
|
|
|
1597
1700
|
return true;
|
|
1598
1701
|
}
|
|
1599
1702
|
async function unconsume(entitlement, resource, amount = 1) {
|
|
1600
|
-
if (!walletStore || !
|
|
1703
|
+
if (!walletStore || !subscriptionStore || !orgResolver)
|
|
1601
1704
|
return;
|
|
1602
1705
|
const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
|
|
1603
1706
|
if (!limitKeys?.length)
|
|
@@ -1606,13 +1709,13 @@ function createAccessContext(config) {
|
|
|
1606
1709
|
if (!orgId)
|
|
1607
1710
|
return;
|
|
1608
1711
|
const overrides = overrideStore ? await overrideStore.get(orgId) : null;
|
|
1609
|
-
const
|
|
1610
|
-
if (!
|
|
1712
|
+
const subscription = await subscriptionStore.get(orgId);
|
|
1713
|
+
if (!subscription)
|
|
1611
1714
|
return;
|
|
1612
|
-
const effectivePlanId = resolveEffectivePlan(
|
|
1715
|
+
const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
|
|
1613
1716
|
if (!effectivePlanId)
|
|
1614
1717
|
return;
|
|
1615
|
-
const limitsToUnconsume = await resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId, accessDef,
|
|
1718
|
+
const limitsToUnconsume = await resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId, accessDef, subscriptionStore, walletStore, subscription, planVersionStore, overrides);
|
|
1616
1719
|
for (const lc of limitsToUnconsume) {
|
|
1617
1720
|
if (lc.effectiveMax === -1)
|
|
1618
1721
|
continue;
|
|
@@ -1633,7 +1736,7 @@ var DENIAL_ORDER = [
|
|
|
1633
1736
|
function orderDenialReasons(reasons) {
|
|
1634
1737
|
return [...reasons].sort((a, b) => DENIAL_ORDER.indexOf(a) - DENIAL_ORDER.indexOf(b));
|
|
1635
1738
|
}
|
|
1636
|
-
async function resolveEffectiveFeatures(orgId, entitlement, effectivePlanId, accessDef,
|
|
1739
|
+
async function resolveEffectiveFeatures(orgId, entitlement, effectivePlanId, accessDef, subscriptionStore, planVersionStore, overrides) {
|
|
1637
1740
|
if (planVersionStore) {
|
|
1638
1741
|
const tenantVersion = await planVersionStore.getTenantVersion(orgId, effectivePlanId);
|
|
1639
1742
|
if (tenantVersion !== null) {
|
|
@@ -1642,7 +1745,7 @@ async function resolveEffectiveFeatures(orgId, entitlement, effectivePlanId, acc
|
|
|
1642
1745
|
const snapshotFeatures = versionInfo.snapshot.features;
|
|
1643
1746
|
if (snapshotFeatures.includes(entitlement))
|
|
1644
1747
|
return true;
|
|
1645
|
-
const addOns2 = await
|
|
1748
|
+
const addOns2 = await subscriptionStore.getAddOns?.(orgId);
|
|
1646
1749
|
if (addOns2) {
|
|
1647
1750
|
for (const addOnId of addOns2) {
|
|
1648
1751
|
const addOnDef = accessDef.plans?.[addOnId];
|
|
@@ -1659,7 +1762,7 @@ async function resolveEffectiveFeatures(orgId, entitlement, effectivePlanId, acc
|
|
|
1659
1762
|
const planDef = accessDef.plans?.[effectivePlanId];
|
|
1660
1763
|
if (planDef?.features?.includes(entitlement))
|
|
1661
1764
|
return true;
|
|
1662
|
-
const addOns = await
|
|
1765
|
+
const addOns = await subscriptionStore.getAddOns?.(orgId);
|
|
1663
1766
|
if (addOns) {
|
|
1664
1767
|
for (const addOnId of addOns) {
|
|
1665
1768
|
const addOnDef = accessDef.plans?.[addOnId];
|
|
@@ -1671,10 +1774,10 @@ async function resolveEffectiveFeatures(orgId, entitlement, effectivePlanId, acc
|
|
|
1671
1774
|
return true;
|
|
1672
1775
|
return false;
|
|
1673
1776
|
}
|
|
1674
|
-
async function resolveAllLimitStates(entitlement, orgId, accessDef,
|
|
1675
|
-
const
|
|
1676
|
-
const effectivePlanId = resolveEffectivePlan(
|
|
1677
|
-
if (!effectivePlanId || !
|
|
1777
|
+
async function resolveAllLimitStates(entitlement, orgId, accessDef, subscriptionStore, walletStore, planVersionStore, overrides) {
|
|
1778
|
+
const subscription = await subscriptionStore.get(orgId);
|
|
1779
|
+
const effectivePlanId = resolveEffectivePlan(subscription, accessDef.plans, accessDef.defaultPlan);
|
|
1780
|
+
if (!effectivePlanId || !subscription)
|
|
1678
1781
|
return [];
|
|
1679
1782
|
const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
|
|
1680
1783
|
if (!limitKeys?.length)
|
|
@@ -1700,7 +1803,7 @@ async function resolveAllLimitStates(entitlement, orgId, accessDef, planStore, w
|
|
|
1700
1803
|
}
|
|
1701
1804
|
if (!limitDef)
|
|
1702
1805
|
continue;
|
|
1703
|
-
const effectiveMax = computeEffectiveLimit(limitDef.max, limitKey, orgId,
|
|
1806
|
+
const effectiveMax = computeEffectiveLimit(limitDef.max, limitKey, orgId, subscription, accessDef, subscriptionStore, overrides);
|
|
1704
1807
|
const resolvedMax = await effectiveMax;
|
|
1705
1808
|
const hasOverage = !!limitDef.overage;
|
|
1706
1809
|
if (resolvedMax === -1) {
|
|
@@ -1708,7 +1811,7 @@ async function resolveAllLimitStates(entitlement, orgId, accessDef, planStore, w
|
|
|
1708
1811
|
continue;
|
|
1709
1812
|
}
|
|
1710
1813
|
const walletKey = limitKey;
|
|
1711
|
-
const period = limitDef.per ? calculateBillingPeriod(
|
|
1814
|
+
const period = limitDef.per ? calculateBillingPeriod(subscription.startedAt, limitDef.per) : { periodStart: subscription.startedAt, periodEnd: new Date("9999-12-31T23:59:59Z") };
|
|
1712
1815
|
const consumed = await walletStore.getConsumption(orgId, walletKey, period.periodStart, period.periodEnd);
|
|
1713
1816
|
states.push({
|
|
1714
1817
|
key: limitKey,
|
|
@@ -1724,7 +1827,7 @@ async function resolveAllLimitStates(entitlement, orgId, accessDef, planStore, w
|
|
|
1724
1827
|
}
|
|
1725
1828
|
return states;
|
|
1726
1829
|
}
|
|
1727
|
-
async function resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId, accessDef,
|
|
1830
|
+
async function resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId, accessDef, subscriptionStore, _walletStore, subscription, planVersionStore, overrides) {
|
|
1728
1831
|
const limitKeys = accessDef._entitlementToLimitKeys[entitlement];
|
|
1729
1832
|
if (!limitKeys?.length)
|
|
1730
1833
|
return [];
|
|
@@ -1749,9 +1852,9 @@ async function resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId,
|
|
|
1749
1852
|
}
|
|
1750
1853
|
if (!limitDef)
|
|
1751
1854
|
continue;
|
|
1752
|
-
const effectiveMax = await computeEffectiveLimit(limitDef.max, limitKey, orgId,
|
|
1855
|
+
const effectiveMax = await computeEffectiveLimit(limitDef.max, limitKey, orgId, subscription, accessDef, subscriptionStore, overrides);
|
|
1753
1856
|
const walletKey = limitKey;
|
|
1754
|
-
const period = limitDef.per ? calculateBillingPeriod(
|
|
1857
|
+
const period = limitDef.per ? calculateBillingPeriod(subscription.startedAt, limitDef.per) : { periodStart: subscription.startedAt, periodEnd: new Date("9999-12-31T23:59:59Z") };
|
|
1755
1858
|
consumptions.push({
|
|
1756
1859
|
walletKey,
|
|
1757
1860
|
periodStart: period.periodStart,
|
|
@@ -1767,9 +1870,9 @@ async function resolveAllLimitConsumptions(orgId, entitlement, effectivePlanId,
|
|
|
1767
1870
|
}
|
|
1768
1871
|
return consumptions;
|
|
1769
1872
|
}
|
|
1770
|
-
async function computeEffectiveLimit(basePlanMax, limitKey, orgId,
|
|
1873
|
+
async function computeEffectiveLimit(basePlanMax, limitKey, orgId, subscription, accessDef, subscriptionStore, overrides) {
|
|
1771
1874
|
let effectiveMax = basePlanMax;
|
|
1772
|
-
const addOns = await
|
|
1875
|
+
const addOns = await subscriptionStore.getAddOns?.(orgId);
|
|
1773
1876
|
if (addOns) {
|
|
1774
1877
|
for (const addOnId of addOns) {
|
|
1775
1878
|
const addOnDef = accessDef.plans?.[addOnId];
|
|
@@ -1781,7 +1884,7 @@ async function computeEffectiveLimit(basePlanMax, limitKey, orgId, orgPlan, acce
|
|
|
1781
1884
|
}
|
|
1782
1885
|
}
|
|
1783
1886
|
}
|
|
1784
|
-
const oldOverride =
|
|
1887
|
+
const oldOverride = subscription.overrides[limitKey];
|
|
1785
1888
|
if (oldOverride) {
|
|
1786
1889
|
effectiveMax = Math.max(effectiveMax, oldOverride.max);
|
|
1787
1890
|
}
|
|
@@ -2091,7 +2194,6 @@ function generateAuthDDL(dialect) {
|
|
|
2091
2194
|
email ${t.text()} NOT NULL UNIQUE,
|
|
2092
2195
|
password_hash ${t.text()},
|
|
2093
2196
|
role ${t.text()} NOT NULL DEFAULT 'user',
|
|
2094
|
-
plan ${t.text()},
|
|
2095
2197
|
email_verified ${t.boolean(false)},
|
|
2096
2198
|
created_at ${t.timestamp()},
|
|
2097
2199
|
updated_at ${t.timestamp()}
|
|
@@ -2344,7 +2446,7 @@ function extractPlanId(obj) {
|
|
|
2344
2446
|
return null;
|
|
2345
2447
|
}
|
|
2346
2448
|
function createWebhookHandler(config) {
|
|
2347
|
-
const {
|
|
2449
|
+
const { subscriptionStore, emitter, defaultPlan, webhookSecret } = config;
|
|
2348
2450
|
return async (request) => {
|
|
2349
2451
|
const signature = request.headers.get("stripe-signature");
|
|
2350
2452
|
if (signature !== webhookSecret) {
|
|
@@ -2370,7 +2472,7 @@ function createWebhookHandler(config) {
|
|
|
2370
2472
|
const tenantId = extractTenantId(obj);
|
|
2371
2473
|
const planId = extractPlanId(obj);
|
|
2372
2474
|
if (tenantId && planId) {
|
|
2373
|
-
await
|
|
2475
|
+
await subscriptionStore.assign(tenantId, planId);
|
|
2374
2476
|
emitter.emit("subscription:created", { tenantId, planId });
|
|
2375
2477
|
}
|
|
2376
2478
|
break;
|
|
@@ -2379,7 +2481,7 @@ function createWebhookHandler(config) {
|
|
|
2379
2481
|
const tenantId = extractTenantId(obj);
|
|
2380
2482
|
const planId = extractPlanId(obj);
|
|
2381
2483
|
if (tenantId && planId) {
|
|
2382
|
-
await
|
|
2484
|
+
await subscriptionStore.assign(tenantId, planId);
|
|
2383
2485
|
}
|
|
2384
2486
|
break;
|
|
2385
2487
|
}
|
|
@@ -2387,7 +2489,7 @@ function createWebhookHandler(config) {
|
|
|
2387
2489
|
const tenantId = extractTenantId(obj);
|
|
2388
2490
|
const planId = extractPlanId(obj);
|
|
2389
2491
|
if (tenantId) {
|
|
2390
|
-
await
|
|
2492
|
+
await subscriptionStore.assign(tenantId, defaultPlan);
|
|
2391
2493
|
emitter.emit("subscription:canceled", {
|
|
2392
2494
|
tenantId,
|
|
2393
2495
|
planId: planId ?? defaultPlan
|
|
@@ -2414,10 +2516,10 @@ function createWebhookHandler(config) {
|
|
|
2414
2516
|
const meta = obj.metadata;
|
|
2415
2517
|
const isAddOn = meta?.isAddOn === "true";
|
|
2416
2518
|
if (tenantId && planId) {
|
|
2417
|
-
if (isAddOn &&
|
|
2418
|
-
await
|
|
2519
|
+
if (isAddOn && subscriptionStore.attachAddOn) {
|
|
2520
|
+
await subscriptionStore.attachAddOn(tenantId, planId);
|
|
2419
2521
|
} else {
|
|
2420
|
-
await
|
|
2522
|
+
await subscriptionStore.assign(tenantId, planId);
|
|
2421
2523
|
}
|
|
2422
2524
|
}
|
|
2423
2525
|
break;
|
|
@@ -2664,100 +2766,8 @@ class DbOAuthAccountStore {
|
|
|
2664
2766
|
}
|
|
2665
2767
|
dispose() {}
|
|
2666
2768
|
}
|
|
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
2769
|
// src/auth/db-role-assignment-store.ts
|
|
2760
|
-
import { sql as
|
|
2770
|
+
import { sql as sql5 } from "@vertz/db/sql";
|
|
2761
2771
|
class DbRoleAssignmentStore {
|
|
2762
2772
|
db;
|
|
2763
2773
|
constructor(db) {
|
|
@@ -2766,23 +2776,23 @@ class DbRoleAssignmentStore {
|
|
|
2766
2776
|
async assign(userId, resourceType, resourceId, role) {
|
|
2767
2777
|
const id = crypto.randomUUID();
|
|
2768
2778
|
const now = new Date().toISOString();
|
|
2769
|
-
const result = await this.db.query(
|
|
2779
|
+
const result = await this.db.query(sql5`INSERT INTO auth_role_assignments (id, user_id, resource_type, resource_id, role, created_at)
|
|
2770
2780
|
VALUES (${id}, ${userId}, ${resourceType}, ${resourceId}, ${role}, ${now})
|
|
2771
2781
|
ON CONFLICT DO NOTHING`);
|
|
2772
2782
|
assertWrite(result, "assign");
|
|
2773
2783
|
}
|
|
2774
2784
|
async revoke(userId, resourceType, resourceId, role) {
|
|
2775
|
-
const result = await this.db.query(
|
|
2785
|
+
const result = await this.db.query(sql5`DELETE FROM auth_role_assignments WHERE user_id = ${userId} AND resource_type = ${resourceType} AND resource_id = ${resourceId} AND role = ${role}`);
|
|
2776
2786
|
assertWrite(result, "revoke");
|
|
2777
2787
|
}
|
|
2778
2788
|
async getRoles(userId, resourceType, resourceId) {
|
|
2779
|
-
const result = await this.db.query(
|
|
2789
|
+
const result = await this.db.query(sql5`SELECT role FROM auth_role_assignments WHERE user_id = ${userId} AND resource_type = ${resourceType} AND resource_id = ${resourceId}`);
|
|
2780
2790
|
if (!result.ok)
|
|
2781
2791
|
return [];
|
|
2782
2792
|
return result.data.rows.map((r) => r.role);
|
|
2783
2793
|
}
|
|
2784
2794
|
async getRolesForUser(userId) {
|
|
2785
|
-
const result = await this.db.query(
|
|
2795
|
+
const result = await this.db.query(sql5`SELECT user_id, resource_type, resource_id, role FROM auth_role_assignments WHERE user_id = ${userId}`);
|
|
2786
2796
|
if (!result.ok)
|
|
2787
2797
|
return [];
|
|
2788
2798
|
return result.data.rows.map((r) => ({
|
|
@@ -2826,7 +2836,7 @@ class DbRoleAssignmentStore {
|
|
|
2826
2836
|
dispose() {}
|
|
2827
2837
|
}
|
|
2828
2838
|
// src/auth/db-session-store.ts
|
|
2829
|
-
import { sql as
|
|
2839
|
+
import { sql as sql6 } from "@vertz/db/sql";
|
|
2830
2840
|
class DbSessionStore {
|
|
2831
2841
|
db;
|
|
2832
2842
|
constructor(db) {
|
|
@@ -2835,7 +2845,7 @@ class DbSessionStore {
|
|
|
2835
2845
|
async createSessionWithId(id, data) {
|
|
2836
2846
|
const now = new Date;
|
|
2837
2847
|
const tokensJson = data.currentTokens ? JSON.stringify(data.currentTokens) : null;
|
|
2838
|
-
const result = await this.db.query(
|
|
2848
|
+
const result = await this.db.query(sql6`INSERT INTO auth_sessions (id, user_id, refresh_token_hash, previous_refresh_hash, current_tokens, ip_address, user_agent, created_at, last_active_at, expires_at, revoked_at)
|
|
2839
2849
|
VALUES (${id}, ${data.userId}, ${data.refreshTokenHash}, ${null}, ${tokensJson}, ${data.ipAddress}, ${data.userAgent}, ${now.toISOString()}, ${now.toISOString()}, ${data.expiresAt.toISOString()}, ${null})`);
|
|
2840
2850
|
assertWrite(result, "createSessionWithId");
|
|
2841
2851
|
return {
|
|
@@ -2853,45 +2863,73 @@ class DbSessionStore {
|
|
|
2853
2863
|
}
|
|
2854
2864
|
async findByRefreshHash(hash) {
|
|
2855
2865
|
const nowStr = new Date().toISOString();
|
|
2856
|
-
const result = await this.db.
|
|
2866
|
+
const result = await this.db.auth_sessions.get({
|
|
2867
|
+
where: {
|
|
2868
|
+
refreshTokenHash: hash,
|
|
2869
|
+
revokedAt: null,
|
|
2870
|
+
expiresAt: { gt: nowStr }
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2857
2873
|
if (!result.ok)
|
|
2858
2874
|
return null;
|
|
2859
|
-
const row = result.data
|
|
2875
|
+
const row = result.data;
|
|
2876
|
+
if (!row)
|
|
2877
|
+
return null;
|
|
2878
|
+
return this.recordToSession(row);
|
|
2879
|
+
}
|
|
2880
|
+
async findActiveSessionById(id) {
|
|
2881
|
+
const nowStr = new Date().toISOString();
|
|
2882
|
+
const result = await this.db.auth_sessions.get({
|
|
2883
|
+
where: {
|
|
2884
|
+
id,
|
|
2885
|
+
revokedAt: null,
|
|
2886
|
+
expiresAt: { gt: nowStr }
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
if (!result.ok)
|
|
2890
|
+
return null;
|
|
2891
|
+
const row = result.data;
|
|
2860
2892
|
if (!row)
|
|
2861
2893
|
return null;
|
|
2862
|
-
return this.
|
|
2894
|
+
return this.recordToSession(row);
|
|
2863
2895
|
}
|
|
2864
2896
|
async findByPreviousRefreshHash(hash) {
|
|
2865
2897
|
const nowStr = new Date().toISOString();
|
|
2866
|
-
const result = await this.db.
|
|
2898
|
+
const result = await this.db.auth_sessions.get({
|
|
2899
|
+
where: {
|
|
2900
|
+
previousRefreshHash: hash,
|
|
2901
|
+
revokedAt: null,
|
|
2902
|
+
expiresAt: { gt: nowStr }
|
|
2903
|
+
}
|
|
2904
|
+
});
|
|
2867
2905
|
if (!result.ok)
|
|
2868
2906
|
return null;
|
|
2869
|
-
const row = result.data
|
|
2907
|
+
const row = result.data;
|
|
2870
2908
|
if (!row)
|
|
2871
2909
|
return null;
|
|
2872
|
-
return this.
|
|
2910
|
+
return this.recordToSession(row);
|
|
2873
2911
|
}
|
|
2874
2912
|
async revokeSession(id) {
|
|
2875
2913
|
const nowStr = new Date().toISOString();
|
|
2876
|
-
const result = await this.db.query(
|
|
2914
|
+
const result = await this.db.query(sql6`UPDATE auth_sessions SET revoked_at = ${nowStr}, current_tokens = ${null} WHERE id = ${id}`);
|
|
2877
2915
|
assertWrite(result, "revokeSession");
|
|
2878
2916
|
}
|
|
2879
2917
|
async listActiveSessions(userId) {
|
|
2880
2918
|
const nowStr = new Date().toISOString();
|
|
2881
|
-
const result = await this.db.query(
|
|
2919
|
+
const result = await this.db.query(sql6`SELECT * FROM auth_sessions WHERE user_id = ${userId} AND revoked_at IS NULL AND expires_at > ${nowStr}`);
|
|
2882
2920
|
if (!result.ok)
|
|
2883
2921
|
return [];
|
|
2884
2922
|
return result.data.rows.map((row) => this.rowToSession(row));
|
|
2885
2923
|
}
|
|
2886
2924
|
async countActiveSessions(userId) {
|
|
2887
2925
|
const nowStr = new Date().toISOString();
|
|
2888
|
-
const result = await this.db.query(
|
|
2926
|
+
const result = await this.db.query(sql6`SELECT COUNT(*) as cnt FROM auth_sessions WHERE user_id = ${userId} AND revoked_at IS NULL AND expires_at > ${nowStr}`);
|
|
2889
2927
|
if (!result.ok)
|
|
2890
2928
|
return 0;
|
|
2891
2929
|
return result.data.rows[0]?.cnt ?? 0;
|
|
2892
2930
|
}
|
|
2893
2931
|
async getCurrentTokens(sessionId) {
|
|
2894
|
-
const result = await this.db.query(
|
|
2932
|
+
const result = await this.db.query(sql6`SELECT current_tokens FROM auth_sessions WHERE id = ${sessionId} LIMIT 1`);
|
|
2895
2933
|
if (!result.ok)
|
|
2896
2934
|
return null;
|
|
2897
2935
|
const row = result.data.rows[0];
|
|
@@ -2905,7 +2943,7 @@ class DbSessionStore {
|
|
|
2905
2943
|
}
|
|
2906
2944
|
async updateSession(id, data) {
|
|
2907
2945
|
const tokensJson = data.currentTokens ? JSON.stringify(data.currentTokens) : null;
|
|
2908
|
-
const result = await this.db.query(
|
|
2946
|
+
const result = await this.db.query(sql6`UPDATE auth_sessions SET refresh_token_hash = ${data.refreshTokenHash}, previous_refresh_hash = ${data.previousRefreshHash}, last_active_at = ${data.lastActiveAt.toISOString()}, current_tokens = ${tokensJson} WHERE id = ${id}`);
|
|
2909
2947
|
assertWrite(result, "updateSession");
|
|
2910
2948
|
}
|
|
2911
2949
|
dispose() {}
|
|
@@ -2923,6 +2961,118 @@ class DbSessionStore {
|
|
|
2923
2961
|
revokedAt: row.revoked_at ? new Date(row.revoked_at) : null
|
|
2924
2962
|
};
|
|
2925
2963
|
}
|
|
2964
|
+
recordToSession(rec) {
|
|
2965
|
+
return {
|
|
2966
|
+
id: rec.id,
|
|
2967
|
+
userId: rec.userId,
|
|
2968
|
+
refreshTokenHash: rec.refreshTokenHash,
|
|
2969
|
+
previousRefreshHash: rec.previousRefreshHash,
|
|
2970
|
+
ipAddress: rec.ipAddress,
|
|
2971
|
+
userAgent: rec.userAgent,
|
|
2972
|
+
createdAt: new Date(rec.createdAt),
|
|
2973
|
+
lastActiveAt: new Date(rec.lastActiveAt),
|
|
2974
|
+
expiresAt: new Date(rec.expiresAt),
|
|
2975
|
+
revokedAt: rec.revokedAt ? new Date(rec.revokedAt) : null
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
// src/auth/db-subscription-store.ts
|
|
2980
|
+
import { sql as sql7 } from "@vertz/db/sql";
|
|
2981
|
+
class DbSubscriptionStore {
|
|
2982
|
+
db;
|
|
2983
|
+
constructor(db) {
|
|
2984
|
+
this.db = db;
|
|
2985
|
+
}
|
|
2986
|
+
async assign(tenantId, planId, startedAt = new Date, expiresAt = null) {
|
|
2987
|
+
await this.db.transaction(async (tx) => {
|
|
2988
|
+
const id = crypto.randomUUID();
|
|
2989
|
+
const startedAtIso = startedAt.toISOString();
|
|
2990
|
+
const expiresAtIso = expiresAt ? expiresAt.toISOString() : null;
|
|
2991
|
+
const upsertResult = await tx.query(sql7`INSERT INTO auth_plans (id, tenant_id, plan_id, started_at, expires_at)
|
|
2992
|
+
VALUES (${id}, ${tenantId}, ${planId}, ${startedAtIso}, ${expiresAtIso})
|
|
2993
|
+
ON CONFLICT(tenant_id) DO UPDATE SET plan_id = ${planId}, started_at = ${startedAtIso}, expires_at = ${expiresAtIso}`);
|
|
2994
|
+
assertWrite(upsertResult, "assign/upsert");
|
|
2995
|
+
const overrideResult = await tx.query(sql7`DELETE FROM auth_overrides WHERE tenant_id = ${tenantId}`);
|
|
2996
|
+
assertWrite(overrideResult, "assign/clearOverrides");
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
async get(tenantId) {
|
|
3000
|
+
const result = await this.db.query(sql7`SELECT tenant_id, plan_id, started_at, expires_at FROM auth_plans WHERE tenant_id = ${tenantId}`);
|
|
3001
|
+
if (!result.ok)
|
|
3002
|
+
return null;
|
|
3003
|
+
const row = result.data.rows[0];
|
|
3004
|
+
if (!row)
|
|
3005
|
+
return null;
|
|
3006
|
+
const overrides = await this.loadOverrides(tenantId);
|
|
3007
|
+
return {
|
|
3008
|
+
tenantId: row.tenant_id,
|
|
3009
|
+
planId: row.plan_id,
|
|
3010
|
+
startedAt: new Date(row.started_at),
|
|
3011
|
+
expiresAt: row.expires_at ? new Date(row.expires_at) : null,
|
|
3012
|
+
overrides
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
async updateOverrides(tenantId, overrides) {
|
|
3016
|
+
const planResult = await this.db.query(sql7`SELECT tenant_id FROM auth_plans WHERE tenant_id = ${tenantId}`);
|
|
3017
|
+
if (!planResult.ok || planResult.data.rows.length === 0)
|
|
3018
|
+
return;
|
|
3019
|
+
await this.db.transaction(async (tx) => {
|
|
3020
|
+
const existing = await this.loadOverrides(tenantId);
|
|
3021
|
+
const merged = { ...existing, ...overrides };
|
|
3022
|
+
const overridesJson = JSON.stringify(merged);
|
|
3023
|
+
const now = new Date().toISOString();
|
|
3024
|
+
const overrideResult = await tx.query(sql7`SELECT tenant_id FROM auth_overrides WHERE tenant_id = ${tenantId}`);
|
|
3025
|
+
if (overrideResult.ok && overrideResult.data.rows.length > 0) {
|
|
3026
|
+
const updateResult = await tx.query(sql7`UPDATE auth_overrides SET overrides = ${overridesJson}, updated_at = ${now} WHERE tenant_id = ${tenantId}`);
|
|
3027
|
+
assertWrite(updateResult, "updateOverrides/update");
|
|
3028
|
+
} else {
|
|
3029
|
+
const id = crypto.randomUUID();
|
|
3030
|
+
const insertResult = await tx.query(sql7`INSERT INTO auth_overrides (id, tenant_id, overrides, updated_at)
|
|
3031
|
+
VALUES (${id}, ${tenantId}, ${overridesJson}, ${now})`);
|
|
3032
|
+
assertWrite(insertResult, "updateOverrides/insert");
|
|
3033
|
+
}
|
|
3034
|
+
});
|
|
3035
|
+
}
|
|
3036
|
+
async remove(tenantId) {
|
|
3037
|
+
await this.db.transaction(async (tx) => {
|
|
3038
|
+
const planResult = await tx.query(sql7`DELETE FROM auth_plans WHERE tenant_id = ${tenantId}`);
|
|
3039
|
+
assertWrite(planResult, "remove/plans");
|
|
3040
|
+
const overrideResult = await tx.query(sql7`DELETE FROM auth_overrides WHERE tenant_id = ${tenantId}`);
|
|
3041
|
+
assertWrite(overrideResult, "remove/overrides");
|
|
3042
|
+
});
|
|
3043
|
+
}
|
|
3044
|
+
async attachAddOn(tenantId, addOnId) {
|
|
3045
|
+
const id = crypto.randomUUID();
|
|
3046
|
+
const now = new Date().toISOString();
|
|
3047
|
+
const result = await this.db.query(sql7`INSERT INTO auth_plan_addons (id, tenant_id, addon_id, quantity, created_at)
|
|
3048
|
+
VALUES (${id}, ${tenantId}, ${addOnId}, ${1}, ${now})
|
|
3049
|
+
ON CONFLICT(tenant_id, addon_id) DO NOTHING`);
|
|
3050
|
+
assertWrite(result, "attachAddOn");
|
|
3051
|
+
}
|
|
3052
|
+
async detachAddOn(tenantId, addOnId) {
|
|
3053
|
+
const result = await this.db.query(sql7`DELETE FROM auth_plan_addons WHERE tenant_id = ${tenantId} AND addon_id = ${addOnId}`);
|
|
3054
|
+
assertWrite(result, "detachAddOn");
|
|
3055
|
+
}
|
|
3056
|
+
async getAddOns(tenantId) {
|
|
3057
|
+
const result = await this.db.query(sql7`SELECT addon_id FROM auth_plan_addons WHERE tenant_id = ${tenantId}`);
|
|
3058
|
+
if (!result.ok)
|
|
3059
|
+
return [];
|
|
3060
|
+
return result.data.rows.map((r) => r.addon_id);
|
|
3061
|
+
}
|
|
3062
|
+
dispose() {}
|
|
3063
|
+
async loadOverrides(tenantId) {
|
|
3064
|
+
const result = await this.db.query(sql7`SELECT overrides FROM auth_overrides WHERE tenant_id = ${tenantId}`);
|
|
3065
|
+
if (!result.ok || result.data.rows.length === 0)
|
|
3066
|
+
return {};
|
|
3067
|
+
try {
|
|
3068
|
+
const overridesStr = result.data.rows[0]?.overrides;
|
|
3069
|
+
if (!overridesStr)
|
|
3070
|
+
return {};
|
|
3071
|
+
return JSON.parse(overridesStr);
|
|
3072
|
+
} catch {
|
|
3073
|
+
return {};
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
2926
3076
|
}
|
|
2927
3077
|
// src/auth/db-user-store.ts
|
|
2928
3078
|
import { sql as sql8 } from "@vertz/db/sql";
|
|
@@ -2932,8 +3082,8 @@ class DbUserStore {
|
|
|
2932
3082
|
this.db = db;
|
|
2933
3083
|
}
|
|
2934
3084
|
async createUser(user, passwordHash) {
|
|
2935
|
-
const result = await this.db.query(sql8`INSERT INTO auth_users (id, email, password_hash, role,
|
|
2936
|
-
VALUES (${user.id}, ${user.email.toLowerCase()}, ${passwordHash}, ${user.role}, ${
|
|
3085
|
+
const result = await this.db.query(sql8`INSERT INTO auth_users (id, email, password_hash, role, email_verified, created_at, updated_at)
|
|
3086
|
+
VALUES (${user.id}, ${user.email.toLowerCase()}, ${passwordHash}, ${user.role}, ${boolVal(this.db, user.emailVerified ?? false)}, ${user.createdAt.toISOString()}, ${user.updatedAt.toISOString()})`);
|
|
2937
3087
|
assertWrite(result, "createUser");
|
|
2938
3088
|
}
|
|
2939
3089
|
async findByEmail(email) {
|
|
@@ -2966,12 +3116,15 @@ class DbUserStore {
|
|
|
2966
3116
|
const result = await this.db.query(sql8`UPDATE auth_users SET email_verified = ${val}, updated_at = ${new Date().toISOString()} WHERE id = ${userId}`);
|
|
2967
3117
|
assertWrite(result, "updateEmailVerified");
|
|
2968
3118
|
}
|
|
3119
|
+
async deleteUser(id) {
|
|
3120
|
+
const result = await this.db.query(sql8`DELETE FROM auth_users WHERE id = ${id}`);
|
|
3121
|
+
assertWrite(result, "deleteUser");
|
|
3122
|
+
}
|
|
2969
3123
|
rowToUser(row) {
|
|
2970
3124
|
return {
|
|
2971
3125
|
id: row.id,
|
|
2972
3126
|
email: row.email,
|
|
2973
3127
|
role: row.role,
|
|
2974
|
-
plan: row.plan ?? undefined,
|
|
2975
3128
|
emailVerified: row.email_verified === 1 || row.email_verified === true,
|
|
2976
3129
|
createdAt: new Date(row.created_at),
|
|
2977
3130
|
updatedAt: new Date(row.updated_at)
|
|
@@ -3295,23 +3448,23 @@ async function computeEntityAccess(entitlements, entity, accessContext) {
|
|
|
3295
3448
|
// src/auth/flag-store.ts
|
|
3296
3449
|
class InMemoryFlagStore {
|
|
3297
3450
|
flags = new Map;
|
|
3298
|
-
setFlag(
|
|
3299
|
-
let
|
|
3300
|
-
if (!
|
|
3301
|
-
|
|
3302
|
-
this.flags.set(
|
|
3451
|
+
setFlag(tenantId, flag, enabled) {
|
|
3452
|
+
let tenantFlags = this.flags.get(tenantId);
|
|
3453
|
+
if (!tenantFlags) {
|
|
3454
|
+
tenantFlags = new Map;
|
|
3455
|
+
this.flags.set(tenantId, tenantFlags);
|
|
3303
3456
|
}
|
|
3304
|
-
|
|
3457
|
+
tenantFlags.set(flag, enabled);
|
|
3305
3458
|
}
|
|
3306
|
-
getFlag(
|
|
3307
|
-
return this.flags.get(
|
|
3459
|
+
getFlag(tenantId, flag) {
|
|
3460
|
+
return this.flags.get(tenantId)?.get(flag) ?? false;
|
|
3308
3461
|
}
|
|
3309
|
-
getFlags(
|
|
3310
|
-
const
|
|
3311
|
-
if (!
|
|
3462
|
+
getFlags(tenantId) {
|
|
3463
|
+
const tenantFlags = this.flags.get(tenantId);
|
|
3464
|
+
if (!tenantFlags)
|
|
3312
3465
|
return {};
|
|
3313
3466
|
const result = {};
|
|
3314
|
-
for (const [key, value] of
|
|
3467
|
+
for (const [key, value] of tenantFlags) {
|
|
3315
3468
|
result[key] = value;
|
|
3316
3469
|
}
|
|
3317
3470
|
return result;
|
|
@@ -3476,7 +3629,7 @@ function resolveGraceEnd(planDef, now) {
|
|
|
3476
3629
|
return new Date(now.getTime() + GRACE_DURATION_MS["1m"]);
|
|
3477
3630
|
}
|
|
3478
3631
|
function createPlanManager(config) {
|
|
3479
|
-
const { plans, versionStore, grandfatheringStore,
|
|
3632
|
+
const { plans, versionStore, grandfatheringStore, subscriptionStore } = config;
|
|
3480
3633
|
const clock = config.clock ?? (() => new Date);
|
|
3481
3634
|
const handlers = [];
|
|
3482
3635
|
function emit(event) {
|
|
@@ -3523,8 +3676,8 @@ function createPlanManager(config) {
|
|
|
3523
3676
|
}
|
|
3524
3677
|
}
|
|
3525
3678
|
async function listTenantsOnPlan(planId) {
|
|
3526
|
-
if (
|
|
3527
|
-
return
|
|
3679
|
+
if (subscriptionStore.listByPlan) {
|
|
3680
|
+
return subscriptionStore.listByPlan(planId);
|
|
3528
3681
|
}
|
|
3529
3682
|
return [];
|
|
3530
3683
|
}
|
|
@@ -3580,10 +3733,10 @@ function createPlanManager(config) {
|
|
|
3580
3733
|
}
|
|
3581
3734
|
}
|
|
3582
3735
|
async function resolve(tenantId) {
|
|
3583
|
-
const
|
|
3584
|
-
if (!
|
|
3736
|
+
const subscription = await subscriptionStore.get(tenantId);
|
|
3737
|
+
if (!subscription)
|
|
3585
3738
|
return null;
|
|
3586
|
-
const planId =
|
|
3739
|
+
const planId = subscription.planId;
|
|
3587
3740
|
const currentVersion = await versionStore.getCurrentVersion(planId);
|
|
3588
3741
|
if (currentVersion === null)
|
|
3589
3742
|
return null;
|
|
@@ -3761,8 +3914,7 @@ function discord(config) {
|
|
|
3761
3914
|
providerId: data.id,
|
|
3762
3915
|
email: data.email ?? "",
|
|
3763
3916
|
emailVerified: data.verified ?? false,
|
|
3764
|
-
|
|
3765
|
-
avatarUrl: data.avatar ? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png` : undefined
|
|
3917
|
+
raw: data
|
|
3766
3918
|
};
|
|
3767
3919
|
}
|
|
3768
3920
|
};
|
|
@@ -3804,6 +3956,9 @@ function github(config) {
|
|
|
3804
3956
|
})
|
|
3805
3957
|
});
|
|
3806
3958
|
const data = await response.json();
|
|
3959
|
+
if (data.error) {
|
|
3960
|
+
throw new Error(`GitHub token exchange failed: ${data.error} — ${data.error_description}`);
|
|
3961
|
+
}
|
|
3807
3962
|
return {
|
|
3808
3963
|
accessToken: data.access_token
|
|
3809
3964
|
};
|
|
@@ -3832,8 +3987,7 @@ function github(config) {
|
|
|
3832
3987
|
providerId: String(userData.id),
|
|
3833
3988
|
email: email?.email ?? "",
|
|
3834
3989
|
emailVerified: email?.verified ?? false,
|
|
3835
|
-
|
|
3836
|
-
avatarUrl: userData.avatar_url
|
|
3990
|
+
raw: userData
|
|
3837
3991
|
};
|
|
3838
3992
|
}
|
|
3839
3993
|
};
|
|
@@ -3905,8 +4059,7 @@ function google(config) {
|
|
|
3905
4059
|
providerId: payload.sub,
|
|
3906
4060
|
email: payload.email,
|
|
3907
4061
|
emailVerified: payload.email_verified,
|
|
3908
|
-
|
|
3909
|
-
avatarUrl: payload.picture
|
|
4062
|
+
raw: payload
|
|
3910
4063
|
};
|
|
3911
4064
|
}
|
|
3912
4065
|
};
|
|
@@ -3997,13 +4150,13 @@ var rules = {
|
|
|
3997
4150
|
// src/auth/wallet-store.ts
|
|
3998
4151
|
class InMemoryWalletStore {
|
|
3999
4152
|
entries = new Map;
|
|
4000
|
-
key(
|
|
4001
|
-
return `${
|
|
4153
|
+
key(tenantId, entitlement, periodStart) {
|
|
4154
|
+
return `${tenantId}:${entitlement}:${periodStart.getTime()}`;
|
|
4002
4155
|
}
|
|
4003
|
-
async consume(
|
|
4004
|
-
const k = this.key(
|
|
4156
|
+
async consume(tenantId, entitlement, periodStart, periodEnd, limit, amount = 1) {
|
|
4157
|
+
const k = this.key(tenantId, entitlement, periodStart);
|
|
4005
4158
|
if (!this.entries.has(k)) {
|
|
4006
|
-
this.entries.set(k, {
|
|
4159
|
+
this.entries.set(k, { tenantId, entitlement, periodStart, periodEnd, consumed: 0 });
|
|
4007
4160
|
}
|
|
4008
4161
|
const entry = this.entries.get(k);
|
|
4009
4162
|
if (entry.consumed + amount > limit) {
|
|
@@ -4022,15 +4175,15 @@ class InMemoryWalletStore {
|
|
|
4022
4175
|
remaining: limit - entry.consumed
|
|
4023
4176
|
};
|
|
4024
4177
|
}
|
|
4025
|
-
async unconsume(
|
|
4026
|
-
const k = this.key(
|
|
4178
|
+
async unconsume(tenantId, entitlement, periodStart, _periodEnd, amount = 1) {
|
|
4179
|
+
const k = this.key(tenantId, entitlement, periodStart);
|
|
4027
4180
|
const entry = this.entries.get(k);
|
|
4028
4181
|
if (!entry)
|
|
4029
4182
|
return;
|
|
4030
4183
|
entry.consumed = Math.max(0, entry.consumed - amount);
|
|
4031
4184
|
}
|
|
4032
|
-
async getConsumption(
|
|
4033
|
-
const k = this.key(
|
|
4185
|
+
async getConsumption(tenantId, entitlement, periodStart, _periodEnd) {
|
|
4186
|
+
const k = this.key(tenantId, entitlement, periodStart);
|
|
4034
4187
|
const entry = this.entries.get(k);
|
|
4035
4188
|
return entry?.consumed ?? 0;
|
|
4036
4189
|
}
|
|
@@ -4069,7 +4222,12 @@ function createAuth(config) {
|
|
|
4069
4222
|
if (session.strategy !== "jwt") {
|
|
4070
4223
|
throw new Error(`Session strategy "${session.strategy}" is not yet supported. Use "jwt".`);
|
|
4071
4224
|
}
|
|
4072
|
-
const
|
|
4225
|
+
const ttlSeconds = Math.floor(parseDuration(session.ttl) / 1000);
|
|
4226
|
+
const cookieConfig = {
|
|
4227
|
+
...DEFAULT_COOKIE_CONFIG,
|
|
4228
|
+
maxAge: ttlSeconds,
|
|
4229
|
+
...session.cookie
|
|
4230
|
+
};
|
|
4073
4231
|
const refreshName = session.refreshName ?? "vertz.ref";
|
|
4074
4232
|
const refreshTtlMs = parseDuration(session.refreshTtl ?? "7d");
|
|
4075
4233
|
const refreshMaxAge = Math.floor(refreshTtlMs / 1000);
|
|
@@ -4082,7 +4240,7 @@ function createAuth(config) {
|
|
|
4082
4240
|
if (!isProduction && cookieConfig.secure === false) {
|
|
4083
4241
|
console.warn("Cookie 'secure' flag is disabled. This is allowed in development but must be enabled in production.");
|
|
4084
4242
|
}
|
|
4085
|
-
const ttlMs =
|
|
4243
|
+
const ttlMs = ttlSeconds * 1000;
|
|
4086
4244
|
const DUMMY_HASH = "$2a$12$000000000000000000000uGWDREoC/y2KhZ5l2QkI4j0LpDjWcaq";
|
|
4087
4245
|
const sessionStore = config.sessionStore ?? new InMemorySessionStore;
|
|
4088
4246
|
const userStore = config.userStore ?? new InMemoryUserStore;
|
|
@@ -4112,6 +4270,9 @@ function createAuth(config) {
|
|
|
4112
4270
|
const passwordResetTtlMs = parseDuration(passwordResetConfig?.tokenTtl ?? "1h");
|
|
4113
4271
|
const revokeSessionsOnReset = passwordResetConfig?.revokeSessionsOnReset ?? true;
|
|
4114
4272
|
const passwordResetStore = config.passwordResetStore ?? (passwordResetEnabled ? new InMemoryPasswordResetStore : undefined);
|
|
4273
|
+
const tenantConfig = config.tenant;
|
|
4274
|
+
const onUserCreated = config.onUserCreated;
|
|
4275
|
+
const entityProxy = config._entityProxy ?? {};
|
|
4115
4276
|
const rateLimitStore = config.rateLimitStore ?? new InMemoryRateLimitStore;
|
|
4116
4277
|
const signInWindowMs = parseDuration(emailPassword?.rateLimit?.window || "15m");
|
|
4117
4278
|
const signUpWindowMs = parseDuration("1h");
|
|
@@ -4121,6 +4282,7 @@ function createAuth(config) {
|
|
|
4121
4282
|
const expiresAt = new Date(Date.now() + ttlMs);
|
|
4122
4283
|
const userClaims = claims ? claims(user) : {};
|
|
4123
4284
|
const fvaClaim = options?.fva !== undefined ? { fva: options.fva } : {};
|
|
4285
|
+
const tenantClaim = options?.tenantId ? { tenantId: options.tenantId } : {};
|
|
4124
4286
|
let aclClaim = {};
|
|
4125
4287
|
if (config.access) {
|
|
4126
4288
|
const accessSet = await computeAccessSet({
|
|
@@ -4129,7 +4291,8 @@ function createAuth(config) {
|
|
|
4129
4291
|
roleStore: config.access.roleStore,
|
|
4130
4292
|
closureStore: config.access.closureStore,
|
|
4131
4293
|
flagStore: config.access.flagStore,
|
|
4132
|
-
|
|
4294
|
+
subscriptionStore: config.access?.subscriptionStore,
|
|
4295
|
+
tenantId: null
|
|
4133
4296
|
});
|
|
4134
4297
|
const encoded = encodeAccessSet(accessSet);
|
|
4135
4298
|
const canonicalJson = JSON.stringify(encoded);
|
|
@@ -4149,6 +4312,7 @@ function createAuth(config) {
|
|
|
4149
4312
|
const jwt = await createJWT(user, jwtSecret, ttlMs, jwtAlgorithm, () => ({
|
|
4150
4313
|
...userClaims,
|
|
4151
4314
|
...fvaClaim,
|
|
4315
|
+
...tenantClaim,
|
|
4152
4316
|
...aclClaim,
|
|
4153
4317
|
jti,
|
|
4154
4318
|
sid: sessionId
|
|
@@ -4164,16 +4328,24 @@ function createAuth(config) {
|
|
|
4164
4328
|
sid: sessionId,
|
|
4165
4329
|
claims: userClaims || undefined,
|
|
4166
4330
|
...options?.fva !== undefined ? { fva: options.fva } : {},
|
|
4331
|
+
...options?.tenantId ? { tenantId: options.tenantId } : {},
|
|
4167
4332
|
...aclClaim
|
|
4168
4333
|
};
|
|
4169
4334
|
return { jwt, refreshToken, payload, expiresAt };
|
|
4170
4335
|
}
|
|
4336
|
+
const FORGOT_PASSWORD_MIN_RESPONSE_MS = 15;
|
|
4171
4337
|
function generateToken() {
|
|
4172
4338
|
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
4173
4339
|
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4174
4340
|
}
|
|
4341
|
+
async function waitForMinimumDuration(startedAt, minDurationMs) {
|
|
4342
|
+
const remaining = minDurationMs - (Date.now() - startedAt);
|
|
4343
|
+
if (remaining > 0) {
|
|
4344
|
+
await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4175
4347
|
async function signUp(data, ctx) {
|
|
4176
|
-
const { email, password,
|
|
4348
|
+
const { email, password, ...additionalFields } = data;
|
|
4177
4349
|
if (!email || !email.includes("@")) {
|
|
4178
4350
|
return err(createAuthValidationError2("Invalid email format", "email", "INVALID_FORMAT"));
|
|
4179
4351
|
}
|
|
@@ -4195,18 +4367,37 @@ function createAuth(config) {
|
|
|
4195
4367
|
id: _id,
|
|
4196
4368
|
createdAt: _c,
|
|
4197
4369
|
updatedAt: _u,
|
|
4370
|
+
role: _role,
|
|
4371
|
+
emailVerified: _emailVerified,
|
|
4198
4372
|
...safeFields
|
|
4199
4373
|
} = additionalFields;
|
|
4200
4374
|
const user = {
|
|
4201
|
-
...safeFields,
|
|
4202
4375
|
id: crypto.randomUUID(),
|
|
4203
4376
|
email: email.toLowerCase(),
|
|
4204
|
-
role,
|
|
4377
|
+
role: "user",
|
|
4205
4378
|
emailVerified: !emailVerificationEnabled,
|
|
4206
4379
|
createdAt: now,
|
|
4207
4380
|
updatedAt: now
|
|
4208
4381
|
};
|
|
4209
4382
|
await userStore.createUser(user, passwordHash);
|
|
4383
|
+
if (onUserCreated) {
|
|
4384
|
+
const callbackCtx = { entities: entityProxy };
|
|
4385
|
+
const payload = {
|
|
4386
|
+
user,
|
|
4387
|
+
provider: null,
|
|
4388
|
+
signUpData: { ...safeFields }
|
|
4389
|
+
};
|
|
4390
|
+
try {
|
|
4391
|
+
await onUserCreated(payload, callbackCtx);
|
|
4392
|
+
} catch (callbackErr) {
|
|
4393
|
+
try {
|
|
4394
|
+
await userStore.deleteUser(user.id);
|
|
4395
|
+
} catch (rollbackErr) {
|
|
4396
|
+
console.error("[Auth] Failed to rollback user after onUserCreated failure:", rollbackErr);
|
|
4397
|
+
}
|
|
4398
|
+
return err(createAuthValidationError2("User setup failed", "general", "CALLBACK_FAILED"));
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4210
4401
|
if (emailVerificationEnabled && emailVerificationStore && emailVerificationConfig?.onSend) {
|
|
4211
4402
|
const token = generateToken();
|
|
4212
4403
|
const tokenHash = await sha256Hex(token);
|
|
@@ -4298,6 +4489,10 @@ function createAuth(config) {
|
|
|
4298
4489
|
if (!payload) {
|
|
4299
4490
|
return ok(null);
|
|
4300
4491
|
}
|
|
4492
|
+
const storedSession = await sessionStore.findActiveSessionById(payload.sid);
|
|
4493
|
+
if (!storedSession || storedSession.userId !== payload.sub) {
|
|
4494
|
+
return ok(null);
|
|
4495
|
+
}
|
|
4301
4496
|
const user = await userStore.findById(payload.sub);
|
|
4302
4497
|
if (!user) {
|
|
4303
4498
|
return ok(null);
|
|
@@ -4380,16 +4575,16 @@ function createAuth(config) {
|
|
|
4380
4575
|
}
|
|
4381
4576
|
const currentSid = sessionResult.data.payload.sid;
|
|
4382
4577
|
const sessions = await sessionStore.listActiveSessions(sessionResult.data.user.id);
|
|
4383
|
-
const infos = sessions.map((
|
|
4384
|
-
id:
|
|
4385
|
-
userId:
|
|
4386
|
-
ipAddress:
|
|
4387
|
-
userAgent:
|
|
4388
|
-
deviceName: parseDeviceName(
|
|
4389
|
-
createdAt:
|
|
4390
|
-
lastActiveAt:
|
|
4391
|
-
expiresAt:
|
|
4392
|
-
isCurrent:
|
|
4578
|
+
const infos = sessions.map((s2) => ({
|
|
4579
|
+
id: s2.id,
|
|
4580
|
+
userId: s2.userId,
|
|
4581
|
+
ipAddress: s2.ipAddress,
|
|
4582
|
+
userAgent: s2.userAgent,
|
|
4583
|
+
deviceName: parseDeviceName(s2.userAgent),
|
|
4584
|
+
createdAt: s2.createdAt,
|
|
4585
|
+
lastActiveAt: s2.lastActiveAt,
|
|
4586
|
+
expiresAt: s2.expiresAt,
|
|
4587
|
+
isCurrent: s2.id === currentSid
|
|
4393
4588
|
}));
|
|
4394
4589
|
return ok(infos);
|
|
4395
4590
|
}
|
|
@@ -4401,7 +4596,7 @@ function createAuth(config) {
|
|
|
4401
4596
|
return err(createSessionExpiredError("Not authenticated"));
|
|
4402
4597
|
}
|
|
4403
4598
|
const targetSessions = await sessionStore.listActiveSessions(sessionResult.data.user.id);
|
|
4404
|
-
const target = targetSessions.find((
|
|
4599
|
+
const target = targetSessions.find((s2) => s2.id === sessionId);
|
|
4405
4600
|
if (!target) {
|
|
4406
4601
|
return err(createSessionNotFoundError("Session not found"));
|
|
4407
4602
|
}
|
|
@@ -4417,9 +4612,9 @@ function createAuth(config) {
|
|
|
4417
4612
|
}
|
|
4418
4613
|
const currentSid = sessionResult.data.payload.sid;
|
|
4419
4614
|
const sessions = await sessionStore.listActiveSessions(sessionResult.data.user.id);
|
|
4420
|
-
for (const
|
|
4421
|
-
if (
|
|
4422
|
-
await sessionStore.revokeSession(
|
|
4615
|
+
for (const s2 of sessions) {
|
|
4616
|
+
if (s2.id !== currentSid) {
|
|
4617
|
+
await sessionStore.revokeSession(s2.id);
|
|
4423
4618
|
}
|
|
4424
4619
|
}
|
|
4425
4620
|
return ok(undefined);
|
|
@@ -4452,6 +4647,32 @@ function createAuth(config) {
|
|
|
4452
4647
|
"Referrer-Policy": "strict-origin-when-cross-origin"
|
|
4453
4648
|
};
|
|
4454
4649
|
}
|
|
4650
|
+
function authValidationResponse(message) {
|
|
4651
|
+
return new Response(JSON.stringify({
|
|
4652
|
+
error: {
|
|
4653
|
+
code: "AUTH_VALIDATION_ERROR",
|
|
4654
|
+
message
|
|
4655
|
+
}
|
|
4656
|
+
}), {
|
|
4657
|
+
status: 400,
|
|
4658
|
+
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
4659
|
+
});
|
|
4660
|
+
}
|
|
4661
|
+
async function parseJsonAuthBody(request, schema) {
|
|
4662
|
+
try {
|
|
4663
|
+
const body = await parseBody(request);
|
|
4664
|
+
const result = schema.safeParse(body);
|
|
4665
|
+
if (!result.ok) {
|
|
4666
|
+
return { ok: false, response: authValidationResponse(result.error.message) };
|
|
4667
|
+
}
|
|
4668
|
+
return { ok: true, data: result.data };
|
|
4669
|
+
} catch (error) {
|
|
4670
|
+
if (error instanceof BadRequestException) {
|
|
4671
|
+
return { ok: false, response: authValidationResponse(error.message) };
|
|
4672
|
+
}
|
|
4673
|
+
throw error;
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4455
4676
|
async function handleAuthRequest(request) {
|
|
4456
4677
|
const url = new URL(request.url);
|
|
4457
4678
|
const path = url.pathname.replace("/api/auth", "") || "/";
|
|
@@ -4495,7 +4716,11 @@ function createAuth(config) {
|
|
|
4495
4716
|
}
|
|
4496
4717
|
try {
|
|
4497
4718
|
if (method === "POST" && path === "/signup") {
|
|
4498
|
-
const
|
|
4719
|
+
const bodyResult = await parseJsonAuthBody(request, signUpInputSchema);
|
|
4720
|
+
if (!bodyResult.ok) {
|
|
4721
|
+
return bodyResult.response;
|
|
4722
|
+
}
|
|
4723
|
+
const body = bodyResult.data;
|
|
4499
4724
|
const result = await signUp(body, { headers: request.headers });
|
|
4500
4725
|
if (!result.ok) {
|
|
4501
4726
|
return new Response(JSON.stringify({ error: result.error }), {
|
|
@@ -4520,7 +4745,11 @@ function createAuth(config) {
|
|
|
4520
4745
|
});
|
|
4521
4746
|
}
|
|
4522
4747
|
if (method === "POST" && path === "/signin") {
|
|
4523
|
-
const
|
|
4748
|
+
const bodyResult = await parseJsonAuthBody(request, signInInputSchema);
|
|
4749
|
+
if (!bodyResult.ok) {
|
|
4750
|
+
return bodyResult.response;
|
|
4751
|
+
}
|
|
4752
|
+
const body = bodyResult.data;
|
|
4524
4753
|
const result = await signIn(body, { headers: request.headers });
|
|
4525
4754
|
if (!result.ok) {
|
|
4526
4755
|
if (result.error.code === "MFA_REQUIRED" && oauthEncryptionKey) {
|
|
@@ -4609,7 +4838,8 @@ function createAuth(config) {
|
|
|
4609
4838
|
roleStore: config.access.roleStore,
|
|
4610
4839
|
closureStore: config.access.closureStore,
|
|
4611
4840
|
flagStore: config.access.flagStore,
|
|
4612
|
-
|
|
4841
|
+
subscriptionStore: config.access?.subscriptionStore,
|
|
4842
|
+
tenantId: sessionResult.data.payload?.tenantId ?? null
|
|
4613
4843
|
});
|
|
4614
4844
|
const encoded = encodeAccessSet(accessSet);
|
|
4615
4845
|
const hashPayload = {
|
|
@@ -4625,8 +4855,9 @@ function createAuth(config) {
|
|
|
4625
4855
|
status: 304,
|
|
4626
4856
|
headers: {
|
|
4627
4857
|
ETag: quotedEtag,
|
|
4858
|
+
Vary: "Cookie",
|
|
4628
4859
|
...securityHeaders(),
|
|
4629
|
-
"Cache-Control": "no-cache"
|
|
4860
|
+
"Cache-Control": "private, no-cache"
|
|
4630
4861
|
}
|
|
4631
4862
|
});
|
|
4632
4863
|
}
|
|
@@ -4635,8 +4866,9 @@ function createAuth(config) {
|
|
|
4635
4866
|
headers: {
|
|
4636
4867
|
"Content-Type": "application/json",
|
|
4637
4868
|
ETag: quotedEtag,
|
|
4869
|
+
Vary: "Cookie",
|
|
4638
4870
|
...securityHeaders(),
|
|
4639
|
-
"Cache-Control": "no-cache"
|
|
4871
|
+
"Cache-Control": "private, no-cache"
|
|
4640
4872
|
}
|
|
4641
4873
|
});
|
|
4642
4874
|
}
|
|
@@ -4710,6 +4942,17 @@ function createAuth(config) {
|
|
|
4710
4942
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
4711
4943
|
});
|
|
4712
4944
|
}
|
|
4945
|
+
if (method === "GET" && path === "/providers") {
|
|
4946
|
+
const providerList = Array.from(providers.values()).map((p) => ({
|
|
4947
|
+
id: p.id,
|
|
4948
|
+
name: p.name,
|
|
4949
|
+
authUrl: `/api/auth/oauth/${p.id}`
|
|
4950
|
+
}));
|
|
4951
|
+
return new Response(JSON.stringify(providerList), {
|
|
4952
|
+
status: 200,
|
|
4953
|
+
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
4954
|
+
});
|
|
4955
|
+
}
|
|
4713
4956
|
if (method === "GET" && path.startsWith("/oauth/") && !path.includes("/callback")) {
|
|
4714
4957
|
const providerId = path.replace("/oauth/", "");
|
|
4715
4958
|
const provider = providers.get(providerId);
|
|
@@ -4756,12 +4999,17 @@ function createAuth(config) {
|
|
|
4756
4999
|
if (method === "GET" && path.includes("/oauth/") && path.endsWith("/callback")) {
|
|
4757
5000
|
const providerId = path.replace("/oauth/", "").replace("/callback", "");
|
|
4758
5001
|
const provider = providers.get(providerId);
|
|
4759
|
-
const
|
|
5002
|
+
const isAbsoluteUrl = /^https?:\/\//.test(oauthErrorRedirect);
|
|
5003
|
+
const errorUrl = (error) => {
|
|
5004
|
+
const url2 = new URL(oauthErrorRedirect, "http://localhost");
|
|
5005
|
+
url2.searchParams.set("error", error);
|
|
5006
|
+
return isAbsoluteUrl ? url2.toString() : url2.pathname + url2.search + url2.hash;
|
|
5007
|
+
};
|
|
4760
5008
|
if (!provider || !oauthEncryptionKey || !oauthAccountStore) {
|
|
4761
5009
|
return new Response(null, {
|
|
4762
5010
|
status: 302,
|
|
4763
5011
|
headers: {
|
|
4764
|
-
Location:
|
|
5012
|
+
Location: errorUrl("provider_not_configured"),
|
|
4765
5013
|
...securityHeaders()
|
|
4766
5014
|
}
|
|
4767
5015
|
});
|
|
@@ -4772,7 +5020,7 @@ function createAuth(config) {
|
|
|
4772
5020
|
return new Response(null, {
|
|
4773
5021
|
status: 302,
|
|
4774
5022
|
headers: {
|
|
4775
|
-
Location:
|
|
5023
|
+
Location: errorUrl("invalid_state"),
|
|
4776
5024
|
...securityHeaders()
|
|
4777
5025
|
}
|
|
4778
5026
|
});
|
|
@@ -4782,7 +5030,7 @@ function createAuth(config) {
|
|
|
4782
5030
|
return new Response(null, {
|
|
4783
5031
|
status: 302,
|
|
4784
5032
|
headers: {
|
|
4785
|
-
Location:
|
|
5033
|
+
Location: errorUrl("invalid_state"),
|
|
4786
5034
|
...securityHeaders()
|
|
4787
5035
|
}
|
|
4788
5036
|
});
|
|
@@ -4793,7 +5041,7 @@ function createAuth(config) {
|
|
|
4793
5041
|
const providerError = url.searchParams.get("error");
|
|
4794
5042
|
if (providerError) {
|
|
4795
5043
|
const headers = new Headers({
|
|
4796
|
-
Location:
|
|
5044
|
+
Location: errorUrl(providerError),
|
|
4797
5045
|
...securityHeaders()
|
|
4798
5046
|
});
|
|
4799
5047
|
headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
|
|
@@ -4801,7 +5049,7 @@ function createAuth(config) {
|
|
|
4801
5049
|
}
|
|
4802
5050
|
if (stateData.state !== queryState || stateData.provider !== providerId) {
|
|
4803
5051
|
const headers = new Headers({
|
|
4804
|
-
Location:
|
|
5052
|
+
Location: errorUrl("invalid_state"),
|
|
4805
5053
|
...securityHeaders()
|
|
4806
5054
|
});
|
|
4807
5055
|
headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
|
|
@@ -4809,7 +5057,7 @@ function createAuth(config) {
|
|
|
4809
5057
|
}
|
|
4810
5058
|
if (stateData.expiresAt < Date.now()) {
|
|
4811
5059
|
const headers = new Headers({
|
|
4812
|
-
Location:
|
|
5060
|
+
Location: errorUrl("invalid_state"),
|
|
4813
5061
|
...securityHeaders()
|
|
4814
5062
|
});
|
|
4815
5063
|
headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
|
|
@@ -4836,7 +5084,7 @@ function createAuth(config) {
|
|
|
4836
5084
|
if (!userId) {
|
|
4837
5085
|
if (!userInfo.email || !userInfo.email.includes("@")) {
|
|
4838
5086
|
const headers = new Headers({
|
|
4839
|
-
Location:
|
|
5087
|
+
Location: errorUrl("email_required"),
|
|
4840
5088
|
...securityHeaders()
|
|
4841
5089
|
});
|
|
4842
5090
|
headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
|
|
@@ -4846,6 +5094,7 @@ function createAuth(config) {
|
|
|
4846
5094
|
const newUser = {
|
|
4847
5095
|
id: crypto.randomUUID(),
|
|
4848
5096
|
email: userInfo.email.toLowerCase(),
|
|
5097
|
+
emailVerified: userInfo.emailVerified,
|
|
4849
5098
|
role: "user",
|
|
4850
5099
|
createdAt: now,
|
|
4851
5100
|
updatedAt: now
|
|
@@ -4853,6 +5102,34 @@ function createAuth(config) {
|
|
|
4853
5102
|
await userStore.createUser(newUser, null);
|
|
4854
5103
|
userId = newUser.id;
|
|
4855
5104
|
await oauthAccountStore.linkAccount(userId, provider.id, userInfo.providerId, userInfo.email);
|
|
5105
|
+
if (onUserCreated) {
|
|
5106
|
+
const callbackCtx = { entities: entityProxy };
|
|
5107
|
+
const payload = {
|
|
5108
|
+
user: newUser,
|
|
5109
|
+
provider: { id: provider.id, name: provider.name },
|
|
5110
|
+
profile: userInfo.raw
|
|
5111
|
+
};
|
|
5112
|
+
try {
|
|
5113
|
+
await onUserCreated(payload, callbackCtx);
|
|
5114
|
+
} catch {
|
|
5115
|
+
try {
|
|
5116
|
+
await oauthAccountStore.unlinkAccount(userId, provider.id);
|
|
5117
|
+
} catch (rollbackErr) {
|
|
5118
|
+
console.error("[Auth] Failed to unlink OAuth account during rollback:", rollbackErr);
|
|
5119
|
+
}
|
|
5120
|
+
try {
|
|
5121
|
+
await userStore.deleteUser(userId);
|
|
5122
|
+
} catch (rollbackErr) {
|
|
5123
|
+
console.error("[Auth] Failed to delete user during rollback:", rollbackErr);
|
|
5124
|
+
}
|
|
5125
|
+
const headers = new Headers({
|
|
5126
|
+
Location: errorUrl("user_setup_failed"),
|
|
5127
|
+
...securityHeaders()
|
|
5128
|
+
});
|
|
5129
|
+
headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
|
|
5130
|
+
return new Response(null, { status: 302, headers });
|
|
5131
|
+
}
|
|
5132
|
+
}
|
|
4856
5133
|
}
|
|
4857
5134
|
}
|
|
4858
5135
|
const user = await userStore.findById(userId);
|
|
@@ -4860,7 +5137,7 @@ function createAuth(config) {
|
|
|
4860
5137
|
return new Response(null, {
|
|
4861
5138
|
status: 302,
|
|
4862
5139
|
headers: {
|
|
4863
|
-
Location:
|
|
5140
|
+
Location: errorUrl("user_info_failed"),
|
|
4864
5141
|
...securityHeaders()
|
|
4865
5142
|
}
|
|
4866
5143
|
});
|
|
@@ -4881,9 +5158,10 @@ function createAuth(config) {
|
|
|
4881
5158
|
responseHeaders.append("Set-Cookie", buildSessionCookie(sessionTokens.jwt, cookieConfig));
|
|
4882
5159
|
responseHeaders.append("Set-Cookie", buildRefreshCookie(sessionTokens.refreshToken, cookieConfig, refreshName, refreshMaxAge));
|
|
4883
5160
|
return new Response(null, { status: 302, headers: responseHeaders });
|
|
4884
|
-
} catch {
|
|
5161
|
+
} catch (oauthErr) {
|
|
5162
|
+
console.error("[Auth] OAuth callback error:", oauthErr);
|
|
4885
5163
|
const headers = new Headers({
|
|
4886
|
-
Location:
|
|
5164
|
+
Location: errorUrl("token_exchange_failed"),
|
|
4887
5165
|
...securityHeaders()
|
|
4888
5166
|
});
|
|
4889
5167
|
headers.append("Set-Cookie", buildOAuthStateCookie("", cookieConfig, true));
|
|
@@ -4933,7 +5211,11 @@ function createAuth(config) {
|
|
|
4933
5211
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
4934
5212
|
});
|
|
4935
5213
|
}
|
|
4936
|
-
const
|
|
5214
|
+
const bodyResult = await parseJsonAuthBody(request, codeInputSchema);
|
|
5215
|
+
if (!bodyResult.ok) {
|
|
5216
|
+
return bodyResult.response;
|
|
5217
|
+
}
|
|
5218
|
+
const body = bodyResult.data;
|
|
4937
5219
|
const userId = challengeData.userId;
|
|
4938
5220
|
const encryptedSecret = await mfaStore.getSecret(userId);
|
|
4939
5221
|
if (!encryptedSecret) {
|
|
@@ -5053,7 +5335,11 @@ function createAuth(config) {
|
|
|
5053
5335
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5054
5336
|
});
|
|
5055
5337
|
}
|
|
5056
|
-
const
|
|
5338
|
+
const bodyResult = await parseJsonAuthBody(request, codeInputSchema);
|
|
5339
|
+
if (!bodyResult.ok) {
|
|
5340
|
+
return bodyResult.response;
|
|
5341
|
+
}
|
|
5342
|
+
const body = bodyResult.data;
|
|
5057
5343
|
const valid = await verifyTotpCode(pendingEntry.secret, body.code);
|
|
5058
5344
|
if (!valid) {
|
|
5059
5345
|
return new Response(JSON.stringify({ error: createMfaInvalidCodeError() }), {
|
|
@@ -5087,7 +5373,11 @@ function createAuth(config) {
|
|
|
5087
5373
|
});
|
|
5088
5374
|
}
|
|
5089
5375
|
const userId = sessionResult.data.user.id;
|
|
5090
|
-
const
|
|
5376
|
+
const bodyResult = await parseJsonAuthBody(request, passwordInputSchema);
|
|
5377
|
+
if (!bodyResult.ok) {
|
|
5378
|
+
return bodyResult.response;
|
|
5379
|
+
}
|
|
5380
|
+
const body = bodyResult.data;
|
|
5091
5381
|
const stored = await userStore.findByEmail(sessionResult.data.user.email);
|
|
5092
5382
|
if (!stored || !stored.passwordHash) {
|
|
5093
5383
|
return new Response(JSON.stringify({ error: createInvalidCredentialsError() }), {
|
|
@@ -5122,7 +5412,11 @@ function createAuth(config) {
|
|
|
5122
5412
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5123
5413
|
});
|
|
5124
5414
|
}
|
|
5125
|
-
const
|
|
5415
|
+
const bodyResult = await parseJsonAuthBody(request, passwordInputSchema);
|
|
5416
|
+
if (!bodyResult.ok) {
|
|
5417
|
+
return bodyResult.response;
|
|
5418
|
+
}
|
|
5419
|
+
const body = bodyResult.data;
|
|
5126
5420
|
const stored = await userStore.findByEmail(sessionResult.data.user.email);
|
|
5127
5421
|
if (!stored || !stored.passwordHash) {
|
|
5128
5422
|
return new Response(JSON.stringify({ error: createInvalidCredentialsError() }), {
|
|
@@ -5209,7 +5503,11 @@ function createAuth(config) {
|
|
|
5209
5503
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5210
5504
|
});
|
|
5211
5505
|
}
|
|
5212
|
-
const
|
|
5506
|
+
const bodyResult = await parseJsonAuthBody(request, codeInputSchema);
|
|
5507
|
+
if (!bodyResult.ok) {
|
|
5508
|
+
return bodyResult.response;
|
|
5509
|
+
}
|
|
5510
|
+
const body = bodyResult.data;
|
|
5213
5511
|
const valid = await verifyTotpCode(totpSecret, body.code);
|
|
5214
5512
|
if (!valid) {
|
|
5215
5513
|
return new Response(JSON.stringify({ error: createMfaInvalidCodeError() }), {
|
|
@@ -5253,7 +5551,11 @@ function createAuth(config) {
|
|
|
5253
5551
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5254
5552
|
});
|
|
5255
5553
|
}
|
|
5256
|
-
const
|
|
5554
|
+
const bodyResult = await parseJsonAuthBody(request, tokenInputSchema);
|
|
5555
|
+
if (!bodyResult.ok) {
|
|
5556
|
+
return bodyResult.response;
|
|
5557
|
+
}
|
|
5558
|
+
const body = bodyResult.data;
|
|
5257
5559
|
if (!body.token) {
|
|
5258
5560
|
return new Response(JSON.stringify({ error: createTokenInvalidError("Token is required") }), {
|
|
5259
5561
|
status: 400,
|
|
@@ -5332,25 +5634,32 @@ function createAuth(config) {
|
|
|
5332
5634
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5333
5635
|
});
|
|
5334
5636
|
}
|
|
5335
|
-
const
|
|
5637
|
+
const bodyResult = await parseJsonAuthBody(request, forgotPasswordInputSchema);
|
|
5638
|
+
if (!bodyResult.ok) {
|
|
5639
|
+
return bodyResult.response;
|
|
5640
|
+
}
|
|
5641
|
+
const body = bodyResult.data;
|
|
5642
|
+
const startedAt = Date.now();
|
|
5336
5643
|
const forgotRateLimit = await rateLimitStore.check(`forgot-password:${(body.email ?? "").toLowerCase()}`, 3, parseDuration("1h"));
|
|
5337
5644
|
if (!forgotRateLimit.allowed) {
|
|
5645
|
+
await waitForMinimumDuration(startedAt, FORGOT_PASSWORD_MIN_RESPONSE_MS);
|
|
5338
5646
|
return new Response(JSON.stringify({ ok: true }), {
|
|
5339
5647
|
status: 200,
|
|
5340
5648
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5341
5649
|
});
|
|
5342
5650
|
}
|
|
5651
|
+
const token = generateToken();
|
|
5652
|
+
const tokenHash = await sha256Hex(token);
|
|
5343
5653
|
const stored = await userStore.findByEmail((body.email ?? "").toLowerCase());
|
|
5344
5654
|
if (stored) {
|
|
5345
|
-
const token = generateToken();
|
|
5346
|
-
const tokenHash = await sha256Hex(token);
|
|
5347
5655
|
await passwordResetStore.createReset({
|
|
5348
5656
|
userId: stored.user.id,
|
|
5349
5657
|
tokenHash,
|
|
5350
5658
|
expiresAt: new Date(Date.now() + passwordResetTtlMs)
|
|
5351
5659
|
});
|
|
5352
|
-
|
|
5660
|
+
passwordResetConfig.onSend(stored.user, token).catch((error) => console.error("[Auth] Failed to send password reset email", error));
|
|
5353
5661
|
}
|
|
5662
|
+
await waitForMinimumDuration(startedAt, FORGOT_PASSWORD_MIN_RESPONSE_MS);
|
|
5354
5663
|
return new Response(JSON.stringify({ ok: true }), {
|
|
5355
5664
|
status: 200,
|
|
5356
5665
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
@@ -5365,7 +5674,11 @@ function createAuth(config) {
|
|
|
5365
5674
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5366
5675
|
});
|
|
5367
5676
|
}
|
|
5368
|
-
const
|
|
5677
|
+
const bodyResult = await parseJsonAuthBody(request, resetPasswordInputSchema);
|
|
5678
|
+
if (!bodyResult.ok) {
|
|
5679
|
+
return bodyResult.response;
|
|
5680
|
+
}
|
|
5681
|
+
const body = bodyResult.data;
|
|
5369
5682
|
if (!body.token) {
|
|
5370
5683
|
return new Response(JSON.stringify({ error: createTokenInvalidError("Token is required") }), {
|
|
5371
5684
|
status: 400,
|
|
@@ -5399,8 +5712,8 @@ function createAuth(config) {
|
|
|
5399
5712
|
await passwordResetStore.deleteByUserId(resetRecord.userId);
|
|
5400
5713
|
if (revokeSessionsOnReset) {
|
|
5401
5714
|
const activeSessions = await sessionStore.listActiveSessions(resetRecord.userId);
|
|
5402
|
-
for (const
|
|
5403
|
-
await sessionStore.revokeSession(
|
|
5715
|
+
for (const s2 of activeSessions) {
|
|
5716
|
+
await sessionStore.revokeSession(s2.id);
|
|
5404
5717
|
}
|
|
5405
5718
|
}
|
|
5406
5719
|
return new Response(JSON.stringify({ ok: true }), {
|
|
@@ -5408,6 +5721,63 @@ function createAuth(config) {
|
|
|
5408
5721
|
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5409
5722
|
});
|
|
5410
5723
|
}
|
|
5724
|
+
if (method === "POST" && path === "/switch-tenant") {
|
|
5725
|
+
if (!tenantConfig) {
|
|
5726
|
+
return new Response(JSON.stringify({ error: "Not found" }), {
|
|
5727
|
+
status: 404,
|
|
5728
|
+
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5729
|
+
});
|
|
5730
|
+
}
|
|
5731
|
+
const sessionResult = await getSession(request.headers);
|
|
5732
|
+
if (!sessionResult.ok || !sessionResult.data) {
|
|
5733
|
+
return new Response(JSON.stringify({ error: { code: "SESSION_EXPIRED", message: "Not authenticated" } }), {
|
|
5734
|
+
status: 401,
|
|
5735
|
+
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5736
|
+
});
|
|
5737
|
+
}
|
|
5738
|
+
const bodyResult = await parseJsonAuthBody(request, switchTenantInputSchema);
|
|
5739
|
+
if (!bodyResult.ok) {
|
|
5740
|
+
return bodyResult.response;
|
|
5741
|
+
}
|
|
5742
|
+
const { tenantId } = bodyResult.data;
|
|
5743
|
+
const userId = sessionResult.data.user.id;
|
|
5744
|
+
const hasMembership = await tenantConfig.verifyMembership(userId, tenantId);
|
|
5745
|
+
if (!hasMembership) {
|
|
5746
|
+
return new Response(JSON.stringify({
|
|
5747
|
+
error: { code: "AUTH_FORBIDDEN", message: "Not a member of this tenant" }
|
|
5748
|
+
}), {
|
|
5749
|
+
status: 403,
|
|
5750
|
+
headers: { "Content-Type": "application/json", ...securityHeaders() }
|
|
5751
|
+
});
|
|
5752
|
+
}
|
|
5753
|
+
const currentPayload = sessionResult.data.payload;
|
|
5754
|
+
const tokens = await createSessionTokens(sessionResult.data.user, currentPayload.sid, {
|
|
5755
|
+
fva: currentPayload.fva,
|
|
5756
|
+
tenantId
|
|
5757
|
+
});
|
|
5758
|
+
const refreshTokenHash = await sha256Hex(tokens.refreshToken);
|
|
5759
|
+
const previousRefreshHash = (await sessionStore.findActiveSessionById(currentPayload.sid))?.refreshTokenHash ?? "";
|
|
5760
|
+
await sessionStore.updateSession(currentPayload.sid, {
|
|
5761
|
+
refreshTokenHash,
|
|
5762
|
+
previousRefreshHash,
|
|
5763
|
+
lastActiveAt: new Date,
|
|
5764
|
+
currentTokens: { jwt: tokens.jwt, refreshToken: tokens.refreshToken }
|
|
5765
|
+
});
|
|
5766
|
+
const headers = new Headers({
|
|
5767
|
+
"Content-Type": "application/json",
|
|
5768
|
+
...securityHeaders()
|
|
5769
|
+
});
|
|
5770
|
+
headers.append("Set-Cookie", buildSessionCookie(tokens.jwt, cookieConfig));
|
|
5771
|
+
headers.append("Set-Cookie", buildRefreshCookie(tokens.refreshToken, cookieConfig, refreshName, refreshMaxAge));
|
|
5772
|
+
return new Response(JSON.stringify({
|
|
5773
|
+
tenantId,
|
|
5774
|
+
user: sessionResult.data.user,
|
|
5775
|
+
expiresAt: tokens.expiresAt.getTime()
|
|
5776
|
+
}), {
|
|
5777
|
+
status: 200,
|
|
5778
|
+
headers
|
|
5779
|
+
});
|
|
5780
|
+
}
|
|
5411
5781
|
return new Response(JSON.stringify({ error: "Not found" }), {
|
|
5412
5782
|
status: 404,
|
|
5413
5783
|
headers: { "Content-Type": "application/json" }
|
|
@@ -5458,9 +5828,49 @@ function createAuth(config) {
|
|
|
5458
5828
|
emailVerificationStore?.dispose();
|
|
5459
5829
|
passwordResetStore?.dispose();
|
|
5460
5830
|
pendingMfaSecrets.clear();
|
|
5831
|
+
},
|
|
5832
|
+
resolveSessionForSSR: resolveSessionForSSR({
|
|
5833
|
+
jwtSecret,
|
|
5834
|
+
jwtAlgorithm,
|
|
5835
|
+
cookieName: cookieConfig.name || "vertz.sid"
|
|
5836
|
+
})
|
|
5837
|
+
};
|
|
5838
|
+
}
|
|
5839
|
+
// src/content/builders.ts
|
|
5840
|
+
function stringDescriptor(contentType) {
|
|
5841
|
+
return {
|
|
5842
|
+
_kind: "content",
|
|
5843
|
+
_contentType: contentType,
|
|
5844
|
+
parse(value) {
|
|
5845
|
+
if (typeof value === "string") {
|
|
5846
|
+
return { ok: true, data: value };
|
|
5847
|
+
}
|
|
5848
|
+
return { ok: false, error: new Error(`Expected string, got ${typeof value}`) };
|
|
5461
5849
|
}
|
|
5462
5850
|
};
|
|
5463
5851
|
}
|
|
5852
|
+
var content = {
|
|
5853
|
+
xml: () => stringDescriptor("application/xml"),
|
|
5854
|
+
html: () => stringDescriptor("text/html"),
|
|
5855
|
+
text: () => stringDescriptor("text/plain"),
|
|
5856
|
+
binary: () => ({
|
|
5857
|
+
_kind: "content",
|
|
5858
|
+
_contentType: "application/octet-stream",
|
|
5859
|
+
parse(value) {
|
|
5860
|
+
if (value instanceof Uint8Array) {
|
|
5861
|
+
return { ok: true, data: value };
|
|
5862
|
+
}
|
|
5863
|
+
return {
|
|
5864
|
+
ok: false,
|
|
5865
|
+
error: new Error(`Expected Uint8Array, got ${typeof value}`)
|
|
5866
|
+
};
|
|
5867
|
+
}
|
|
5868
|
+
})
|
|
5869
|
+
};
|
|
5870
|
+
// src/content/content-descriptor.ts
|
|
5871
|
+
function isContentDescriptor(value) {
|
|
5872
|
+
return value != null && typeof value === "object" && "_kind" in value && value._kind === "content";
|
|
5873
|
+
}
|
|
5464
5874
|
// src/create-server.ts
|
|
5465
5875
|
import { createServer as coreCreateServer } from "@vertz/core";
|
|
5466
5876
|
import {
|
|
@@ -5541,10 +5951,15 @@ function narrowRelationFields(relationsConfig, data) {
|
|
|
5541
5951
|
if (config === undefined || config === true) {
|
|
5542
5952
|
result[key] = value;
|
|
5543
5953
|
} else if (config === false) {} else if (typeof config === "object" && value !== null && typeof value === "object") {
|
|
5954
|
+
const selectMap = config.select;
|
|
5955
|
+
if (!selectMap) {
|
|
5956
|
+
result[key] = value;
|
|
5957
|
+
continue;
|
|
5958
|
+
}
|
|
5544
5959
|
if (Array.isArray(value)) {
|
|
5545
5960
|
result[key] = value.map((item) => {
|
|
5546
5961
|
const narrowed = {};
|
|
5547
|
-
for (const field of Object.keys(
|
|
5962
|
+
for (const field of Object.keys(selectMap)) {
|
|
5548
5963
|
if (field in item) {
|
|
5549
5964
|
narrowed[field] = item[field];
|
|
5550
5965
|
}
|
|
@@ -5553,7 +5968,7 @@ function narrowRelationFields(relationsConfig, data) {
|
|
|
5553
5968
|
});
|
|
5554
5969
|
} else {
|
|
5555
5970
|
const narrowed = {};
|
|
5556
|
-
for (const field of Object.keys(
|
|
5971
|
+
for (const field of Object.keys(selectMap)) {
|
|
5557
5972
|
if (field in value) {
|
|
5558
5973
|
narrowed[field] = value[field];
|
|
5559
5974
|
}
|
|
@@ -5606,7 +6021,138 @@ import {
|
|
|
5606
6021
|
|
|
5607
6022
|
// src/entity/access-enforcer.ts
|
|
5608
6023
|
import { EntityForbiddenError, err as err2, ok as ok2 } from "@vertz/errors";
|
|
5609
|
-
|
|
6024
|
+
function deny(operation) {
|
|
6025
|
+
return err2(new EntityForbiddenError(`Access denied for operation "${operation}"`));
|
|
6026
|
+
}
|
|
6027
|
+
function isUserMarker(value) {
|
|
6028
|
+
return typeof value === "object" && value !== null && "__marker" in value;
|
|
6029
|
+
}
|
|
6030
|
+
function resolveMarker(marker, ctx) {
|
|
6031
|
+
switch (marker.__marker) {
|
|
6032
|
+
case "user.id":
|
|
6033
|
+
return ctx.userId;
|
|
6034
|
+
case "user.tenantId":
|
|
6035
|
+
return ctx.tenantId;
|
|
6036
|
+
default:
|
|
6037
|
+
return;
|
|
6038
|
+
}
|
|
6039
|
+
}
|
|
6040
|
+
async function evaluateRule(rule, operation, ctx, row, options) {
|
|
6041
|
+
switch (rule.type) {
|
|
6042
|
+
case "public":
|
|
6043
|
+
return ok2(undefined);
|
|
6044
|
+
case "authenticated":
|
|
6045
|
+
return ctx.authenticated() ? ok2(undefined) : deny(operation);
|
|
6046
|
+
case "role":
|
|
6047
|
+
return ctx.role(...rule.roles) ? ok2(undefined) : deny(operation);
|
|
6048
|
+
case "entitlement": {
|
|
6049
|
+
if (!options.can) {
|
|
6050
|
+
return deny(operation);
|
|
6051
|
+
}
|
|
6052
|
+
const allowed = await options.can(rule.entitlement);
|
|
6053
|
+
return allowed ? ok2(undefined) : deny(operation);
|
|
6054
|
+
}
|
|
6055
|
+
case "where": {
|
|
6056
|
+
for (const [key, expected] of Object.entries(rule.conditions)) {
|
|
6057
|
+
const resolved = isUserMarker(expected) ? resolveMarker(expected, ctx) : expected;
|
|
6058
|
+
if (row[key] !== resolved) {
|
|
6059
|
+
return deny(operation);
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6062
|
+
return ok2(undefined);
|
|
6063
|
+
}
|
|
6064
|
+
case "all": {
|
|
6065
|
+
for (const sub of rule.rules) {
|
|
6066
|
+
const result = await evaluateDescriptor(sub, operation, ctx, row, options);
|
|
6067
|
+
if (!result.ok)
|
|
6068
|
+
return result;
|
|
6069
|
+
}
|
|
6070
|
+
return ok2(undefined);
|
|
6071
|
+
}
|
|
6072
|
+
case "any": {
|
|
6073
|
+
for (const sub of rule.rules) {
|
|
6074
|
+
const result = await evaluateDescriptor(sub, operation, ctx, row, options);
|
|
6075
|
+
if (result.ok)
|
|
6076
|
+
return result;
|
|
6077
|
+
}
|
|
6078
|
+
return deny(operation);
|
|
6079
|
+
}
|
|
6080
|
+
case "fva": {
|
|
6081
|
+
if (options.fvaAge === undefined) {
|
|
6082
|
+
return deny(operation);
|
|
6083
|
+
}
|
|
6084
|
+
return options.fvaAge <= rule.maxAge ? ok2(undefined) : deny(operation);
|
|
6085
|
+
}
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
async function evaluateDescriptor(rule, operation, ctx, row, options) {
|
|
6089
|
+
if (typeof rule === "function") {
|
|
6090
|
+
const allowed = await rule(ctx, row);
|
|
6091
|
+
return allowed ? ok2(undefined) : deny(operation);
|
|
6092
|
+
}
|
|
6093
|
+
return evaluateRule(rule, operation, ctx, row, options);
|
|
6094
|
+
}
|
|
6095
|
+
async function evaluateRuleSkipWhere(rule, operation, ctx, row, options) {
|
|
6096
|
+
if (rule.type === "where") {
|
|
6097
|
+
return ok2(undefined);
|
|
6098
|
+
}
|
|
6099
|
+
if (rule.type === "all") {
|
|
6100
|
+
for (const sub of rule.rules) {
|
|
6101
|
+
const result = await evaluateDescriptorSkipWhere(sub, operation, ctx, row, options);
|
|
6102
|
+
if (!result.ok)
|
|
6103
|
+
return result;
|
|
6104
|
+
}
|
|
6105
|
+
return ok2(undefined);
|
|
6106
|
+
}
|
|
6107
|
+
if (rule.type === "any") {
|
|
6108
|
+
for (const sub of rule.rules) {
|
|
6109
|
+
const result = await evaluateDescriptorSkipWhere(sub, operation, ctx, row, options);
|
|
6110
|
+
if (result.ok)
|
|
6111
|
+
return result;
|
|
6112
|
+
}
|
|
6113
|
+
return deny(operation);
|
|
6114
|
+
}
|
|
6115
|
+
return evaluateRule(rule, operation, ctx, row, options);
|
|
6116
|
+
}
|
|
6117
|
+
async function evaluateDescriptorSkipWhere(rule, operation, ctx, row, options) {
|
|
6118
|
+
if (typeof rule === "function") {
|
|
6119
|
+
const allowed = await rule(ctx, row);
|
|
6120
|
+
return allowed ? ok2(undefined) : deny(operation);
|
|
6121
|
+
}
|
|
6122
|
+
return evaluateRuleSkipWhere(rule, operation, ctx, row, options);
|
|
6123
|
+
}
|
|
6124
|
+
function extractFromDescriptor(rule, ctx) {
|
|
6125
|
+
if (typeof rule === "function")
|
|
6126
|
+
return null;
|
|
6127
|
+
switch (rule.type) {
|
|
6128
|
+
case "where": {
|
|
6129
|
+
const resolved = {};
|
|
6130
|
+
for (const [key, value] of Object.entries(rule.conditions)) {
|
|
6131
|
+
resolved[key] = isUserMarker(value) ? resolveMarker(value, ctx) : value;
|
|
6132
|
+
}
|
|
6133
|
+
return resolved;
|
|
6134
|
+
}
|
|
6135
|
+
case "all": {
|
|
6136
|
+
let merged = null;
|
|
6137
|
+
for (const sub of rule.rules) {
|
|
6138
|
+
const extracted = extractFromDescriptor(sub, ctx);
|
|
6139
|
+
if (extracted) {
|
|
6140
|
+
merged = { ...merged ?? {}, ...extracted };
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
6143
|
+
return merged;
|
|
6144
|
+
}
|
|
6145
|
+
default:
|
|
6146
|
+
return null;
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
function extractWhereConditions(operation, accessRules, ctx) {
|
|
6150
|
+
const rule = accessRules[operation];
|
|
6151
|
+
if (rule === undefined || rule === false)
|
|
6152
|
+
return null;
|
|
6153
|
+
return extractFromDescriptor(rule, ctx);
|
|
6154
|
+
}
|
|
6155
|
+
async function enforceAccess(operation, accessRules, ctx, row, options) {
|
|
5610
6156
|
const rule = accessRules[operation];
|
|
5611
6157
|
if (rule === undefined) {
|
|
5612
6158
|
return err2(new EntityForbiddenError(`Access denied: no access rule for operation "${operation}"`));
|
|
@@ -5614,14 +6160,10 @@ async function enforceAccess(operation, accessRules, ctx, row) {
|
|
|
5614
6160
|
if (rule === false) {
|
|
5615
6161
|
return err2(new EntityForbiddenError(`Operation "${operation}" is disabled`));
|
|
5616
6162
|
}
|
|
5617
|
-
if (
|
|
5618
|
-
|
|
5619
|
-
if (!allowed) {
|
|
5620
|
-
return err2(new EntityForbiddenError(`Access denied for operation "${operation}"`));
|
|
5621
|
-
}
|
|
5622
|
-
return ok2(undefined);
|
|
6163
|
+
if (options?.skipWhere) {
|
|
6164
|
+
return evaluateDescriptorSkipWhere(rule, operation, ctx, row ?? {}, options);
|
|
5623
6165
|
}
|
|
5624
|
-
return
|
|
6166
|
+
return evaluateDescriptor(rule, operation, ctx, row ?? {}, options ?? {});
|
|
5625
6167
|
}
|
|
5626
6168
|
|
|
5627
6169
|
// src/entity/action-pipeline.ts
|
|
@@ -5663,6 +6205,7 @@ function createEntityContext(request, entityOps, registryProxy) {
|
|
|
5663
6205
|
const tenantId = request.tenantId ?? null;
|
|
5664
6206
|
return {
|
|
5665
6207
|
userId,
|
|
6208
|
+
tenantId,
|
|
5666
6209
|
authenticated() {
|
|
5667
6210
|
return userId !== null;
|
|
5668
6211
|
},
|
|
@@ -5678,7 +6221,12 @@ function createEntityContext(request, entityOps, registryProxy) {
|
|
|
5678
6221
|
}
|
|
5679
6222
|
|
|
5680
6223
|
// src/entity/crud-pipeline.ts
|
|
5681
|
-
import {
|
|
6224
|
+
import {
|
|
6225
|
+
EntityForbiddenError as EntityForbiddenError2,
|
|
6226
|
+
EntityNotFoundError as EntityNotFoundError2,
|
|
6227
|
+
err as err4,
|
|
6228
|
+
ok as ok4
|
|
6229
|
+
} from "@vertz/errors";
|
|
5682
6230
|
function resolvePrimaryKeyColumn(table) {
|
|
5683
6231
|
for (const key of Object.keys(table._columns)) {
|
|
5684
6232
|
const col = table._columns[key];
|
|
@@ -5687,20 +6235,82 @@ function resolvePrimaryKeyColumn(table) {
|
|
|
5687
6235
|
}
|
|
5688
6236
|
return "id";
|
|
5689
6237
|
}
|
|
5690
|
-
function createCrudHandlers(def, db) {
|
|
6238
|
+
function createCrudHandlers(def, db, options) {
|
|
5691
6239
|
const table = def.model.table;
|
|
6240
|
+
const isTenantScoped = def.tenantScoped;
|
|
6241
|
+
const tenantChain = options?.tenantChain ?? def.tenantChain ?? null;
|
|
6242
|
+
const isIndirectlyScoped = tenantChain !== null;
|
|
6243
|
+
const queryParentIds = options?.queryParentIds ?? null;
|
|
6244
|
+
function notFound(id) {
|
|
6245
|
+
return err4(new EntityNotFoundError2(`${def.name} with id "${id}" not found`));
|
|
6246
|
+
}
|
|
6247
|
+
function isSameTenant(ctx, row) {
|
|
6248
|
+
if (!isTenantScoped)
|
|
6249
|
+
return true;
|
|
6250
|
+
if (isIndirectlyScoped)
|
|
6251
|
+
return true;
|
|
6252
|
+
return row.tenantId === ctx.tenantId;
|
|
6253
|
+
}
|
|
6254
|
+
function withTenantFilter(ctx, where) {
|
|
6255
|
+
if (!isTenantScoped)
|
|
6256
|
+
return where;
|
|
6257
|
+
if (isIndirectlyScoped)
|
|
6258
|
+
return where;
|
|
6259
|
+
return { ...where, tenantId: ctx.tenantId };
|
|
6260
|
+
}
|
|
6261
|
+
async function resolveIndirectTenantWhere(ctx) {
|
|
6262
|
+
if (!isIndirectlyScoped || !tenantChain || !queryParentIds)
|
|
6263
|
+
return null;
|
|
6264
|
+
if (!ctx.tenantId) {
|
|
6265
|
+
return { [tenantChain.hops[0].foreignKey]: { in: [] } };
|
|
6266
|
+
}
|
|
6267
|
+
const { hops, tenantColumn } = tenantChain;
|
|
6268
|
+
const lastHop = hops[hops.length - 1];
|
|
6269
|
+
let currentIds = await queryParentIds(lastHop.tableName, { [tenantColumn]: ctx.tenantId });
|
|
6270
|
+
for (let i = hops.length - 2;i >= 0; i--) {
|
|
6271
|
+
if (currentIds.length === 0)
|
|
6272
|
+
break;
|
|
6273
|
+
const hop = hops[i];
|
|
6274
|
+
const nextHop = hops[i + 1];
|
|
6275
|
+
currentIds = await queryParentIds(hop.tableName, {
|
|
6276
|
+
[nextHop.foreignKey]: { in: currentIds }
|
|
6277
|
+
});
|
|
6278
|
+
}
|
|
6279
|
+
return { [hops[0].foreignKey]: { in: currentIds } };
|
|
6280
|
+
}
|
|
6281
|
+
async function verifyIndirectTenantOwnership(ctx, row) {
|
|
6282
|
+
if (!isIndirectlyScoped || !tenantChain || !queryParentIds)
|
|
6283
|
+
return true;
|
|
6284
|
+
if (!ctx.tenantId)
|
|
6285
|
+
return false;
|
|
6286
|
+
const indirectWhere = await resolveIndirectTenantWhere(ctx);
|
|
6287
|
+
if (!indirectWhere)
|
|
6288
|
+
return false;
|
|
6289
|
+
const firstHopFK = tenantChain.hops[0].foreignKey;
|
|
6290
|
+
const allowed = indirectWhere[firstHopFK];
|
|
6291
|
+
if (!allowed)
|
|
6292
|
+
return false;
|
|
6293
|
+
return allowed.in.includes(row[firstHopFK]);
|
|
6294
|
+
}
|
|
5692
6295
|
return {
|
|
5693
|
-
async list(ctx,
|
|
5694
|
-
const
|
|
6296
|
+
async list(ctx, options2) {
|
|
6297
|
+
const accessWhere = extractWhereConditions("list", def.access, ctx);
|
|
6298
|
+
const accessResult = await enforceAccess("list", def.access, ctx, undefined, {
|
|
6299
|
+
skipWhere: accessWhere !== null
|
|
6300
|
+
});
|
|
5695
6301
|
if (!accessResult.ok)
|
|
5696
6302
|
return err4(accessResult.error);
|
|
5697
|
-
const rawWhere =
|
|
6303
|
+
const rawWhere = options2?.where;
|
|
5698
6304
|
const safeWhere = rawWhere ? stripHiddenFields(table, rawWhere) : undefined;
|
|
5699
|
-
const
|
|
5700
|
-
const
|
|
5701
|
-
const
|
|
5702
|
-
const
|
|
5703
|
-
const
|
|
6305
|
+
const cleanWhere = safeWhere && Object.keys(safeWhere).length > 0 ? safeWhere : undefined;
|
|
6306
|
+
const directWhere = withTenantFilter(ctx, { ...accessWhere, ...cleanWhere });
|
|
6307
|
+
const indirectWhere = await resolveIndirectTenantWhere(ctx);
|
|
6308
|
+
const where = indirectWhere ? { ...directWhere, ...indirectWhere } : directWhere;
|
|
6309
|
+
const limit = Math.max(0, options2?.limit ?? 20);
|
|
6310
|
+
const after = options2?.after && options2.after.length <= 512 ? options2.after : undefined;
|
|
6311
|
+
const orderBy = options2?.orderBy;
|
|
6312
|
+
const include = options2?.include;
|
|
6313
|
+
const { data: rows, total } = await db.list({ where, orderBy, limit, after, include });
|
|
5704
6314
|
const data = rows.map((row) => narrowRelationFields(def.relations, stripHiddenFields(table, row)));
|
|
5705
6315
|
const pkColumn = resolvePrimaryKeyColumn(table);
|
|
5706
6316
|
const lastRow = rows[rows.length - 1];
|
|
@@ -5708,10 +6318,15 @@ function createCrudHandlers(def, db) {
|
|
|
5708
6318
|
const hasNextPage = nextCursor !== null;
|
|
5709
6319
|
return ok4({ status: 200, body: { items: data, total, limit, nextCursor, hasNextPage } });
|
|
5710
6320
|
},
|
|
5711
|
-
async get(ctx, id) {
|
|
5712
|
-
const
|
|
5713
|
-
|
|
5714
|
-
|
|
6321
|
+
async get(ctx, id, options2) {
|
|
6322
|
+
const getOptions = options2?.include ? { include: options2.include } : undefined;
|
|
6323
|
+
const row = await db.get(id, getOptions);
|
|
6324
|
+
if (!row)
|
|
6325
|
+
return notFound(id);
|
|
6326
|
+
if (!isSameTenant(ctx, row))
|
|
6327
|
+
return notFound(id);
|
|
6328
|
+
if (isIndirectlyScoped && !await verifyIndirectTenantOwnership(ctx, row)) {
|
|
6329
|
+
return notFound(id);
|
|
5715
6330
|
}
|
|
5716
6331
|
const accessResult = await enforceAccess("get", def.access, ctx, row);
|
|
5717
6332
|
if (!accessResult.ok)
|
|
@@ -5726,6 +6341,29 @@ function createCrudHandlers(def, db) {
|
|
|
5726
6341
|
if (!accessResult.ok)
|
|
5727
6342
|
return err4(accessResult.error);
|
|
5728
6343
|
let input = stripReadOnlyFields(table, data);
|
|
6344
|
+
if (isIndirectlyScoped && tenantChain && queryParentIds && ctx.tenantId) {
|
|
6345
|
+
const firstHop = tenantChain.hops[0];
|
|
6346
|
+
const parentId = input[firstHop.foreignKey];
|
|
6347
|
+
if (!parentId) {
|
|
6348
|
+
return err4(new EntityNotFoundError2(`Referenced parent not found: ${firstHop.foreignKey} is required`));
|
|
6349
|
+
}
|
|
6350
|
+
const parentExists = await queryParentIds(firstHop.tableName, {
|
|
6351
|
+
[firstHop.targetColumn]: parentId
|
|
6352
|
+
});
|
|
6353
|
+
if (parentExists.length === 0) {
|
|
6354
|
+
return err4(new EntityNotFoundError2(`Referenced parent not found: ${firstHop.tableName} with ${firstHop.targetColumn} "${parentId}" does not exist`));
|
|
6355
|
+
}
|
|
6356
|
+
const indirectWhere = await resolveIndirectTenantWhere(ctx);
|
|
6357
|
+
if (indirectWhere) {
|
|
6358
|
+
const allowed = indirectWhere[firstHop.foreignKey];
|
|
6359
|
+
if (!allowed || !allowed.in.includes(parentId)) {
|
|
6360
|
+
return err4(new EntityForbiddenError2(`Referenced ${firstHop.tableName} does not belong to your tenant`));
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
6363
|
+
}
|
|
6364
|
+
if (isTenantScoped && !isIndirectlyScoped && ctx.tenantId) {
|
|
6365
|
+
input = { ...input, tenantId: ctx.tenantId };
|
|
6366
|
+
}
|
|
5729
6367
|
if (def.before.create) {
|
|
5730
6368
|
input = await def.before.create(input, ctx);
|
|
5731
6369
|
}
|
|
@@ -5736,12 +6374,19 @@ function createCrudHandlers(def, db) {
|
|
|
5736
6374
|
await def.after.create(strippedResult, ctx);
|
|
5737
6375
|
} catch {}
|
|
5738
6376
|
}
|
|
5739
|
-
return ok4({
|
|
6377
|
+
return ok4({
|
|
6378
|
+
status: 201,
|
|
6379
|
+
body: narrowRelationFields(def.relations, strippedResult)
|
|
6380
|
+
});
|
|
5740
6381
|
},
|
|
5741
6382
|
async update(ctx, id, data) {
|
|
5742
6383
|
const existing = await db.get(id);
|
|
5743
|
-
if (!existing)
|
|
5744
|
-
return
|
|
6384
|
+
if (!existing)
|
|
6385
|
+
return notFound(id);
|
|
6386
|
+
if (!isSameTenant(ctx, existing))
|
|
6387
|
+
return notFound(id);
|
|
6388
|
+
if (isIndirectlyScoped && !await verifyIndirectTenantOwnership(ctx, existing)) {
|
|
6389
|
+
return notFound(id);
|
|
5745
6390
|
}
|
|
5746
6391
|
const accessResult = await enforceAccess("update", def.access, ctx, existing);
|
|
5747
6392
|
if (!accessResult.ok)
|
|
@@ -5765,8 +6410,12 @@ function createCrudHandlers(def, db) {
|
|
|
5765
6410
|
},
|
|
5766
6411
|
async delete(ctx, id) {
|
|
5767
6412
|
const existing = await db.get(id);
|
|
5768
|
-
if (!existing)
|
|
5769
|
-
return
|
|
6413
|
+
if (!existing)
|
|
6414
|
+
return notFound(id);
|
|
6415
|
+
if (!isSameTenant(ctx, existing))
|
|
6416
|
+
return notFound(id);
|
|
6417
|
+
if (isIndirectlyScoped && !await verifyIndirectTenantOwnership(ctx, existing)) {
|
|
6418
|
+
return notFound(id);
|
|
5770
6419
|
}
|
|
5771
6420
|
const accessResult = await enforceAccess("delete", def.access, ctx, existing);
|
|
5772
6421
|
if (!accessResult.ok)
|
|
@@ -5847,14 +6496,7 @@ function entityErrorHandler(error) {
|
|
|
5847
6496
|
// src/entity/vertzql-parser.ts
|
|
5848
6497
|
var MAX_LIMIT = 1000;
|
|
5849
6498
|
var MAX_Q_BASE64_LENGTH = 10240;
|
|
5850
|
-
var ALLOWED_Q_KEYS = new Set([
|
|
5851
|
-
"select",
|
|
5852
|
-
"include",
|
|
5853
|
-
"where",
|
|
5854
|
-
"orderBy",
|
|
5855
|
-
"limit",
|
|
5856
|
-
"offset"
|
|
5857
|
-
]);
|
|
6499
|
+
var ALLOWED_Q_KEYS = new Set(["select", "include", "where", "orderBy", "limit", "offset"]);
|
|
5858
6500
|
function parseVertzQL(query) {
|
|
5859
6501
|
const result = {};
|
|
5860
6502
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -5964,22 +6606,94 @@ function validateVertzQL(options, table, relationsConfig) {
|
|
|
5964
6606
|
}
|
|
5965
6607
|
}
|
|
5966
6608
|
if (options.include && relationsConfig) {
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
6609
|
+
const includeResult = validateInclude(options.include, relationsConfig, "");
|
|
6610
|
+
if (!includeResult.ok)
|
|
6611
|
+
return includeResult;
|
|
6612
|
+
}
|
|
6613
|
+
return { ok: true };
|
|
6614
|
+
}
|
|
6615
|
+
function validateInclude(include, relationsConfig, pathPrefix) {
|
|
6616
|
+
for (const [relation, requested] of Object.entries(include)) {
|
|
6617
|
+
const entityConfig = relationsConfig[relation];
|
|
6618
|
+
const relationPath = pathPrefix ? `${pathPrefix}.${relation}` : relation;
|
|
6619
|
+
if (entityConfig === undefined || entityConfig === false) {
|
|
6620
|
+
return { ok: false, error: `Relation "${relationPath}" is not exposed` };
|
|
6621
|
+
}
|
|
6622
|
+
if (requested === true)
|
|
6623
|
+
continue;
|
|
6624
|
+
const configObj = typeof entityConfig === "object" ? entityConfig : undefined;
|
|
6625
|
+
if (requested.where) {
|
|
6626
|
+
if (!configObj || !configObj.allowWhere || configObj.allowWhere.length === 0) {
|
|
6627
|
+
return {
|
|
6628
|
+
ok: false,
|
|
6629
|
+
error: `Filtering is not enabled on relation '${relationPath}'. ` + "Add 'allowWhere' to the entity relations config."
|
|
6630
|
+
};
|
|
6631
|
+
}
|
|
6632
|
+
const allowedSet = new Set(configObj.allowWhere);
|
|
6633
|
+
for (const field of Object.keys(requested.where)) {
|
|
6634
|
+
if (!allowedSet.has(field)) {
|
|
6635
|
+
return {
|
|
6636
|
+
ok: false,
|
|
6637
|
+
error: `Field '${field}' is not filterable on relation '${relationPath}'. ` + `Allowed: ${configObj.allowWhere.join(", ")}`
|
|
6638
|
+
};
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
if (requested.orderBy) {
|
|
6643
|
+
if (!configObj || !configObj.allowOrderBy || configObj.allowOrderBy.length === 0) {
|
|
6644
|
+
return {
|
|
6645
|
+
ok: false,
|
|
6646
|
+
error: `Sorting is not enabled on relation '${relationPath}'. ` + "Add 'allowOrderBy' to the entity relations config."
|
|
6647
|
+
};
|
|
6648
|
+
}
|
|
6649
|
+
const allowedSet = new Set(configObj.allowOrderBy);
|
|
6650
|
+
for (const [field, dir] of Object.entries(requested.orderBy)) {
|
|
6651
|
+
if (!allowedSet.has(field)) {
|
|
6652
|
+
return {
|
|
6653
|
+
ok: false,
|
|
6654
|
+
error: `Field '${field}' is not sortable on relation '${relationPath}'. ` + `Allowed: ${configObj.allowOrderBy.join(", ")}`
|
|
6655
|
+
};
|
|
6656
|
+
}
|
|
6657
|
+
if (dir !== "asc" && dir !== "desc") {
|
|
6658
|
+
return {
|
|
6659
|
+
ok: false,
|
|
6660
|
+
error: `Invalid orderBy direction '${String(dir)}' for field '${field}' on relation '${relationPath}'. Must be 'asc' or 'desc'.`
|
|
6661
|
+
};
|
|
5980
6662
|
}
|
|
5981
6663
|
}
|
|
5982
6664
|
}
|
|
6665
|
+
if (requested.limit !== undefined) {
|
|
6666
|
+
if (typeof requested.limit !== "number" || !Number.isFinite(requested.limit)) {
|
|
6667
|
+
return {
|
|
6668
|
+
ok: false,
|
|
6669
|
+
error: `Invalid limit on relation '${relationPath}': must be a finite number`
|
|
6670
|
+
};
|
|
6671
|
+
}
|
|
6672
|
+
if (requested.limit < 0) {
|
|
6673
|
+
requested.limit = 0;
|
|
6674
|
+
}
|
|
6675
|
+
if (configObj?.maxLimit !== undefined && requested.limit > configObj.maxLimit) {
|
|
6676
|
+
requested.limit = configObj.maxLimit;
|
|
6677
|
+
}
|
|
6678
|
+
}
|
|
6679
|
+
if (requested.select && configObj?.select) {
|
|
6680
|
+
for (const field of Object.keys(requested.select)) {
|
|
6681
|
+
if (!(field in configObj.select)) {
|
|
6682
|
+
return {
|
|
6683
|
+
ok: false,
|
|
6684
|
+
error: `Field "${field}" is not exposed on relation "${relationPath}"`
|
|
6685
|
+
};
|
|
6686
|
+
}
|
|
6687
|
+
}
|
|
6688
|
+
}
|
|
6689
|
+
if (requested.include) {
|
|
6690
|
+
if (entityConfig === true) {
|
|
6691
|
+
return {
|
|
6692
|
+
ok: false,
|
|
6693
|
+
error: `Nested includes are not supported on relation '${relationPath}' ` + "without a structured relations config."
|
|
6694
|
+
};
|
|
6695
|
+
}
|
|
6696
|
+
}
|
|
5983
6697
|
}
|
|
5984
6698
|
return { ok: true };
|
|
5985
6699
|
}
|
|
@@ -6007,7 +6721,11 @@ function getParams(ctx) {
|
|
|
6007
6721
|
function generateEntityRoutes(def, registry, db, options) {
|
|
6008
6722
|
const prefix = options?.apiPrefix ?? "/api";
|
|
6009
6723
|
const basePath = `${prefix}/${def.name}`;
|
|
6010
|
-
const
|
|
6724
|
+
const tenantChain = options?.tenantChain ?? null;
|
|
6725
|
+
const crudHandlers = createCrudHandlers(def, db, {
|
|
6726
|
+
tenantChain,
|
|
6727
|
+
queryParentIds: options?.queryParentIds
|
|
6728
|
+
});
|
|
6011
6729
|
const inject = def.inject ?? {};
|
|
6012
6730
|
const registryProxy = Object.keys(inject).length > 0 ? registry.createScopedProxy(inject) : {};
|
|
6013
6731
|
const routes = [];
|
|
@@ -6046,7 +6764,8 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6046
6764
|
where: parsed.where ? parsed.where : undefined,
|
|
6047
6765
|
orderBy: parsed.orderBy,
|
|
6048
6766
|
limit: parsed.limit,
|
|
6049
|
-
after: parsed.after
|
|
6767
|
+
after: parsed.after,
|
|
6768
|
+
include: parsed.include
|
|
6050
6769
|
};
|
|
6051
6770
|
const result = await crudHandlers.list(entityCtx, options2);
|
|
6052
6771
|
if (!result.ok) {
|
|
@@ -6088,7 +6807,8 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6088
6807
|
where: parsed.where,
|
|
6089
6808
|
orderBy: parsed.orderBy,
|
|
6090
6809
|
limit: parsed.limit,
|
|
6091
|
-
after: parsed.after
|
|
6810
|
+
after: parsed.after,
|
|
6811
|
+
include: parsed.include
|
|
6092
6812
|
};
|
|
6093
6813
|
const result = await crudHandlers.list(entityCtx, options2);
|
|
6094
6814
|
if (!result.ok) {
|
|
@@ -6133,7 +6853,8 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6133
6853
|
if (!validation.ok) {
|
|
6134
6854
|
return jsonResponse({ error: { code: "BadRequest", message: validation.error } }, 400);
|
|
6135
6855
|
}
|
|
6136
|
-
const
|
|
6856
|
+
const getOptions = parsed.include ? { include: parsed.include } : undefined;
|
|
6857
|
+
const result = await crudHandlers.get(entityCtx, id, getOptions);
|
|
6137
6858
|
if (!result.ok) {
|
|
6138
6859
|
const { status, body: body2 } = entityErrorHandler(result.error);
|
|
6139
6860
|
return jsonResponse(body2, status);
|
|
@@ -6298,13 +7019,106 @@ function generateEntityRoutes(def, registry, db, options) {
|
|
|
6298
7019
|
return routes;
|
|
6299
7020
|
}
|
|
6300
7021
|
|
|
7022
|
+
// src/entity/tenant-chain.ts
|
|
7023
|
+
function resolveTenantChain(entityKey, tenantGraph, registry) {
|
|
7024
|
+
if (!tenantGraph.indirectlyScoped.includes(entityKey)) {
|
|
7025
|
+
return null;
|
|
7026
|
+
}
|
|
7027
|
+
if (tenantGraph.root === null) {
|
|
7028
|
+
return null;
|
|
7029
|
+
}
|
|
7030
|
+
const rootEntry = registry[tenantGraph.root];
|
|
7031
|
+
if (!rootEntry)
|
|
7032
|
+
return null;
|
|
7033
|
+
const rootTableName = rootEntry.table._name;
|
|
7034
|
+
const tableNameToKey = new Map;
|
|
7035
|
+
for (const [key, entry] of Object.entries(registry)) {
|
|
7036
|
+
tableNameToKey.set(entry.table._name, key);
|
|
7037
|
+
}
|
|
7038
|
+
const directlyScopedTableNames = new Set;
|
|
7039
|
+
for (const key of tenantGraph.directlyScoped) {
|
|
7040
|
+
const entry = registry[key];
|
|
7041
|
+
if (entry)
|
|
7042
|
+
directlyScopedTableNames.add(entry.table._name);
|
|
7043
|
+
}
|
|
7044
|
+
directlyScopedTableNames.add(rootTableName);
|
|
7045
|
+
const hops = [];
|
|
7046
|
+
let currentKey = entityKey;
|
|
7047
|
+
const visited = new Set;
|
|
7048
|
+
while (true) {
|
|
7049
|
+
if (visited.has(currentKey)) {
|
|
7050
|
+
return null;
|
|
7051
|
+
}
|
|
7052
|
+
visited.add(currentKey);
|
|
7053
|
+
const currentEntry = registry[currentKey];
|
|
7054
|
+
if (!currentEntry)
|
|
7055
|
+
return null;
|
|
7056
|
+
let foundHop = false;
|
|
7057
|
+
for (const [, rel] of Object.entries(currentEntry.relations)) {
|
|
7058
|
+
if (rel._type !== "one" || !rel._foreignKey)
|
|
7059
|
+
continue;
|
|
7060
|
+
const targetTableName = rel._target()._name;
|
|
7061
|
+
const targetKey = tableNameToKey.get(targetTableName);
|
|
7062
|
+
if (!targetKey)
|
|
7063
|
+
continue;
|
|
7064
|
+
const targetIsDirectlyScoped = directlyScopedTableNames.has(targetTableName);
|
|
7065
|
+
const targetIsIndirectlyScoped = tenantGraph.indirectlyScoped.includes(targetKey);
|
|
7066
|
+
if (!targetIsDirectlyScoped && !targetIsIndirectlyScoped)
|
|
7067
|
+
continue;
|
|
7068
|
+
const targetEntry = registry[targetKey];
|
|
7069
|
+
if (!targetEntry)
|
|
7070
|
+
continue;
|
|
7071
|
+
const targetPk = resolvePrimaryKey(targetEntry.table._columns);
|
|
7072
|
+
hops.push({
|
|
7073
|
+
tableName: targetTableName,
|
|
7074
|
+
foreignKey: rel._foreignKey,
|
|
7075
|
+
targetColumn: targetPk
|
|
7076
|
+
});
|
|
7077
|
+
if (targetIsDirectlyScoped && targetKey !== tenantGraph.root) {
|
|
7078
|
+
const tenantColumn = resolveTenantFk(targetKey, registry);
|
|
7079
|
+
if (!tenantColumn)
|
|
7080
|
+
return null;
|
|
7081
|
+
return { hops, tenantColumn };
|
|
7082
|
+
}
|
|
7083
|
+
if (targetKey === tenantGraph.root) {
|
|
7084
|
+
return { hops, tenantColumn: rel._foreignKey };
|
|
7085
|
+
}
|
|
7086
|
+
currentKey = targetKey;
|
|
7087
|
+
foundHop = true;
|
|
7088
|
+
break;
|
|
7089
|
+
}
|
|
7090
|
+
if (!foundHop)
|
|
7091
|
+
return null;
|
|
7092
|
+
}
|
|
7093
|
+
}
|
|
7094
|
+
function resolvePrimaryKey(columns) {
|
|
7095
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
7096
|
+
if (col && typeof col === "object" && "_meta" in col) {
|
|
7097
|
+
const meta = col._meta;
|
|
7098
|
+
if (meta.primary)
|
|
7099
|
+
return key;
|
|
7100
|
+
}
|
|
7101
|
+
}
|
|
7102
|
+
return "id";
|
|
7103
|
+
}
|
|
7104
|
+
function resolveTenantFk(modelKey, registry) {
|
|
7105
|
+
const entry = registry[modelKey];
|
|
7106
|
+
if (!entry || !entry._tenant)
|
|
7107
|
+
return null;
|
|
7108
|
+
const tenantRel = entry.relations[entry._tenant];
|
|
7109
|
+
if (!tenantRel || !tenantRel._foreignKey)
|
|
7110
|
+
return null;
|
|
7111
|
+
return tenantRel._foreignKey;
|
|
7112
|
+
}
|
|
7113
|
+
|
|
6301
7114
|
// src/service/context.ts
|
|
6302
|
-
function createServiceContext(request, registryProxy) {
|
|
7115
|
+
function createServiceContext(request, registryProxy, rawRequest) {
|
|
6303
7116
|
const userId = request.userId ?? null;
|
|
6304
7117
|
const roles = request.roles ?? [];
|
|
6305
7118
|
const tenantId = request.tenantId ?? null;
|
|
6306
7119
|
return {
|
|
6307
7120
|
userId,
|
|
7121
|
+
tenantId,
|
|
6308
7122
|
authenticated() {
|
|
6309
7123
|
return userId !== null;
|
|
6310
7124
|
},
|
|
@@ -6314,7 +7128,8 @@ function createServiceContext(request, registryProxy) {
|
|
|
6314
7128
|
role(...rolesToCheck) {
|
|
6315
7129
|
return rolesToCheck.some((r) => roles.includes(r));
|
|
6316
7130
|
},
|
|
6317
|
-
entities: registryProxy
|
|
7131
|
+
entities: registryProxy,
|
|
7132
|
+
request: rawRequest ?? { url: "", method: "", headers: new Headers, body: undefined }
|
|
6318
7133
|
};
|
|
6319
7134
|
}
|
|
6320
7135
|
|
|
@@ -6363,23 +7178,56 @@ function generateServiceRoutes(def, registry, options) {
|
|
|
6363
7178
|
handler: async (ctx) => {
|
|
6364
7179
|
try {
|
|
6365
7180
|
const requestInfo = extractRequestInfo2(ctx);
|
|
6366
|
-
const
|
|
7181
|
+
const raw = ctx.raw;
|
|
7182
|
+
const ctxHeaders = ctx.headers;
|
|
7183
|
+
const rawRequest = {
|
|
7184
|
+
url: raw?.url ?? "",
|
|
7185
|
+
method: raw?.method ?? "",
|
|
7186
|
+
headers: new Headers(ctxHeaders ?? {}),
|
|
7187
|
+
body: ctx.body
|
|
7188
|
+
};
|
|
7189
|
+
const serviceCtx = createServiceContext(requestInfo, registryProxy, rawRequest);
|
|
6367
7190
|
const accessResult = await enforceAccess(handlerName, def.access, serviceCtx);
|
|
6368
7191
|
if (!accessResult.ok) {
|
|
6369
7192
|
return jsonResponse2({ error: { code: "Forbidden", message: accessResult.error.message } }, 403);
|
|
6370
7193
|
}
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
const
|
|
6375
|
-
|
|
7194
|
+
if (handlerDef.body && isContentDescriptor(handlerDef.body)) {
|
|
7195
|
+
const reqContentType = rawRequest.headers.get("content-type")?.toLowerCase() ?? "";
|
|
7196
|
+
const expected = handlerDef.body._contentType;
|
|
7197
|
+
const isXml = expected === "application/xml";
|
|
7198
|
+
const matches = isXml ? reqContentType.includes("application/xml") || reqContentType.includes("text/xml") : reqContentType.includes(expected);
|
|
7199
|
+
if (reqContentType && !matches) {
|
|
7200
|
+
return jsonResponse2({
|
|
7201
|
+
error: {
|
|
7202
|
+
code: "UnsupportedMediaType",
|
|
7203
|
+
message: `Expected content-type ${expected}, got ${reqContentType}`
|
|
7204
|
+
}
|
|
7205
|
+
}, 415);
|
|
7206
|
+
}
|
|
7207
|
+
}
|
|
7208
|
+
let input;
|
|
7209
|
+
if (handlerDef.body) {
|
|
7210
|
+
const rawBody = ctx.body ?? {};
|
|
7211
|
+
const parsed = handlerDef.body.parse(rawBody);
|
|
7212
|
+
if (!parsed.ok) {
|
|
7213
|
+
const message = parsed.error instanceof Error ? parsed.error.message : "Invalid request body";
|
|
7214
|
+
return jsonResponse2({ error: { code: "BadRequest", message } }, 400);
|
|
7215
|
+
}
|
|
7216
|
+
input = parsed.data;
|
|
6376
7217
|
}
|
|
6377
|
-
const result = await handlerDef.handler(
|
|
7218
|
+
const result = await handlerDef.handler(input, serviceCtx);
|
|
6378
7219
|
const responseParsed = handlerDef.response.parse(result);
|
|
6379
7220
|
if (!responseParsed.ok) {
|
|
6380
7221
|
const message = responseParsed.error instanceof Error ? responseParsed.error.message : "Response validation failed";
|
|
6381
7222
|
console.warn(`[vertz] Service response validation warning: ${message}`);
|
|
6382
7223
|
}
|
|
7224
|
+
if (isContentDescriptor(handlerDef.response)) {
|
|
7225
|
+
const body = result instanceof Uint8Array ? result : String(result);
|
|
7226
|
+
return new Response(body, {
|
|
7227
|
+
status: 200,
|
|
7228
|
+
headers: { "content-type": handlerDef.response._contentType }
|
|
7229
|
+
});
|
|
7230
|
+
}
|
|
6383
7231
|
return jsonResponse2(result, 200);
|
|
6384
7232
|
} catch (error) {
|
|
6385
7233
|
const message = error instanceof Error ? error.message : "Internal server error";
|
|
@@ -6459,20 +7307,55 @@ function createServer(config) {
|
|
|
6459
7307
|
}
|
|
6460
7308
|
if (config.entities && config.entities.length > 0) {
|
|
6461
7309
|
let dbFactory;
|
|
7310
|
+
const tableOf = (e) => e.table ?? e.name;
|
|
6462
7311
|
if (hasDbClient) {
|
|
6463
7312
|
const dbModels = db._internals.models;
|
|
6464
|
-
const missing = config.entities.filter((e) => !(e
|
|
6465
|
-
|
|
7313
|
+
const missing = config.entities.filter((e) => !(tableOf(e) in dbModels)).map((e) => `"${tableOf(e)}"`);
|
|
7314
|
+
const uniqueMissing = [...new Set(missing)];
|
|
7315
|
+
if (uniqueMissing.length > 0) {
|
|
6466
7316
|
const registered = Object.keys(dbModels).map((k) => `"${k}"`).join(", ");
|
|
6467
|
-
const plural =
|
|
6468
|
-
throw new Error(`${plural ? "Entities" : "Entity"} ${
|
|
7317
|
+
const plural = uniqueMissing.length > 1;
|
|
7318
|
+
throw new Error(`${plural ? "Entities" : "Entity"} ${uniqueMissing.join(", ")} ${plural ? "are" : "is"} not registered in createDb(). ` + `Add the missing model${plural ? "s" : ""} to the models object in your createDb() call. ` + `Registered models: ${registered || "(none)"}`);
|
|
6469
7319
|
}
|
|
6470
|
-
dbFactory = (entityDef) => createDatabaseBridgeAdapter(db, entityDef
|
|
7320
|
+
dbFactory = (entityDef) => createDatabaseBridgeAdapter(db, tableOf(entityDef));
|
|
6471
7321
|
} else if (db) {
|
|
6472
7322
|
dbFactory = () => db;
|
|
6473
7323
|
} else {
|
|
6474
7324
|
dbFactory = config._entityDbFactory ?? createNoopDbAdapter;
|
|
6475
7325
|
}
|
|
7326
|
+
const tenantChains = new Map;
|
|
7327
|
+
let queryParentIds;
|
|
7328
|
+
if (hasDbClient) {
|
|
7329
|
+
const dbClient = db;
|
|
7330
|
+
const tenantGraph = dbClient._internals.tenantGraph;
|
|
7331
|
+
const dbModelsMap = dbClient._internals.models;
|
|
7332
|
+
for (const entityDef of config.entities) {
|
|
7333
|
+
const eDef = entityDef;
|
|
7334
|
+
if (eDef.tenantScoped === false)
|
|
7335
|
+
continue;
|
|
7336
|
+
const modelKey = tableOf(eDef);
|
|
7337
|
+
const chain = resolveTenantChain(modelKey, tenantGraph, dbModelsMap);
|
|
7338
|
+
if (chain) {
|
|
7339
|
+
tenantChains.set(eDef.name, chain);
|
|
7340
|
+
}
|
|
7341
|
+
}
|
|
7342
|
+
if (tenantChains.size > 0) {
|
|
7343
|
+
queryParentIds = async (tableName, where) => {
|
|
7344
|
+
const delegate = dbClient[tableName];
|
|
7345
|
+
if (!delegate)
|
|
7346
|
+
return [];
|
|
7347
|
+
const result = await delegate.list({ where });
|
|
7348
|
+
if (!result.ok || !result.data)
|
|
7349
|
+
return [];
|
|
7350
|
+
return result.data.map((row) => row.id);
|
|
7351
|
+
};
|
|
7352
|
+
}
|
|
7353
|
+
}
|
|
7354
|
+
if (config._tenantChains) {
|
|
7355
|
+
for (const [name, chain] of config._tenantChains) {
|
|
7356
|
+
tenantChains.set(name, chain);
|
|
7357
|
+
}
|
|
7358
|
+
}
|
|
6476
7359
|
for (const entityDef of config.entities) {
|
|
6477
7360
|
const entityDb = dbFactory(entityDef);
|
|
6478
7361
|
const ops = createEntityOps(entityDef, entityDb);
|
|
@@ -6480,8 +7363,11 @@ function createServer(config) {
|
|
|
6480
7363
|
}
|
|
6481
7364
|
for (const entityDef of config.entities) {
|
|
6482
7365
|
const entityDb = dbFactory(entityDef);
|
|
7366
|
+
const tenantChain = tenantChains.get(entityDef.name) ?? null;
|
|
6483
7367
|
const routes = generateEntityRoutes(entityDef, registry, entityDb, {
|
|
6484
|
-
apiPrefix
|
|
7368
|
+
apiPrefix,
|
|
7369
|
+
tenantChain,
|
|
7370
|
+
queryParentIds: tenantChain ? queryParentIds ?? config._queryParentIds : undefined
|
|
6485
7371
|
});
|
|
6486
7372
|
allRoutes.push(...routes);
|
|
6487
7373
|
}
|
|
@@ -6501,15 +7387,41 @@ function createServer(config) {
|
|
|
6501
7387
|
const authConfig = {
|
|
6502
7388
|
...config.auth,
|
|
6503
7389
|
userStore: config.auth.userStore ?? new DbUserStore(dbClient),
|
|
6504
|
-
sessionStore: config.auth.sessionStore ?? new DbSessionStore(dbClient)
|
|
7390
|
+
sessionStore: config.auth.sessionStore ?? new DbSessionStore(dbClient),
|
|
7391
|
+
oauthAccountStore: config.auth.oauthAccountStore ?? (config.auth.providers?.length ? new DbOAuthAccountStore(dbClient) : undefined),
|
|
7392
|
+
_entityProxy: config.auth.onUserCreated ? config.auth._entityProxy ?? registry.createProxy() : undefined
|
|
6505
7393
|
};
|
|
6506
7394
|
const auth = createAuth(authConfig);
|
|
7395
|
+
if (apiPrefix !== "/api") {
|
|
7396
|
+
throw new Error(`requestHandler requires apiPrefix to be '/api' (got '${apiPrefix}'). ` + "Custom API prefixes are not yet supported with auth.");
|
|
7397
|
+
}
|
|
7398
|
+
const authPrefix = `${apiPrefix}/auth`;
|
|
7399
|
+
const authPrefixSlash = `${authPrefix}/`;
|
|
6507
7400
|
const serverInstance = app;
|
|
6508
7401
|
serverInstance.auth = auth;
|
|
6509
7402
|
serverInstance.initialize = async () => {
|
|
6510
7403
|
await initializeAuthTables(dbClient);
|
|
6511
7404
|
await auth.initialize();
|
|
6512
7405
|
};
|
|
7406
|
+
let cachedRequestHandler = null;
|
|
7407
|
+
Object.defineProperty(serverInstance, "requestHandler", {
|
|
7408
|
+
get() {
|
|
7409
|
+
if (!cachedRequestHandler) {
|
|
7410
|
+
const entityHandler = this.handler;
|
|
7411
|
+
const authHandler = this.auth.handler;
|
|
7412
|
+
cachedRequestHandler = (request) => {
|
|
7413
|
+
const pathname = new URL(request.url).pathname;
|
|
7414
|
+
if (pathname === authPrefix || pathname.startsWith(authPrefixSlash)) {
|
|
7415
|
+
return authHandler(request);
|
|
7416
|
+
}
|
|
7417
|
+
return entityHandler(request);
|
|
7418
|
+
};
|
|
7419
|
+
}
|
|
7420
|
+
return cachedRequestHandler;
|
|
7421
|
+
},
|
|
7422
|
+
enumerable: true,
|
|
7423
|
+
configurable: false
|
|
7424
|
+
});
|
|
6513
7425
|
return serverInstance;
|
|
6514
7426
|
}
|
|
6515
7427
|
return app;
|
|
@@ -6524,6 +7436,8 @@ function entity(name, config) {
|
|
|
6524
7436
|
if (!config.model) {
|
|
6525
7437
|
throw new Error("entity() requires a model in the config.");
|
|
6526
7438
|
}
|
|
7439
|
+
const hasTenantIdColumn = "tenantId" in config.model.table._columns;
|
|
7440
|
+
const tenantScoped = config.tenantScoped ?? hasTenantIdColumn;
|
|
6527
7441
|
const def = {
|
|
6528
7442
|
kind: "entity",
|
|
6529
7443
|
name,
|
|
@@ -6533,7 +7447,10 @@ function entity(name, config) {
|
|
|
6533
7447
|
before: config.before ?? {},
|
|
6534
7448
|
after: config.after ?? {},
|
|
6535
7449
|
actions: config.actions ?? {},
|
|
6536
|
-
relations: config.relations ?? {}
|
|
7450
|
+
relations: config.relations ?? {},
|
|
7451
|
+
table: config.table ?? name,
|
|
7452
|
+
tenantScoped,
|
|
7453
|
+
tenantChain: null
|
|
6537
7454
|
};
|
|
6538
7455
|
return deepFreeze(def);
|
|
6539
7456
|
}
|
|
@@ -6566,7 +7483,9 @@ export {
|
|
|
6566
7483
|
stripHiddenFields,
|
|
6567
7484
|
service,
|
|
6568
7485
|
rules,
|
|
7486
|
+
resolveTenantChain,
|
|
6569
7487
|
makeImmutable,
|
|
7488
|
+
isContentDescriptor,
|
|
6570
7489
|
initializeAuthTables,
|
|
6571
7490
|
hashPassword,
|
|
6572
7491
|
google,
|
|
@@ -6596,6 +7515,7 @@ export {
|
|
|
6596
7515
|
createAccessEventBroadcaster,
|
|
6597
7516
|
createAccessContext,
|
|
6598
7517
|
createAccess,
|
|
7518
|
+
content,
|
|
6599
7519
|
computePlanHash,
|
|
6600
7520
|
computeOverage,
|
|
6601
7521
|
computeEntityAccess,
|
|
@@ -6612,11 +7532,11 @@ export {
|
|
|
6612
7532
|
InternalServerErrorException,
|
|
6613
7533
|
InMemoryWalletStore,
|
|
6614
7534
|
InMemoryUserStore,
|
|
7535
|
+
InMemorySubscriptionStore,
|
|
6615
7536
|
InMemorySessionStore,
|
|
6616
7537
|
InMemoryRoleAssignmentStore,
|
|
6617
7538
|
InMemoryRateLimitStore,
|
|
6618
7539
|
InMemoryPlanVersionStore,
|
|
6619
|
-
InMemoryPlanStore,
|
|
6620
7540
|
InMemoryPasswordResetStore,
|
|
6621
7541
|
InMemoryOverrideStore,
|
|
6622
7542
|
InMemoryOAuthAccountStore,
|
|
@@ -6628,14 +7548,14 @@ export {
|
|
|
6628
7548
|
ForbiddenException,
|
|
6629
7549
|
EntityRegistry,
|
|
6630
7550
|
DbUserStore,
|
|
7551
|
+
DbSubscriptionStore,
|
|
6631
7552
|
DbSessionStore,
|
|
6632
7553
|
DbRoleAssignmentStore,
|
|
6633
|
-
DbPlanStore,
|
|
6634
7554
|
DbOAuthAccountStore,
|
|
6635
7555
|
DbFlagStore,
|
|
6636
7556
|
DbClosureStore,
|
|
6637
7557
|
ConflictException,
|
|
6638
|
-
BadRequestException,
|
|
7558
|
+
BadRequestException2 as BadRequestException,
|
|
6639
7559
|
AuthorizationError,
|
|
6640
7560
|
AUTH_TABLE_NAMES
|
|
6641
7561
|
};
|