ts-typed-api 0.2.12 → 0.2.13

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.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,6 +411,10 @@ middlewares) {
367
411
  middlewares.forEach(middleware => {
368
412
  const wrappedMiddleware = async (req, res, next) => {
369
413
  try {
414
+ // Add respond method to res for middleware compatibility
415
+ res.respond = createRespondFunction(routeDefinition, (status, data) => {
416
+ res.status(status).json(data);
417
+ });
370
418
  await middleware(req, res, next, { domain: currentDomain, routeKey: currentRouteKey });
371
419
  }
372
420
  catch (error) {
@@ -415,10 +415,56 @@ 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 (middleware shouldn't use it)
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
+ };
420
462
  // Call Express-style middleware
421
463
  await middleware(fakeReq, fakeRes, next, { domain: currentDomain, routeKey: currentRouteKey });
464
+ // Check if middleware responded directly
465
+ if (c.__response) {
466
+ return c.__response;
467
+ }
422
468
  }
423
469
  catch (error) {
424
470
  console.error('Middleware error:', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-typed-api",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "A lightweight, type-safe RPC library for TypeScript with Zod validation",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/handler.ts CHANGED
@@ -79,6 +79,61 @@ function preprocessQueryParams(query: any, querySchema?: z.ZodTypeAny): any {
79
79
  return processedQuery;
80
80
  }
81
81
 
82
+ // Helper function to create respond method for middleware compatibility
83
+ function createRespondFunction(
84
+ routeDefinition: RouteSchema,
85
+ responseSetter: (status: number, data: any) => void
86
+ ) {
87
+ return (status: number, data: any) => {
88
+ const responseSchema = routeDefinition.responses[status];
89
+
90
+ if (!responseSchema) {
91
+ console.error(`No response schema defined for status ${status}`);
92
+ responseSetter(500, {
93
+ data: null,
94
+ error: [{ field: "general", type: "general", message: "Internal server error: Undefined response schema for status." }]
95
+ });
96
+ return;
97
+ }
98
+
99
+ let responseBody: any;
100
+
101
+ if (status === 422) {
102
+ responseBody = {
103
+ data: null,
104
+ error: data
105
+ };
106
+ } else {
107
+ responseBody = {
108
+ data: data,
109
+ error: null
110
+ };
111
+ }
112
+
113
+ const validationResult = responseSchema.safeParse(responseBody);
114
+
115
+ if (validationResult.success) {
116
+ // Handle 204 responses specially - they must not have a body
117
+ if (status === 204) {
118
+ responseSetter(status, null);
119
+ } else {
120
+ responseSetter(status, validationResult.data);
121
+ }
122
+ } else {
123
+ console.error(
124
+ `FATAL: Constructed response body failed Zod validation for status ${status}.`,
125
+ validationResult.error.issues,
126
+ 'Provided data:', data,
127
+ 'Constructed response body:', responseBody
128
+ );
129
+ responseSetter(500, {
130
+ data: null,
131
+ error: [{ field: "general", type: "general", message: "Internal server error: Constructed response failed validation." }]
132
+ });
133
+ }
134
+ };
135
+ }
136
+
82
137
  // Helper function to create multer middleware based on file upload configuration
83
138
  function createFileUploadMiddleware(config: FileUploadConfig): express.RequestHandler {
84
139
  // Default multer configuration
@@ -418,6 +473,10 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
418
473
  middlewares.forEach(middleware => {
419
474
  const wrappedMiddleware: express.RequestHandler = async (req, res, next) => {
420
475
  try {
476
+ // Add respond method to res for middleware compatibility
477
+ (res as any).respond = createRespondFunction(routeDefinition, (status, data) => {
478
+ res.status(status).json(data);
479
+ });
421
480
  await middleware(req, res, next, { domain: currentDomain, routeKey: currentRouteKey } as any);
422
481
  } catch (error) {
423
482
  next(error);
@@ -497,12 +497,66 @@ export function registerHonoRouteHandlers<
497
497
  originalUrl: c.req.url
498
498
  };
499
499
 
500
- // Create minimal res object (middleware shouldn't use it)
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
+ };
502
551
 
503
552
  // Call Express-style middleware
504
553
  await middleware(fakeReq as any, fakeRes as any, next, { domain: currentDomain, routeKey: currentRouteKey });
505
554
 
555
+ // Check if middleware responded directly
556
+ if ((c as any).__response) {
557
+ return (c as any).__response;
558
+ }
559
+
506
560
  } catch (error) {
507
561
  console.error('Middleware error:', error);
508
562
  await next();
@@ -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
@@ -7,7 +7,6 @@ import { PublicApiDefinition as SimplePublicApiDefinition, PrivateApiDefinition
7
7
  import { PublicApiDefinition as AdvancedPublicApiDefinition, PrivateApiDefinition as AdvancedPrivateApiDefinition } from '../examples/advanced/definitions';
8
8
  import { RegisterHandlers, RegisterHonoHandlers, CreateApiDefinition, CreateResponses, CreateTypedHonoHandlerWithContext } from '../src';
9
9
  import { z } from 'zod';
10
- import { EndpointMiddlewareCtx } from '../src/object-handlers';
11
10
 
12
11
  // Shared handler definitions for simple API
13
12
  const simplePublicHandlers = {
@@ -219,7 +218,8 @@ export const MiddlewareTestApiDefinition = CreateApiDefinition({
219
218
  path: '/protected',
220
219
  responses: CreateResponses({
221
220
  200: z.object({ message: z.string(), user: z.string() }),
222
- 401: z.object({ error: z.string() })
221
+ 401: z.object({ error: z.string() }),
222
+ 403: z.object({ error: z.string() })
223
223
  })
224
224
  },
225
225
  context: {
@@ -444,62 +444,111 @@ async function startHonoServer(): Promise<void> {
444
444
  });
445
445
  }
446
446
 
447
- type Ctx = { user: string }
447
+ type Ctx = { user?: string; noAuth?: boolean; forbidden?: boolean }
448
448
 
449
- // Middleware test servers
450
- async function startMiddlewareExpressServer(): Promise<void> {
451
- return new Promise((resolve) => {
452
- const app = express();
453
- app.use(express.json());
449
+ // Shared handlers for middleware tests
450
+ const middlewareTestHandlers = {
451
+ public: {
452
+ ping: async (req: any, res: any) => {
453
+ res.respond(200, { message: "pong" });
454
+ },
455
+ protected: async (req: any, res: any) => {
456
+ // Middleware has already validated auth, so we only handle success case
457
+ res.respond(200, {
458
+ message: "protected content",
459
+ user: req.ctx?.user || "unknown"
460
+ });
461
+ },
462
+ context: async (req: any, res: any) => {
463
+ res.respond(200, {
464
+ message: "context test",
465
+ contextData: req.ctx?.middlewareData || "default"
466
+ });
467
+ }
468
+ }
469
+ };
454
470
 
455
- // Define middleware functions
456
- const loggingMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction, endpointInfo: any) => {
471
+ // Generic middleware setup function
472
+ function setupMiddlewareApp(app: any, isHono: boolean) {
473
+ // Define middleware functions
474
+ const loggingMiddleware = isHono ?
475
+ async (req: any, res: any, next: any, endpointInfo: any) => {
476
+ console.log(`[Test Hono] ${req.method} ${req.path} - Domain: ${endpointInfo.domain}, Route: ${endpointInfo.routeKey}`);
477
+ await next();
478
+ } :
479
+ (req: express.Request, res: express.Response, next: express.NextFunction, endpointInfo: any) => {
457
480
  console.log(`[Test] ${req.method} ${req.path} - Domain: ${endpointInfo.domain}, Route: ${endpointInfo.routeKey}`);
458
481
  next();
459
482
  };
460
483
 
461
- const contextMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
484
+ const contextMiddleware = isHono ?
485
+ async (req: any, res: any, next: any) => {
486
+ req.ctx = { ...req.ctx, middlewareData: "middleware-added-data" };
487
+ await next();
488
+ } :
489
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
462
490
  (req as any).ctx = { middlewareData: "middleware-added-data" };
463
491
  next();
464
492
  };
465
493
 
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' };
494
+ const authMiddleware = isHono ?
495
+ async (req: any, res: any, next: any, endpointInfo: any) => {
496
+ // Only apply auth checks to protected routes
497
+ if (endpointInfo.domain === 'public' && endpointInfo.routeKey === 'protected') {
498
+ const authHeader = req.headers?.authorization;
499
+ if (!authHeader) {
500
+ (res as any).respond(401, { error: "No authorization header" });
501
+ } else if (authHeader === 'Bearer valid-token') {
502
+ req.ctx = { ...req.ctx, user: 'testuser' };
503
+ await next();
504
+ } else {
505
+ (res as any).respond(403, { error: "Forbidden" });
506
+ }
507
+ } else {
508
+ await next();
470
509
  }
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
- });
510
+ } :
511
+ (req: any, res: any, next: any, endpointInfo: any) => {
512
+ // Only apply auth checks to protected routes
513
+ if (endpointInfo.domain === 'public' && endpointInfo.routeKey === 'protected') {
514
+ const authHeader = req.headers?.authorization;
515
+ if (!authHeader) {
516
+ (res as any).respond(401, { error: "No authorization header" });
517
+ } else if (authHeader === 'Bearer valid-token') {
518
+ req.ctx = { ...req.ctx, user: 'testuser' };
519
+ next();
520
+ } else {
521
+ (res as any).respond(403, { error: "Forbidden" });
496
522
  }
523
+ } else {
524
+ next();
497
525
  }
498
- }, [
526
+ };
527
+
528
+ // Register handlers with middleware
529
+ if (isHono) {
530
+ const hndl = CreateTypedHonoHandlerWithContext<Ctx>();
531
+ hndl(app, MiddlewareTestApiDefinition, middlewareTestHandlers, [
532
+ loggingMiddleware,
533
+ contextMiddleware,
534
+ authMiddleware
535
+ ]);
536
+ } else {
537
+ RegisterHandlers(app, MiddlewareTestApiDefinition, middlewareTestHandlers, [
499
538
  loggingMiddleware,
500
539
  contextMiddleware,
501
540
  authMiddleware
502
541
  ]);
542
+ }
543
+ }
544
+
545
+ // Middleware test servers
546
+ async function startMiddlewareExpressServer(): Promise<void> {
547
+ return new Promise((resolve) => {
548
+ const app = express();
549
+ app.use(express.json());
550
+
551
+ setupMiddlewareApp(app, false);
503
552
 
504
553
  middlewareExpressServer = app.listen(MIDDLEWARE_EXPRESS_PORT, () => {
505
554
  resolve();
@@ -511,55 +560,7 @@ async function startMiddlewareHonoServer(): Promise<void> {
511
560
  return new Promise((resolve) => {
512
561
  const app = new Hono();
513
562
 
514
- // Define middleware functions
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
- ]);
563
+ setupMiddlewareApp(app, true);
563
564
 
564
565
  // Create HTTP server from Hono app
565
566
  const server = app.fetch;