vitek-plugin 0.1.2-beta.6 → 0.2.0-beta

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 (166) hide show
  1. package/README.md +35 -4
  2. package/dist/adapters/vite/dev-server-middleware.d.ts +8 -0
  3. package/dist/adapters/vite/dev-server-middleware.d.ts.map +1 -0
  4. package/dist/adapters/vite/dev-server-middleware.js +30 -0
  5. package/dist/adapters/vite/dev-server-state.d.ts +41 -0
  6. package/dist/adapters/vite/dev-server-state.d.ts.map +1 -0
  7. package/dist/adapters/vite/dev-server-state.js +191 -0
  8. package/dist/adapters/vite/dev-server.d.ts +2 -21
  9. package/dist/adapters/vite/dev-server.d.ts.map +1 -1
  10. package/dist/adapters/vite/dev-server.js +7 -216
  11. package/dist/adapters/vite/path-utils.d.ts +20 -0
  12. package/dist/adapters/vite/path-utils.d.ts.map +1 -0
  13. package/dist/adapters/vite/path-utils.js +46 -0
  14. package/dist/adapters/vite/path-utils.test.d.ts +2 -0
  15. package/dist/adapters/vite/path-utils.test.d.ts.map +1 -0
  16. package/dist/adapters/vite/path-utils.test.js +79 -0
  17. package/dist/build/build-api-bundle.d.ts +1 -0
  18. package/dist/build/build-api-bundle.d.ts.map +1 -1
  19. package/dist/build/build-api-bundle.js +38 -3
  20. package/dist/build/build-api-bundle.test.d.ts +2 -0
  21. package/dist/build/build-api-bundle.test.d.ts.map +1 -0
  22. package/dist/build/build-api-bundle.test.js +50 -0
  23. package/dist/build/build-sockets-bundle.test.d.ts +2 -0
  24. package/dist/build/build-sockets-bundle.test.d.ts.map +1 -0
  25. package/dist/build/build-sockets-bundle.test.js +49 -0
  26. package/dist/cli/cli.d.ts +8 -0
  27. package/dist/cli/cli.d.ts.map +1 -0
  28. package/dist/cli/cli.js +25 -0
  29. package/dist/cli/fixtures/serve-config/vitek.config.d.mts +6 -0
  30. package/dist/cli/fixtures/serve-config/vitek.config.d.mts.map +1 -0
  31. package/dist/cli/fixtures/serve-config/vitek.config.mjs +19 -0
  32. package/dist/cli/init.d.ts +15 -0
  33. package/dist/cli/init.d.ts.map +1 -0
  34. package/dist/cli/init.js +99 -0
  35. package/dist/cli/init.test.d.ts +2 -0
  36. package/dist/cli/init.test.d.ts.map +1 -0
  37. package/dist/cli/init.test.js +117 -0
  38. package/dist/cli/mcp-project-config.d.ts +8 -0
  39. package/dist/cli/mcp-project-config.d.ts.map +1 -0
  40. package/dist/cli/mcp-project-config.js +26 -0
  41. package/dist/cli/mcp-project.d.ts +2 -0
  42. package/dist/cli/mcp-project.d.ts.map +1 -0
  43. package/dist/cli/mcp-project.js +101 -0
  44. package/dist/cli/serve.d.ts +27 -1
  45. package/dist/cli/serve.d.ts.map +1 -1
  46. package/dist/cli/serve.js +85 -10
  47. package/dist/cli/serve.test.d.ts +2 -0
  48. package/dist/cli/serve.test.d.ts.map +1 -0
  49. package/dist/cli/serve.test.js +108 -0
  50. package/dist/core/asyncapi/generate.test.d.ts +2 -0
  51. package/dist/core/asyncapi/generate.test.d.ts.map +1 -0
  52. package/dist/core/asyncapi/generate.test.js +120 -0
  53. package/dist/core/context/create-context.d.ts +2 -0
  54. package/dist/core/context/create-context.d.ts.map +1 -1
  55. package/dist/core/file-system/watch-api-dir.d.ts +4 -1
  56. package/dist/core/file-system/watch-api-dir.d.ts.map +1 -1
  57. package/dist/core/file-system/watch-api-dir.js +31 -6
  58. package/dist/core/file-system/watch-api-dir.test.d.ts +2 -0
  59. package/dist/core/file-system/watch-api-dir.test.d.ts.map +1 -0
  60. package/dist/core/file-system/watch-api-dir.test.js +38 -0
  61. package/dist/core/generation/run-file-generation.d.ts +2 -0
  62. package/dist/core/generation/run-file-generation.d.ts.map +1 -1
  63. package/dist/core/generation/run-file-generation.js +4 -1
  64. package/dist/core/introspection/manifest.d.ts +24 -0
  65. package/dist/core/introspection/manifest.d.ts.map +1 -0
  66. package/dist/core/introspection/manifest.js +41 -0
  67. package/dist/core/introspection/manifest.test.d.ts +2 -0
  68. package/dist/core/introspection/manifest.test.d.ts.map +1 -0
  69. package/dist/core/introspection/manifest.test.js +62 -0
  70. package/dist/core/middleware/get-applicable-middlewares.d.ts +7 -0
  71. package/dist/core/middleware/get-applicable-middlewares.d.ts.map +1 -1
  72. package/dist/core/middleware/get-applicable-middlewares.js +23 -15
  73. package/dist/core/middleware/get-applicable-middlewares.test.js +36 -1
  74. package/dist/core/openapi/generate.d.ts +3 -79
  75. package/dist/core/openapi/generate.d.ts.map +1 -1
  76. package/dist/core/openapi/generate.js +4 -419
  77. package/dist/core/openapi/generate.test.d.ts +2 -0
  78. package/dist/core/openapi/generate.test.d.ts.map +1 -0
  79. package/dist/core/openapi/generate.test.js +184 -0
  80. package/dist/core/openapi/jsdoc.d.ts +3 -0
  81. package/dist/core/openapi/jsdoc.d.ts.map +1 -0
  82. package/dist/core/openapi/jsdoc.js +68 -0
  83. package/dist/core/openapi/jsdoc.test.d.ts +2 -0
  84. package/dist/core/openapi/jsdoc.test.d.ts.map +1 -0
  85. package/dist/core/openapi/jsdoc.test.js +111 -0
  86. package/dist/core/openapi/spec-builder.d.ts +4 -0
  87. package/dist/core/openapi/spec-builder.d.ts.map +1 -0
  88. package/dist/core/openapi/spec-builder.js +257 -0
  89. package/dist/core/openapi/spec-builder.test.d.ts +2 -0
  90. package/dist/core/openapi/spec-builder.test.d.ts.map +1 -0
  91. package/dist/core/openapi/spec-builder.test.js +93 -0
  92. package/dist/core/openapi/types.d.ts +42 -0
  93. package/dist/core/openapi/types.d.ts.map +1 -0
  94. package/dist/core/openapi/types.js +5 -0
  95. package/dist/core/server/cors.d.ts +29 -0
  96. package/dist/core/server/cors.d.ts.map +1 -0
  97. package/dist/core/server/cors.js +55 -0
  98. package/dist/core/server/cors.test.d.ts +2 -0
  99. package/dist/core/server/cors.test.d.ts.map +1 -0
  100. package/dist/core/server/cors.test.js +49 -0
  101. package/dist/core/server/proxy.d.ts +16 -0
  102. package/dist/core/server/proxy.d.ts.map +1 -0
  103. package/dist/core/server/proxy.js +20 -0
  104. package/dist/core/server/proxy.test.d.ts +2 -0
  105. package/dist/core/server/proxy.test.d.ts.map +1 -0
  106. package/dist/core/server/proxy.test.js +53 -0
  107. package/dist/core/server/request-handler.d.ts +17 -3
  108. package/dist/core/server/request-handler.d.ts.map +1 -1
  109. package/dist/core/server/request-handler.js +192 -84
  110. package/dist/core/server/request-handler.test.js +287 -22
  111. package/dist/core/socket/socket-handler.test.d.ts +2 -0
  112. package/dist/core/socket/socket-handler.test.d.ts.map +1 -0
  113. package/dist/core/socket/socket-handler.test.js +107 -0
  114. package/dist/core/types/schema.test.d.ts +2 -0
  115. package/dist/core/types/schema.test.d.ts.map +1 -0
  116. package/dist/core/types/schema.test.js +41 -0
  117. package/dist/core/validation/types.d.ts +2 -1
  118. package/dist/core/validation/types.d.ts.map +1 -1
  119. package/dist/core/validation/validator.d.ts +4 -16
  120. package/dist/core/validation/validator.d.ts.map +1 -1
  121. package/dist/core/validation/validator.js +4 -16
  122. package/dist/index.d.ts +6 -1
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js +2 -1
  125. package/dist/plugin/context.d.ts +15 -0
  126. package/dist/plugin/context.d.ts.map +1 -0
  127. package/dist/plugin/context.js +12 -0
  128. package/dist/plugin/options.d.ts +46 -0
  129. package/dist/plugin/options.d.ts.map +1 -0
  130. package/dist/plugin/options.js +1 -0
  131. package/dist/plugin/plugin-api.d.ts +49 -0
  132. package/dist/plugin/plugin-api.d.ts.map +1 -0
  133. package/dist/plugin/plugin-api.js +5 -0
  134. package/dist/plugin/vitek-build.d.ts +7 -0
  135. package/dist/plugin/vitek-build.d.ts.map +1 -0
  136. package/dist/plugin/vitek-build.js +104 -0
  137. package/dist/plugin/vitek-config.d.ts +4 -0
  138. package/dist/plugin/vitek-config.d.ts.map +1 -0
  139. package/dist/plugin/vitek-config.js +51 -0
  140. package/dist/plugin/vitek-config.test.d.ts +2 -0
  141. package/dist/plugin/vitek-config.test.d.ts.map +1 -0
  142. package/dist/plugin/vitek-config.test.js +62 -0
  143. package/dist/plugin/vitek-dev.d.ts +7 -0
  144. package/dist/plugin/vitek-dev.d.ts.map +1 -0
  145. package/dist/plugin/vitek-dev.js +71 -0
  146. package/dist/plugin/vitek-preview.d.ts +7 -0
  147. package/dist/plugin/vitek-preview.d.ts.map +1 -0
  148. package/dist/plugin/vitek-preview.js +107 -0
  149. package/dist/plugin/vitek-resolve.d.ts +7 -0
  150. package/dist/plugin/vitek-resolve.d.ts.map +1 -0
  151. package/dist/plugin/vitek-resolve.js +25 -0
  152. package/dist/plugin/vitek-transform.d.ts +7 -0
  153. package/dist/plugin/vitek-transform.d.ts.map +1 -0
  154. package/dist/plugin/vitek-transform.js +55 -0
  155. package/dist/plugin/vitek.d.ts +10 -0
  156. package/dist/plugin/vitek.d.ts.map +1 -0
  157. package/dist/plugin/vitek.js +27 -0
  158. package/dist/plugin.d.ts +3 -32
  159. package/dist/plugin.d.ts.map +1 -1
  160. package/dist/plugin.js +2 -274
  161. package/dist/plugin.test.js +99 -29
  162. package/dist/shared/response-helpers.d.ts +21 -0
  163. package/dist/shared/response-helpers.d.ts.map +1 -1
  164. package/dist/shared/response-helpers.js +41 -0
  165. package/dist/shared/response-helpers.test.js +54 -1
  166. package/package.json +19 -4
@@ -0,0 +1,55 @@
1
+ /**
2
+ * CORS header helpers for the request handler
3
+ */
4
+ const DEFAULT_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
5
+ const DEFAULT_ALLOWED_HEADERS = ['Content-Type', 'Authorization', 'Accept', 'Origin', 'X-Requested-With'];
6
+ export function normalizeCorsOptions(cors) {
7
+ if (cors === true) {
8
+ return {
9
+ origin: '*',
10
+ methods: DEFAULT_METHODS,
11
+ allowedHeaders: DEFAULT_ALLOWED_HEADERS,
12
+ exposeHeaders: [],
13
+ credentials: false,
14
+ maxAge: undefined,
15
+ };
16
+ }
17
+ const opts = cors;
18
+ return {
19
+ origin: opts.origin ?? '*',
20
+ methods: opts.methods ?? DEFAULT_METHODS,
21
+ allowedHeaders: opts.allowedHeaders ?? DEFAULT_ALLOWED_HEADERS,
22
+ exposeHeaders: opts.exposeHeaders ?? [],
23
+ credentials: opts.credentials ?? false,
24
+ maxAge: opts.maxAge,
25
+ };
26
+ }
27
+ function resolveOrigin(requestOrigin, option) {
28
+ if (option === '*')
29
+ return '*';
30
+ if (Array.isArray(option)) {
31
+ if (requestOrigin && option.includes(requestOrigin))
32
+ return requestOrigin;
33
+ return option[0] ?? '*';
34
+ }
35
+ return option;
36
+ }
37
+ export function getCorsHeaders(req, options) {
38
+ const requestOrigin = req.headers.origin;
39
+ const origin = resolveOrigin(requestOrigin, options.origin);
40
+ const headers = {
41
+ 'Access-Control-Allow-Origin': origin,
42
+ 'Access-Control-Allow-Methods': options.methods.join(', '),
43
+ 'Access-Control-Allow-Headers': options.allowedHeaders.join(', '),
44
+ };
45
+ if (options.exposeHeaders.length > 0) {
46
+ headers['Access-Control-Expose-Headers'] = options.exposeHeaders.join(', ');
47
+ }
48
+ if (options.credentials) {
49
+ headers['Access-Control-Allow-Credentials'] = 'true';
50
+ }
51
+ if (options.maxAge != null) {
52
+ headers['Access-Control-Max-Age'] = String(options.maxAge);
53
+ }
54
+ return headers;
55
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cors.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cors.test.d.ts","sourceRoot":"","sources":["../../../src/core/server/cors.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { normalizeCorsOptions, getCorsHeaders } from './cors.js';
3
+ describe('normalizeCorsOptions', () => {
4
+ it('returns permissive defaults when cors is true', () => {
5
+ const opts = normalizeCorsOptions(true);
6
+ expect(opts.origin).toBe('*');
7
+ expect(opts.methods).toContain('GET');
8
+ expect(opts.methods).toContain('POST');
9
+ expect(opts.methods).toContain('OPTIONS');
10
+ expect(opts.allowedHeaders.length).toBeGreaterThan(0);
11
+ expect(opts.credentials).toBe(false);
12
+ });
13
+ it('merges partial CorsOptions with defaults', () => {
14
+ const opts = normalizeCorsOptions({ origin: 'https://app.example.com' });
15
+ expect(opts.origin).toBe('https://app.example.com');
16
+ expect(opts.methods).toContain('GET');
17
+ expect(opts.credentials).toBe(false);
18
+ });
19
+ it('allows custom methods and maxAge', () => {
20
+ const opts = normalizeCorsOptions({
21
+ methods: ['GET', 'POST'],
22
+ maxAge: 3600,
23
+ });
24
+ expect(opts.methods).toEqual(['GET', 'POST']);
25
+ expect(opts.maxAge).toBe(3600);
26
+ });
27
+ });
28
+ describe('getCorsHeaders', () => {
29
+ function mockReq(origin) {
30
+ return { headers: origin ? { origin } : {} };
31
+ }
32
+ it('returns Allow-Origin * when option is *', () => {
33
+ const opts = normalizeCorsOptions(true);
34
+ const headers = getCorsHeaders(mockReq(), opts);
35
+ expect(headers['Access-Control-Allow-Origin']).toBe('*');
36
+ expect(headers['Access-Control-Allow-Methods']).toBeDefined();
37
+ expect(headers['Access-Control-Allow-Headers']).toBeDefined();
38
+ });
39
+ it('returns request origin when it matches allowed origin', () => {
40
+ const opts = normalizeCorsOptions({ origin: 'https://app.example.com' });
41
+ const headers = getCorsHeaders(mockReq('https://app.example.com'), opts);
42
+ expect(headers['Access-Control-Allow-Origin']).toBe('https://app.example.com');
43
+ });
44
+ it('includes maxAge when set', () => {
45
+ const opts = normalizeCorsOptions({ maxAge: 600 });
46
+ const headers = getCorsHeaders(mockReq(), opts);
47
+ expect(headers['Access-Control-Max-Age']).toBe('600');
48
+ });
49
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Proxy (X-Forwarded-*) helpers for the request handler
3
+ */
4
+ import type { IncomingMessage } from 'http';
5
+ export interface EffectiveRequest {
6
+ /** Effective URL (derived from X-Forwarded-* when trustProxy is true). */
7
+ url: string;
8
+ /** Client IP (X-Forwarded-For or socket.remoteAddress). */
9
+ clientIp?: string;
10
+ }
11
+ /**
12
+ * Derives effective URL and client IP from request when behind a reverse proxy.
13
+ * When trustProxy is false, returns the request url as-is and no clientIp.
14
+ */
15
+ export declare function getEffectiveRequest(req: IncomingMessage, trustProxy: boolean): EffectiveRequest;
16
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../src/core/server/proxy.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAE5C,MAAM,WAAW,gBAAgB;IAC/B,0EAA0E;IAC1E,GAAG,EAAE,MAAM,CAAC;IACZ,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,eAAe,EACpB,UAAU,EAAE,OAAO,GAClB,gBAAgB,CAYlB"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Proxy (X-Forwarded-*) helpers for the request handler
3
+ */
4
+ /**
5
+ * Derives effective URL and client IP from request when behind a reverse proxy.
6
+ * When trustProxy is false, returns the request url as-is and no clientIp.
7
+ */
8
+ export function getEffectiveRequest(req, trustProxy) {
9
+ if (!trustProxy) {
10
+ return { url: req.url ?? '' };
11
+ }
12
+ const proto = req.headers['x-forwarded-proto']?.split(',')[0]?.trim() || 'http';
13
+ const host = req.headers['x-forwarded-host']?.split(',')[0]?.trim() || req.headers.host || 'localhost';
14
+ const path = req.url?.split('?')[0] ?? '/';
15
+ const query = req.url?.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
16
+ const effectiveUrl = `${proto}://${host}${path}${query}`;
17
+ const forwardedFor = req.headers['x-forwarded-for']?.split(',')[0]?.trim();
18
+ const clientIp = forwardedFor || (req.socket?.remoteAddress);
19
+ return { url: effectiveUrl, clientIp };
20
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=proxy.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.test.d.ts","sourceRoot":"","sources":["../../../src/core/server/proxy.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { getEffectiveRequest } from './proxy.js';
3
+ function mockReq(overrides = {}) {
4
+ return {
5
+ url: overrides.url ?? '/api/health',
6
+ headers: overrides.headers ?? {},
7
+ socket: overrides.socket,
8
+ };
9
+ }
10
+ describe('getEffectiveRequest', () => {
11
+ it('returns req.url as-is when trustProxy is false', () => {
12
+ const req = mockReq({ url: '/api/health?foo=bar' });
13
+ const result = getEffectiveRequest(req, false);
14
+ expect(result.url).toBe('/api/health?foo=bar');
15
+ expect(result.clientIp).toBeUndefined();
16
+ });
17
+ it('returns req.url when trustProxy is true but no X-Forwarded-* headers', () => {
18
+ const req = mockReq({ url: '/api/health', headers: { host: 'localhost' } });
19
+ const result = getEffectiveRequest(req, true);
20
+ expect(result.url).toMatch(/^http:\/\/localhost\/api\/health/);
21
+ expect(result.clientIp).toBeUndefined();
22
+ });
23
+ it('builds effective URL from X-Forwarded-* when trustProxy is true', () => {
24
+ const req = mockReq({
25
+ url: '/api/users/1',
26
+ headers: {
27
+ 'x-forwarded-proto': 'https',
28
+ 'x-forwarded-host': 'api.example.com',
29
+ 'x-forwarded-for': '1.2.3.4',
30
+ },
31
+ });
32
+ const result = getEffectiveRequest(req, true);
33
+ expect(result.url).toBe('https://api.example.com/api/users/1');
34
+ expect(result.clientIp).toBe('1.2.3.4');
35
+ });
36
+ it('uses first value when X-Forwarded-For has multiple entries', () => {
37
+ const req = mockReq({
38
+ url: '/api/health',
39
+ headers: { 'x-forwarded-for': ' client, proxy1, proxy2 ' },
40
+ });
41
+ const result = getEffectiveRequest(req, true);
42
+ expect(result.clientIp).toBe('client');
43
+ });
44
+ it('falls back to socket.remoteAddress for clientIp when no X-Forwarded-For', () => {
45
+ const req = mockReq({
46
+ url: '/api/health',
47
+ headers: { 'x-forwarded-proto': 'https', 'x-forwarded-host': 'api.example.com' },
48
+ socket: { remoteAddress: '192.168.1.1' },
49
+ });
50
+ const result = getEffectiveRequest(req, true);
51
+ expect(result.clientIp).toBe('192.168.1.1');
52
+ });
53
+ });
@@ -7,9 +7,22 @@ import type { Connect } from 'vite';
7
7
  import type { Route } from '../routing/route-types.js';
8
8
  import type { LoadedMiddleware } from '../middleware/get-applicable-middlewares.js';
9
9
  import type { SocketEmitter } from '../shared/vitek-app.js';
10
+ /** Callback for beforeApiRequest hook. Call next() to continue, or send response and return without next() to short-circuit. */
11
+ export type BeforeApiRequestHook = (ctx: {
12
+ req: IncomingMessage;
13
+ res: ServerResponse;
14
+ path: string;
15
+ method: string;
16
+ }, next: () => void) => void | Promise<void>;
10
17
  export interface RequestHandlerOptions {
11
18
  routes: Route[];
12
19
  middlewares: LoadedMiddleware[];
20
+ /** Hooks called before each API request. Call next() to continue. */
21
+ beforeApiRequest?: BeforeApiRequestHook[];
22
+ /** Enable CORS. true or CorsOptions. When set, OPTIONS preflight and CORS headers on responses are handled. */
23
+ cors?: boolean | import('./cors.js').CorsOptions;
24
+ /** When true, trust X-Forwarded-* headers and set context.clientIp / effective url. */
25
+ trustProxy?: boolean;
13
26
  logger?: {
14
27
  routeMatched?(pattern: string, method: string): void;
15
28
  requestStart?(method: string, path: string): void;
@@ -21,9 +34,10 @@ export interface RequestHandlerOptions {
21
34
  shared?: {
22
35
  sockets: SocketEmitter;
23
36
  };
37
+ /** Max request body size in bytes. When exceeded, responds with 413 Payload Too Large. Omit for no limit. */
38
+ maxBodySize?: number;
39
+ /** Called when a non-HttpError is thrown. May send a custom response; if res is not ended, default 500 JSON is sent. */
40
+ onError?: (err: Error, req: IncomingMessage, res: ServerResponse) => void | Promise<void>;
24
41
  }
25
- /**
26
- * Creates a Connect-style middleware that handles /api/* requests using the given routes and middlewares.
27
- */
28
42
  export declare function createRequestHandler(options: RequestHandlerOptions): (req: IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => Promise<void>;
29
43
  //# sourceMappingURL=request-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"request-handler.d.ts","sourceRoot":"","sources":["../../../src/core/server/request-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAOpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAE5D,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,MAAM,CAAC,EAAE;QACP,YAAY,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACrD,YAAY,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAClD,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACpF,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QAC7D,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC/D,CAAC;IACF,6FAA6F;IAC7F,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,aAAa,CAAA;KAAE,CAAC;CACrC;AAID;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,CAAC,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAsJ7J"}
1
+ {"version":3,"file":"request-handler.d.ts","sourceRoot":"","sources":["../../../src/core/server/request-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAOpC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6CAA6C,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAQ5D,gIAAgI;AAChI,MAAM,MAAM,oBAAoB,GAAG,CACjC,GAAG,EAAE;IAAE,GAAG,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,cAAc,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAChF,IAAI,EAAE,MAAM,IAAI,KACb,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1B,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,WAAW,EAAE,gBAAgB,EAAE,CAAC;IAChC,qEAAqE;IACrE,gBAAgB,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAC1C,+GAA+G;IAC/G,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,WAAW,EAAE,WAAW,CAAC;IACjD,uFAAuF;IACvF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE;QACP,YAAY,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QACrD,YAAY,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QAClD,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACpF,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;QAC7D,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;KAC/D,CAAC;IACF,6FAA6F;IAC7F,MAAM,CAAC,EAAE;QAAE,OAAO,EAAE,aAAa,CAAA;KAAE,CAAC;IACpC,6GAA6G;IAC7G,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wHAAwH;IACxH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3F;AA8BD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,qBAAqB,GAAG,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,CAAC,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAoO7J"}
@@ -8,12 +8,32 @@ import { getApplicableMiddlewares } from '../middleware/get-applicable-middlewar
8
8
  import { compose } from '../middleware/compose.js';
9
9
  import { API_BASE_PATH } from '../../shared/constants.js';
10
10
  import { HttpError } from '../../shared/errors.js';
11
+ import { normalizeCorsOptions, getCorsHeaders, } from './cors.js';
12
+ import { getEffectiveRequest } from './proxy.js';
11
13
  const noop = () => { };
12
- /**
13
- * Creates a Connect-style middleware that handles /api/* requests using the given routes and middlewares.
14
- */
14
+ function sanitizeHeaderValue(value) {
15
+ const s = Array.isArray(value)
16
+ ? value.map((v) => String(v).replace(/\r|\n/g, '')).join(', ')
17
+ : String(value).replace(/\r|\n/g, '');
18
+ return s;
19
+ }
20
+ function safeSetHeader(res, key, value) {
21
+ res.setHeader(key, sanitizeHeaderValue(value));
22
+ }
23
+ /** True if value is a Node.js Readable stream (has .pipe). Used for streaming response body. */
24
+ function isReadableStream(value) {
25
+ return (value != null &&
26
+ typeof value === 'object' &&
27
+ typeof value.pipe === 'function');
28
+ }
29
+ function applyCorsHeaders(res, corsHeaders) {
30
+ for (const [key, value] of Object.entries(corsHeaders)) {
31
+ safeSetHeader(res, key, value);
32
+ }
33
+ }
15
34
  export function createRequestHandler(options) {
16
- const { routes, middlewares, logger, shared } = options;
35
+ const { routes, middlewares, beforeApiRequest = [], cors, trustProxy = false, logger, shared, maxBodySize, onError } = options;
36
+ const corsOpts = cors != null ? normalizeCorsOptions(cors) : null;
17
37
  const logRouteMatched = logger?.routeMatched ?? noop;
18
38
  const logRequestStart = logger?.requestStart ?? noop;
19
39
  const logRequest = logger?.request ?? noop;
@@ -29,105 +49,185 @@ export function createRequestHandler(options) {
29
49
  const startTime = Date.now();
30
50
  const requestMethod = req.method?.toLowerCase() || 'get';
31
51
  const requestPath = pathname;
52
+ const effective = getEffectiveRequest(req, trustProxy);
53
+ const requestUrl = effective.url || req.url;
54
+ if (corsOpts) {
55
+ const corsHeaders = getCorsHeaders(req, corsOpts);
56
+ applyCorsHeaders(res, corsHeaders);
57
+ if (req.method === 'OPTIONS') {
58
+ res.statusCode = 204;
59
+ res.end();
60
+ return;
61
+ }
62
+ }
32
63
  try {
33
- const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
64
+ const url = new URL(requestUrl, 'http://localhost');
34
65
  const routePath = url.pathname.replace(API_BASE_PATH, '') || '/';
35
66
  const method = requestMethod;
36
- const match = matchRoute(routes, routePath, method);
37
- if (!match) {
38
- const duration = Date.now() - startTime;
39
- res.statusCode = 404;
40
- res.setHeader('Content-Type', 'application/json');
41
- res.end(JSON.stringify({ error: 'Route not found' }));
42
- logRequest(requestMethod, requestPath, 404, duration);
43
- return;
44
- }
45
- logRouteMatched(match.route.pattern, method);
46
- logRequestStart(requestMethod, requestPath);
47
- const query = {};
48
- url.searchParams.forEach((value, key) => {
49
- if (query[key]) {
50
- const existing = query[key];
51
- query[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
67
+ const doHandleRequest = async () => {
68
+ const match = matchRoute(routes, routePath, method);
69
+ if (!match) {
70
+ const duration = Date.now() - startTime;
71
+ res.statusCode = 404;
72
+ safeSetHeader(res, 'Content-Type', 'application/json');
73
+ if (corsOpts)
74
+ applyCorsHeaders(res, getCorsHeaders(req, corsOpts));
75
+ res.end(JSON.stringify({ error: 'Route not found' }));
76
+ logRequest(requestMethod, requestPath, 404, duration);
77
+ return;
52
78
  }
53
- else {
54
- query[key] = value;
79
+ logRouteMatched(match.route.pattern, method);
80
+ logRequestStart(requestMethod, requestPath);
81
+ const query = {};
82
+ url.searchParams.forEach((value, key) => {
83
+ if (query[key]) {
84
+ const existing = query[key];
85
+ query[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
86
+ }
87
+ else {
88
+ query[key] = value;
89
+ }
90
+ });
91
+ const PAYLOAD_TOO_LARGE_SENTINEL = Symbol('PAYLOAD_TOO_LARGE');
92
+ let body;
93
+ if (['post', 'put', 'patch'].includes(method)) {
94
+ body = await new Promise((resolve, reject) => {
95
+ const chunks = [];
96
+ let totalSize = 0;
97
+ const onData = (chunk) => {
98
+ if (maxBodySize != null) {
99
+ totalSize += chunk.length;
100
+ if (totalSize > maxBodySize) {
101
+ req.removeListener('data', onData);
102
+ req.removeListener('end', onEnd);
103
+ req.destroy();
104
+ reject(new Error('PAYLOAD_TOO_LARGE'));
105
+ return;
106
+ }
107
+ }
108
+ chunks.push(chunk);
109
+ };
110
+ const onEnd = () => {
111
+ const rawBody = Buffer.concat(chunks).toString();
112
+ if (!rawBody) {
113
+ resolve(undefined);
114
+ return;
115
+ }
116
+ try {
117
+ resolve(JSON.parse(rawBody));
118
+ }
119
+ catch {
120
+ resolve(rawBody);
121
+ }
122
+ };
123
+ req.on('data', onData);
124
+ req.on('end', onEnd);
125
+ }).catch((err) => {
126
+ if (err?.message === 'PAYLOAD_TOO_LARGE') {
127
+ const duration = Date.now() - startTime;
128
+ res.statusCode = 413;
129
+ safeSetHeader(res, 'Content-Type', 'application/json');
130
+ if (corsOpts)
131
+ applyCorsHeaders(res, getCorsHeaders(req, corsOpts));
132
+ res.end(JSON.stringify({ error: 'Payload Too Large' }));
133
+ logRequest(requestMethod, requestPath, 413, duration);
134
+ return PAYLOAD_TOO_LARGE_SENTINEL;
135
+ }
136
+ throw err;
137
+ });
138
+ if (body === PAYLOAD_TOO_LARGE_SENTINEL)
139
+ return;
55
140
  }
56
- });
57
- let body;
58
- if (['post', 'put', 'patch'].includes(method)) {
59
- body = await new Promise((resolve) => {
60
- const chunks = [];
61
- req.on('data', (chunk) => chunks.push(chunk));
62
- req.on('end', () => {
63
- const rawBody = Buffer.concat(chunks).toString();
64
- if (!rawBody) {
65
- resolve(undefined);
66
- return;
141
+ const context = createContext({
142
+ url: requestUrl,
143
+ method,
144
+ headers: (req.headers || {}),
145
+ body,
146
+ }, match.params, query);
147
+ if (effective.clientIp)
148
+ context.clientIp = effective.clientIp;
149
+ if (shared?.sockets) {
150
+ context.sockets = shared.sockets;
151
+ }
152
+ const applicableMiddlewares = getApplicableMiddlewares(middlewares, match.route.pattern);
153
+ const composed = compose(applicableMiddlewares);
154
+ const handler = async () => {
155
+ const result = await match.route.handler(context);
156
+ if (isVitekResponse(result)) {
157
+ const response = result;
158
+ const statusCode = response.status || 200;
159
+ if (corsOpts)
160
+ applyCorsHeaders(res, getCorsHeaders(req, corsOpts));
161
+ if (response.headers) {
162
+ for (const [key, value] of Object.entries(response.headers)) {
163
+ safeSetHeader(res, key, value);
164
+ }
67
165
  }
68
- try {
69
- resolve(JSON.parse(rawBody));
166
+ if (!response.headers || !response.headers['Content-Type']) {
167
+ if (response.body !== undefined && !isReadableStream(response.body)) {
168
+ safeSetHeader(res, 'Content-Type', 'application/json');
169
+ }
70
170
  }
71
- catch {
72
- resolve(rawBody);
171
+ res.statusCode = statusCode;
172
+ if (response.body === undefined) {
173
+ res.end();
174
+ logRequest(requestMethod, requestPath, statusCode, Date.now() - startTime);
73
175
  }
74
- });
75
- });
76
- }
77
- const context = createContext({
78
- url: req.url,
79
- method,
80
- headers: (req.headers || {}),
81
- body,
82
- }, match.params, query);
83
- if (shared?.sockets) {
84
- context.sockets = shared.sockets;
85
- }
86
- const applicableMiddlewares = getApplicableMiddlewares(middlewares, match.route.pattern);
87
- const composed = compose(applicableMiddlewares);
88
- const handler = async () => {
89
- const result = await match.route.handler(context);
90
- if (isVitekResponse(result)) {
91
- const response = result;
92
- const statusCode = response.status || 200;
93
- if (response.headers) {
94
- for (const [key, value] of Object.entries(response.headers)) {
95
- res.setHeader(key, value);
176
+ else if (isReadableStream(response.body)) {
177
+ const stream = response.body;
178
+ res.once('finish', () => logRequest(requestMethod, requestPath, statusCode, Date.now() - startTime));
179
+ stream.pipe(res);
96
180
  }
97
- }
98
- if (!response.headers || !response.headers['Content-Type']) {
99
- if (response.body !== undefined) {
100
- res.setHeader('Content-Type', 'application/json');
181
+ else if (typeof response.body === 'string') {
182
+ res.end(response.body);
183
+ logRequest(requestMethod, requestPath, statusCode, Date.now() - startTime);
184
+ }
185
+ else {
186
+ res.end(JSON.stringify(response.body));
187
+ logRequest(requestMethod, requestPath, statusCode, Date.now() - startTime);
101
188
  }
102
- }
103
- res.statusCode = statusCode;
104
- if (response.body === undefined) {
105
- res.end();
106
- }
107
- else if (typeof response.body === 'string') {
108
- res.end(response.body);
109
189
  }
110
190
  else {
111
- res.end(JSON.stringify(response.body));
191
+ if (corsOpts)
192
+ applyCorsHeaders(res, getCorsHeaders(req, corsOpts));
193
+ safeSetHeader(res, 'Content-Type', 'application/json');
194
+ res.statusCode = 200;
195
+ res.end(JSON.stringify(result));
196
+ logRequest(requestMethod, requestPath, 200, Date.now() - startTime);
112
197
  }
113
- logRequest(requestMethod, requestPath, statusCode, Date.now() - startTime);
114
- }
115
- else {
116
- res.setHeader('Content-Type', 'application/json');
117
- res.statusCode = 200;
118
- res.end(JSON.stringify(result));
119
- logRequest(requestMethod, requestPath, 200, Date.now() - startTime);
120
- }
198
+ };
199
+ await composed(context, handler);
121
200
  };
122
- await composed(context, handler);
201
+ if (beforeApiRequest.length > 0) {
202
+ for (const hook of beforeApiRequest) {
203
+ await new Promise((resolve, reject) => {
204
+ let done = false;
205
+ const next = () => { if (!done) {
206
+ done = true;
207
+ resolve();
208
+ } };
209
+ Promise.resolve(hook({ req, res, path: routePath, method }, next))
210
+ .then(() => { if (!done && res.writableEnded) {
211
+ done = true;
212
+ resolve();
213
+ } })
214
+ .catch(reject);
215
+ });
216
+ if (res.writableEnded)
217
+ return;
218
+ }
219
+ }
220
+ await doHandleRequest();
123
221
  }
124
222
  catch (error) {
125
223
  const duration = Date.now() - startTime;
224
+ if (corsOpts)
225
+ applyCorsHeaders(res, getCorsHeaders(req, corsOpts));
126
226
  if (error instanceof HttpError) {
127
227
  const httpError = error;
128
228
  logWarn(`HTTP Error ${httpError.statusCode}: ${httpError.message}`);
129
229
  res.statusCode = httpError.statusCode;
130
- res.setHeader('Content-Type', 'application/json');
230
+ safeSetHeader(res, 'Content-Type', 'application/json');
131
231
  res.end(JSON.stringify({
132
232
  error: httpError.name,
133
233
  message: httpError.message,
@@ -136,10 +236,18 @@ export function createRequestHandler(options) {
136
236
  logRequest(requestMethod, requestPath, httpError.statusCode, duration);
137
237
  }
138
238
  else {
139
- const errorMessage = error instanceof Error ? error.message : String(error);
239
+ const err = error instanceof Error ? error : new Error(String(error));
240
+ if (onError) {
241
+ await Promise.resolve(onError(err, req, res));
242
+ if (res.writableEnded) {
243
+ logRequest(requestMethod, requestPath, res.statusCode, duration);
244
+ return;
245
+ }
246
+ }
247
+ const errorMessage = err.message;
140
248
  logError(`Error handling request: ${errorMessage}`);
141
249
  res.statusCode = 500;
142
- res.setHeader('Content-Type', 'application/json');
250
+ safeSetHeader(res, 'Content-Type', 'application/json');
143
251
  res.end(JSON.stringify({
144
252
  error: 'Internal server error',
145
253
  message: errorMessage,