@vertz/server 0.2.1 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { AccumulateProvides, AppBuilder as AppBuilder2, AppConfig as AppConfig2, BootInstruction, BootSequence, CorsConfig, Ctx, DeepReadonly, Deps, EnvConfig, ExtractMethods, HandlerCtx, HttpMethod, HttpStatusCode, Infer, InferSchema, ListenOptions, MiddlewareDef, Module, ModuleBootInstruction, ModuleDef, NamedMiddlewareDef, NamedModule, NamedModuleDef, NamedRouterDef, NamedServiceDef, RawRequest, ResolveInjectMap, RouterDef, ServerAdapter, ServerHandle, ServiceBootInstruction, ServiceDef, ServiceFactory } from "@vertz/core";
2
- import { BadRequestException, ConflictException, createEnv, createImmutableProxy, createMiddleware, createModule, createModuleDef, deepFreeze, ForbiddenException, InternalServerErrorException, makeImmutable, NotFoundException, ServiceUnavailableException, UnauthorizedException, ValidationException, VertzException, vertz } from "@vertz/core";
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, createModuleDef, createModule, createMiddleware, createImmutableProxy, createEnv, createEntityContext, createCrudHandlers, createAuth, createAccess, action, VertzException, ValidationException, UnauthorizedException, SignUpInput, SignInInput, SessionStrategy, SessionPayload, SessionConfig, Session, ServiceUnavailableException, ServiceFactory, ServiceDef, ServiceBootInstruction, ServerHandle, ServerConfig, ServerAdapter, RouterDef, Resource, ResolveInjectMap, RequestInfo, RawRequest, RateLimitResult, RateLimitConfig, PasswordRequirements, NotFoundException, NamedServiceDef, NamedRouterDef, NamedModuleDef, NamedModule, NamedMiddlewareDef, ModuleDef, ModuleBootInstruction, Module, MiddlewareDef, ListenOptions, ListResult, ListOptions2 as ListOptions, InternalServerErrorException, InferSchema, Infer, HttpStatusCode, HttpMethod, HandlerCtx, ForbiddenException, ExtractMethods, EnvConfig, EntityRouteOptions, EntityRelationsConfig, EntityRegistry, EntityOperations, EntityErrorResult, EntityDefinition, EntityDbAdapter2 as EntityDbAdapter, EntityContext, EntityConfig, EntityActionDef, EntitlementDefinition, Entitlement, EmailPasswordConfig, Deps, DeepReadonly, Ctx, CrudResult, CrudHandlers, CorsConfig, CookieConfig, ConflictException, BootSequence, BootInstruction, BaseContext, BadRequestException, AuthorizationError, AuthUser, AuthInstance, AuthContext, AuthConfig, AuthApi, AppConfig2 as AppConfig, AppBuilder2 as AppBuilder, ActionDefinition, ActionContext, ActionConfig, ActionActionDef, AccumulateProvides, AccessRule, AccessInstance, AccessConfig };
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 true;
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, _resource, user) {
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
- console.warn("Using insecure default JWT secret. Provide jwtSecret in createAuth() config for production.");
302
- jwtSecret = "dev-secret-change-in-production";
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
- if (!origin && !referer) {
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 result = await actionDef.handler(input, ctx, row);
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.1",
3
+ "version": "0.2.4",
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.1",
35
- "@vertz/db": "0.2.1",
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": "latest",
43
+ "bunup": "^0.16.31",
44
44
  "typescript": "^5.7.0",
45
45
  "vitest": "^4.0.18"
46
46
  },