bazis 0.98.6 → 0.98.8

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.
@@ -6,7 +6,8 @@ import { HttpContext } from "./HttpContext/HttpContext";
6
6
  import { holdResponseScope } from "./HttpContext/responseLifetime";
7
7
  import { inspectableRedirectResponse } from "./HttpContext/inspectableRedirects";
8
8
  import { accessLog } from "./Middleware/accessLog";
9
- import { cors, isPreflight, preflightResponse } from "./Middleware/cors";
9
+ import { cors, corsOptionsOf, isPreflight, preflightResponse, type CorsOptions } from "./Middleware/cors";
10
+ import { isCorrelationIdMiddleware } from "./correlation/createCorrelationIdMiddleware";
10
11
  import { errorHandler } from "./Middleware/errorHandler";
11
12
  import { runPipeline } from "./Middleware/pipeline";
12
13
  import { applySecurityHeaders, securityHeaders } from "./Middleware/securityHeaders";
@@ -75,6 +76,8 @@ export class HttpServer implements HostedService {
75
76
  private server?: ReturnType<typeof Bun.serve>;
76
77
  private router?: Router;
77
78
  private readVersion: VersionReader = () => undefined;
79
+ /** CORS options for every preflight: `cors` or a `cors()` among the global middleware. */
80
+ private globalCors: CorsOptions | undefined;
78
81
  /** Body cap in bytes; `undefined` when the limit is disabled (`maxBodyBytes: 0`). */
79
82
  private readonly maxBodyBytes?: number;
80
83
  private readonly requestScopeDisposeTimeoutMs?: number;
@@ -151,6 +154,12 @@ export class HttpServer implements HostedService {
151
154
  if (this.options.securityHeaders !== false) {
152
155
  serverChain.push(securityHeaders(this.options.securityHeaders ?? {}));
153
156
  }
157
+ const serverMiddleware = this.collectServerMiddleware();
158
+ // A global correlation middleware wraps the error boundary, so error
159
+ // responses (401, 403, 500, a thrown NotFoundError) and the responses
160
+ // produced before routing (404, 405, 413, preflight, docs, health) carry
161
+ // the request id, and the error log line runs inside its request context.
162
+ serverChain.push(...serverMiddleware.filter(isCorrelationIdMiddleware));
154
163
  // Unexpected errors go to the application logger, like the access log.
155
164
  const errorLogger = this.resolver.tryResolve(LOGGER);
156
165
  serverChain.push(
@@ -162,7 +171,9 @@ export class HttpServer implements HostedService {
162
171
  }),
163
172
  );
164
173
  this.shortCircuitChain = [...serverChain];
165
- serverChain.push(...this.collectServerMiddleware());
174
+ // A global `cors()` among the middleware answers preflights like `cors`.
175
+ this.globalCors = this.options.cors || serverMiddleware.map(corsOptionsOf).find((options) => options !== undefined);
176
+ serverChain.push(...serverMiddleware.filter((middleware) => !isCorrelationIdMiddleware(middleware)));
166
177
 
167
178
  const builder = new RouterBuilder(
168
179
  serverChain,
@@ -367,9 +378,11 @@ export class HttpServer implements HostedService {
367
378
  }
368
379
  }
369
380
 
370
- if (this.options.cors && isPreflight(request)) {
371
- const corsOptions = this.options.cors;
372
- return this.runShortCircuit(request, url, clientIp, () => preflightResponse(corsOptions, request));
381
+ if (isPreflight(request)) {
382
+ const corsOptions = this.globalCors ?? this.routeCors(request, url);
383
+ if (corsOptions !== undefined) {
384
+ return this.runShortCircuit(request, url, clientIp, () => preflightResponse(corsOptions, request));
385
+ }
373
386
  }
374
387
 
375
388
  const segments = parseRequestPath(url.pathname);
@@ -430,6 +443,15 @@ export class HttpServer implements HostedService {
430
443
  }
431
444
  }
432
445
 
446
+ /** CORS options of the route a preflight asks about (its `Access-Control-Request-Method`). */
447
+ private routeCors(request: Request, url: URL): CorsOptions | undefined {
448
+ const segments = parseRequestPath(url.pathname);
449
+ const method = request.headers.get("access-control-request-method")?.trim().toUpperCase();
450
+ if (!segments || !method) return undefined;
451
+ const match = this.router!.match(method, segments, this.readVersion(request, url));
452
+ return match.kind === "matched" ? match.action.cors : undefined;
453
+ }
454
+
433
455
  private async runShortCircuit(
434
456
  request: Request,
435
457
  url: URL,
@@ -7,7 +7,7 @@ export interface AccessLogEntry {
7
7
  readonly method: string;
8
8
  readonly path: string;
9
9
  readonly status: number;
10
- /** Wall-clock time spent in the pipeline, in milliseconds. */
10
+ /** Wall-clock time spent in the pipeline, in milliseconds, rounded to 0.01. */
11
11
  readonly durationMs: number;
12
12
  /** Correlation id from {@link createCorrelationIdMiddleware}, if present. */
13
13
  readonly requestId?: string;
@@ -73,7 +73,7 @@ export function accessLog(options: AccessLogOptions = {}): HttpMiddleware {
73
73
  method: ctx.method,
74
74
  path: ctx.path,
75
75
  status: ctx.response?.status ?? 0,
76
- durationMs: performance.now() - startedAt,
76
+ durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
77
77
  ...(typeof requestId === "string" ? { requestId } : {}),
78
78
  });
79
79
  }
@@ -22,6 +22,14 @@ export interface CorsOptions {
22
22
 
23
23
  const DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"] as const;
24
24
 
25
+ /** Options of a middleware created by {@link cors}; the server answers preflights with them. */
26
+ const CORS_OPTIONS = Symbol("bazis:http:cors-options");
27
+
28
+ /** Options of a {@link cors} middleware, or undefined for any other middleware. */
29
+ export function corsOptionsOf(middleware: HttpMiddleware): CorsOptions | undefined {
30
+ return (middleware as HttpMiddleware & { [CORS_OPTIONS]?: CorsOptions })[CORS_OPTIONS];
31
+ }
32
+
25
33
  function assertSafeCorsOptions(options: CorsOptions): void {
26
34
  if (!options.credentials) {
27
35
  return;
@@ -119,12 +127,14 @@ export function preflightResponse(options: CorsOptions, request: Request): Respo
119
127
 
120
128
  /**
121
129
  * CORS middleware: adds response headers for cross-origin requests.
122
- * Use globally via `httpModule({ cors })` (which also answers preflights)
123
- * or per controller/route via `@Middleware(cors({...}))`.
130
+ * Use globally via `httpModule({ cors })` or per controller/route via
131
+ * `@Middleware(cors({...}))`. Either way the server answers preflights
132
+ * (`OPTIONS` with `Access-Control-Request-Method`) with these options: a
133
+ * route's own `cors()` applies to preflights of that route.
124
134
  */
125
135
  export function cors(options: CorsOptions = {}): HttpMiddleware {
126
136
  assertSafeCorsOptions(options);
127
- return async (ctx, next) => {
137
+ const middleware: HttpMiddleware = async (ctx, next) => {
128
138
  await next();
129
139
  const requestOrigin = ctx.header("origin");
130
140
  if (!ctx.response) {
@@ -157,4 +167,6 @@ export function cors(options: CorsOptions = {}): HttpMiddleware {
157
167
  }
158
168
  }
159
169
  };
170
+ Object.defineProperty(middleware, CORS_OPTIONS, { value: options });
171
+ return middleware;
160
172
  }
@@ -1,4 +1,5 @@
1
1
  import type { HttpMiddleware } from "../Middleware/types";
2
+ import type { CorsOptions } from "../Middleware/cors";
2
3
  import { HttpSetupError } from "../Errors/HttpError";
3
4
  import { INVALID } from "../Binding/convert";
4
5
  import type { SegmentConstraint, TemplateSegment } from "./template";
@@ -13,6 +14,8 @@ export interface RouteAction {
13
14
  readonly name: string;
14
15
  /** Action-specific body cap in bytes; undefined inherits the server cap. */
15
16
  readonly maxBodyBytes?: number;
17
+ /** Options of the action's or controller's `cors()` middleware, for preflights. */
18
+ readonly cors?: CorsOptions;
16
19
  }
17
20
 
18
21
  interface RouteEntry {
@@ -6,6 +6,7 @@ import { controllerMetaOf, type ActionMeta, type ControllerMeta } from "../Decor
6
6
  import { HttpSetupError } from "../Errors/HttpError";
7
7
  import type { HttpContext } from "../HttpContext/HttpContext";
8
8
  import type { ActionFilterHooks, HttpMiddleware } from "../Middleware/types";
9
+ import { corsOptionsOf } from "../Middleware/cors";
9
10
  import { normalizeResult } from "../Results/normalize";
10
11
  import type { ApiVersioningOptions, RouteMiddlewareComposer } from "../options";
11
12
  import { createAuthorizeComposer } from "../Authorization/authorizeComposer";
@@ -84,6 +85,7 @@ export class RouterBuilder {
84
85
  version,
85
86
  name: `${controllerClass.name}.${String(methodName)}`,
86
87
  maxBodyBytes: action.maxBodyBytes,
88
+ cors: routeCors(meta, action),
87
89
  };
88
90
  for (const declaration of action.routes) {
89
91
  const versionSegment = useUrlVersioning && version !== undefined ? `v${version}` : undefined;
@@ -192,6 +194,19 @@ export class RouterBuilder {
192
194
  * but has no bindings was most likely added or changed after the last codegen:
193
195
  * its arguments would silently get wrong values, so the server refuses to start.
194
196
  */
197
+ /** The action's own `cors()` wins over the controller's. */
198
+ function routeCors(meta: ControllerMeta, action: ActionMeta): ReturnType<typeof corsOptionsOf> {
199
+ for (const middleware of [...action.middleware].reverse()) {
200
+ const options = corsOptionsOf(middleware);
201
+ if (options !== undefined) return options;
202
+ }
203
+ for (const middleware of [...meta.middleware].reverse()) {
204
+ const options = corsOptionsOf(middleware);
205
+ if (options !== undefined) return options;
206
+ }
207
+ return undefined;
208
+ }
209
+
195
210
  function assertBindingsFresh(
196
211
  controllerClass: Class<object>,
197
212
  methodName: string | symbol,
@@ -14,6 +14,14 @@ export interface CorrelationIdMiddlewareOptions {
14
14
  readonly validateIncomingId?: (value: string) => boolean;
15
15
  }
16
16
 
17
+ /** Marks the middleware so the server also applies it to responses produced before routing. */
18
+ const CORRELATION_ID_MIDDLEWARE = Symbol("bazis:http:correlation-id");
19
+
20
+ /** True for a middleware created by {@link createCorrelationIdMiddleware}. */
21
+ export function isCorrelationIdMiddleware(middleware: HttpMiddleware): boolean {
22
+ return (middleware as HttpMiddleware & { [CORRELATION_ID_MIDDLEWARE]?: true })[CORRELATION_ID_MIDDLEWARE] === true;
23
+ }
24
+
17
25
  const DEFAULT_REQUEST_ID = /^[A-Za-z0-9][A-Za-z0-9._:@/+\-=]{0,127}$/;
18
26
  const TRACEPARENT = /^[\da-f]{2}-([\da-f]{32})-([\da-f]{16})-[\da-f]{2}(?:-[\x21-\x7e]+)?$/i;
19
27
 
@@ -21,6 +29,8 @@ const TRACEPARENT = /^[\da-f]{2}-([\da-f]{32})-([\da-f]{16})-[\da-f]{2}(?:-[\x21
21
29
  * Ensures one correlation ID per HTTP request: reads `x-request-id` or
22
30
  * generates a UUID, stores it on {@link HttpContext.state}, echoes it on the
23
31
  * response, and binds it to async local storage for application logs.
32
+ * Registered among the server's global middleware, it also applies to the
33
+ * responses the server produces before routing (404, 405, 413, preflight).
24
34
  */
25
35
  export function createCorrelationIdMiddleware(
26
36
  options: CorrelationIdMiddlewareOptions = {},
@@ -29,7 +39,7 @@ export function createCorrelationIdMiddleware(
29
39
  const generateId = options.generateId ?? (() => crypto.randomUUID());
30
40
  const validateIncomingId = options.validateIncomingId ?? ((value: string) => DEFAULT_REQUEST_ID.test(value));
31
41
 
32
- return async (ctx, next) => {
42
+ const middleware: HttpMiddleware = async (ctx, next) => {
33
43
  const incoming = ctx.header(headerName);
34
44
  const trimmedIncoming = incoming?.trim();
35
45
  const generated = (): string => {
@@ -64,6 +74,8 @@ export function createCorrelationIdMiddleware(
64
74
  });
65
75
  }
66
76
  };
77
+ Object.defineProperty(middleware, CORRELATION_ID_MIDDLEWARE, { value: true });
78
+ return middleware;
67
79
  }
68
80
 
69
81
  function normalizeTraceparent(value: string | undefined): string | undefined {
@@ -232,7 +232,7 @@ export class CodexAppServerClient implements CodexClient {
232
232
  if (method === "account/updated") this.modelCache = undefined;
233
233
  }, () => { this.clearLogin(); this.modelCache = undefined; });
234
234
  const initialized = await client.request("initialize", {
235
- clientInfo: { name: "bazis", title: "Bazis", version: "0.98.6" }, capabilities: { experimentalApi: true },
235
+ clientInfo: { name: "bazis", title: "Bazis", version: "0.98.8" }, capabilities: { experimentalApi: true },
236
236
  }, 15_000, signal);
237
237
  if (resolve(text(initialized.codexHome, 4096)) !== this.paths.home) throw new CodexError("PROTOCOL_ERROR");
238
238
  client.notify("initialized");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bazis",
3
- "version": "0.98.6",
3
+ "version": "0.98.8",
4
4
  "description": "Modular backend framework for Bun in TypeScript: DI wired by codegen, HTTP, a PostgreSQL ORM, JWT, WebSocket, agents and a CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Alenin",