ts-typed-api 0.2.12 → 0.2.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.
- package/dist/handler.d.ts +2 -1
- package/dist/handler.js +50 -1
- package/dist/hono-cloudflare-workers.js +62 -2
- package/dist/object-handlers.d.ts +11 -4
- package/package.json +1 -1
- package/src/handler.ts +63 -2
- package/src/hono-cloudflare-workers.ts +70 -2
- package/src/object-handlers.ts +13 -4
- package/tests/middleware.test.ts +24 -0
- package/tests/setup.ts +72 -99
package/dist/handler.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ApiDefinitionSchema } from "./definition";
|
|
2
2
|
import { createRouteHandler } from "./router";
|
|
3
|
+
import { MiddlewareResponse } from "./object-handlers";
|
|
3
4
|
import express from "express";
|
|
4
5
|
export type SpecificRouteHandler<TDef extends ApiDefinitionSchema> = {
|
|
5
6
|
[TDomain_ in keyof TDef['endpoints']]: {
|
|
6
7
|
[TRouteKey_ in keyof TDef['endpoints'][TDomain_]]: ReturnType<typeof createRouteHandler<TDef, TDomain_, TRouteKey_>>;
|
|
7
8
|
}[keyof TDef['endpoints'][TDomain_]];
|
|
8
9
|
}[keyof TDef['endpoints']];
|
|
9
|
-
export type EndpointMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = (req: express.Request, res:
|
|
10
|
+
export type EndpointMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = (req: express.Request, res: MiddlewareResponse, next: express.NextFunction, endpointInfo: {
|
|
10
11
|
[TDomain in keyof TDef['endpoints']]: {
|
|
11
12
|
domain: TDomain;
|
|
12
13
|
routeKey: keyof TDef['endpoints'][TDomain];
|
package/dist/handler.js
CHANGED
|
@@ -55,6 +55,50 @@ function preprocessQueryParams(query, querySchema) {
|
|
|
55
55
|
}
|
|
56
56
|
return processedQuery;
|
|
57
57
|
}
|
|
58
|
+
// Helper function to create respond method for middleware compatibility
|
|
59
|
+
function createRespondFunction(routeDefinition, responseSetter) {
|
|
60
|
+
return (status, data) => {
|
|
61
|
+
const responseSchema = routeDefinition.responses[status];
|
|
62
|
+
if (!responseSchema) {
|
|
63
|
+
console.error(`No response schema defined for status ${status}`);
|
|
64
|
+
responseSetter(500, {
|
|
65
|
+
data: null,
|
|
66
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Undefined response schema for status." }]
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
let responseBody;
|
|
71
|
+
if (status === 422) {
|
|
72
|
+
responseBody = {
|
|
73
|
+
data: null,
|
|
74
|
+
error: data
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
responseBody = {
|
|
79
|
+
data: data,
|
|
80
|
+
error: null
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const validationResult = responseSchema.safeParse(responseBody);
|
|
84
|
+
if (validationResult.success) {
|
|
85
|
+
// Handle 204 responses specially - they must not have a body
|
|
86
|
+
if (status === 204) {
|
|
87
|
+
responseSetter(status, null);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
responseSetter(status, validationResult.data);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.error(`FATAL: Constructed response body failed Zod validation for status ${status}.`, validationResult.error.issues, 'Provided data:', data, 'Constructed response body:', responseBody);
|
|
95
|
+
responseSetter(500, {
|
|
96
|
+
data: null,
|
|
97
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Constructed response failed validation." }]
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
58
102
|
// Helper function to create multer middleware based on file upload configuration
|
|
59
103
|
function createFileUploadMiddleware(config) {
|
|
60
104
|
// Default multer configuration
|
|
@@ -367,7 +411,12 @@ middlewares) {
|
|
|
367
411
|
middlewares.forEach(middleware => {
|
|
368
412
|
const wrappedMiddleware = async (req, res, next) => {
|
|
369
413
|
try {
|
|
370
|
-
|
|
414
|
+
// Add respond method to res for middleware compatibility
|
|
415
|
+
const middlewareRes = res;
|
|
416
|
+
middlewareRes.respond = createRespondFunction(routeDefinition, (status, data) => {
|
|
417
|
+
res.status(status).json(data);
|
|
418
|
+
});
|
|
419
|
+
await middleware(req, middlewareRes, next, { domain: currentDomain, routeKey: currentRouteKey });
|
|
371
420
|
}
|
|
372
421
|
catch (error) {
|
|
373
422
|
next(error);
|
|
@@ -415,10 +415,70 @@ function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middleware
|
|
|
415
415
|
path: c.req.path,
|
|
416
416
|
originalUrl: c.req.url
|
|
417
417
|
};
|
|
418
|
-
// Create minimal res object
|
|
419
|
-
const fakeRes = {
|
|
418
|
+
// Create minimal res object with respond method for middleware compatibility
|
|
419
|
+
const fakeRes = {
|
|
420
|
+
respond: (status, data) => {
|
|
421
|
+
const responseSchema = routeDefinition.responses[status];
|
|
422
|
+
if (!responseSchema) {
|
|
423
|
+
console.error(`No response schema defined for status ${status}`);
|
|
424
|
+
c.json({
|
|
425
|
+
data: null,
|
|
426
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Undefined response schema for status." }]
|
|
427
|
+
}, 500);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
let responseBody;
|
|
431
|
+
if (status === 422) {
|
|
432
|
+
responseBody = {
|
|
433
|
+
data: null,
|
|
434
|
+
error: data
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
responseBody = {
|
|
439
|
+
data: data,
|
|
440
|
+
error: null
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
const validationResult = responseSchema.safeParse(responseBody);
|
|
444
|
+
if (validationResult.success) {
|
|
445
|
+
// Handle 204 responses specially - they must not have a body
|
|
446
|
+
if (status === 204) {
|
|
447
|
+
c.__response = new Response(null, { status: status });
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
c.__response = c.json(validationResult.data, status);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
console.error(`FATAL: Constructed response body failed Zod validation for status ${status}.`, validationResult.error.issues, 'Provided data:', data, 'Constructed response body:', responseBody);
|
|
455
|
+
c.__response = c.json({
|
|
456
|
+
data: null,
|
|
457
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Constructed response failed validation." }]
|
|
458
|
+
}, 500);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
status: (status) => {
|
|
462
|
+
// For middleware that might use status, but since not used, stub
|
|
463
|
+
c.__response = new Response(null, { status: status });
|
|
464
|
+
},
|
|
465
|
+
json: (data) => {
|
|
466
|
+
// Send json response
|
|
467
|
+
c.__response = c.json(data);
|
|
468
|
+
},
|
|
469
|
+
setHeader: (name, value) => {
|
|
470
|
+
c.header(name, value);
|
|
471
|
+
},
|
|
472
|
+
end: () => {
|
|
473
|
+
// Perhaps do nothing or set response
|
|
474
|
+
}
|
|
475
|
+
};
|
|
420
476
|
// Call Express-style middleware
|
|
421
477
|
await middleware(fakeReq, fakeRes, next, { domain: currentDomain, routeKey: currentRouteKey });
|
|
478
|
+
// Check if middleware responded directly
|
|
479
|
+
if (c.__response) {
|
|
480
|
+
return c.__response;
|
|
481
|
+
}
|
|
422
482
|
}
|
|
423
483
|
catch (error) {
|
|
424
484
|
console.error('Middleware error:', error);
|
|
@@ -7,15 +7,22 @@ export type EndpointInfo<TDef extends ApiDefinitionSchema = ApiDefinitionSchema>
|
|
|
7
7
|
routeKey: keyof TDef['endpoints'][TDomain];
|
|
8
8
|
};
|
|
9
9
|
}[keyof TDef['endpoints']];
|
|
10
|
-
export type EndpointMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = (req: express.Request, res:
|
|
11
|
-
export type SimpleMiddleware = (req: express.Request, res:
|
|
12
|
-
export type UniversalEndpointMiddleware = (req: express.Request, res:
|
|
10
|
+
export type EndpointMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = (req: express.Request, res: MiddlewareResponse, next: express.NextFunction, endpointInfo: EndpointInfo<TDef>) => void | Promise<void>;
|
|
11
|
+
export type SimpleMiddleware = (req: express.Request, res: MiddlewareResponse, next: express.NextFunction) => void | Promise<void>;
|
|
12
|
+
export type UniversalEndpointMiddleware = (req: express.Request, res: MiddlewareResponse, next: express.NextFunction, endpointInfo: {
|
|
13
13
|
domain: string;
|
|
14
14
|
routeKey: string;
|
|
15
15
|
}) => void | Promise<void>;
|
|
16
|
+
export interface MiddlewareResponse {
|
|
17
|
+
respond(status: number, data: any): void;
|
|
18
|
+
status(code: number): this;
|
|
19
|
+
json(data: any): void;
|
|
20
|
+
setHeader(name: string, value: string): void;
|
|
21
|
+
end(): void;
|
|
22
|
+
}
|
|
16
23
|
export type EndpointMiddlewareCtx<Ctx extends Record<string, any> = Record<string, any>, TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = ((req: express.Request & {
|
|
17
24
|
ctx?: Ctx;
|
|
18
|
-
}, res:
|
|
25
|
+
}, res: MiddlewareResponse, next: express.NextFunction, endpointInfo: EndpointInfo<TDef>) => void | Promise<void>) | ((c: any, next: any) => void | Promise<void>);
|
|
19
26
|
export type AnyMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = EndpointMiddleware<TDef> | UniversalEndpointMiddleware | SimpleMiddleware;
|
|
20
27
|
type HandlerFunction<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain], Ctx extends Record<string, any> = Record<string, any>> = (req: TypedRequest<TDef, TDomain, TRouteKey, ApiParams<TDef, TDomain, TRouteKey>, ApiBody<TDef, TDomain, TRouteKey>, ApiQuery<TDef, TDomain, TRouteKey>, Record<string, any>, Ctx>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void;
|
|
21
28
|
export type ObjectHandlers<TDef extends ApiDefinitionSchema, Ctx extends Record<string, any> = Record<string, any>> = {
|
package/package.json
CHANGED
package/src/handler.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ApiDefinitionSchema, RouteSchema, UnifiedError, FileUploadConfig } from "./definition";
|
|
3
3
|
import { createRouteHandler, TypedRequest, TypedResponse } from "./router";
|
|
4
|
+
import { MiddlewareResponse } from "./object-handlers";
|
|
4
5
|
import express from "express";
|
|
5
6
|
import multer from "multer";
|
|
6
7
|
|
|
@@ -16,7 +17,7 @@ export type SpecificRouteHandler<TDef extends ApiDefinitionSchema> = {
|
|
|
16
17
|
// Type for middleware function that receives endpoint information with type safety
|
|
17
18
|
export type EndpointMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = (
|
|
18
19
|
req: express.Request,
|
|
19
|
-
res:
|
|
20
|
+
res: MiddlewareResponse,
|
|
20
21
|
next: express.NextFunction,
|
|
21
22
|
endpointInfo: {
|
|
22
23
|
[TDomain in keyof TDef['endpoints']]: {
|
|
@@ -79,6 +80,61 @@ function preprocessQueryParams(query: any, querySchema?: z.ZodTypeAny): any {
|
|
|
79
80
|
return processedQuery;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
// Helper function to create respond method for middleware compatibility
|
|
84
|
+
function createRespondFunction(
|
|
85
|
+
routeDefinition: RouteSchema,
|
|
86
|
+
responseSetter: (status: number, data: any) => void
|
|
87
|
+
) {
|
|
88
|
+
return (status: number, data: any) => {
|
|
89
|
+
const responseSchema = routeDefinition.responses[status];
|
|
90
|
+
|
|
91
|
+
if (!responseSchema) {
|
|
92
|
+
console.error(`No response schema defined for status ${status}`);
|
|
93
|
+
responseSetter(500, {
|
|
94
|
+
data: null,
|
|
95
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Undefined response schema for status." }]
|
|
96
|
+
});
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let responseBody: any;
|
|
101
|
+
|
|
102
|
+
if (status === 422) {
|
|
103
|
+
responseBody = {
|
|
104
|
+
data: null,
|
|
105
|
+
error: data
|
|
106
|
+
};
|
|
107
|
+
} else {
|
|
108
|
+
responseBody = {
|
|
109
|
+
data: data,
|
|
110
|
+
error: null
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const validationResult = responseSchema.safeParse(responseBody);
|
|
115
|
+
|
|
116
|
+
if (validationResult.success) {
|
|
117
|
+
// Handle 204 responses specially - they must not have a body
|
|
118
|
+
if (status === 204) {
|
|
119
|
+
responseSetter(status, null);
|
|
120
|
+
} else {
|
|
121
|
+
responseSetter(status, validationResult.data);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
console.error(
|
|
125
|
+
`FATAL: Constructed response body failed Zod validation for status ${status}.`,
|
|
126
|
+
validationResult.error.issues,
|
|
127
|
+
'Provided data:', data,
|
|
128
|
+
'Constructed response body:', responseBody
|
|
129
|
+
);
|
|
130
|
+
responseSetter(500, {
|
|
131
|
+
data: null,
|
|
132
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Constructed response failed validation." }]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
82
138
|
// Helper function to create multer middleware based on file upload configuration
|
|
83
139
|
function createFileUploadMiddleware(config: FileUploadConfig): express.RequestHandler {
|
|
84
140
|
// Default multer configuration
|
|
@@ -418,7 +474,12 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
418
474
|
middlewares.forEach(middleware => {
|
|
419
475
|
const wrappedMiddleware: express.RequestHandler = async (req, res, next) => {
|
|
420
476
|
try {
|
|
421
|
-
|
|
477
|
+
// Add respond method to res for middleware compatibility
|
|
478
|
+
const middlewareRes = res as any;
|
|
479
|
+
middlewareRes.respond = createRespondFunction(routeDefinition, (status, data) => {
|
|
480
|
+
res.status(status).json(data);
|
|
481
|
+
});
|
|
482
|
+
await middleware(req, middlewareRes as MiddlewareResponse, next, { domain: currentDomain, routeKey: currentRouteKey } as any);
|
|
422
483
|
} catch (error) {
|
|
423
484
|
next(error);
|
|
424
485
|
}
|
|
@@ -497,12 +497,80 @@ export function registerHonoRouteHandlers<
|
|
|
497
497
|
originalUrl: c.req.url
|
|
498
498
|
};
|
|
499
499
|
|
|
500
|
-
// Create minimal res object
|
|
501
|
-
const fakeRes = {
|
|
500
|
+
// Create minimal res object with respond method for middleware compatibility
|
|
501
|
+
const fakeRes = {
|
|
502
|
+
respond: (status: number, data: any) => {
|
|
503
|
+
const responseSchema = routeDefinition.responses[status];
|
|
504
|
+
|
|
505
|
+
if (!responseSchema) {
|
|
506
|
+
console.error(`No response schema defined for status ${status}`);
|
|
507
|
+
c.json({
|
|
508
|
+
data: null,
|
|
509
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Undefined response schema for status." }]
|
|
510
|
+
}, 500);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
let responseBody: any;
|
|
515
|
+
|
|
516
|
+
if (status === 422) {
|
|
517
|
+
responseBody = {
|
|
518
|
+
data: null,
|
|
519
|
+
error: data
|
|
520
|
+
};
|
|
521
|
+
} else {
|
|
522
|
+
responseBody = {
|
|
523
|
+
data: data,
|
|
524
|
+
error: null
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const validationResult = responseSchema.safeParse(responseBody);
|
|
529
|
+
|
|
530
|
+
if (validationResult.success) {
|
|
531
|
+
// Handle 204 responses specially - they must not have a body
|
|
532
|
+
if (status === 204) {
|
|
533
|
+
(c as any).__response = new Response(null, { status: status as any });
|
|
534
|
+
} else {
|
|
535
|
+
(c as any).__response = c.json(validationResult.data, status as any);
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
console.error(
|
|
539
|
+
`FATAL: Constructed response body failed Zod validation for status ${status}.`,
|
|
540
|
+
validationResult.error.issues,
|
|
541
|
+
'Provided data:', data,
|
|
542
|
+
'Constructed response body:', responseBody
|
|
543
|
+
);
|
|
544
|
+
(c as any).__response = c.json({
|
|
545
|
+
data: null,
|
|
546
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Constructed response failed validation." }]
|
|
547
|
+
}, 500);
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
status: (status: number) => {
|
|
551
|
+
// For middleware that might use status, but since not used, stub
|
|
552
|
+
(c as any).__response = new Response(null, { status: status });
|
|
553
|
+
},
|
|
554
|
+
json: (data: any) => {
|
|
555
|
+
// Send json response
|
|
556
|
+
(c as any).__response = c.json(data);
|
|
557
|
+
},
|
|
558
|
+
setHeader: (name: string, value: string) => {
|
|
559
|
+
c.header(name, value);
|
|
560
|
+
},
|
|
561
|
+
end: () => {
|
|
562
|
+
// Perhaps do nothing or set response
|
|
563
|
+
}
|
|
564
|
+
};
|
|
502
565
|
|
|
503
566
|
// Call Express-style middleware
|
|
504
567
|
await middleware(fakeReq as any, fakeRes as any, next, { domain: currentDomain, routeKey: currentRouteKey });
|
|
505
568
|
|
|
569
|
+
// Check if middleware responded directly
|
|
570
|
+
if ((c as any).__response) {
|
|
571
|
+
return (c as any).__response;
|
|
572
|
+
}
|
|
573
|
+
|
|
506
574
|
} catch (error) {
|
|
507
575
|
console.error('Middleware error:', error);
|
|
508
576
|
await next();
|
package/src/object-handlers.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type EndpointInfo<TDef extends ApiDefinitionSchema = ApiDefinitionSchema>
|
|
|
13
13
|
// Type for middleware function that receives endpoint information
|
|
14
14
|
export type EndpointMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = (
|
|
15
15
|
req: express.Request,
|
|
16
|
-
res:
|
|
16
|
+
res: MiddlewareResponse,
|
|
17
17
|
next: express.NextFunction,
|
|
18
18
|
endpointInfo: EndpointInfo<TDef>
|
|
19
19
|
) => void | Promise<void>;
|
|
@@ -21,14 +21,14 @@ export type EndpointMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionS
|
|
|
21
21
|
// Type for simple middleware that doesn't need endpoint information
|
|
22
22
|
export type SimpleMiddleware = (
|
|
23
23
|
req: express.Request,
|
|
24
|
-
res:
|
|
24
|
+
res: MiddlewareResponse,
|
|
25
25
|
next: express.NextFunction
|
|
26
26
|
) => void | Promise<void>;
|
|
27
27
|
|
|
28
28
|
// Type for middleware that can work with any API definition
|
|
29
29
|
export type UniversalEndpointMiddleware = (
|
|
30
30
|
req: express.Request,
|
|
31
|
-
res:
|
|
31
|
+
res: MiddlewareResponse,
|
|
32
32
|
next: express.NextFunction,
|
|
33
33
|
endpointInfo: {
|
|
34
34
|
domain: string;
|
|
@@ -36,13 +36,22 @@ export type UniversalEndpointMiddleware = (
|
|
|
36
36
|
}
|
|
37
37
|
) => void | Promise<void>;
|
|
38
38
|
|
|
39
|
+
// Custom response interface for framework-agnostic middleware
|
|
40
|
+
export interface MiddlewareResponse {
|
|
41
|
+
respond(status: number, data: any): void;
|
|
42
|
+
status(code: number): this;
|
|
43
|
+
json(data: any): void;
|
|
44
|
+
setHeader(name: string, value: string): void;
|
|
45
|
+
end(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
// Unified middleware type that works for both Express and Hono with context typing
|
|
40
49
|
export type EndpointMiddlewareCtx<
|
|
41
50
|
Ctx extends Record<string, any> = Record<string, any>,
|
|
42
51
|
TDef extends ApiDefinitionSchema = ApiDefinitionSchema
|
|
43
52
|
> =
|
|
44
53
|
// Express version: (req, res, next, endpointInfo)
|
|
45
|
-
((req: express.Request & { ctx?: Ctx }, res:
|
|
54
|
+
((req: express.Request & { ctx?: Ctx }, res: MiddlewareResponse, next: express.NextFunction, endpointInfo: EndpointInfo<TDef>) => void | Promise<void>) |
|
|
46
55
|
// Hono version: (c, next) - context is passed via c.req.ctx -> c.ctx copying
|
|
47
56
|
((c: any, next: any) => void | Promise<void>);
|
|
48
57
|
|
package/tests/middleware.test.ts
CHANGED
|
@@ -58,6 +58,9 @@ describe.each([
|
|
|
58
58
|
401: ({ data }) => {
|
|
59
59
|
throw new Error(`Authentication failed: ${data.error}`);
|
|
60
60
|
},
|
|
61
|
+
403: ({ data }) => {
|
|
62
|
+
throw new Error(`Forbidden: ${data.error}`);
|
|
63
|
+
},
|
|
61
64
|
422: ({ error }) => {
|
|
62
65
|
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
63
66
|
}
|
|
@@ -74,11 +77,32 @@ describe.each([
|
|
|
74
77
|
expect(data.error).toBe('No authorization header');
|
|
75
78
|
throw new Error('Authentication failed as expected');
|
|
76
79
|
},
|
|
80
|
+
403: ({ data }) => {
|
|
81
|
+
throw new Error(`Unexpected forbidden: ${data.error}`);
|
|
82
|
+
},
|
|
77
83
|
422: ({ error }) => {
|
|
78
84
|
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
79
85
|
}
|
|
80
86
|
})
|
|
81
87
|
).rejects.toThrow('Authentication failed as expected');
|
|
82
88
|
});
|
|
89
|
+
|
|
90
|
+
test('should deny access with invalid auth header', async () => {
|
|
91
|
+
await expect(
|
|
92
|
+
client.callApi('public', 'protected', { headers: { Authorization: 'Bearer invalid-token' } }, {
|
|
93
|
+
200: ({ data }) => data,
|
|
94
|
+
401: ({ data }) => {
|
|
95
|
+
throw new Error(`Unexpected auth failed: ${data.error}`);
|
|
96
|
+
},
|
|
97
|
+
403: ({ data }) => {
|
|
98
|
+
expect(data.error).toBe('Forbidden');
|
|
99
|
+
throw new Error('Forbidden as expected');
|
|
100
|
+
},
|
|
101
|
+
422: ({ error }) => {
|
|
102
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
).rejects.toThrow('Forbidden as expected');
|
|
106
|
+
});
|
|
83
107
|
});
|
|
84
108
|
});
|
package/tests/setup.ts
CHANGED
|
@@ -219,7 +219,8 @@ export const MiddlewareTestApiDefinition = CreateApiDefinition({
|
|
|
219
219
|
path: '/protected',
|
|
220
220
|
responses: CreateResponses({
|
|
221
221
|
200: z.object({ message: z.string(), user: z.string() }),
|
|
222
|
-
401: z.object({ error: z.string() })
|
|
222
|
+
401: z.object({ error: z.string() }),
|
|
223
|
+
403: z.object({ error: z.string() })
|
|
223
224
|
})
|
|
224
225
|
},
|
|
225
226
|
context: {
|
|
@@ -444,7 +445,74 @@ async function startHonoServer(): Promise<void> {
|
|
|
444
445
|
});
|
|
445
446
|
}
|
|
446
447
|
|
|
447
|
-
type Ctx = { user
|
|
448
|
+
type Ctx = { user?: string; noAuth?: boolean; forbidden?: boolean, middlewareData?: string }
|
|
449
|
+
|
|
450
|
+
// Shared handlers for middleware tests
|
|
451
|
+
const middlewareTestHandlers = {
|
|
452
|
+
public: {
|
|
453
|
+
ping: async (req: any, res: any) => {
|
|
454
|
+
res.respond(200, { message: "pong" });
|
|
455
|
+
},
|
|
456
|
+
protected: async (req: any, res: any) => {
|
|
457
|
+
// Middleware has already validated auth, so we only handle success case
|
|
458
|
+
res.respond(200, {
|
|
459
|
+
message: "protected content",
|
|
460
|
+
user: req.ctx?.user || "unknown"
|
|
461
|
+
});
|
|
462
|
+
},
|
|
463
|
+
context: async (req: any, res: any) => {
|
|
464
|
+
res.respond(200, {
|
|
465
|
+
message: "context test",
|
|
466
|
+
contextData: req.ctx?.middlewareData || "default"
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// Generic middleware setup function
|
|
473
|
+
function setupMiddlewareApp(app: any, isHono: boolean) {
|
|
474
|
+
// Define middleware functions
|
|
475
|
+
const loggingMiddleware: EndpointMiddlewareCtx<Ctx> = async (req, res, next, endpointInfo) => {
|
|
476
|
+
console.log(`[Test] ${req.method} ${req.path} - Domain: ${endpointInfo.domain}, Route: ${endpointInfo.routeKey}`);
|
|
477
|
+
await next();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const contextMiddleware: EndpointMiddlewareCtx<Ctx> = async (req, res, next) => {
|
|
481
|
+
req.ctx = { ...req.ctx, middlewareData: "middleware-added-data" };
|
|
482
|
+
await next();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const authMiddleware: EndpointMiddlewareCtx<Ctx> = async (req, res, next, endpointInfo) => {
|
|
486
|
+
// Only apply auth checks to protected routes
|
|
487
|
+
if (endpointInfo.domain === 'public' && endpointInfo.routeKey === 'protected') {
|
|
488
|
+
const authHeader = req.headers?.authorization;
|
|
489
|
+
if (!authHeader) {
|
|
490
|
+
res.respond(401, { error: "No authorization header" });
|
|
491
|
+
} else if (authHeader === 'Bearer valid-token') {
|
|
492
|
+
req.ctx = { ...req.ctx, user: 'testuser' };
|
|
493
|
+
await next();
|
|
494
|
+
} else {
|
|
495
|
+
res.respond(403, { error: "Forbidden" });
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
await next();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const middlewares = [
|
|
503
|
+
loggingMiddleware,
|
|
504
|
+
contextMiddleware,
|
|
505
|
+
authMiddleware
|
|
506
|
+
]
|
|
507
|
+
|
|
508
|
+
// Register handlers with middleware
|
|
509
|
+
if (isHono) {
|
|
510
|
+
const hndl = CreateTypedHonoHandlerWithContext<Ctx>();
|
|
511
|
+
hndl(app, MiddlewareTestApiDefinition, middlewareTestHandlers, middlewares);
|
|
512
|
+
} else {
|
|
513
|
+
RegisterHandlers(app, MiddlewareTestApiDefinition, middlewareTestHandlers, middlewares);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
448
516
|
|
|
449
517
|
// Middleware test servers
|
|
450
518
|
async function startMiddlewareExpressServer(): Promise<void> {
|
|
@@ -452,54 +520,7 @@ async function startMiddlewareExpressServer(): Promise<void> {
|
|
|
452
520
|
const app = express();
|
|
453
521
|
app.use(express.json());
|
|
454
522
|
|
|
455
|
-
|
|
456
|
-
const loggingMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction, endpointInfo: any) => {
|
|
457
|
-
console.log(`[Test] ${req.method} ${req.path} - Domain: ${endpointInfo.domain}, Route: ${endpointInfo.routeKey}`);
|
|
458
|
-
next();
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const contextMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
462
|
-
(req as any).ctx = { middlewareData: "middleware-added-data" };
|
|
463
|
-
next();
|
|
464
|
-
};
|
|
465
|
-
|
|
466
|
-
const authMiddleware: EndpointMiddlewareCtx<Ctx> = (req, res, next) => {
|
|
467
|
-
const authHeader = req.headers.authorization;
|
|
468
|
-
if (authHeader === 'Bearer valid-token') {
|
|
469
|
-
req.ctx = { user: 'testuser' };
|
|
470
|
-
}
|
|
471
|
-
next();
|
|
472
|
-
};
|
|
473
|
-
|
|
474
|
-
// Register handlers with middleware
|
|
475
|
-
RegisterHandlers(app, MiddlewareTestApiDefinition, {
|
|
476
|
-
public: {
|
|
477
|
-
ping: async (req: any, res: any) => {
|
|
478
|
-
res.respond(200, { message: "pong" });
|
|
479
|
-
},
|
|
480
|
-
protected: async (req, res) => {
|
|
481
|
-
// Check if user is authenticated via context
|
|
482
|
-
if (req.ctx && req.ctx.user) {
|
|
483
|
-
res.respond(200, {
|
|
484
|
-
message: "protected content",
|
|
485
|
-
user: req.ctx.user
|
|
486
|
-
});
|
|
487
|
-
} else {
|
|
488
|
-
res.respond(401, { error: "No authorization header" });
|
|
489
|
-
}
|
|
490
|
-
},
|
|
491
|
-
context: async (req: any, res: any) => {
|
|
492
|
-
res.respond(200, {
|
|
493
|
-
message: "context test",
|
|
494
|
-
contextData: req.ctx?.middlewareData || "default"
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
}, [
|
|
499
|
-
loggingMiddleware,
|
|
500
|
-
contextMiddleware,
|
|
501
|
-
authMiddleware
|
|
502
|
-
]);
|
|
523
|
+
setupMiddlewareApp(app, false);
|
|
503
524
|
|
|
504
525
|
middlewareExpressServer = app.listen(MIDDLEWARE_EXPRESS_PORT, () => {
|
|
505
526
|
resolve();
|
|
@@ -511,55 +532,7 @@ async function startMiddlewareHonoServer(): Promise<void> {
|
|
|
511
532
|
return new Promise((resolve) => {
|
|
512
533
|
const app = new Hono();
|
|
513
534
|
|
|
514
|
-
|
|
515
|
-
const loggingMiddleware = async (req: any, res: any, next: any, endpointInfo: any) => {
|
|
516
|
-
console.log(`[Test Hono] ${req.method} ${req.path} - Domain: ${endpointInfo.domain}, Route: ${endpointInfo.routeKey}`);
|
|
517
|
-
await next();
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
const contextMiddleware = async (req: any, res: any, next: any) => {
|
|
521
|
-
req.ctx = { ...req.ctx, middlewareData: "middleware-added-data" };
|
|
522
|
-
await next();
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
const authMiddleware: EndpointMiddlewareCtx<Ctx> = async (req, res, next) => {
|
|
526
|
-
const authHeader = req.headers?.authorization;
|
|
527
|
-
if (authHeader === 'Bearer valid-token') {
|
|
528
|
-
req.ctx = { ...req.ctx, user: 'testuser' };
|
|
529
|
-
}
|
|
530
|
-
await next();
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
const hdnl = CreateTypedHonoHandlerWithContext<Ctx>()
|
|
534
|
-
// Register handlers with middleware
|
|
535
|
-
hdnl(app, MiddlewareTestApiDefinition, {
|
|
536
|
-
public: {
|
|
537
|
-
ping: async (req: any, res: any) => {
|
|
538
|
-
res.respond(200, { message: "pong" });
|
|
539
|
-
},
|
|
540
|
-
protected: async (req, res) => {
|
|
541
|
-
// Check if user is authenticated via context
|
|
542
|
-
if (req.ctx && req.ctx.user) {
|
|
543
|
-
res.respond(200, {
|
|
544
|
-
message: "protected content",
|
|
545
|
-
user: req.ctx.user
|
|
546
|
-
});
|
|
547
|
-
} else {
|
|
548
|
-
res.respond(401, { error: "No authorization header" });
|
|
549
|
-
}
|
|
550
|
-
},
|
|
551
|
-
context: async (req: any, res: any) => {
|
|
552
|
-
res.respond(200, {
|
|
553
|
-
message: "context test",
|
|
554
|
-
contextData: req.ctx?.middlewareData || "default"
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}, [
|
|
559
|
-
loggingMiddleware,
|
|
560
|
-
contextMiddleware,
|
|
561
|
-
authMiddleware
|
|
562
|
-
]);
|
|
535
|
+
setupMiddlewareApp(app, true);
|
|
563
536
|
|
|
564
537
|
// Create HTTP server from Hono app
|
|
565
538
|
const server = app.fetch;
|