@vertz/server 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +9 -3
- package/dist/index.js +73 -10
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AccumulateProvides, AppBuilder as AppBuilder2, AppConfig as AppConfig2,
|
|
2
|
-
import { BadRequestException, ConflictException, createEnv, createImmutableProxy, createMiddleware,
|
|
1
|
+
import { AccumulateProvides, AppBuilder as AppBuilder2, AppConfig as AppConfig2, CorsConfig, Ctx, DeepReadonly, Deps, EnvConfig, HandlerCtx, HttpMethod, HttpStatusCode, Infer, InferSchema, ListenOptions, MiddlewareDef, NamedMiddlewareDef, RawRequest, ServerAdapter, ServerHandle } from "@vertz/core";
|
|
2
|
+
import { BadRequestException, ConflictException, createEnv, createImmutableProxy, createMiddleware, deepFreeze, ForbiddenException, InternalServerErrorException, makeImmutable, NotFoundException, ServiceUnavailableException, UnauthorizedException, ValidationException, VertzException, vertz } from "@vertz/core";
|
|
3
3
|
import { ModelDef as ModelDef2, RelationDef, SchemaLike, TableDef } from "@vertz/db";
|
|
4
4
|
import { ModelDef } from "@vertz/db";
|
|
5
5
|
import { EntityDbAdapter, ListOptions } from "@vertz/db";
|
|
@@ -205,6 +205,12 @@ interface AuthConfig {
|
|
|
205
205
|
* Defaults to true when process.env is unavailable (secure-by-default for edge runtimes).
|
|
206
206
|
*/
|
|
207
207
|
isProduction?: boolean;
|
|
208
|
+
/**
|
|
209
|
+
* Directory to persist auto-generated dev JWT secret.
|
|
210
|
+
* Defaults to `.vertz` in the current working directory.
|
|
211
|
+
* Only used in non-production mode when jwtSecret is not provided.
|
|
212
|
+
*/
|
|
213
|
+
devSecretPath?: string;
|
|
208
214
|
}
|
|
209
215
|
interface AuthUser {
|
|
210
216
|
id: string;
|
|
@@ -420,4 +426,4 @@ interface EntityRouteOptions {
|
|
|
420
426
|
* Operations explicitly disabled (access: false) get a 405 handler.
|
|
421
427
|
*/
|
|
422
428
|
declare function generateEntityRoutes(def: EntityDefinition, registry: EntityRegistry, db: EntityDbAdapter2, options?: EntityRouteOptions): EntityRouteEntry[];
|
|
423
|
-
export { vertz, verifyPassword, validatePassword, stripReadOnlyFields, stripHiddenFields, makeImmutable, hashPassword, generateEntityRoutes, entityErrorHandler, entity, enforceAccess, defaultAccess, deepFreeze, createServer,
|
|
429
|
+
export { vertz, verifyPassword, validatePassword, stripReadOnlyFields, stripHiddenFields, makeImmutable, hashPassword, generateEntityRoutes, entityErrorHandler, entity, enforceAccess, defaultAccess, deepFreeze, createServer, createMiddleware, createImmutableProxy, createEnv, createEntityContext, createCrudHandlers, createAuth, createAccess, action, VertzException, ValidationException, UnauthorizedException, SignUpInput, SignInInput, SessionStrategy, SessionPayload, SessionConfig, Session, ServiceUnavailableException, ServerHandle, ServerConfig, ServerAdapter, Resource, RequestInfo, RawRequest, RateLimitResult, RateLimitConfig, PasswordRequirements, NotFoundException, NamedMiddlewareDef, MiddlewareDef, ListenOptions, ListResult, ListOptions2 as ListOptions, InternalServerErrorException, InferSchema, Infer, HttpStatusCode, HttpMethod, HandlerCtx, ForbiddenException, EnvConfig, EntityRouteOptions, EntityRelationsConfig, EntityRegistry, EntityOperations, EntityErrorResult, EntityDefinition, EntityDbAdapter2 as EntityDbAdapter, EntityContext, EntityConfig, EntityActionDef, EntitlementDefinition, Entitlement, EmailPasswordConfig, Deps, DeepReadonly, Ctx, CrudResult, CrudHandlers, CorsConfig, CookieConfig, ConflictException, BaseContext, BadRequestException, AuthorizationError, AuthUser, AuthInstance, AuthContext, AuthConfig, AuthApi, AppConfig2 as AppConfig, AppBuilder2 as AppBuilder, ActionDefinition, ActionContext, ActionConfig, ActionActionDef, AccumulateProvides, AccessRule, AccessInstance, AccessConfig };
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,6 @@ import {
|
|
|
5
5
|
createEnv,
|
|
6
6
|
createImmutableProxy,
|
|
7
7
|
createMiddleware,
|
|
8
|
-
createModule,
|
|
9
|
-
createModuleDef,
|
|
10
8
|
deepFreeze as deepFreeze3,
|
|
11
9
|
ForbiddenException,
|
|
12
10
|
InternalServerErrorException,
|
|
@@ -39,6 +37,8 @@ function action(name, config) {
|
|
|
39
37
|
return deepFreeze(def);
|
|
40
38
|
}
|
|
41
39
|
// src/auth/index.ts
|
|
40
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
41
|
+
import { join } from "node:path";
|
|
42
42
|
import {
|
|
43
43
|
createAuthRateLimitedError,
|
|
44
44
|
createAuthValidationError,
|
|
@@ -91,17 +91,20 @@ function createAccess(config) {
|
|
|
91
91
|
return false;
|
|
92
92
|
const allowedRoles = entitlementRoles.get(entitlement);
|
|
93
93
|
if (!allowedRoles) {
|
|
94
|
-
return
|
|
94
|
+
return false;
|
|
95
95
|
}
|
|
96
96
|
return allowedRoles.has(user.role) || roleHasEntitlement(user.role, entitlement);
|
|
97
97
|
}
|
|
98
98
|
async function can(entitlement, user) {
|
|
99
99
|
return checkEntitlement(entitlement, user);
|
|
100
100
|
}
|
|
101
|
-
async function canWithResource(entitlement,
|
|
101
|
+
async function canWithResource(entitlement, resource, user) {
|
|
102
102
|
const hasEntitlement = await checkEntitlement(entitlement, user);
|
|
103
103
|
if (!hasEntitlement)
|
|
104
104
|
return false;
|
|
105
|
+
if (resource.ownerId != null && resource.ownerId !== "" && resource.ownerId !== user?.id) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
105
108
|
return true;
|
|
106
109
|
}
|
|
107
110
|
async function authorize(entitlement, user) {
|
|
@@ -298,10 +301,27 @@ function createAuth(config) {
|
|
|
298
301
|
} else if (isProduction) {
|
|
299
302
|
throw new Error('jwtSecret is required in production. Provide it via createAuth({ jwtSecret: "..." }).');
|
|
300
303
|
} else {
|
|
301
|
-
|
|
302
|
-
|
|
304
|
+
const secretDir = config.devSecretPath ?? join(process.cwd(), ".vertz");
|
|
305
|
+
const secretFile = join(secretDir, "jwt-secret");
|
|
306
|
+
if (existsSync(secretFile)) {
|
|
307
|
+
jwtSecret = readFileSync(secretFile, "utf-8").trim();
|
|
308
|
+
} else {
|
|
309
|
+
jwtSecret = crypto.randomUUID() + crypto.randomUUID();
|
|
310
|
+
mkdirSync(secretDir, { recursive: true });
|
|
311
|
+
writeFileSync(secretFile, jwtSecret, "utf-8");
|
|
312
|
+
console.warn(`[Auth] Auto-generated dev JWT secret at ${secretFile}. Add this path to .gitignore.`);
|
|
313
|
+
}
|
|
303
314
|
}
|
|
304
315
|
const cookieConfig = { ...DEFAULT_COOKIE_CONFIG, ...session.cookie };
|
|
316
|
+
if (cookieConfig.sameSite === "none" && cookieConfig.secure !== true) {
|
|
317
|
+
throw new Error("SameSite=None requires secure=true");
|
|
318
|
+
}
|
|
319
|
+
if (isProduction && cookieConfig.secure === false) {
|
|
320
|
+
throw new Error("Cookie 'secure' flag cannot be disabled in production");
|
|
321
|
+
}
|
|
322
|
+
if (!isProduction && cookieConfig.secure === false) {
|
|
323
|
+
console.warn("Cookie 'secure' flag is disabled. This is allowed in development but must be enabled in production.");
|
|
324
|
+
}
|
|
305
325
|
const ttlMs = parseDuration(session.ttl);
|
|
306
326
|
const signInLimiter = new RateLimiter(emailPassword?.rateLimit?.window || "15m");
|
|
307
327
|
const signUpLimiter = new RateLimiter("1h");
|
|
@@ -468,12 +488,37 @@ function createAuth(config) {
|
|
|
468
488
|
if (["POST", "PUT", "DELETE", "PATCH"].includes(method)) {
|
|
469
489
|
const origin = request.headers.get("origin");
|
|
470
490
|
const referer = request.headers.get("referer");
|
|
471
|
-
|
|
491
|
+
const expectedOrigin = new URL(request.url).origin;
|
|
492
|
+
let originValid = false;
|
|
493
|
+
if (origin) {
|
|
494
|
+
originValid = origin === expectedOrigin;
|
|
495
|
+
} else if (referer) {
|
|
496
|
+
try {
|
|
497
|
+
const refererOrigin = new URL(referer).origin;
|
|
498
|
+
originValid = refererOrigin === expectedOrigin;
|
|
499
|
+
} catch {
|
|
500
|
+
originValid = false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (!originValid) {
|
|
472
504
|
if (isProduction) {
|
|
473
505
|
return new Response(JSON.stringify({ error: "CSRF validation failed" }), {
|
|
474
506
|
status: 403,
|
|
475
507
|
headers: { "Content-Type": "application/json" }
|
|
476
508
|
});
|
|
509
|
+
} else {
|
|
510
|
+
console.warn("[Auth] CSRF warning: Origin/Referer missing or mismatched (allowed in development)");
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const vtzHeader = request.headers.get("x-vtz-request");
|
|
514
|
+
if (vtzHeader !== "1") {
|
|
515
|
+
if (isProduction) {
|
|
516
|
+
return new Response(JSON.stringify({ error: "Missing required X-VTZ-Request header" }), {
|
|
517
|
+
status: 403,
|
|
518
|
+
headers: { "Content-Type": "application/json" }
|
|
519
|
+
});
|
|
520
|
+
} else {
|
|
521
|
+
console.warn("[Auth] CSRF warning: Missing X-VTZ-Request header (allowed in development)");
|
|
477
522
|
}
|
|
478
523
|
}
|
|
479
524
|
}
|
|
@@ -875,7 +920,9 @@ function createActionHandler(def, actionName, actionDef, db, hasId) {
|
|
|
875
920
|
return err3(new BadRequestError(parseResult.error.message));
|
|
876
921
|
}
|
|
877
922
|
const input = parseResult.data;
|
|
878
|
-
const
|
|
923
|
+
const rawResult = await actionDef.handler(input, ctx, row);
|
|
924
|
+
const table = def.model.table;
|
|
925
|
+
const result = rawResult && typeof rawResult === "object" && !Array.isArray(rawResult) ? stripHiddenFields(table, rawResult) : rawResult;
|
|
879
926
|
const afterHooks = def.after;
|
|
880
927
|
const afterHook = afterHooks[actionName];
|
|
881
928
|
if (afterHook) {
|
|
@@ -1077,6 +1124,15 @@ function entityErrorHandler(error) {
|
|
|
1077
1124
|
|
|
1078
1125
|
// src/entity/vertzql-parser.ts
|
|
1079
1126
|
var MAX_LIMIT = 1000;
|
|
1127
|
+
var MAX_Q_BASE64_LENGTH = 10240;
|
|
1128
|
+
var ALLOWED_Q_KEYS = new Set([
|
|
1129
|
+
"select",
|
|
1130
|
+
"include",
|
|
1131
|
+
"where",
|
|
1132
|
+
"orderBy",
|
|
1133
|
+
"limit",
|
|
1134
|
+
"offset"
|
|
1135
|
+
]);
|
|
1080
1136
|
function parseVertzQL(query) {
|
|
1081
1137
|
const result = {};
|
|
1082
1138
|
for (const [key, value] of Object.entries(query)) {
|
|
@@ -1124,9 +1180,18 @@ function parseVertzQL(query) {
|
|
|
1124
1180
|
if (key === "q") {
|
|
1125
1181
|
try {
|
|
1126
1182
|
const urlDecoded = decodeURIComponent(value);
|
|
1183
|
+
if (urlDecoded.length > MAX_Q_BASE64_LENGTH) {
|
|
1184
|
+
result._qError = "q= parameter exceeds maximum allowed size";
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1127
1187
|
const b64 = urlDecoded.replace(/-/g, "+").replace(/_/g, "/");
|
|
1128
1188
|
const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
|
|
1129
1189
|
const decoded = JSON.parse(atob(padded));
|
|
1190
|
+
for (const k of Object.keys(decoded)) {
|
|
1191
|
+
if (!ALLOWED_Q_KEYS.has(k)) {
|
|
1192
|
+
delete decoded[k];
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1130
1195
|
if (decoded.select && typeof decoded.select === "object") {
|
|
1131
1196
|
result.select = decoded.select;
|
|
1132
1197
|
}
|
|
@@ -1644,8 +1709,6 @@ export {
|
|
|
1644
1709
|
defaultAccess,
|
|
1645
1710
|
deepFreeze3 as deepFreeze,
|
|
1646
1711
|
createServer,
|
|
1647
|
-
createModuleDef,
|
|
1648
|
-
createModule,
|
|
1649
1712
|
createMiddleware,
|
|
1650
1713
|
createImmutableProxy,
|
|
1651
1714
|
createEnv,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz server runtime — modules, routing, and auth",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@vertz/core": "0.2.
|
|
35
|
-
"@vertz/db": "0.2.
|
|
34
|
+
"@vertz/core": "0.2.2",
|
|
35
|
+
"@vertz/db": "0.2.2",
|
|
36
36
|
"@vertz/errors": "0.2.1",
|
|
37
37
|
"bcryptjs": "^3.0.3",
|
|
38
38
|
"jose": "^6.0.11"
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^25.3.1",
|
|
42
42
|
"@vitest/coverage-v8": "^4.0.18",
|
|
43
|
-
"bunup": "
|
|
43
|
+
"bunup": "^0.16.31",
|
|
44
44
|
"typescript": "^5.7.0",
|
|
45
45
|
"vitest": "^4.0.18"
|
|
46
46
|
},
|