impulse-api 3.0.1 → 3.0.2

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/package.json CHANGED
@@ -4,12 +4,15 @@
4
4
  "name": "impulse-api",
5
5
  "description": "A efficient and powerful API with Express under the hood.",
6
6
  "license": "MIT",
7
+ "pre-commit": [
8
+ "test"
9
+ ],
7
10
  "scripts": {
8
11
  "lint": "eslint --ignore-path .gitignore .",
9
12
  "test": "nyc --reporter=lcov --reporter=text-summary mocha",
10
13
  "test-server": "node ./test/integration/test-server.js"
11
14
  },
12
- "version": "3.0.1",
15
+ "version": "3.0.2",
13
16
  "engines": {
14
17
  "node": ">=22"
15
18
  },
@@ -19,9 +22,9 @@
19
22
  "express": "4.21.2",
20
23
  "express-fileupload": "1.5.1",
21
24
  "http-errors": "2.0.0",
22
- "jsonwebtoken": "^9.0.2",
25
+ "jsonwebtoken": "8.5.1",
23
26
  "lodash": "4.17.21",
24
- "morgan": "^1.10.1",
27
+ "morgan": "1.10.0",
25
28
  "multer": "1.4.5-lts.2",
26
29
  "path-to-regexp": "0.1.12"
27
30
  },
@@ -29,6 +32,7 @@
29
32
  "eslint": "^9.23.0",
30
33
  "eslint-plugin-import": "^2.31.0",
31
34
  "nyc": "^17.1.0",
35
+ "pre-commit": "^1.2.2",
32
36
  "sinon": "4.4.6"
33
37
  },
34
38
  "keywords": [
package/readme.md CHANGED
@@ -107,6 +107,94 @@ const { Auth } = require('impulse-api');
107
107
  TokenExpiredError: [Function: TokenExpiredError] }
108
108
  ```
109
109
 
110
+ #### Custom JWT Validation
111
+
112
+ You can override JWT validation to use custom authentication providers (like Google OAuth, Auth0, etc.) instead of the default JWT validation. This is typically configured once at the server level.
113
+
114
+ ##### Server-Level Custom Validation (Recommended)
115
+ ```javascript
116
+ const config = {
117
+ name: 'my-api',
118
+ port: 3000,
119
+ secretKey: 'fallback-secret',
120
+ routeDir: './routes',
121
+ tokenValidator: async (token, services) => {
122
+ // Global token validation for all routes with tokenAuth: true
123
+ const userInfo = await services.googleOAuth.verifyToken(token);
124
+ return {
125
+ userId: userInfo.googleId,
126
+ email: userInfo.email,
127
+ name: userInfo.name,
128
+ picture: userInfo.picture
129
+ };
130
+ },
131
+ services: {
132
+ googleOAuth: {
133
+ verifyToken: async (token) => {
134
+ // Your Google OAuth validation logic
135
+ // This would typically make an API call to Google
136
+ return {
137
+ googleId: 'google-123',
138
+ email: 'user@example.com',
139
+ name: 'John Doe',
140
+ picture: 'https://example.com/photo.jpg'
141
+ };
142
+ }
143
+ }
144
+ }
145
+ };
146
+
147
+ const api = new Impulse(config);
148
+ ```
149
+
150
+ ##### Per-Route Override (Optional)
151
+ If you need different validation for specific routes, you can override the global validator:
152
+
153
+ ```javascript
154
+ exports.specialRoute = {
155
+ name: 'specialRoute',
156
+ method: 'post',
157
+ endpoint: '/api/special',
158
+ version: 'v1',
159
+ tokenAuth: true,
160
+ validateToken: async (token, services) => {
161
+ // Override global validation for this specific route
162
+ const userInfo = await services.specialAuth.verifyToken(token);
163
+ return {
164
+ userId: userInfo.specialId,
165
+ role: userInfo.role,
166
+ permissions: userInfo.permissions
167
+ };
168
+ },
169
+ run: (services, inputs, next) => {
170
+ const user = inputs.decoded; // From route-specific validation
171
+ next(200, { message: 'Success', user });
172
+ }
173
+ };
174
+ ```
175
+
176
+ ##### Validation Flow
177
+ 1. **Global token validator** (`config.tokenValidator`) - primary validation
178
+ 2. **Route-specific validator** (`route.validateToken`) - optional override
179
+ 3. **Default JWT validation** - fallback when no token validator is provided
180
+
181
+ ##### Backward Compatibility
182
+ Existing routes without custom validators continue to work with default JWT validation. No breaking changes to the existing API.
183
+
184
+ ##### Error Handling
185
+ Custom validators should throw errors for invalid tokens. The framework will catch these and return appropriate HTTP 401 responses.
186
+
187
+ ```javascript
188
+ validateToken: async (token, services) => {
189
+ try {
190
+ const userInfo = await services.oauth.verifyToken(token);
191
+ return userInfo;
192
+ } catch (error) {
193
+ throw new Error('Invalid authentication token');
194
+ }
195
+ }
196
+ ```
197
+
110
198
  #### Application Auth
111
199
  For `admin` type routes, Impulse-Api also provides `applicationAuth` which secures any routes behind basic key authorization provided by `appKey` in the server configuration. The `applicationAuth` can be verified and passed in the header, parameters, query, or body as `key`.
112
200
 
@@ -0,0 +1,83 @@
1
+ // Example route using server-level custom JWT validation for Google OAuth
2
+ // The tokenValidator is configured at server initialization
3
+ exports.scanDocument = {
4
+ name: 'scanDocument',
5
+ description: 'Scan a document with Google OAuth authentication',
6
+ method: 'post',
7
+ endpoint: '/api/scan',
8
+ version: 'v1',
9
+ tokenAuth: true,
10
+ // No custom validator needed - uses server-level tokenValidator
11
+ inputs: {
12
+ documentId: {
13
+ required: true,
14
+ validate: (value) => typeof value === 'string' && value.length > 0
15
+ },
16
+ scanType: {
17
+ required: false,
18
+ validate: (value) => ['text', 'image', 'pdf'].includes(value)
19
+ }
20
+ },
21
+ run: (services, inputs, next) => {
22
+ const user = inputs.decoded; // Contains Google OAuth user data from server-level tokenValidator
23
+ const { documentId, scanType } = inputs;
24
+
25
+ // Process the document scan with authenticated user
26
+ const result = {
27
+ success: true,
28
+ documentId,
29
+ scanType: scanType || 'text',
30
+ user: {
31
+ id: user.userId,
32
+ email: user.email,
33
+ name: user.name,
34
+ picture: user.picture
35
+ },
36
+ timestamp: new Date().toISOString()
37
+ };
38
+
39
+ next(200, result);
40
+ }
41
+ };
42
+
43
+ // Example route using global custom validator
44
+ exports.getUserProfile = {
45
+ name: 'getUserProfile',
46
+ description: 'Get user profile with custom authentication',
47
+ method: 'get',
48
+ endpoint: '/api/profile',
49
+ version: 'v1',
50
+ tokenAuth: true,
51
+ // This route will use the global tokenValidator from config
52
+ run: (services, inputs, next) => {
53
+ const user = inputs.decoded; // From global tokenValidator
54
+
55
+ const profile = {
56
+ userId: user.userId,
57
+ email: user.email,
58
+ lastLogin: new Date().toISOString(),
59
+ preferences: user.preferences || {}
60
+ };
61
+
62
+ next(200, profile);
63
+ }
64
+ };
65
+
66
+ // Example route with default JWT validation (backward compatibility)
67
+ exports.legacyRoute = {
68
+ name: 'legacyRoute',
69
+ description: 'Legacy route using default JWT validation',
70
+ method: 'get',
71
+ endpoint: '/api/legacy',
72
+ version: 'v1',
73
+ tokenAuth: true,
74
+ // No custom validator - uses default JWT validation
75
+ run: (services, inputs, next) => {
76
+ const user = inputs.decoded; // From default JWT validation
77
+
78
+ next(200, {
79
+ message: 'Legacy route works with default JWT',
80
+ user: user
81
+ });
82
+ }
83
+ };
package/src/auth.js CHANGED
@@ -5,6 +5,13 @@ const auth = {
5
5
  },
6
6
  verifyToken(token, secretKey) {
7
7
  return jwt.verify(token, secretKey);
8
+ },
9
+ // Helper function to create custom validators
10
+ createCustomValidator(validatorFn) {
11
+ if (typeof validatorFn !== 'function') {
12
+ throw new Error('Custom validator must be a function');
13
+ }
14
+ return validatorFn;
8
15
  }
9
16
  };
10
17
 
package/src/server.js CHANGED
@@ -36,6 +36,7 @@ class Server {
36
36
  this.heartbeat = config.heartbeat || null;
37
37
  this.container = new Container(config.services);
38
38
  this.secretKey = config.secretKey;
39
+ this.tokenValidator = config.tokenValidator || null;
39
40
  this.errors = config.errors || Errors;
40
41
  process.env.NODE_ENV = this.env || 'dev';
41
42
  this.configureHttpServer();
@@ -214,7 +215,19 @@ class Server {
214
215
  }
215
216
 
216
217
  try {
217
- decoded = await Auth.verifyToken(token, this.secretKey);
218
+ // Use global token validator if provided, otherwise fall back to default JWT validation
219
+ if (this.tokenValidator && typeof this.tokenValidator === 'function') {
220
+ decoded = await this.tokenValidator(token, this.container);
221
+ } else {
222
+ // Default JWT validation
223
+ decoded = await Auth.verifyToken(token, this.secretKey);
224
+ }
225
+
226
+ // Allow route-specific validation to override the global validator
227
+ if (route.validateToken && typeof route.validateToken === 'function') {
228
+ decoded = await route.validateToken(token, this.container);
229
+ }
230
+
218
231
  if (decoded.username) {
219
232
  username = decoded.username;
220
233
  }
@@ -0,0 +1,298 @@
1
+ const assert = require('assert');
2
+ const Api = require('../src/api');
3
+ const Auth = require('../src/auth');
4
+
5
+ describe('Custom JWT Validation', () => {
6
+ let testServer;
7
+ let testRouteDir;
8
+
9
+ beforeEach(() => {
10
+ // Create a temporary route directory for testing
11
+ testRouteDir = '/tmp/test-routes';
12
+ require('fs').mkdirSync(testRouteDir, { recursive: true });
13
+ });
14
+
15
+ afterEach(() => {
16
+ // Clean up test route directory
17
+ if (require('fs').existsSync(testRouteDir)) {
18
+ require('fs').rmSync(testRouteDir, { recursive: true, force: true });
19
+ }
20
+ });
21
+
22
+ describe('Server-Level Custom Validator', () => {
23
+ it('should use server-level custom validator when provided', async () => {
24
+ const customValidator = async (token, services) => {
25
+ if (token === 'valid-custom-token') {
26
+ return {
27
+ userId: 'test-user-123',
28
+ email: 'test@example.com',
29
+ customField: 'custom-value'
30
+ };
31
+ }
32
+ throw new Error('Invalid token');
33
+ };
34
+
35
+ const config = {
36
+ name: 'test-api',
37
+ routeDir: testRouteDir,
38
+ secretKey: 'test-secret',
39
+ tokenValidator: customValidator,
40
+ services: {
41
+ testService: { name: 'test' }
42
+ }
43
+ };
44
+
45
+ // Create a test route file
46
+ const testRoute = `
47
+ exports.testRoute = {
48
+ name: 'testRoute',
49
+ method: 'get',
50
+ endpoint: '/test',
51
+ version: 'v1',
52
+ tokenAuth: true,
53
+ run: (services, inputs, next) => {
54
+ next(200, {
55
+ message: 'Success',
56
+ user: inputs.decoded
57
+ });
58
+ }
59
+ };
60
+ `;
61
+ require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
62
+
63
+ const api = new Api(config);
64
+ await api.init();
65
+
66
+ // Test would require actual HTTP request, but we can test the validator function directly
67
+ const result = await customValidator('valid-custom-token', {});
68
+ assert.deepStrictEqual(result, {
69
+ userId: 'test-user-123',
70
+ email: 'test@example.com',
71
+ customField: 'custom-value'
72
+ });
73
+ });
74
+
75
+ it('should fall back to default JWT validation when no custom validator', async () => {
76
+ const config = {
77
+ name: 'test-api',
78
+ routeDir: testRouteDir,
79
+ secretKey: 'test-secret',
80
+ services: {}
81
+ };
82
+
83
+ // Create a test route file
84
+ const testRoute = `
85
+ exports.testRoute = {
86
+ name: 'testRoute',
87
+ method: 'get',
88
+ endpoint: '/test',
89
+ version: 'v1',
90
+ tokenAuth: true,
91
+ run: (services, inputs, next) => {
92
+ next(200, { message: 'Success' });
93
+ }
94
+ };
95
+ `;
96
+ require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
97
+
98
+ const api = new Api(config);
99
+ await api.init();
100
+
101
+ // Test default JWT validation
102
+ const token = Auth.generateToken({ userId: 'test-user' }, { secretKey: 'test-secret' });
103
+ const decoded = Auth.verifyToken(token, 'test-secret');
104
+ assert.strictEqual(decoded.userId, 'test-user');
105
+ });
106
+ });
107
+
108
+ describe('Route-Specific Override', () => {
109
+ it('should allow route-specific validator to override server-level validator', async () => {
110
+ const serverValidator = async (token, services) => {
111
+ return { userId: 'server-user', source: 'server' };
112
+ };
113
+
114
+ const routeValidator = async (token, services) => {
115
+ if (token === 'route-specific-token') {
116
+ return {
117
+ userId: 'route-user-456',
118
+ email: 'route@example.com',
119
+ source: 'route'
120
+ };
121
+ }
122
+ throw new Error('Invalid route token');
123
+ };
124
+
125
+ const config = {
126
+ name: 'test-api',
127
+ routeDir: testRouteDir,
128
+ secretKey: 'test-secret',
129
+ tokenValidator: serverValidator,
130
+ services: {}
131
+ };
132
+
133
+ // Create a test route file with custom validator override
134
+ const testRoute = `
135
+ exports.testRoute = {
136
+ name: 'testRoute',
137
+ method: 'get',
138
+ endpoint: '/test',
139
+ version: 'v1',
140
+ tokenAuth: true,
141
+ validateToken: async (token, services) => {
142
+ if (token === 'route-specific-token') {
143
+ return {
144
+ userId: 'route-user-456',
145
+ email: 'route@example.com',
146
+ source: 'route'
147
+ };
148
+ }
149
+ throw new Error('Invalid route token');
150
+ },
151
+ run: (services, inputs, next) => {
152
+ next(200, {
153
+ message: 'Success',
154
+ user: inputs.decoded
155
+ });
156
+ }
157
+ };
158
+ `;
159
+ require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
160
+
161
+ const api = new Api(config);
162
+ await api.init();
163
+
164
+ // Test the route-specific validator
165
+ const result = await routeValidator('route-specific-token', {});
166
+ assert.deepStrictEqual(result, {
167
+ userId: 'route-user-456',
168
+ email: 'route@example.com',
169
+ source: 'route'
170
+ });
171
+ });
172
+
173
+ it('should use server-level validator when no route-specific validator', async () => {
174
+ const serverValidator = async (token, services) => {
175
+ return { userId: 'server-user', source: 'server' };
176
+ };
177
+
178
+ const config = {
179
+ name: 'test-api',
180
+ routeDir: testRouteDir,
181
+ secretKey: 'test-secret',
182
+ tokenValidator: serverValidator,
183
+ services: {}
184
+ };
185
+
186
+ // Create a test route file without custom validator
187
+ const testRoute = `
188
+ exports.testRoute = {
189
+ name: 'testRoute',
190
+ method: 'get',
191
+ endpoint: '/test',
192
+ version: 'v1',
193
+ tokenAuth: true,
194
+ run: (services, inputs, next) => {
195
+ next(200, {
196
+ message: 'Success',
197
+ user: inputs.decoded
198
+ });
199
+ }
200
+ };
201
+ `;
202
+ require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
203
+
204
+ const api = new Api(config);
205
+ await api.init();
206
+
207
+ // Test that server validator is used
208
+ const result = await serverValidator('any-token', {});
209
+ assert.strictEqual(result.source, 'server');
210
+ });
211
+ });
212
+
213
+ describe('Error Handling', () => {
214
+ it('should handle custom validator errors gracefully', async () => {
215
+ const failingValidator = async (token, services) => {
216
+ throw new Error('Custom validation failed');
217
+ };
218
+
219
+ const config = {
220
+ name: 'test-api',
221
+ routeDir: testRouteDir,
222
+ secretKey: 'test-secret',
223
+ tokenValidator: failingValidator,
224
+ services: {}
225
+ };
226
+
227
+ // Create a test route file
228
+ const testRoute = `
229
+ exports.testRoute = {
230
+ name: 'testRoute',
231
+ method: 'get',
232
+ endpoint: '/test',
233
+ version: 'v1',
234
+ tokenAuth: true,
235
+ run: (services, inputs, next) => {
236
+ next(200, { message: 'Success' });
237
+ }
238
+ };
239
+ `;
240
+ require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
241
+
242
+ const api = new Api(config);
243
+ await api.init();
244
+
245
+ // Test that validator throws expected error
246
+ try {
247
+ await failingValidator('invalid-token', {});
248
+ assert.fail('Should have thrown an error');
249
+ } catch (error) {
250
+ assert.strictEqual(error.message, 'Custom validation failed');
251
+ }
252
+ });
253
+
254
+ it('should validate custom validator function type', () => {
255
+ assert.throws(() => {
256
+ Auth.createCustomValidator('not-a-function');
257
+ }, /Custom validator must be a function/);
258
+
259
+ assert.doesNotThrow(() => {
260
+ Auth.createCustomValidator(() => {});
261
+ });
262
+ });
263
+ });
264
+
265
+ describe('Backward Compatibility', () => {
266
+ it('should work with existing routes without custom validation', async () => {
267
+ const config = {
268
+ name: 'test-api',
269
+ routeDir: testRouteDir,
270
+ secretKey: 'test-secret',
271
+ services: {}
272
+ };
273
+
274
+ // Create a test route file without custom validation
275
+ const testRoute = `
276
+ exports.testRoute = {
277
+ name: 'testRoute',
278
+ method: 'get',
279
+ endpoint: '/test',
280
+ version: 'v1',
281
+ tokenAuth: true,
282
+ run: (services, inputs, next) => {
283
+ next(200, { message: 'Success' });
284
+ }
285
+ };
286
+ `;
287
+ require('fs').writeFileSync(`${testRouteDir}/test.js`, testRoute);
288
+
289
+ const api = new Api(config);
290
+ await api.init();
291
+
292
+ // Test that default JWT validation still works
293
+ const token = Auth.generateToken({ userId: 'legacy-user' }, { secretKey: 'test-secret' });
294
+ const decoded = Auth.verifyToken(token, 'test-secret');
295
+ assert.strictEqual(decoded.userId, 'legacy-user');
296
+ });
297
+ });
298
+ });