serverless-event-orchestrator 1.0.1

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +377 -0
  3. package/dist/dispatcher.d.ts +18 -0
  4. package/dist/dispatcher.d.ts.map +1 -0
  5. package/dist/dispatcher.js +345 -0
  6. package/dist/dispatcher.js.map +1 -0
  7. package/dist/http/body-parser.d.ts +27 -0
  8. package/dist/http/body-parser.d.ts.map +1 -0
  9. package/dist/http/body-parser.js +56 -0
  10. package/dist/http/body-parser.js.map +1 -0
  11. package/dist/http/cors.d.ts +32 -0
  12. package/dist/http/cors.d.ts.map +1 -0
  13. package/dist/http/cors.js +69 -0
  14. package/dist/http/cors.js.map +1 -0
  15. package/dist/http/index.d.ts +4 -0
  16. package/dist/http/index.d.ts.map +1 -0
  17. package/dist/http/index.js +20 -0
  18. package/dist/http/index.js.map +1 -0
  19. package/dist/http/response.d.ts +104 -0
  20. package/dist/http/response.d.ts.map +1 -0
  21. package/dist/http/response.js +164 -0
  22. package/dist/http/response.js.map +1 -0
  23. package/dist/identity/extractor.d.ts +39 -0
  24. package/dist/identity/extractor.d.ts.map +1 -0
  25. package/dist/identity/extractor.js +88 -0
  26. package/dist/identity/extractor.js.map +1 -0
  27. package/dist/identity/index.d.ts +2 -0
  28. package/dist/identity/index.d.ts.map +1 -0
  29. package/dist/identity/index.js +18 -0
  30. package/dist/identity/index.js.map +1 -0
  31. package/dist/index.d.ts +17 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +62 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/types/event-type.enum.d.ts +20 -0
  36. package/dist/types/event-type.enum.d.ts.map +1 -0
  37. package/dist/types/event-type.enum.js +25 -0
  38. package/dist/types/event-type.enum.js.map +1 -0
  39. package/dist/types/index.d.ts +3 -0
  40. package/dist/types/index.d.ts.map +1 -0
  41. package/dist/types/index.js +19 -0
  42. package/dist/types/index.js.map +1 -0
  43. package/dist/types/routes.d.ts +163 -0
  44. package/dist/types/routes.d.ts.map +1 -0
  45. package/dist/types/routes.js +3 -0
  46. package/dist/types/routes.js.map +1 -0
  47. package/dist/utils/headers.d.ts +28 -0
  48. package/dist/utils/headers.d.ts.map +1 -0
  49. package/dist/utils/headers.js +61 -0
  50. package/dist/utils/headers.js.map +1 -0
  51. package/dist/utils/index.d.ts +3 -0
  52. package/dist/utils/index.d.ts.map +1 -0
  53. package/dist/utils/index.js +19 -0
  54. package/dist/utils/index.js.map +1 -0
  55. package/dist/utils/path-matcher.d.ts +33 -0
  56. package/dist/utils/path-matcher.d.ts.map +1 -0
  57. package/dist/utils/path-matcher.js +74 -0
  58. package/dist/utils/path-matcher.js.map +1 -0
  59. package/jest.config.js +32 -0
  60. package/package.json +68 -0
  61. package/src/dispatcher.ts +415 -0
  62. package/src/http/body-parser.ts +60 -0
  63. package/src/http/cors.ts +76 -0
  64. package/src/http/index.ts +3 -0
  65. package/src/http/response.ts +194 -0
  66. package/src/identity/extractor.ts +89 -0
  67. package/src/identity/index.ts +1 -0
  68. package/src/index.ts +92 -0
  69. package/src/types/event-type.enum.ts +20 -0
  70. package/src/types/index.ts +2 -0
  71. package/src/types/routes.ts +182 -0
  72. package/src/utils/headers.ts +72 -0
  73. package/src/utils/index.ts +2 -0
  74. package/src/utils/path-matcher.ts +79 -0
  75. package/tests/cors.test.ts +133 -0
  76. package/tests/dispatcher.test.ts +425 -0
  77. package/tests/headers.test.ts +99 -0
  78. package/tests/identity.test.ts +171 -0
  79. package/tests/path-matcher.test.ts +102 -0
  80. package/tests/response.test.ts +155 -0
  81. package/tsconfig.json +24 -0
@@ -0,0 +1,99 @@
1
+ import { normalizeHeaders, getHeader, getCorsHeaders } from '../src/utils/headers';
2
+
3
+ describe('normalizeHeaders', () => {
4
+ it('should convert all header keys to lowercase', () => {
5
+ const headers = {
6
+ 'Content-Type': 'application/json',
7
+ 'X-Custom-Header': 'value',
8
+ 'AUTHORIZATION': 'Bearer token'
9
+ };
10
+
11
+ const normalized = normalizeHeaders(headers);
12
+
13
+ expect(normalized['content-type']).toBe('application/json');
14
+ expect(normalized['x-custom-header']).toBe('value');
15
+ expect(normalized['authorization']).toBe('Bearer token');
16
+ });
17
+
18
+ it('should return empty object for undefined headers', () => {
19
+ expect(normalizeHeaders(undefined)).toEqual({});
20
+ });
21
+
22
+ it('should return empty object for empty headers', () => {
23
+ expect(normalizeHeaders({})).toEqual({});
24
+ });
25
+ });
26
+
27
+ describe('getHeader', () => {
28
+ const headers = {
29
+ 'Content-Type': 'application/json',
30
+ 'X-Request-ID': '12345'
31
+ };
32
+
33
+ it('should get header case-insensitively', () => {
34
+ expect(getHeader(headers, 'content-type')).toBe('application/json');
35
+ expect(getHeader(headers, 'Content-Type')).toBe('application/json');
36
+ expect(getHeader(headers, 'CONTENT-TYPE')).toBe('application/json');
37
+ });
38
+
39
+ it('should return undefined for missing header', () => {
40
+ expect(getHeader(headers, 'Authorization')).toBeUndefined();
41
+ });
42
+
43
+ it('should return undefined for undefined headers', () => {
44
+ expect(getHeader(undefined, 'Content-Type')).toBeUndefined();
45
+ });
46
+ });
47
+
48
+ describe('getCorsHeaders', () => {
49
+ it('should return default CORS headers without config', () => {
50
+ const corsHeaders = getCorsHeaders();
51
+
52
+ expect(corsHeaders['Access-Control-Allow-Origin']).toBe('*');
53
+ expect(corsHeaders['Access-Control-Allow-Methods']).toContain('GET');
54
+ expect(corsHeaders['Access-Control-Allow-Methods']).toContain('POST');
55
+ expect(corsHeaders['Access-Control-Allow-Headers']).toContain('Content-Type');
56
+ });
57
+
58
+ it('should use custom origins', () => {
59
+ const corsHeaders = getCorsHeaders({
60
+ origins: ['https://example.com', 'https://app.example.com']
61
+ });
62
+
63
+ expect(corsHeaders['Access-Control-Allow-Origin']).toBe('https://example.com, https://app.example.com');
64
+ });
65
+
66
+ it('should use wildcard origin', () => {
67
+ const corsHeaders = getCorsHeaders({ origins: '*' });
68
+
69
+ expect(corsHeaders['Access-Control-Allow-Origin']).toBe('*');
70
+ });
71
+
72
+ it('should set credentials header', () => {
73
+ const corsHeaders = getCorsHeaders({ credentials: true });
74
+
75
+ expect(corsHeaders['Access-Control-Allow-Credentials']).toBe('true');
76
+ });
77
+
78
+ it('should set max age header', () => {
79
+ const corsHeaders = getCorsHeaders({ maxAge: 86400 });
80
+
81
+ expect(corsHeaders['Access-Control-Max-Age']).toBe('86400');
82
+ });
83
+
84
+ it('should use custom methods', () => {
85
+ const corsHeaders = getCorsHeaders({
86
+ methods: ['GET', 'POST']
87
+ });
88
+
89
+ expect(corsHeaders['Access-Control-Allow-Methods']).toBe('GET, POST');
90
+ });
91
+
92
+ it('should use custom headers', () => {
93
+ const corsHeaders = getCorsHeaders({
94
+ headers: ['X-Custom-Header', 'X-Another-Header']
95
+ });
96
+
97
+ expect(corsHeaders['Access-Control-Allow-Headers']).toBe('X-Custom-Header, X-Another-Header');
98
+ });
99
+ });
@@ -0,0 +1,171 @@
1
+ import {
2
+ extractIdentity,
3
+ extractUserPoolId,
4
+ validateIssuer,
5
+ hasAnyGroup,
6
+ hasAllGroups
7
+ } from '../src/identity/extractor';
8
+ import { IdentityContext } from '../src/types/routes';
9
+
10
+ describe('extractIdentity', () => {
11
+ it('should return undefined when no claims present', () => {
12
+ const event = { requestContext: {} };
13
+ expect(extractIdentity(event)).toBeUndefined();
14
+ });
15
+
16
+ it('should return undefined when event has no requestContext', () => {
17
+ const event = {};
18
+ expect(extractIdentity(event)).toBeUndefined();
19
+ });
20
+
21
+ it('should extract identity from Cognito claims', () => {
22
+ const event = {
23
+ requestContext: {
24
+ authorizer: {
25
+ claims: {
26
+ sub: 'user-123',
27
+ email: 'user@example.com',
28
+ 'cognito:groups': 'Admin,User',
29
+ iss: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123'
30
+ }
31
+ }
32
+ }
33
+ };
34
+
35
+ const identity = extractIdentity(event);
36
+
37
+ expect(identity).toEqual({
38
+ userId: 'user-123',
39
+ email: 'user@example.com',
40
+ groups: ['Admin', 'User'],
41
+ issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123',
42
+ claims: event.requestContext.authorizer.claims
43
+ });
44
+ });
45
+
46
+ it('should handle cognito:username as userId fallback', () => {
47
+ const event = {
48
+ requestContext: {
49
+ authorizer: {
50
+ claims: {
51
+ 'cognito:username': 'john_doe'
52
+ }
53
+ }
54
+ }
55
+ };
56
+
57
+ const identity = extractIdentity(event);
58
+ expect(identity?.userId).toBe('john_doe');
59
+ });
60
+
61
+ it('should handle groups as array', () => {
62
+ const event = {
63
+ requestContext: {
64
+ authorizer: {
65
+ claims: {
66
+ sub: 'user-123',
67
+ 'cognito:groups': ['Admin', 'User']
68
+ }
69
+ }
70
+ }
71
+ };
72
+
73
+ const identity = extractIdentity(event);
74
+ expect(identity?.groups).toEqual(['Admin', 'User']);
75
+ });
76
+ });
77
+
78
+ describe('extractUserPoolId', () => {
79
+ it('should extract User Pool ID from issuer URL', () => {
80
+ const issuer = 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123';
81
+ expect(extractUserPoolId(issuer)).toBe('us-east-1_ABC123');
82
+ });
83
+
84
+ it('should return undefined for undefined issuer', () => {
85
+ expect(extractUserPoolId(undefined)).toBeUndefined();
86
+ });
87
+
88
+ it('should handle issuer with trailing slash', () => {
89
+ const issuer = 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XYZ789';
90
+ expect(extractUserPoolId(issuer)).toBe('us-east-1_XYZ789');
91
+ });
92
+ });
93
+
94
+ describe('validateIssuer', () => {
95
+ const validIdentity: IdentityContext = {
96
+ userId: 'user-123',
97
+ issuer: 'https://cognito-idp.us-east-1.amazonaws.com/us-east-1_ABC123'
98
+ };
99
+
100
+ it('should return true when User Pool matches', () => {
101
+ expect(validateIssuer(validIdentity, 'us-east-1_ABC123')).toBe(true);
102
+ });
103
+
104
+ it('should return false when User Pool does not match', () => {
105
+ expect(validateIssuer(validIdentity, 'us-east-1_WRONG')).toBe(false);
106
+ });
107
+
108
+ it('should return false when identity is undefined', () => {
109
+ expect(validateIssuer(undefined, 'us-east-1_ABC123')).toBe(false);
110
+ });
111
+
112
+ it('should return false when issuer is missing', () => {
113
+ const identity: IdentityContext = { userId: 'user-123' };
114
+ expect(validateIssuer(identity, 'us-east-1_ABC123')).toBe(false);
115
+ });
116
+ });
117
+
118
+ describe('hasAnyGroup', () => {
119
+ const identity: IdentityContext = {
120
+ userId: 'user-123',
121
+ groups: ['User', 'Premium']
122
+ };
123
+
124
+ it('should return true if user has any of the allowed groups', () => {
125
+ expect(hasAnyGroup(identity, ['Admin', 'Premium'])).toBe(true);
126
+ expect(hasAnyGroup(identity, ['User'])).toBe(true);
127
+ });
128
+
129
+ it('should return false if user has none of the allowed groups', () => {
130
+ expect(hasAnyGroup(identity, ['Admin', 'SuperAdmin'])).toBe(false);
131
+ });
132
+
133
+ it('should return false for undefined identity', () => {
134
+ expect(hasAnyGroup(undefined, ['Admin'])).toBe(false);
135
+ });
136
+
137
+ it('should return false for identity with no groups', () => {
138
+ const noGroupsIdentity: IdentityContext = { userId: 'user-123' };
139
+ expect(hasAnyGroup(noGroupsIdentity, ['Admin'])).toBe(false);
140
+ });
141
+
142
+ it('should return false for empty groups array', () => {
143
+ const emptyGroupsIdentity: IdentityContext = { userId: 'user-123', groups: [] };
144
+ expect(hasAnyGroup(emptyGroupsIdentity, ['Admin'])).toBe(false);
145
+ });
146
+ });
147
+
148
+ describe('hasAllGroups', () => {
149
+ const identity: IdentityContext = {
150
+ userId: 'user-123',
151
+ groups: ['User', 'Premium', 'Verified']
152
+ };
153
+
154
+ it('should return true if user has all required groups', () => {
155
+ expect(hasAllGroups(identity, ['User', 'Premium'])).toBe(true);
156
+ expect(hasAllGroups(identity, ['Verified'])).toBe(true);
157
+ });
158
+
159
+ it('should return false if user is missing any required group', () => {
160
+ expect(hasAllGroups(identity, ['User', 'Admin'])).toBe(false);
161
+ });
162
+
163
+ it('should return false for undefined identity', () => {
164
+ expect(hasAllGroups(undefined, ['Admin'])).toBe(false);
165
+ });
166
+
167
+ it('should return false for identity with no groups', () => {
168
+ const noGroupsIdentity: IdentityContext = { userId: 'user-123' };
169
+ expect(hasAllGroups(noGroupsIdentity, ['Admin'])).toBe(false);
170
+ });
171
+ });
@@ -0,0 +1,102 @@
1
+ import {
2
+ matchPath,
3
+ patternToRegex,
4
+ hasPathParameters,
5
+ normalizePath
6
+ } from '../src/utils/path-matcher';
7
+
8
+ describe('patternToRegex', () => {
9
+ it('should convert simple pattern without parameters', () => {
10
+ const { regex, paramNames } = patternToRegex('/users');
11
+ expect(paramNames).toEqual([]);
12
+ expect(regex.test('/users')).toBe(true);
13
+ expect(regex.test('/users/')).toBe(false);
14
+ expect(regex.test('/other')).toBe(false);
15
+ });
16
+
17
+ it('should extract single parameter name', () => {
18
+ const { regex, paramNames } = patternToRegex('/users/{id}');
19
+ expect(paramNames).toEqual(['id']);
20
+ expect(regex.test('/users/123')).toBe(true);
21
+ expect(regex.test('/users/abc-def')).toBe(true);
22
+ expect(regex.test('/users/')).toBe(false);
23
+ });
24
+
25
+ it('should extract multiple parameter names', () => {
26
+ const { regex, paramNames } = patternToRegex('/users/{userId}/posts/{postId}');
27
+ expect(paramNames).toEqual(['userId', 'postId']);
28
+ expect(regex.test('/users/123/posts/456')).toBe(true);
29
+ });
30
+
31
+ it('should escape special regex characters', () => {
32
+ const { regex } = patternToRegex('/api/v1.0/users');
33
+ expect(regex.test('/api/v1.0/users')).toBe(true);
34
+ expect(regex.test('/api/v1X0/users')).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('matchPath', () => {
39
+ it('should return null for non-matching paths', () => {
40
+ expect(matchPath('/users', '/posts')).toBeNull();
41
+ expect(matchPath('/users/{id}', '/posts/123')).toBeNull();
42
+ });
43
+
44
+ it('should return empty params for exact match without parameters', () => {
45
+ const result = matchPath('/users', '/users');
46
+ expect(result).toEqual({});
47
+ });
48
+
49
+ it('should extract single path parameter', () => {
50
+ const result = matchPath('/users/{id}', '/users/123');
51
+ expect(result).toEqual({ id: '123' });
52
+ });
53
+
54
+ it('should extract multiple path parameters', () => {
55
+ const result = matchPath('/users/{userId}/posts/{postId}', '/users/abc/posts/xyz');
56
+ expect(result).toEqual({ userId: 'abc', postId: 'xyz' });
57
+ });
58
+
59
+ it('should handle parameters with special characters', () => {
60
+ const result = matchPath('/users/{id}', '/users/user-123_abc');
61
+ expect(result).toEqual({ id: 'user-123_abc' });
62
+ });
63
+
64
+ it('should not match partial paths', () => {
65
+ expect(matchPath('/users/{id}', '/users/123/extra')).toBeNull();
66
+ expect(matchPath('/users/{id}/posts', '/users/123')).toBeNull();
67
+ });
68
+ });
69
+
70
+ describe('hasPathParameters', () => {
71
+ it('should return true for patterns with parameters', () => {
72
+ expect(hasPathParameters('/users/{id}')).toBe(true);
73
+ expect(hasPathParameters('/users/{userId}/posts/{postId}')).toBe(true);
74
+ });
75
+
76
+ it('should return false for patterns without parameters', () => {
77
+ expect(hasPathParameters('/users')).toBe(false);
78
+ expect(hasPathParameters('/users/list')).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('normalizePath', () => {
83
+ it('should add leading slash if missing', () => {
84
+ expect(normalizePath('users')).toBe('/users');
85
+ });
86
+
87
+ it('should remove trailing slash', () => {
88
+ expect(normalizePath('/users/')).toBe('/users');
89
+ });
90
+
91
+ it('should keep root path as is', () => {
92
+ expect(normalizePath('/')).toBe('/');
93
+ });
94
+
95
+ it('should handle empty string', () => {
96
+ expect(normalizePath('')).toBe('/');
97
+ });
98
+
99
+ it('should normalize complex paths', () => {
100
+ expect(normalizePath('users/123/')).toBe('/users/123');
101
+ });
102
+ });
@@ -0,0 +1,155 @@
1
+ import {
2
+ HttpStatus,
3
+ DefaultResponseCode,
4
+ createStandardResponse,
5
+ successResponse,
6
+ createdResponse,
7
+ badRequestResponse,
8
+ unauthorizedResponse,
9
+ forbiddenResponse,
10
+ notFoundResponse,
11
+ conflictResponse,
12
+ validationErrorResponse,
13
+ internalErrorResponse,
14
+ customErrorResponse
15
+ } from '../src/http/response';
16
+
17
+ describe('createStandardResponse', () => {
18
+ it('should create response with data', () => {
19
+ const response = createStandardResponse(200, { user: { id: 1 } });
20
+ const body = JSON.parse(response.body);
21
+
22
+ expect(response.statusCode).toBe(200);
23
+ expect(body.status).toBe(200);
24
+ expect(body.code).toBe(DefaultResponseCode.SUCCESS);
25
+ expect(body.data).toEqual({ user: { id: 1 } });
26
+ });
27
+
28
+ it('should create response with custom code', () => {
29
+ const response = createStandardResponse(200, null, 'CUSTOM_CODE');
30
+ const body = JSON.parse(response.body);
31
+
32
+ expect(body.code).toBe('CUSTOM_CODE');
33
+ });
34
+
35
+ it('should create response with message', () => {
36
+ const response = createStandardResponse(400, undefined, undefined, 'Error message');
37
+ const body = JSON.parse(response.body);
38
+
39
+ expect(body.message).toBe('Error message');
40
+ });
41
+
42
+ it('should include custom headers', () => {
43
+ const response = createStandardResponse(200, null, undefined, undefined, {
44
+ 'X-Custom-Header': 'value'
45
+ });
46
+
47
+ expect(response.headers?.['X-Custom-Header']).toBe('value');
48
+ expect(response.headers?.['Content-Type']).toBe('application/json');
49
+ });
50
+ });
51
+
52
+ describe('successResponse', () => {
53
+ it('should return 200 with data', () => {
54
+ const response = successResponse({ items: [1, 2, 3] });
55
+ const body = JSON.parse(response.body);
56
+
57
+ expect(response.statusCode).toBe(200);
58
+ expect(body.code).toBe(DefaultResponseCode.SUCCESS);
59
+ expect(body.data).toEqual({ items: [1, 2, 3] });
60
+ });
61
+
62
+ it('should return 200 without data', () => {
63
+ const response = successResponse();
64
+ expect(response.statusCode).toBe(200);
65
+ });
66
+ });
67
+
68
+ describe('createdResponse', () => {
69
+ it('should return 201 with data', () => {
70
+ const response = createdResponse({ id: 123 });
71
+ const body = JSON.parse(response.body);
72
+
73
+ expect(response.statusCode).toBe(201);
74
+ expect(body.code).toBe(DefaultResponseCode.CREATED);
75
+ expect(body.data).toEqual({ id: 123 });
76
+ });
77
+ });
78
+
79
+ describe('error responses', () => {
80
+ it('badRequestResponse should return 400', () => {
81
+ const response = badRequestResponse('Invalid input');
82
+ const body = JSON.parse(response.body);
83
+
84
+ expect(response.statusCode).toBe(400);
85
+ expect(body.code).toBe(DefaultResponseCode.BAD_REQUEST);
86
+ expect(body.message).toBe('Invalid input');
87
+ });
88
+
89
+ it('unauthorizedResponse should return 401', () => {
90
+ const response = unauthorizedResponse('Token expired');
91
+
92
+ expect(response.statusCode).toBe(401);
93
+ });
94
+
95
+ it('forbiddenResponse should return 403', () => {
96
+ const response = forbiddenResponse('Access denied');
97
+
98
+ expect(response.statusCode).toBe(403);
99
+ });
100
+
101
+ it('notFoundResponse should return 404', () => {
102
+ const response = notFoundResponse('User not found');
103
+
104
+ expect(response.statusCode).toBe(404);
105
+ });
106
+
107
+ it('conflictResponse should return 409', () => {
108
+ const response = conflictResponse('Email already exists');
109
+
110
+ expect(response.statusCode).toBe(409);
111
+ });
112
+
113
+ it('validationErrorResponse should return 422', () => {
114
+ const response = validationErrorResponse('Invalid email format');
115
+
116
+ expect(response.statusCode).toBe(422);
117
+ });
118
+
119
+ it('internalErrorResponse should return 500', () => {
120
+ const response = internalErrorResponse('Something went wrong');
121
+
122
+ expect(response.statusCode).toBe(500);
123
+ });
124
+ });
125
+
126
+ describe('customErrorResponse', () => {
127
+ enum MyErrorCodes {
128
+ USER_SUSPENDED = 'USER_SUSPENDED',
129
+ QUOTA_EXCEEDED = 'QUOTA_EXCEEDED'
130
+ }
131
+
132
+ const codeToStatus: Record<MyErrorCodes, HttpStatus> = {
133
+ [MyErrorCodes.USER_SUSPENDED]: HttpStatus.FORBIDDEN,
134
+ [MyErrorCodes.QUOTA_EXCEEDED]: HttpStatus.UNPROCESSABLE_ENTITY
135
+ };
136
+
137
+ it('should map custom code to correct status', () => {
138
+ const response = customErrorResponse(
139
+ MyErrorCodes.USER_SUSPENDED,
140
+ 'Account suspended',
141
+ codeToStatus
142
+ );
143
+ const body = JSON.parse(response.body);
144
+
145
+ expect(response.statusCode).toBe(403);
146
+ expect(body.code).toBe('USER_SUSPENDED');
147
+ expect(body.message).toBe('Account suspended');
148
+ });
149
+
150
+ it('should default to 500 for unmapped codes', () => {
151
+ const response = customErrorResponse('UNKNOWN_CODE', 'Unknown error');
152
+
153
+ expect(response.statusCode).toBe(500);
154
+ });
155
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "noImplicitAny": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "noImplicitReturns": true,
20
+ "noFallthroughCasesInSwitch": true
21
+ },
22
+ "include": ["src/**/*"],
23
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
24
+ }