@vertz/core 0.2.0 → 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/README.md CHANGED
@@ -629,6 +629,50 @@ router.post('/send', {
629
629
  });
630
630
  ```
631
631
 
632
+ ### Environment Validation
633
+
634
+ `createEnv` validates environment variables against a schema at startup, returning a frozen, typed configuration object.
635
+
636
+ ```typescript
637
+ import { createEnv } from '@vertz/core';
638
+ import { s } from '@vertz/schema';
639
+
640
+ const env = createEnv({
641
+ schema: s.object({
642
+ DATABASE_URL: s.string(),
643
+ PORT: s.coerce.number().default(3000),
644
+ NODE_ENV: s.enum(['development', 'production', 'test']),
645
+ }),
646
+ });
647
+
648
+ // env.DATABASE_URL — fully typed, validated, immutable
649
+ ```
650
+
651
+ By default, `createEnv` reads from `process.env`. You can pass an explicit `env` record instead — useful for edge runtimes (Cloudflare Workers, Deno Deploy) or testing:
652
+
653
+ ```typescript
654
+ // Edge runtime — pass env explicitly
655
+ const env = createEnv({
656
+ schema: s.object({
657
+ DATABASE_URL: s.string(),
658
+ API_KEY: s.string(),
659
+ }),
660
+ env: context.env, // Cloudflare Workers env bindings
661
+ });
662
+
663
+ // Testing — inject controlled values
664
+ const env = createEnv({
665
+ schema: s.object({ PORT: s.coerce.number() }),
666
+ env: { PORT: '4000' },
667
+ });
668
+ ```
669
+
670
+ | Option | Type | Description |
671
+ |--------|------|-------------|
672
+ | `schema` | `Schema<T>` | A `@vertz/schema` schema to validate against |
673
+ | `env` | `Record<string, string \| undefined>` | Explicit env record. Defaults to `process.env` with a `typeof process` guard for non-Node runtimes |
674
+ | `load` | `string[]` | Dotenv file paths to load before validation |
675
+
632
676
  ### Custom Server Adapters
633
677
 
634
678
  Use the `.handler` property to integrate with custom servers:
package/dist/index.d.ts CHANGED
@@ -39,172 +39,6 @@ declare function createMiddleware<
39
39
  TRequires extends Record<string, unknown> = Record<string, unknown>,
40
40
  TProvides extends Record<string, unknown> = Record<string, unknown>
41
41
  >(def: NamedMiddlewareDef<TRequires, TProvides>): NamedMiddlewareDef<TRequires, TProvides>;
42
- import { Schema as Schema2 } from "@vertz/schema";
43
- interface ModuleDef<
44
- TImports extends Record<string, unknown> = Record<string, unknown>,
45
- TOptions extends Record<string, unknown> = Record<string, unknown>
46
- > {
47
- name: string;
48
- imports?: TImports;
49
- options?: Schema2<TOptions>;
50
- }
51
- interface ServiceDef<
52
- TDeps = unknown,
53
- TState = unknown,
54
- TMethods = unknown,
55
- TOptions extends Record<string, unknown> = Record<string, unknown>,
56
- TEnv extends Record<string, unknown> = Record<string, unknown>
57
- > {
58
- inject?: Record<string, unknown>;
59
- options?: Schema2<TOptions>;
60
- env?: Schema2<TEnv>;
61
- onInit?: (deps: TDeps, opts: TOptions, env: TEnv) => Promise<TState> | TState;
62
- methods: (deps: TDeps, state: TState, opts: TOptions, env: TEnv) => TMethods;
63
- onDestroy?: (deps: TDeps, state: TState) => Promise<void> | void;
64
- }
65
- interface RouterDef<TInject extends Record<string, unknown> = Record<string, unknown>> {
66
- prefix: string;
67
- inject?: TInject;
68
- }
69
- interface Module<TDef extends ModuleDef = ModuleDef> {
70
- definition: TDef;
71
- services: ServiceDef[];
72
- routers: RouterDef[];
73
- exports: ServiceDef[];
74
- }
75
- type Primitive = string | number | boolean | bigint | symbol | undefined | null;
76
- type BuiltinObject = Date | RegExp | Error | Map<unknown, unknown> | Set<unknown> | WeakMap<object, unknown> | WeakSet<object> | Promise<unknown> | Request | Response | Headers | ReadableStream | WritableStream;
77
- type DeepReadonly<T> = unknown extends T ? T : T extends Primitive ? T : T extends BuiltinObject ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>> : { readonly [K in keyof T] : DeepReadonly<T[K]> };
78
- interface RawRequest {
79
- readonly request: Request;
80
- readonly method: string;
81
- readonly url: string;
82
- readonly headers: Headers;
83
- }
84
- interface HandlerCtx {
85
- params: Record<string, unknown>;
86
- body: unknown;
87
- query: Record<string, string>;
88
- headers: Record<string, unknown>;
89
- raw: RawRequest;
90
- options: Record<string, unknown>;
91
- env: Record<string, unknown>;
92
- [key: string]: unknown;
93
- }
94
- type Deps<T extends Record<string, unknown>> = DeepReadonly<T>;
95
- type Ctx<T extends Record<string, unknown>> = DeepReadonly<T>;
96
- interface NamedServiceDef<
97
- TDeps = unknown,
98
- TState = unknown,
99
- TMethods = unknown,
100
- TOptions extends Record<string, unknown> = Record<string, unknown>,
101
- TEnv extends Record<string, unknown> = Record<string, unknown>
102
- > extends ServiceDef<TDeps, TState, TMethods, TOptions, TEnv> {
103
- moduleName: string;
104
- }
105
- type InferOutput<
106
- T,
107
- TDefault = unknown
108
- > = T extends {
109
- _output: infer O;
110
- } ? O : T extends {
111
- parse(v: unknown): infer P;
112
- } ? P : TDefault;
113
- type TypedHandlerCtx<
114
- TParams = unknown,
115
- TQuery = unknown,
116
- THeaders = unknown,
117
- TBody = unknown,
118
- TMiddleware extends Record<string, unknown> = Record<string, unknown>
119
- > = Omit<HandlerCtx, "params" | "query" | "headers" | "body"> & {
120
- params: TParams;
121
- query: TQuery;
122
- headers: THeaders;
123
- body: TBody;
124
- } & TMiddleware;
125
- interface RouteConfig<
126
- TParams = unknown,
127
- TQuery = unknown,
128
- THeaders = unknown,
129
- TBody = unknown,
130
- TMiddleware extends Record<string, unknown> = Record<string, unknown>
131
- > {
132
- params?: TParams;
133
- body?: TBody;
134
- query?: TQuery;
135
- response?: unknown;
136
- /** Error schemas for errors-as-values pattern. Keys are HTTP status codes. */
137
- errors?: Record<number, unknown>;
138
- headers?: THeaders;
139
- middlewares?: unknown[];
140
- handler: (ctx: TypedHandlerCtx<InferOutput<TParams>, InferOutput<TQuery, Record<string, string>>, InferOutput<THeaders>, InferOutput<TBody>, TMiddleware>) => unknown;
141
- }
142
- interface Route {
143
- method: string;
144
- path: string;
145
- config: RouteConfig<unknown, unknown, unknown, unknown, Record<string, unknown>>;
146
- }
147
- /**
148
- * Extracts the TMethods type from a NamedServiceDef or ServiceDef.
149
- * Returns `unknown` for non-service types.
150
- */
151
- type ExtractMethods<T> = T extends NamedServiceDef<any, any, infer M> ? M : T extends ServiceDef<any, any, infer M> ? M : unknown;
152
- /**
153
- * Resolves an inject map by extracting TMethods from each NamedServiceDef value.
154
- * Given `{ userService: NamedServiceDef<..., ..., UserMethods> }`,
155
- * produces `{ userService: UserMethods }`.
156
- */
157
- type ResolveInjectMap<T extends Record<string, unknown>> = { [K in keyof T] : ExtractMethods<T[K]> };
158
- type HttpMethodFn<
159
- TMiddleware extends Record<string, unknown> = Record<string, unknown>,
160
- TInject extends Record<string, unknown> = Record<string, unknown>
161
- > = <
162
- TParams,
163
- TQuery,
164
- THeaders,
165
- TBody
166
- >(path: `/${string}`, config: RouteConfig<TParams, TQuery, THeaders, TBody, TMiddleware & ResolveInjectMap<TInject>>) => NamedRouterDef<TMiddleware, TInject>;
167
- interface NamedRouterDef<
168
- TMiddleware extends Record<string, unknown> = Record<string, unknown>,
169
- TInject extends Record<string, unknown> = Record<string, unknown>
170
- > extends RouterDef<TInject> {
171
- moduleName: string;
172
- routes: Route[];
173
- get: HttpMethodFn<TMiddleware, TInject>;
174
- post: HttpMethodFn<TMiddleware, TInject>;
175
- put: HttpMethodFn<TMiddleware, TInject>;
176
- patch: HttpMethodFn<TMiddleware, TInject>;
177
- delete: HttpMethodFn<TMiddleware, TInject>;
178
- head: HttpMethodFn<TMiddleware, TInject>;
179
- }
180
- interface NamedModuleDef<
181
- TImports extends Record<string, unknown> = Record<string, unknown>,
182
- TOptions extends Record<string, unknown> = Record<string, unknown>,
183
- TMiddleware extends Record<string, unknown> = Record<string, unknown>
184
- > extends ModuleDef<TImports, TOptions> {
185
- service: <
186
- TDeps,
187
- TState,
188
- TMethods
189
- >(config: ServiceDef<TDeps, TState, TMethods>) => NamedServiceDef<TDeps, TState, TMethods>;
190
- router: <TInject extends Record<string, unknown> = Record<string, unknown>>(config: RouterDef<TInject>) => NamedRouterDef<TMiddleware, TInject>;
191
- }
192
- declare function createModuleDef<
193
- TImports extends Record<string, unknown> = Record<string, unknown>,
194
- TOptions extends Record<string, unknown> = Record<string, unknown>,
195
- TMiddleware extends Record<string, unknown> = Record<string, unknown>
196
- >(config: ModuleDef<TImports, TOptions>): NamedModuleDef<TImports, TOptions, TMiddleware>;
197
- interface NamedModule {
198
- definition: NamedModuleDef;
199
- services: NamedServiceDef[];
200
- routers: NamedRouterDef<any, any>[];
201
- exports: NamedServiceDef[];
202
- }
203
- declare function createModule(definition: NamedModuleDef, config: {
204
- services: NamedServiceDef[];
205
- routers: NamedRouterDef<any, any>[];
206
- exports: NamedServiceDef[];
207
- }): NamedModule;
208
42
  interface CorsConfig {
209
43
  origins?: string | string[] | boolean;
210
44
  methods?: string[];
@@ -213,25 +47,52 @@ interface CorsConfig {
213
47
  maxAge?: number;
214
48
  exposedHeaders?: string[];
215
49
  }
216
- interface DomainDefinition {
50
+ interface EntityDefinition {
51
+ readonly kind?: string;
217
52
  readonly name: string;
218
- readonly type: string;
219
- readonly table: unknown;
220
- readonly exposedRelations: Record<string, unknown>;
53
+ readonly model: unknown;
221
54
  readonly access: Record<string, unknown>;
222
- readonly handlers: Record<string, unknown>;
55
+ readonly before: Record<string, unknown>;
56
+ readonly after: Record<string, unknown>;
223
57
  readonly actions: Record<string, unknown>;
58
+ readonly relations: Record<string, unknown>;
59
+ }
60
+ type SchemaLike = {
61
+ parse(value: unknown): {
62
+ ok: boolean;
63
+ data?: unknown;
64
+ error?: unknown;
65
+ };
66
+ };
67
+ /**
68
+ * An entity route entry generated by @vertz/server's route generator.
69
+ * Core doesn't know about entity internals — it just registers these as handlers.
70
+ */
71
+ interface EntityRouteEntry {
72
+ method: string;
73
+ path: string;
74
+ handler: (ctx: Record<string, unknown>) => unknown;
75
+ paramsSchema?: SchemaLike;
76
+ bodySchema?: SchemaLike;
77
+ querySchema?: SchemaLike;
78
+ headersSchema?: SchemaLike;
79
+ responseSchema?: SchemaLike;
80
+ errorsSchema?: Record<number, SchemaLike>;
224
81
  }
225
82
  interface AppConfig {
226
83
  basePath?: string;
227
84
  version?: string;
228
85
  cors?: CorsConfig;
229
- /** Domain definitions for auto-CRUD route generation */
230
- domains?: DomainDefinition[];
231
- /** API prefix for domain routes (default: '/api/') */
86
+ /** Entity definitions for auto-CRUD route generation */
87
+ entities?: EntityDefinition[];
88
+ /** API prefix for entity routes (default: '/api/') */
232
89
  apiPrefix?: string;
233
90
  /** Enable response schema validation in dev mode (logs warnings but doesn't break response) */
234
91
  validateResponses?: boolean;
92
+ /** Internal: pre-built entity route handlers injected by @vertz/server */
93
+ _entityRoutes?: EntityRouteEntry[];
94
+ /** Internal: factory for creating DB adapters per entity (used by @vertz/server) */
95
+ _entityDbFactory?: (entityDef: EntityDefinition) => unknown;
235
96
  }
236
97
  interface ListenOptions {
237
98
  hostname?: string;
@@ -249,8 +110,7 @@ interface RouteInfo {
249
110
  method: string;
250
111
  path: string;
251
112
  }
252
- interface AppBuilder<TMiddlewareCtx extends Record<string, unknown> = Record<string, unknown>> {
253
- register(module: NamedModule, options?: Record<string, unknown>): AppBuilder<TMiddlewareCtx>;
113
+ interface AppBuilder<_TMiddlewareCtx extends Record<string, unknown> = Record<string, unknown>> {
254
114
  middlewares<const M extends readonly NamedMiddlewareDef<any, any>[]>(list: M): AppBuilder<AccumulateProvides<M>>;
255
115
  readonly handler: (request: Request) => Promise<Response>;
256
116
  listen(port?: number, options?: ListenOptions): Promise<ServerHandle>;
@@ -260,36 +120,32 @@ interface AppBuilder<TMiddlewareCtx extends Record<string, unknown> = Record<str
260
120
  };
261
121
  }
262
122
  declare function createApp(config: AppConfig): AppBuilder;
263
- type ServiceFactory<
264
- TDeps = unknown,
265
- TState = unknown,
266
- TMethods = unknown,
267
- TOptions extends Record<string, unknown> = Record<string, unknown>,
268
- TEnv extends Record<string, unknown> = Record<string, unknown>
269
- > = ServiceDef<TDeps, TState, TMethods, TOptions, TEnv>;
270
- interface ServiceBootInstruction {
271
- type: "service";
272
- id: string;
273
- deps: string[];
274
- factory: ServiceFactory;
275
- options?: Record<string, unknown>;
276
- env?: Record<string, unknown>;
277
- }
278
- interface ModuleBootInstruction {
279
- type: "module";
280
- id: string;
281
- services: string[];
282
- options?: Record<string, unknown>;
123
+ type Primitive = string | number | boolean | bigint | symbol | undefined | null;
124
+ type BuiltinObject = Date | RegExp | Error | Map<unknown, unknown> | Set<unknown> | WeakMap<object, unknown> | WeakSet<object> | Promise<unknown> | Request | Response | Headers | ReadableStream | WritableStream;
125
+ type DeepReadonly<T> = unknown extends T ? T : T extends Primitive ? T : T extends BuiltinObject ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepReadonly<U>> : T extends Array<infer U> ? ReadonlyArray<DeepReadonly<U>> : { readonly [K in keyof T] : DeepReadonly<T[K]> };
126
+ interface RawRequest {
127
+ readonly request: Request;
128
+ readonly method: string;
129
+ readonly url: string;
130
+ readonly headers: Headers;
283
131
  }
284
- type BootInstruction = ServiceBootInstruction | ModuleBootInstruction;
285
- interface BootSequence {
286
- instructions: BootInstruction[];
287
- shutdownOrder: string[];
132
+ interface HandlerCtx {
133
+ params: Record<string, unknown>;
134
+ body: unknown;
135
+ query: Record<string, string>;
136
+ headers: Record<string, unknown>;
137
+ raw: RawRequest;
138
+ options: Record<string, unknown>;
139
+ env: Record<string, unknown>;
140
+ [key: string]: unknown;
288
141
  }
289
- import { Schema as Schema3 } from "@vertz/schema";
142
+ type Deps<T extends Record<string, unknown>> = DeepReadonly<T>;
143
+ type Ctx<T extends Record<string, unknown>> = DeepReadonly<T>;
144
+ import { Schema as Schema2 } from "@vertz/schema";
290
145
  interface EnvConfig<T = unknown> {
291
146
  load?: string[];
292
- schema: Schema3<T>;
147
+ schema: Schema2<T>;
148
+ env?: Record<string, string | undefined>;
293
149
  }
294
150
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
295
151
  type HttpStatusCode = 200 | 201 | 204 | 301 | 302 | 304 | 400 | 401 | 403 | 404 | 405 | 409 | 422 | 429 | 500 | 502 | 503 | 504;
@@ -300,7 +156,13 @@ declare class VertzException extends Error {
300
156
  readonly code: string;
301
157
  readonly details?: unknown;
302
158
  constructor(message: string, statusCode?: number, code?: string, details?: unknown);
303
- toJSON(): Record<string, unknown>;
159
+ toJSON(): {
160
+ error: {
161
+ code: string;
162
+ message: string;
163
+ details?: unknown;
164
+ };
165
+ };
304
166
  }
305
167
  declare class BadRequestException extends VertzException {
306
168
  constructor(message: string, details?: unknown);
@@ -326,7 +188,16 @@ declare class ValidationException extends VertzException {
326
188
  path: string;
327
189
  message: string;
328
190
  }>);
329
- toJSON(): Record<string, unknown>;
191
+ toJSON(): {
192
+ error: {
193
+ code: string;
194
+ message: string;
195
+ details?: ReadonlyArray<{
196
+ path: string;
197
+ message: string;
198
+ }>;
199
+ };
200
+ };
330
201
  }
331
202
  declare class InternalServerErrorException extends VertzException {
332
203
  constructor(message: string, details?: unknown);
@@ -340,128 +211,11 @@ declare function makeImmutable<T extends object>(obj: T, contextName: string): D
340
211
  declare const vertz: {
341
212
  readonly env: typeof createEnv;
342
213
  readonly middleware: typeof createMiddleware;
343
- readonly moduleDef: typeof createModuleDef;
344
- readonly module: typeof createModule;
345
214
  readonly app: typeof createApp;
346
215
  /** @since 0.2.0 — preferred alias for `app` */
347
216
  readonly server: typeof createApp;
348
217
  };
349
218
  /**
350
- * Symbol used to brand Result objects to prevent accidental matches with user data.
351
- * Using a Symbol makes it impossible for user objects to accidentally match isResult().
352
- */
353
- declare const RESULT_BRAND: unique symbol;
354
- /**
355
- * Result type for explicit error handling in route handlers.
356
- *
357
- * This provides an alternative to exception-based error handling,
358
- * making error cases visible in type signatures.
359
- *
360
- * @example
361
- * ```typescript
362
- * router.get('/:id', {
363
- * handler: async (ctx) => {
364
- * const user = await ctx.userService.find(ctx.params.id);
365
- * if (!user) {
366
- * return err(404, { message: 'User not found' });
367
- * }
368
- * return ok({ id: user.id, name: user.name });
369
- * }
370
- * });
371
- * ```
372
- */
373
- /**
374
- * Represents a successful result containing data.
375
- */
376
- interface Ok<T> {
377
- readonly ok: true;
378
- readonly data: T;
379
- readonly [RESULT_BRAND]: true;
380
- }
381
- /**
382
- * Represents an error result containing status code and error body.
383
- */
384
- interface Err<E> {
385
- readonly ok: false;
386
- readonly status: number;
387
- readonly body: E;
388
- readonly [RESULT_BRAND]: true;
389
- }
390
- /**
391
- * A discriminated union type representing either a success (Ok) or failure (Err).
392
- *
393
- * @typeParam T - The type of the success data
394
- * @typeParam E - The type of the error body
395
- *
396
- * @example
397
- * ```typescript
398
- * type UserResult = Result<{ id: number; name: string }, { message: string }>;
399
- * ```
400
- */
401
- type Result<
402
- T,
403
- E = unknown
404
- > = Ok<T> | Err<E>;
405
- /**
406
- * Creates a successful Result containing the given data.
407
- *
408
- * @param data - The success data
409
- * @returns An Ok result with the data
410
- *
411
- * @example
412
- * ```typescript
413
- * return ok({ id: 1, name: 'John' });
414
- * ```
415
- */
416
- declare function ok<T>(data: T): Ok<T>;
417
- /**
418
- * Creates an error Result with the given status code and body.
419
- *
420
- * @param status - HTTP status code for the error
421
- * @param body - Error body/response
422
- * @returns An Err result with status and body
423
- *
424
- * @example
425
- * ```typescript
426
- * return err(404, { message: 'Not found' });
427
- * ```
428
- */
429
- declare function err<E>(status: number, body: E): Err<E>;
430
- /**
431
- * Type guard to check if a Result is Ok.
432
- *
433
- * @param result - The result to check
434
- * @returns True if the result is Ok
435
- *
436
- * @example
437
- * ```typescript
438
- * if (isOk(result)) {
439
- * console.log(result.data);
440
- * }
441
- * ```
442
- */
443
- declare function isOk<
444
- T,
445
- E
446
- >(result: Result<T, E>): result is Ok<T>;
447
- /**
448
- * Type guard to check if a Result is Err.
449
- *
450
- * @param result - The result to check
451
- * @returns True if the result is Err
452
- *
453
- * @example
454
- * ```typescript
455
- * if (isErr(result)) {
456
- * console.log(result.status, result.body);
457
- * }
458
- * ```
459
- */
460
- declare function isErr<
461
- T,
462
- E
463
- >(result: Result<T, E>): result is Err<E>;
464
- /**
465
219
  * Creates an HTTP server. Preferred entry point for building Vertz services.
466
220
  * @since 0.2.0
467
221
  */
@@ -470,4 +224,4 @@ declare const createServer: (config: AppConfig) => AppBuilder;
470
224
  * @deprecated Use `createServer` instead. `createApp` will be removed in v0.3.0.
471
225
  */
472
226
  declare const createApp2: (config: AppConfig) => AppBuilder;
473
- export { vertz, ok, makeImmutable, isOk, isErr, err, deepFreeze, createServer, createModuleDef, createModule, createMiddleware, createImmutableProxy, createEnv, createApp2 as createApp, VertzException, ValidationException, UnauthorizedException, ServiceUnavailableException, ServiceFactory, ServiceDef, ServiceBootInstruction, ServerHandle, ServerAdapter, RouterDef, RouteInfo, Result, ResolveInjectMap, RawRequest, Ok, NotFoundException, NamedServiceDef, NamedRouterDef, NamedModuleDef, NamedModule, NamedMiddlewareDef, ModuleDef, ModuleBootInstruction, Module, MiddlewareDef, ListenOptions, InternalServerErrorException, Infer2 as InferSchema, Infer, HttpStatusCode, HttpMethod, HandlerCtx, ForbiddenException, ExtractMethods, Err, EnvConfig, Deps, DeepReadonly, Ctx, CorsConfig, ConflictException, BootSequence, BootInstruction, BadRequestException, AppConfig, AppBuilder, AccumulateProvides };
227
+ export { vertz, makeImmutable, deepFreeze, createServer, createMiddleware, createImmutableProxy, createEnv, createApp2 as createApp, VertzException, ValidationException, UnauthorizedException, ServiceUnavailableException, ServerHandle, ServerAdapter, RouteInfo, RawRequest, NotFoundException, NamedMiddlewareDef, MiddlewareDef, ListenOptions, InternalServerErrorException, Infer2 as InferSchema, Infer, HttpStatusCode, HttpMethod, HandlerCtx, ForbiddenException, EnvConfig, EntityRouteEntry, Deps, DeepReadonly, Ctx, CorsConfig, ConflictException, BadRequestException, AppConfig, AppBuilder, AccumulateProvides };
package/dist/index.js CHANGED
@@ -18,7 +18,19 @@ import {
18
18
  parseBody,
19
19
  parseRequest,
20
20
  runMiddlewareChain
21
- } from "./shared/chunk-c77pg5gx.js";
21
+ } from "./shared/chunk-m3w3ytn5.js";
22
+
23
+ // src/result.ts
24
+ var RESULT_BRAND = Symbol.for("vertz.result");
25
+ function isOk(result) {
26
+ return result.ok === true;
27
+ }
28
+ function isResult(value) {
29
+ if (value === null || typeof value !== "object")
30
+ return false;
31
+ const obj = value;
32
+ return obj[RESULT_BRAND] === true;
33
+ }
22
34
 
23
35
  // src/server/cors.ts
24
36
  var DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
@@ -37,27 +49,34 @@ function resolveOrigin(config, requestOrigin) {
37
49
  return null;
38
50
  }
39
51
  function handleCors(config, request) {
52
+ if (config.credentials && (config.origins === true || config.origins === "*")) {
53
+ throw new Error("CORS misconfiguration: credentials cannot be used with wildcard origins. " + "Browsers will reject the response. Use an explicit origin allowlist instead.");
54
+ }
40
55
  const requestOrigin = request.headers.get("origin");
41
56
  const origin = resolveOrigin(config, requestOrigin);
42
57
  if (request.method === "OPTIONS") {
43
58
  const headers = {};
44
- if (origin)
59
+ if (origin) {
45
60
  headers["access-control-allow-origin"] = origin;
46
- const methods = config.methods ?? DEFAULT_METHODS;
47
- headers["access-control-allow-methods"] = methods.join(", ");
48
- const allowHeaders = config.headers ?? DEFAULT_HEADERS;
49
- headers["access-control-allow-headers"] = allowHeaders.join(", ");
50
- if (config.maxAge !== undefined) {
51
- headers["access-control-max-age"] = String(config.maxAge);
52
- }
53
- if (config.credentials) {
54
- headers["access-control-allow-credentials"] = "true";
61
+ const methods = config.methods ?? DEFAULT_METHODS;
62
+ headers["access-control-allow-methods"] = methods.join(", ");
63
+ const allowHeaders = config.headers ?? DEFAULT_HEADERS;
64
+ headers["access-control-allow-headers"] = allowHeaders.join(", ");
65
+ if (config.maxAge !== undefined) {
66
+ headers["access-control-max-age"] = String(config.maxAge);
67
+ }
68
+ if (config.credentials) {
69
+ headers["access-control-allow-credentials"] = "true";
70
+ }
55
71
  }
56
72
  return new Response(null, { status: 204, headers });
57
73
  }
58
74
  return null;
59
75
  }
60
76
  function applyCorsHeaders(config, request, response) {
77
+ if (config.credentials && (config.origins === true || config.origins === "*")) {
78
+ throw new Error("CORS misconfiguration: credentials cannot be used with wildcard origins. " + "Browsers will reject the response. Use an explicit origin allowlist instead.");
79
+ }
61
80
  const requestOrigin = request.headers.get("origin");
62
81
  const origin = resolveOrigin(config, requestOrigin);
63
82
  if (!origin)
@@ -77,27 +96,6 @@ function applyCorsHeaders(config, request, response) {
77
96
  });
78
97
  }
79
98
 
80
- // src/result.ts
81
- var RESULT_BRAND = Symbol.for("vertz.result");
82
- function ok(data) {
83
- return { ok: true, data, [RESULT_BRAND]: true };
84
- }
85
- function err(status, body) {
86
- return { ok: false, status, body, [RESULT_BRAND]: true };
87
- }
88
- function isOk(result) {
89
- return result.ok === true;
90
- }
91
- function isErr(result) {
92
- return result.ok === false;
93
- }
94
- function isResult(value) {
95
- if (value === null || typeof value !== "object")
96
- return false;
97
- const obj = value;
98
- return obj[RESULT_BRAND] === true;
99
- }
100
-
101
99
  // src/app/app-runner.ts
102
100
  function createResponseWithCors(data, status, config, request) {
103
101
  const response = createJsonResponse(data, status);
@@ -106,36 +104,40 @@ function createResponseWithCors(data, status, config, request) {
106
104
  }
107
105
  return response;
108
106
  }
109
- function validateSchema(schema, value, label) {
110
- try {
111
- return schema.parse(value);
112
- } catch (error) {
113
- if (error instanceof BadRequestException)
114
- throw error;
115
- const message = error instanceof Error ? error.message : `Invalid ${label}`;
116
- throw new BadRequestException(message);
107
+ var INTERNAL_DETAIL_PATTERNS = [
108
+ /(?:^|[\s:])\/[\w./-]+/,
109
+ /[A-Z]:\\[\w\\.-]+/,
110
+ /node_modules/,
111
+ /at\s+\w+\s+\(/,
112
+ /\.ts:\d+:\d+/,
113
+ /\.js:\d+:\d+/
114
+ ];
115
+ function sanitizeValidationMessage(error, label) {
116
+ if (error != null && typeof error === "object" && "issues" in error && Array.isArray(error.issues)) {
117
+ const issues = error.issues;
118
+ const messages = issues.map((issue) => typeof issue.message === "string" ? issue.message : "").filter(Boolean);
119
+ if (messages.length > 0) {
120
+ return messages.join(", ");
121
+ }
117
122
  }
118
- }
119
- function resolveServices(registrations) {
120
- const serviceMap = new Map;
121
- for (const { module, options } of registrations) {
122
- for (const service of module.services) {
123
- if (!serviceMap.has(service)) {
124
- let parsedOptions = {};
125
- if (service.options && options) {
126
- const parsed = service.options.safeParse(options);
127
- if (parsed.success) {
128
- parsedOptions = parsed.data;
129
- } else {
130
- throw new Error(`Invalid options for service ${service.moduleName}: ${parsed.error.issues.map((i) => i.message).join(", ")}`);
131
- }
132
- }
133
- const env = {};
134
- serviceMap.set(service, service.methods({}, undefined, parsedOptions, env));
135
- }
123
+ const message = error instanceof Error ? error.message : "";
124
+ if (!message) {
125
+ return `Invalid ${label}`;
126
+ }
127
+ for (const pattern of INTERNAL_DETAIL_PATTERNS) {
128
+ if (pattern.test(message)) {
129
+ return `Invalid ${label}`;
136
130
  }
137
131
  }
138
- return serviceMap;
132
+ return message;
133
+ }
134
+ function validateSchema(schema, value, label) {
135
+ const result = schema.parse(value);
136
+ if (!result.ok) {
137
+ const message = sanitizeValidationMessage(result.error, label);
138
+ throw new BadRequestException(message);
139
+ }
140
+ return result.data;
139
141
  }
140
142
  function resolveMiddlewares(globalMiddlewares) {
141
143
  return globalMiddlewares.map((mw) => ({
@@ -144,51 +146,26 @@ function resolveMiddlewares(globalMiddlewares) {
144
146
  resolvedInject: {}
145
147
  }));
146
148
  }
147
- function resolveRouterServices(inject, serviceMap) {
148
- if (!inject)
149
- return {};
150
- const resolved = {};
151
- for (const [name, serviceDef] of Object.entries(inject)) {
152
- const methods = serviceMap.get(serviceDef);
153
- if (methods)
154
- resolved[name] = methods;
155
- }
156
- return resolved;
157
- }
158
- function registerRoutes(trie, basePath, registrations, serviceMap) {
159
- for (const { module, options } of registrations) {
160
- for (const router of module.routers) {
161
- const resolvedServices = resolveRouterServices(router.inject, serviceMap);
162
- for (const route of router.routes) {
163
- const fullPath = basePath + router.prefix + route.path;
164
- const routeMiddlewares = (route.config.middlewares ?? []).map((mw) => ({
165
- name: mw.name,
166
- handler: mw.handler,
167
- resolvedInject: {}
168
- }));
169
- const entry = {
170
- handler: route.config.handler,
171
- options: options ?? {},
172
- services: resolvedServices,
173
- middlewares: routeMiddlewares,
174
- paramsSchema: route.config.params,
175
- bodySchema: route.config.body,
176
- querySchema: route.config.query,
177
- headersSchema: route.config.headers,
178
- responseSchema: route.config.response,
179
- errorsSchema: route.config.errors
180
- };
181
- trie.add(route.method, fullPath, entry);
182
- }
183
- }
184
- }
185
- }
186
- function buildHandler(config, registrations, globalMiddlewares) {
149
+ function buildHandler(config, globalMiddlewares) {
187
150
  const trie = new Trie;
188
- const basePath = config.basePath ?? "";
189
151
  const resolvedMiddlewares = resolveMiddlewares(globalMiddlewares);
190
- const serviceMap = resolveServices(registrations);
191
- registerRoutes(trie, basePath, registrations, serviceMap);
152
+ if (config._entityRoutes) {
153
+ for (const route of config._entityRoutes) {
154
+ const entry = {
155
+ handler: route.handler,
156
+ options: {},
157
+ services: {},
158
+ middlewares: [],
159
+ paramsSchema: route.paramsSchema,
160
+ bodySchema: route.bodySchema,
161
+ querySchema: route.querySchema,
162
+ headersSchema: route.headersSchema,
163
+ responseSchema: route.responseSchema,
164
+ errorsSchema: route.errorsSchema
165
+ };
166
+ trie.add(route.method, route.path, entry);
167
+ }
168
+ }
192
169
  return async (request) => {
193
170
  try {
194
171
  if (config.cors) {
@@ -201,9 +178,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
201
178
  if (!match) {
202
179
  const allowed = trie.getAllowedMethods(parsed.path);
203
180
  if (allowed.length > 0) {
204
- return createJsonResponse({ error: "MethodNotAllowed", message: "Method Not Allowed", statusCode: 405 }, 405, { allow: allowed.join(", ") });
181
+ return createJsonResponse({ error: { code: "MethodNotAllowed", message: "Method Not Allowed" } }, 405, { allow: allowed.join(", ") });
205
182
  }
206
- return createJsonResponse({ error: "NotFound", message: "Not Found", statusCode: 404 }, 404);
183
+ return createJsonResponse({ error: { code: "NotFound", message: "Not Found" } }, 404);
207
184
  }
208
185
  const body = await parseBody(request);
209
186
  const raw = {
@@ -224,7 +201,11 @@ function buildHandler(config, registrations, globalMiddlewares) {
224
201
  if (entry.middlewares.length > 0) {
225
202
  const routeCtx = { ...requestCtx, ...middlewareState };
226
203
  const routeState = await runMiddlewareChain(entry.middlewares, routeCtx);
227
- Object.assign(middlewareState, routeState);
204
+ for (const key of Object.keys(routeState)) {
205
+ if (key !== "__proto__" && key !== "constructor" && key !== "prototype") {
206
+ middlewareState[key] = routeState[key];
207
+ }
208
+ }
228
209
  }
229
210
  const validatedParams = entry.paramsSchema ? validateSchema(entry.paramsSchema, match.params, "params") : match.params;
230
211
  const validatedBody = entry.bodySchema ? validateSchema(entry.bodySchema, body, "body") : body;
@@ -246,10 +227,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
246
227
  if (isOk(result)) {
247
228
  const data = result.data;
248
229
  if (config.validateResponses && entry.responseSchema) {
249
- try {
250
- entry.responseSchema.parse(data);
251
- } catch (error) {
252
- const message = error instanceof Error ? error.message : "Response schema validation failed";
230
+ const validation = entry.responseSchema.parse(data);
231
+ if (!validation.ok) {
232
+ const message = validation.error instanceof Error ? validation.error.message : "Response schema validation failed";
253
233
  console.warn(`[vertz] Response validation warning: ${message}`);
254
234
  }
255
235
  }
@@ -260,10 +240,9 @@ function buildHandler(config, registrations, globalMiddlewares) {
260
240
  if (config.validateResponses && entry.errorsSchema) {
261
241
  const errorSchema = entry.errorsSchema[errorStatus];
262
242
  if (errorSchema) {
263
- try {
264
- errorSchema.parse(errorBody);
265
- } catch (error) {
266
- const message = error instanceof Error ? `Error schema validation failed for status ${errorStatus}: ${error.message}` : `Error schema validation failed for status ${errorStatus}`;
243
+ const validation = errorSchema.parse(errorBody);
244
+ if (!validation.ok) {
245
+ const message = validation.error instanceof Error ? `Error schema validation failed for status ${errorStatus}: ${validation.error.message}` : `Error schema validation failed for status ${errorStatus}`;
267
246
  console.warn(`[vertz] Response validation warning: ${message}`);
268
247
  }
269
248
  }
@@ -272,14 +251,13 @@ function buildHandler(config, registrations, globalMiddlewares) {
272
251
  }
273
252
  }
274
253
  if (config.validateResponses && entry.responseSchema) {
275
- try {
276
- entry.responseSchema.parse(result);
277
- } catch (error) {
278
- const message = error instanceof Error ? error.message : "Response schema validation failed";
254
+ const validation = entry.responseSchema.parse(result);
255
+ if (!validation.ok) {
256
+ const message = validation.error instanceof Error ? validation.error.message : "Response schema validation failed";
279
257
  console.warn(`[vertz] Response validation warning: ${message}`);
280
258
  }
281
259
  }
282
- const response = result === undefined ? new Response(null, { status: 204 }) : createJsonResponse(result);
260
+ const response = result === undefined ? new Response(null, { status: 204 }) : result instanceof Response ? result : createJsonResponse(result);
283
261
  if (config.cors) {
284
262
  return applyCorsHeaders(config.cors, request, response);
285
263
  }
@@ -323,27 +301,6 @@ function detectAdapter(hints) {
323
301
  }
324
302
 
325
303
  // src/app/route-log.ts
326
- function normalizePath(path) {
327
- let normalized = path.replace(/\/+/g, "/");
328
- if (normalized.length > 1 && normalized.endsWith("/")) {
329
- normalized = normalized.slice(0, -1);
330
- }
331
- return normalized || "/";
332
- }
333
- function collectRoutes(basePath, registrations) {
334
- const routes = [];
335
- for (const { module } of registrations) {
336
- for (const router of module.routers) {
337
- for (const route of router.routes) {
338
- routes.push({
339
- method: route.method,
340
- path: normalizePath(basePath + router.prefix + route.path)
341
- });
342
- }
343
- }
344
- }
345
- return routes;
346
- }
347
304
  function formatRouteLog(listenUrl, routes) {
348
305
  const header = `vertz server listening on ${listenUrl}`;
349
306
  if (routes.length === 0) {
@@ -363,28 +320,18 @@ function formatRouteLog(listenUrl, routes) {
363
320
  // src/app/app-builder.ts
364
321
  var DEFAULT_PORT = 3000;
365
322
  function createApp(config) {
366
- const registrations = [];
367
323
  let globalMiddlewares = [];
368
324
  let cachedHandler = null;
369
325
  const registeredRoutes = [];
326
+ const entityRoutes = [];
370
327
  const builder = {
371
- register(module, options) {
372
- registrations.push({ module, options });
373
- for (const router of module.routers) {
374
- for (const route of router.routes) {
375
- registeredRoutes.push({ method: route.method, path: router.prefix + route.path });
376
- }
377
- }
378
- cachedHandler = null;
379
- return builder;
380
- },
381
328
  middlewares(list) {
382
329
  globalMiddlewares = [...list];
383
330
  return builder;
384
331
  },
385
332
  get handler() {
386
333
  if (!cachedHandler) {
387
- cachedHandler = buildHandler(config, registrations, globalMiddlewares);
334
+ cachedHandler = buildHandler(config, globalMiddlewares);
388
335
  }
389
336
  return cachedHandler;
390
337
  },
@@ -395,35 +342,45 @@ function createApp(config) {
395
342
  const adapter = detectAdapter();
396
343
  const serverHandle = await adapter.listen(port ?? DEFAULT_PORT, builder.handler, options);
397
344
  if (options?.logRoutes !== false) {
398
- const routes = collectRoutes(config.basePath ?? "", registrations);
399
345
  const url = `http://${serverHandle.hostname}:${serverHandle.port}`;
400
- console.log(formatRouteLog(url, routes));
346
+ console.log(formatRouteLog(url, entityRoutes));
401
347
  }
402
348
  return serverHandle;
403
349
  }
404
350
  };
405
- if (config.domains && config.domains.length > 0) {
351
+ if (config._entityRoutes) {
352
+ for (const route of config._entityRoutes) {
353
+ const info = { method: route.method, path: route.path };
354
+ registeredRoutes.push(info);
355
+ entityRoutes.push(info);
356
+ }
357
+ } else if (config.entities && config.entities.length > 0) {
406
358
  const rawPrefix = config.apiPrefix === undefined ? "/api/" : config.apiPrefix;
407
- for (const domain of config.domains) {
408
- const domainPath = rawPrefix === "" ? "/" + domain.name : (rawPrefix.endsWith("/") ? rawPrefix : rawPrefix + "/") + domain.name;
409
- registeredRoutes.push({ method: "GET", path: domainPath });
410
- registeredRoutes.push({ method: "GET", path: `${domainPath}/:id` });
411
- registeredRoutes.push({ method: "POST", path: domainPath });
412
- registeredRoutes.push({ method: "PUT", path: `${domainPath}/:id` });
413
- registeredRoutes.push({ method: "DELETE", path: `${domainPath}/:id` });
414
- if (domain.actions) {
415
- for (const actionName of Object.keys(domain.actions)) {
416
- registeredRoutes.push({ method: "POST", path: `${domainPath}/:id/${actionName}` });
359
+ for (const entity of config.entities) {
360
+ const entityPath = rawPrefix === "" ? `/${entity.name}` : (rawPrefix.endsWith("/") ? rawPrefix : `${rawPrefix}/`) + entity.name;
361
+ const routes = [
362
+ { method: "GET", path: entityPath },
363
+ { method: "GET", path: `${entityPath}/:id` },
364
+ { method: "POST", path: entityPath },
365
+ { method: "PATCH", path: `${entityPath}/:id` },
366
+ { method: "DELETE", path: `${entityPath}/:id` }
367
+ ];
368
+ if (entity.actions) {
369
+ for (const actionName of Object.keys(entity.actions)) {
370
+ routes.push({ method: "POST", path: `${entityPath}/:id/${actionName}` });
417
371
  }
418
372
  }
373
+ registeredRoutes.push(...routes);
374
+ entityRoutes.push(...routes);
419
375
  }
420
376
  }
421
377
  return builder;
422
378
  }
423
379
  // src/env/env-validator.ts
424
380
  function createEnv(config) {
425
- const result = config.schema.safeParse(process.env);
426
- if (!result.success) {
381
+ const envRecord = config.env ?? (typeof process !== "undefined" ? process.env : {});
382
+ const result = config.schema.safeParse(envRecord);
383
+ if (!result.ok) {
427
384
  throw new Error(`Environment validation failed:
428
385
  ${result.error.message}`);
429
386
  }
@@ -433,72 +390,10 @@ ${result.error.message}`);
433
390
  function createMiddleware(def) {
434
391
  return deepFreeze(def);
435
392
  }
436
- // src/module/module.ts
437
- function validateOwnership(items, kind, expectedModule) {
438
- for (const item of items) {
439
- if (item.moduleName !== expectedModule) {
440
- throw new Error(`${kind} belongs to module "${item.moduleName}", cannot add to module "${expectedModule}"`);
441
- }
442
- }
443
- }
444
- function createModule(definition, config) {
445
- validateOwnership(config.services, "Service", definition.name);
446
- validateOwnership(config.routers, "Router", definition.name);
447
- for (const exp of config.exports) {
448
- if (!config.services.includes(exp)) {
449
- throw new Error("exports must be a subset of services");
450
- }
451
- }
452
- return deepFreeze({
453
- definition,
454
- ...config
455
- });
456
- }
457
- // src/module/router-def.ts
458
- var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head"];
459
- function createRouterDef(moduleName, config) {
460
- const routes = [];
461
- function addRoute(method, path, routeConfig) {
462
- if (!path.startsWith("/")) {
463
- throw new Error(`Route path must start with '/', got '${path}'`);
464
- }
465
- routes.push({ method, path, config: routeConfig });
466
- return router;
467
- }
468
- const router = {
469
- ...config,
470
- moduleName,
471
- routes
472
- };
473
- for (const method of HTTP_METHODS) {
474
- router[method] = (path, cfg) => addRoute(method.toUpperCase(), path, cfg);
475
- }
476
- return router;
477
- }
478
-
479
- // src/module/service.ts
480
- function createServiceDef(moduleName, config) {
481
- return deepFreeze({
482
- ...config,
483
- moduleName
484
- });
485
- }
486
-
487
- // src/module/module-def.ts
488
- function createModuleDef(config) {
489
- const def = {
490
- ...config,
491
- service: (serviceConfig) => createServiceDef(config.name, serviceConfig),
492
- router: (routerConfig) => createRouterDef(config.name, routerConfig)
493
- };
494
- return deepFreeze(def);
495
- }
496
393
  // src/vertz.ts
497
394
  var vertz = /* @__PURE__ */ deepFreeze({
498
395
  env: createEnv,
499
396
  middleware: createMiddleware,
500
- moduleDef: createModuleDef,
501
- module: createModule,
502
397
  app: createApp,
503
398
  server: createApp
504
399
  });
@@ -511,15 +406,9 @@ var createApp2 = (...args) => {
511
406
  };
512
407
  export {
513
408
  vertz,
514
- ok,
515
409
  makeImmutable,
516
- isOk,
517
- isErr,
518
- err,
519
410
  deepFreeze,
520
411
  createServer,
521
- createModuleDef,
522
- createModule,
523
412
  createMiddleware,
524
413
  createImmutableProxy,
525
414
  createEnv,
@@ -53,7 +53,7 @@ interface ParsedRequest {
53
53
  raw: Request;
54
54
  }
55
55
  declare function parseRequest(request: Request): ParsedRequest;
56
- declare function parseBody(request: Request): Promise<unknown>;
56
+ declare function parseBody(request: Request, maxBodySize?: number): Promise<unknown>;
57
57
  declare function createJsonResponse(data: unknown, status?: number, headers?: Record<string, string>): Response;
58
58
  declare function createErrorResponse(error: unknown): Response;
59
59
  export { runMiddlewareChain, parseRequest, parseBody, createJsonResponse, createErrorResponse, buildCtx, Trie, ResolvedMiddleware };
package/dist/internals.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  parseBody,
7
7
  parseRequest,
8
8
  runMiddlewareChain
9
- } from "./shared/chunk-c77pg5gx.js";
9
+ } from "./shared/chunk-m3w3ytn5.js";
10
10
  export {
11
11
  runMiddlewareChain,
12
12
  parseRequest,
@@ -41,7 +41,7 @@ function deepFreeze(obj, visited = new WeakSet) {
41
41
 
42
42
  // src/immutability/make-immutable.ts
43
43
  function makeImmutable(obj, contextName) {
44
- if (true) {
44
+ if (typeof process !== "undefined" && true) {
45
45
  return createImmutableProxy(obj, contextName);
46
46
  }
47
47
  return obj;
@@ -65,7 +65,7 @@ function validateCollisions(config) {
65
65
  }
66
66
  }
67
67
  function buildCtx(config) {
68
- if (true) {
68
+ if (typeof process !== "undefined" && true) {
69
69
  validateCollisions(config);
70
70
  }
71
71
  return makeImmutable({
@@ -98,11 +98,11 @@ class VertzException extends Error {
98
98
  }
99
99
  toJSON() {
100
100
  return {
101
- error: this.name,
102
- message: this.message,
103
- statusCode: this.statusCode,
104
- code: this.code,
105
- ...this.details !== undefined && { details: this.details }
101
+ error: {
102
+ code: this.code,
103
+ message: this.message,
104
+ ...this.details !== undefined && { details: this.details }
105
+ }
106
106
  };
107
107
  }
108
108
  }
@@ -110,70 +110,81 @@ class VertzException extends Error {
110
110
  // src/exceptions/http-exceptions.ts
111
111
  class BadRequestException extends VertzException {
112
112
  constructor(message, details) {
113
- super(message, 400, undefined, details);
113
+ super(message, 400, "BadRequest", details);
114
114
  }
115
115
  }
116
116
 
117
117
  class UnauthorizedException extends VertzException {
118
118
  constructor(message, details) {
119
- super(message, 401, undefined, details);
119
+ super(message, 401, "Unauthorized", details);
120
120
  }
121
121
  }
122
122
 
123
123
  class ForbiddenException extends VertzException {
124
124
  constructor(message, details) {
125
- super(message, 403, undefined, details);
125
+ super(message, 403, "Forbidden", details);
126
126
  }
127
127
  }
128
128
 
129
129
  class NotFoundException extends VertzException {
130
130
  constructor(message, details) {
131
- super(message, 404, undefined, details);
131
+ super(message, 404, "NotFound", details);
132
132
  }
133
133
  }
134
134
 
135
135
  class ConflictException extends VertzException {
136
136
  constructor(message, details) {
137
- super(message, 409, undefined, details);
137
+ super(message, 409, "Conflict", details);
138
138
  }
139
139
  }
140
140
 
141
141
  class ValidationException extends VertzException {
142
142
  errors;
143
143
  constructor(errors) {
144
- super("Validation failed", 422, undefined, undefined);
144
+ super("Validation failed", 422, "ValidationError", undefined);
145
145
  this.errors = errors;
146
146
  }
147
147
  toJSON() {
148
148
  return {
149
- ...super.toJSON(),
150
- errors: this.errors
149
+ error: {
150
+ code: this.code,
151
+ message: this.message,
152
+ details: this.errors
153
+ }
151
154
  };
152
155
  }
153
156
  }
154
157
 
155
158
  class InternalServerErrorException extends VertzException {
156
159
  constructor(message, details) {
157
- super(message, 500, undefined, details);
160
+ super(message, 500, "InternalError", details);
158
161
  }
159
162
  }
160
163
 
161
164
  class ServiceUnavailableException extends VertzException {
162
165
  constructor(message, details) {
163
- super(message, 503, undefined, details);
166
+ super(message, 503, "ServiceUnavailable", details);
164
167
  }
165
168
  }
166
169
  // src/middleware/middleware-runner.ts
170
+ var DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);
167
171
  function isPlainObject(value) {
168
172
  return value !== null && typeof value === "object" && !Array.isArray(value);
169
173
  }
174
+ function safeAssign(target, source) {
175
+ for (const key of Object.keys(source)) {
176
+ if (!DANGEROUS_KEYS.has(key)) {
177
+ target[key] = source[key];
178
+ }
179
+ }
180
+ }
170
181
  async function runMiddlewareChain(middlewares, requestCtx) {
171
- const accumulated = {};
182
+ const accumulated = Object.create(null);
172
183
  for (const mw of middlewares) {
173
184
  const ctx = { ...requestCtx, ...mw.resolvedInject, ...accumulated };
174
185
  const contribution = await mw.handler(ctx);
175
186
  if (isPlainObject(contribution)) {
176
- Object.assign(accumulated, contribution);
187
+ safeAssign(accumulated, contribution);
177
188
  }
178
189
  }
179
190
  return accumulated;
@@ -297,11 +308,16 @@ function parseRequest(request) {
297
308
  method: request.method,
298
309
  path: url.pathname,
299
310
  query: Object.fromEntries(url.searchParams),
300
- headers: Object.fromEntries(request.headers),
311
+ headers: Object.fromEntries([...request.headers].map(([k, v]) => [k.toLowerCase(), v])),
301
312
  raw: request
302
313
  };
303
314
  }
304
- async function parseBody(request) {
315
+ var DEFAULT_MAX_BODY_SIZE = 10 * 1024 * 1024;
316
+ async function parseBody(request, maxBodySize = DEFAULT_MAX_BODY_SIZE) {
317
+ const contentLength = parseInt(request.headers.get("content-length") ?? "0", 10);
318
+ if (maxBodySize && contentLength > maxBodySize) {
319
+ throw new BadRequestException("Request body too large");
320
+ }
305
321
  const contentType = request.headers.get("content-type") ?? "";
306
322
  if (contentType.includes("application/json")) {
307
323
  try {
@@ -325,7 +341,7 @@ function createJsonResponse(data, status = 200, headers) {
325
341
  return new Response(JSON.stringify(data), {
326
342
  status,
327
343
  headers: {
328
- "content-type": "application/json",
344
+ "content-type": "application/json; charset=utf-8",
329
345
  ...headers
330
346
  }
331
347
  });
@@ -334,7 +350,7 @@ function createErrorResponse(error) {
334
350
  if (error instanceof VertzException) {
335
351
  return createJsonResponse(error.toJSON(), error.statusCode);
336
352
  }
337
- return createJsonResponse({ error: "InternalServerError", message: "Internal Server Error", statusCode: 500 }, 500);
353
+ return createJsonResponse({ error: { code: "InternalServerError", message: "Internal Server Error" } }, 500);
338
354
  }
339
355
 
340
356
  export { createImmutableProxy, deepFreeze, makeImmutable, buildCtx, VertzException, BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException, ConflictException, ValidationException, InternalServerErrorException, ServiceUnavailableException, runMiddlewareChain, Trie, parseRequest, parseBody, createJsonResponse, createErrorResponse };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Vertz core framework primitives",
@@ -30,17 +30,18 @@
30
30
  ],
31
31
  "scripts": {
32
32
  "build": "bunup",
33
- "test": "vitest run",
33
+ "test": "bun test",
34
+ "test:coverage": "vitest run --coverage",
34
35
  "test:watch": "vitest",
35
36
  "typecheck": "tsc --noEmit"
36
37
  },
37
38
  "dependencies": {
38
- "@vertz/schema": "workspace:*"
39
+ "@vertz/schema": "0.2.2"
39
40
  },
40
41
  "devDependencies": {
41
- "@types/node": "^22.0.0",
42
+ "@types/node": "^25.3.1",
42
43
  "@vitest/coverage-v8": "^4.0.18",
43
- "bunup": "latest",
44
+ "bunup": "^0.16.31",
44
45
  "typescript": "^5.7.0",
45
46
  "vitest": "^4.0.18"
46
47
  },