adorn-api 1.1.12 → 1.1.14

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.
Files changed (43) hide show
  1. package/README.md +614 -913
  2. package/dist/adapter/express/controllers.d.ts +3 -1
  3. package/dist/adapter/express/controllers.js +4 -1
  4. package/dist/adapter/express/index.js +5 -1
  5. package/dist/adapter/express/types.d.ts +3 -0
  6. package/dist/adapter/fastify/controllers.d.ts +3 -1
  7. package/dist/adapter/fastify/controllers.js +2 -25
  8. package/dist/adapter/fastify/index.js +7 -1
  9. package/dist/adapter/fastify/types.d.ts +3 -0
  10. package/dist/adapter/metal-orm/index.d.ts +1 -1
  11. package/dist/adapter/metal-orm/types.d.ts +23 -0
  12. package/dist/adapter/native/controllers.d.ts +3 -0
  13. package/dist/adapter/native/controllers.js +2 -25
  14. package/dist/adapter/native/index.js +14 -1
  15. package/dist/adapter/native/types.d.ts +3 -0
  16. package/dist/core/auth.d.ts +33 -3
  17. package/dist/core/auth.js +74 -22
  18. package/dist/core/openapi.d.ts +2 -0
  19. package/dist/core/openapi.js +19 -1
  20. package/examples/bearer-auth-swagger/app.ts +28 -0
  21. package/examples/bearer-auth-swagger/auth.controller.ts +45 -0
  22. package/examples/bearer-auth-swagger/index.ts +20 -0
  23. package/examples/bearer-auth-swagger/session.dtos.ts +19 -0
  24. package/package.json +3 -1
  25. package/src/adapter/express/controllers.ts +23 -18
  26. package/src/adapter/express/index.ts +12 -1
  27. package/src/adapter/express/types.ts +13 -10
  28. package/src/adapter/fastify/controllers.ts +16 -41
  29. package/src/adapter/fastify/index.ts +27 -13
  30. package/src/adapter/fastify/types.ts +13 -10
  31. package/src/adapter/metal-orm/index.ts +3 -0
  32. package/src/adapter/metal-orm/types.ts +25 -0
  33. package/src/adapter/native/controllers.ts +16 -41
  34. package/src/adapter/native/index.ts +28 -15
  35. package/src/adapter/native/types.ts +13 -10
  36. package/src/core/auth.ts +134 -56
  37. package/src/core/openapi.ts +22 -1
  38. package/tests/e2e/bearer-auth.e2e.test.ts +158 -0
  39. package/tests/typecheck/query-params.typecheck.ts +42 -0
  40. package/tests/unit/auth.test.ts +96 -12
  41. package/tests/unit/openapi-parameters.test.ts +54 -6
  42. package/tsconfig.typecheck.json +8 -0
  43. package/vitest.config.ts +47 -7
@@ -0,0 +1,20 @@
1
+ import { createApp } from "./app";
2
+ import { startExampleServer } from "../utils/start-server";
3
+
4
+ async function start() {
5
+ const app = await createApp();
6
+ startExampleServer(app, {
7
+ name: "Bearer Auth Swagger Demo",
8
+ port: 3001,
9
+ extraLogs: [
10
+ (port) => `Swagger UI: http://localhost:${port}/docs`,
11
+ (port) => `OpenAPI JSON: http://localhost:${port}/openapi.json`,
12
+ () => "Try tokens in Swagger Authorize: user-token or admin-token"
13
+ ]
14
+ });
15
+ }
16
+
17
+ start().catch((err) => {
18
+ console.error(err);
19
+ process.exit(1);
20
+ });
@@ -0,0 +1,19 @@
1
+ import { Dto, Field, t } from "../../src";
2
+
3
+ @Dto({ description: "Authenticated user session returned by protected routes." })
4
+ export class SessionDto {
5
+ @Field(t.string({ description: "User identifier." }))
6
+ id!: string;
7
+
8
+ @Field(t.array(t.string(), { description: "Roles granted to the bearer token." }))
9
+ roles!: string[];
10
+
11
+ @Field(t.string({ description: "Human-readable route result." }))
12
+ message!: string;
13
+ }
14
+
15
+ @Dto({ description: "Public health response." })
16
+ export class PublicStatusDto {
17
+ @Field(t.string())
18
+ status!: string;
19
+ }
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "adorn-api",
3
- "version": "1.1.12",
3
+ "version": "1.1.14",
4
4
  "description": "Decorator-first web framework with OpenAPI 3.1 schema generation.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc -p tsconfig.json",
9
+ "pretest": "npm run build",
9
10
  "dev": "tsx examples/basic/index.ts",
10
11
  "test": "vitest run",
11
12
  "test:watch": "vitest",
13
+ "typecheck:tests": "tsc -p tsconfig.typecheck.json --noEmit",
12
14
  "example": "node scripts/run-example.js",
13
15
  "check": "tsc -p tsconfig.json --noEmit && npm test",
14
16
  "lint": "eslint . --ext .ts,.tsx,.js"
@@ -1,8 +1,9 @@
1
1
  import type { Express, Request, Response, NextFunction } from "express";
2
- import type { Constructor } from "../../core/types";
3
- import type { SchemaSource } from "../../core/schema";
4
- import { getControllerMeta } from "../../core/metadata";
5
- import { isHttpError, type HttpError } from "../../core/errors";
2
+ import type { Constructor } from "../../core/types";
3
+ import type { SchemaSource } from "../../core/schema";
4
+ import { getControllerMeta } from "../../core/metadata";
5
+ import { assertRouteAuthorized, getRouteAuthMeta } from "../../core/auth";
6
+ import { isHttpError, type HttpError } from "../../core/errors";
6
7
  import { isHttpResponse } from "../../core/response";
7
8
  import type { InputCoercionSetting, MultipartOptions, RequestContext, ValidationOptions } from "./types";
8
9
  import { createInputCoercer } from "./coercion";
@@ -25,13 +26,14 @@ import { ValidationErrors, isValidationErrors } from "../../core/validation-erro
25
26
  * @param inputCoercion - Input coercion setting
26
27
  * @param multipart - Multipart file upload configuration
27
28
  */
28
- export async function attachControllers(
29
- app: Express,
30
- controllers: Constructor[],
31
- inputCoercion: InputCoercionSetting = "safe",
32
- multipart?: boolean | MultipartOptions,
33
- validation?: boolean | ValidationOptions
34
- ): Promise<void> {
29
+ export async function attachControllers(
30
+ app: Express,
31
+ controllers: Constructor[],
32
+ inputCoercion: InputCoercionSetting = "safe",
33
+ multipart?: boolean | MultipartOptions,
34
+ validation?: boolean | ValidationOptions,
35
+ auth?: { userProperty?: string }
36
+ ): Promise<void> {
35
37
  const multipartOptions = normalizeMultipartOptions(multipart);
36
38
  for (const controller of controllers) {
37
39
  const meta = getControllerMeta(controller);
@@ -68,13 +70,16 @@ export async function attachControllers(
68
70
  middlewares.push(createMultipartMiddleware(route.files!, multipartOptions));
69
71
  }
70
72
 
71
- // Determine if validation is enabled for this route
72
- const isValidationEnabled = validation !== false && (validation as ValidationOptions)?.enabled !== false;
73
-
74
- // Main route handler
75
- const routeHandler = async (req: Request, res: Response, next: NextFunction) => {
76
- try {
77
- const files = extractFiles(req);
73
+ // Determine if validation is enabled for this route
74
+ const isValidationEnabled = validation !== false && (validation as ValidationOptions)?.enabled !== false;
75
+ const authMeta = getRouteAuthMeta(controller, route.handlerName);
76
+
77
+ // Main route handler
78
+ const routeHandler = async (req: Request, res: Response, next: NextFunction) => {
79
+ try {
80
+ await assertRouteAuthorized(authMeta, req, auth);
81
+
82
+ const files = extractFiles(req);
78
83
 
79
84
  // Create context
80
85
  const ctx = {
@@ -4,6 +4,7 @@ import { attachCors } from "./cors";
4
4
  import { attachControllers } from "./controllers";
5
5
  import { attachOpenApi } from "./openapi";
6
6
  import { lifecycleRegistry } from "../../core/lifecycle";
7
+ import { createBearerAuthMiddleware } from "../../core/auth";
7
8
 
8
9
  export * from "./types";
9
10
  export { attachCors } from "./cors";
@@ -23,8 +24,18 @@ export async function createExpressApp(options: ExpressAdapterOptions): Promise<
23
24
  if (options.jsonBody ?? true) {
24
25
  app.use(express.json({ limit: options.jsonLimit }));
25
26
  }
27
+ if (options.bearerAuth) {
28
+ app.use(createBearerAuthMiddleware(options.bearerAuth));
29
+ }
26
30
  const inputCoercion = options.inputCoercion ?? "safe";
27
- await attachControllers(app, options.controllers, inputCoercion, options.multipart, options.validation);
31
+ await attachControllers(
32
+ app,
33
+ options.controllers,
34
+ inputCoercion,
35
+ options.multipart,
36
+ options.validation,
37
+ { userProperty: options.bearerAuth?.userProperty }
38
+ );
28
39
  if (options.openApi) {
29
40
  attachOpenApi(app, options.controllers, options.openApi);
30
41
  }
@@ -1,9 +1,10 @@
1
- import type {
2
- Constructor,
3
- RequestContext as CoreRequestContext,
4
- UploadedFileInfo
5
- } from "../../core/types";
6
- import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
1
+ import type {
2
+ Constructor,
3
+ RequestContext as CoreRequestContext,
4
+ UploadedFileInfo
5
+ } from "../../core/types";
6
+ import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
7
+ import type { BearerAuthOptions } from "../../core/auth";
7
8
 
8
9
  export { UploadedFileInfo };
9
10
 
@@ -112,10 +113,12 @@ export interface ExpressAdapterOptions {
112
113
  openApi?: OpenApiExpressOptions;
113
114
  /** Input coercion setting */
114
115
  inputCoercion?: InputCoercionSetting;
115
- /** CORS configuration. Set to true for permissive defaults, or provide options. */
116
- cors?: boolean | CorsOptions;
117
- /** Multipart file upload configuration. Set to true for defaults, or provide options. */
118
- multipart?: boolean | MultipartOptions;
116
+ /** CORS configuration. Set to true for permissive defaults, or provide options. */
117
+ cors?: boolean | CorsOptions;
118
+ /** Built-in bearer token authentication for protected routes. */
119
+ bearerAuth?: BearerAuthOptions;
120
+ /** Multipart file upload configuration. Set to true for defaults, or provide options. */
121
+ multipart?: boolean | MultipartOptions;
119
122
  /** Validation configuration. Set to false to disable validation, or provide options. */
120
123
  validation?: boolean | ValidationOptions;
121
124
  }
@@ -1,9 +1,9 @@
1
1
  import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
2
- import type { Constructor, RequestContext } from "../../core/types";
3
- import type { SchemaSource } from "../../core/schema";
4
- import { getControllerMeta } from "../../core/metadata";
5
- import { getRouteAuthMeta } from "../../core/auth";
6
- import { isHttpError, HttpError } from "../../core/errors";
2
+ import type { Constructor, RequestContext } from "../../core/types";
3
+ import type { SchemaSource } from "../../core/schema";
4
+ import { getControllerMeta } from "../../core/metadata";
5
+ import { assertRouteAuthorized, getRouteAuthMeta } from "../../core/auth";
6
+ import { isHttpError } from "../../core/errors";
7
7
  import { isHttpResponse } from "../../core/response";
8
8
  import type { InputCoercionSetting, MultipartOptions, ValidationOptions } from "./types";
9
9
  import { createInputCoercer } from "./coercion";
@@ -21,13 +21,14 @@ import { ValidationErrors, isValidationErrors } from "../../core/validation-erro
21
21
  /**
22
22
  * Attaches controllers to a Fastify application.
23
23
  */
24
- export async function attachControllers(
25
- app: FastifyInstance,
26
- controllers: Constructor[],
27
- inputCoercion: InputCoercionSetting = "safe",
28
- multipart?: boolean | MultipartOptions,
29
- validation?: boolean | ValidationOptions
30
- ): Promise<void> {
24
+ export async function attachControllers(
25
+ app: FastifyInstance,
26
+ controllers: Constructor[],
27
+ inputCoercion: InputCoercionSetting = "safe",
28
+ multipart?: boolean | MultipartOptions,
29
+ validation?: boolean | ValidationOptions,
30
+ auth?: { userProperty?: string }
31
+ ): Promise<void> {
31
32
  const multipartOptions = normalizeMultipartOptions(multipart);
32
33
 
33
34
  for (const controller of controllers) {
@@ -68,35 +69,9 @@ export async function attachControllers(
68
69
  method: route.httpMethod.toUpperCase() as any,
69
70
  url: path,
70
71
  handler: async (req: FastifyRequest, reply: FastifyReply) => {
71
- try {
72
- // Apply auth guard if metadata exists
73
- if (authMeta && authMeta.requiresAuth && !authMeta.isPublic) {
74
- const user = (req as any).user || (req.raw as any).user;
75
- if (!user) {
76
- throw new HttpError(401, "Unauthorized");
77
- }
78
-
79
- if (authMeta.roles?.length) {
80
- const hasRole = authMeta.roles.some((role: string) => user.roles?.includes(role));
81
- if (!hasRole) {
82
- throw new HttpError(403, "Insufficient permissions");
83
- }
84
- }
85
-
86
- if (authMeta.allRoles?.length) {
87
- const hasAllRoles = authMeta.allRoles.every((role: string) => user.roles?.includes(role));
88
- if (!hasAllRoles) {
89
- throw new HttpError(403, "Insufficient permissions");
90
- }
91
- }
92
-
93
- if (authMeta.guard) {
94
- const allowed = await authMeta.guard(user, req);
95
- if (!allowed) {
96
- throw new HttpError(403, "Access denied by guard");
97
- }
98
- }
99
- }
72
+ try {
73
+ // Apply auth guard if metadata exists
74
+ await assertRouteAuthorized(authMeta, req, auth);
100
75
 
101
76
  let files: any = undefined;
102
77
  if (multipartOptions && hasFileUploads(route.files)) {
@@ -2,9 +2,10 @@ import fastify from "fastify";
2
2
  import type { FastifyAdapterOptions } from "./types";
3
3
  import { attachControllers } from "./controllers";
4
4
  import { attachOpenApi } from "./openapi";
5
- import { lifecycleRegistry } from "../../core/lifecycle";
6
- import cors from "@fastify/cors";
7
- import multipart from "@fastify/multipart";
5
+ import { lifecycleRegistry } from "../../core/lifecycle";
6
+ import { authenticateBearerRequest } from "../../core/auth";
7
+ import cors from "@fastify/cors";
8
+ import multipart from "@fastify/multipart";
8
9
 
9
10
  export * from "./types";
10
11
  export { attachControllers } from "./controllers";
@@ -24,16 +25,29 @@ export async function createFastifyApp(options: FastifyAdapterOptions): Promise<
24
25
  app.register(cors, options.cors === true ? {} : options.cors);
25
26
  }
26
27
 
27
- if (options.multipart) {
28
- app.register(multipart, {
29
- limits: {
30
- fileSize: typeof options.multipart === "object" ? options.multipart.maxFileSize : undefined
31
- }
32
- });
33
- }
34
-
35
- const inputCoercion = options.inputCoercion ?? "safe";
36
- await attachControllers(app, options.controllers, inputCoercion, options.multipart, options.validation);
28
+ if (options.multipart) {
29
+ app.register(multipart, {
30
+ limits: {
31
+ fileSize: typeof options.multipart === "object" ? options.multipart.maxFileSize : undefined
32
+ }
33
+ });
34
+ }
35
+
36
+ if (options.bearerAuth) {
37
+ app.addHook("preHandler", async (req) => {
38
+ await authenticateBearerRequest(req, options.bearerAuth!);
39
+ });
40
+ }
41
+
42
+ const inputCoercion = options.inputCoercion ?? "safe";
43
+ await attachControllers(
44
+ app,
45
+ options.controllers,
46
+ inputCoercion,
47
+ options.multipart,
48
+ options.validation,
49
+ { userProperty: options.bearerAuth?.userProperty }
50
+ );
37
51
 
38
52
  if (options.openApi) {
39
53
  attachOpenApi(app, options.controllers, options.openApi);
@@ -1,9 +1,10 @@
1
- import type {
2
- Constructor,
3
- RequestContext as CoreRequestContext,
4
- UploadedFileInfo
5
- } from "../../core/types";
6
- import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
1
+ import type {
2
+ Constructor,
3
+ RequestContext as CoreRequestContext,
4
+ UploadedFileInfo
5
+ } from "../../core/types";
6
+ import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
7
+ import type { BearerAuthOptions } from "../../core/auth";
7
8
 
8
9
  /**
9
10
  * Request context provided to Fastify route handlers.
@@ -110,10 +111,12 @@ export interface FastifyAdapterOptions {
110
111
  openApi?: OpenApiFastifyOptions;
111
112
  /** Input coercion setting */
112
113
  inputCoercion?: InputCoercionSetting;
113
- /** CORS configuration. Set to true for permissive defaults, or provide options. */
114
- cors?: boolean | CorsOptions;
115
- /** Multipart file upload configuration. Set to true for defaults, or provide options. */
116
- multipart?: boolean | MultipartOptions;
114
+ /** CORS configuration. Set to true for permissive defaults, or provide options. */
115
+ cors?: boolean | CorsOptions;
116
+ /** Built-in bearer token authentication for protected routes. */
117
+ bearerAuth?: BearerAuthOptions;
118
+ /** Multipart file upload configuration. Set to true for defaults, or provide options. */
119
+ multipart?: boolean | MultipartOptions;
117
120
  /** Validation configuration. Set to false to disable validation, or provide options. */
118
121
  validation?: boolean | ValidationOptions;
119
122
  }
@@ -78,6 +78,7 @@ export type {
78
78
  PaginationConfig,
79
79
  PaginationOptions,
80
80
  ParsedPagination,
81
+ PaginationQueryParams,
81
82
  Filter,
82
83
  FilterMapping,
83
84
  FilterFieldMapping,
@@ -89,6 +90,8 @@ export type {
89
90
  ParseSortOptions,
90
91
  ParsedSort,
91
92
  SortDirection,
93
+ SortingQueryParams,
94
+ PagedQueryParams,
92
95
  CrudListSortTerm,
93
96
  RunPagedListOptions,
94
97
  ExecuteCrudListOptions,
@@ -74,6 +74,16 @@ export interface ParsedPagination {
74
74
  pageSize: number;
75
75
  }
76
76
 
77
+ /**
78
+ * Pagination query params for consumer-side TypeScript interfaces.
79
+ */
80
+ export interface PaginationQueryParams {
81
+ /** Page number */
82
+ page?: number;
83
+ /** Page size */
84
+ pageSize?: number;
85
+ }
86
+
77
87
  /**
78
88
  * Filter field mapping.
79
89
  */
@@ -155,6 +165,21 @@ export interface ParseFilterOptions<T = Record<string, unknown>> {
155
165
  */
156
166
  export type SortDirection = "asc" | "desc";
157
167
 
168
+ /**
169
+ * Sorting query params for consumer-side TypeScript interfaces.
170
+ */
171
+ export interface SortingQueryParams {
172
+ /** Requested sort key */
173
+ sortBy?: string;
174
+ /** Sort direction */
175
+ sortDirection?: SortDirection;
176
+ }
177
+
178
+ /**
179
+ * Combined pagination + sorting query params.
180
+ */
181
+ export interface PagedQueryParams extends PaginationQueryParams, SortingQueryParams {}
182
+
158
183
  /**
159
184
  * Sort parsing options.
160
185
  */
@@ -1,9 +1,9 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type { Constructor, RequestContext } from "../../core/types";
3
- import type { SchemaSource } from "../../core/schema";
4
- import { getControllerMeta } from "../../core/metadata";
5
- import { getRouteAuthMeta } from "../../core/auth";
6
- import { isHttpError, HttpError } from "../../core/errors";
2
+ import type { Constructor, RequestContext } from "../../core/types";
3
+ import type { SchemaSource } from "../../core/schema";
4
+ import { getControllerMeta } from "../../core/metadata";
5
+ import { assertRouteAuthorized, getRouteAuthMeta } from "../../core/auth";
6
+ import { isHttpError } from "../../core/errors";
7
7
  import { isHttpResponse } from "../../core/response";
8
8
  import type { InputCoercionSetting, ValidationOptions, RequestContext as NativeRequestContext } from "./types";
9
9
  import { createInputCoercer } from "./coercion";
@@ -45,13 +45,14 @@ export async function dispatchRequest(
45
45
  match: any,
46
46
  options: {
47
47
  inputCoercion: InputCoercionSetting;
48
- validation?: boolean | ValidationOptions;
49
- body?: any;
50
- query?: Record<string, any>;
51
- }
52
- ): Promise<void> {
53
- const { controller: instance, route, params: rawParams } = match;
54
- const { inputCoercion, validation, body: rawBody, query: rawQuery } = options;
48
+ validation?: boolean | ValidationOptions;
49
+ body?: any;
50
+ query?: Record<string, any>;
51
+ auth?: { userProperty?: string };
52
+ }
53
+ ): Promise<void> {
54
+ const { controller: instance, route, params: rawParams } = match;
55
+ const { inputCoercion, validation, body: rawBody, query: rawQuery, auth } = options;
55
56
 
56
57
  const handler = instance[route.handlerName];
57
58
  if (typeof handler !== "function") {
@@ -75,35 +76,9 @@ export async function dispatchRequest(
75
76
 
76
77
  const authMeta = getRouteAuthMeta(instance.constructor as Constructor, route.handlerName);
77
78
 
78
- try {
79
- // Apply auth guard if metadata exists
80
- if (authMeta && authMeta.requiresAuth && !authMeta.isPublic) {
81
- const user = (req as any).user;
82
- if (!user) {
83
- throw new HttpError(401, "Unauthorized");
84
- }
85
-
86
- if (authMeta.roles?.length) {
87
- const hasRole = authMeta.roles.some((role: string) => user.roles?.includes(role));
88
- if (!hasRole) {
89
- throw new HttpError(403, "Insufficient permissions");
90
- }
91
- }
92
-
93
- if (authMeta.allRoles?.length) {
94
- const hasAllRoles = authMeta.allRoles.every((role: string) => user.roles?.includes(role));
95
- if (!hasAllRoles) {
96
- throw new HttpError(403, "Insufficient permissions");
97
- }
98
- }
99
-
100
- if (authMeta.guard) {
101
- const allowed = await authMeta.guard(user, req);
102
- if (!allowed) {
103
- throw new HttpError(403, "Access denied by guard");
104
- }
105
- }
106
- }
79
+ try {
80
+ // Apply auth guard if metadata exists
81
+ await assertRouteAuthorized(authMeta, req, auth);
107
82
 
108
83
  const body = (coerceBody && rawBody) ? coerceBody(rawBody) : rawBody;
109
84
  const query = (coerceQuery && rawQuery) ? coerceQuery(rawQuery) : rawQuery;
@@ -1,9 +1,10 @@
1
1
  import { createServer, IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { NativeAdapterOptions, NativeApp } from "./types";
3
3
  import { registerControllers, dispatchRequest } from "./controllers";
4
- import { registerOpenApi } from "./openapi";
5
- import { lifecycleRegistry } from "../../core/lifecycle";
6
- import { Router } from "./router";
4
+ import { registerOpenApi } from "./openapi";
5
+ import { lifecycleRegistry } from "../../core/lifecycle";
6
+ import { authenticateBearerRequest } from "../../core/auth";
7
+ import { Router } from "./router";
7
8
 
8
9
  export * from "./types";
9
10
  export { registerControllers as attachControllers } from "./controllers";
@@ -28,12 +29,23 @@ export async function createNativeApp(options: NativeAdapterOptions): Promise<Na
28
29
  const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
29
30
  const match = router.match(req.method || "GET", url.pathname);
30
31
 
31
- if (!match) {
32
- res.statusCode = 404;
33
- res.setHeader("Content-Type", "application/json");
34
- res.end(JSON.stringify({ message: `Not Found: ${req.method} ${url.pathname}` }));
35
- return;
36
- }
32
+ if (!match) {
33
+ res.statusCode = 404;
34
+ res.setHeader("Content-Type", "application/json");
35
+ res.end(JSON.stringify({ message: `Not Found: ${req.method} ${url.pathname}` }));
36
+ return;
37
+ }
38
+
39
+ if (options.bearerAuth) {
40
+ try {
41
+ await authenticateBearerRequest(req, options.bearerAuth);
42
+ } catch {
43
+ res.statusCode = 500;
44
+ res.setHeader("Content-Type", "application/json");
45
+ res.end(JSON.stringify({ message: "Internal server error" }));
46
+ return;
47
+ }
48
+ }
37
49
 
38
50
  const query: Record<string, any> = {};
39
51
  url.searchParams.forEach((value, key) => {
@@ -60,12 +72,13 @@ export async function createNativeApp(options: NativeAdapterOptions): Promise<Na
60
72
  }
61
73
  }
62
74
 
63
- await dispatchRequest(req, res, match, {
64
- inputCoercion,
65
- validation: options.validation,
66
- body,
67
- query
68
- });
75
+ await dispatchRequest(req, res, match, {
76
+ inputCoercion,
77
+ validation: options.validation,
78
+ body,
79
+ query,
80
+ auth: { userProperty: options.bearerAuth?.userProperty }
81
+ });
69
82
  };
70
83
 
71
84
  await lifecycleRegistry.callOnApplicationBootstrap();
@@ -1,10 +1,11 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type {
3
- Constructor,
4
- RequestContext as CoreRequestContext,
5
- UploadedFileInfo
6
- } from "../../core/types";
7
- import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
2
+ import type {
3
+ Constructor,
4
+ RequestContext as CoreRequestContext,
5
+ UploadedFileInfo
6
+ } from "../../core/types";
7
+ import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
8
+ import type { BearerAuthOptions } from "../../core/auth";
8
9
 
9
10
  export { UploadedFileInfo };
10
11
 
@@ -79,10 +80,12 @@ export interface NativeAdapterOptions {
79
80
  bodyLimit?: number;
80
81
  /** OpenAPI configuration */
81
82
  openApi?: OpenApiNativeOptions;
82
- /** Input coercion setting */
83
- inputCoercion?: InputCoercionSetting;
84
- /** Validation configuration. Set to false to disable validation, or provide options. */
85
- validation?: boolean | ValidationOptions;
83
+ /** Input coercion setting */
84
+ inputCoercion?: InputCoercionSetting;
85
+ /** Built-in bearer token authentication for protected routes. */
86
+ bearerAuth?: BearerAuthOptions;
87
+ /** Validation configuration. Set to false to disable validation, or provide options. */
88
+ validation?: boolean | ValidationOptions;
86
89
  }
87
90
 
88
91
  /**