@mimik/api-helper 2.0.7 → 2.0.8
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/.claude/settings.local.json +9 -0
- package/.husky/pre-commit +2 -0
- package/.husky/pre-push +2 -0
- package/README.md +57 -63
- package/eslint.config.js +30 -11
- package/index.js +113 -117
- package/lib/ajvHelpers.js +1 -1
- package/lib/baseHandlers.js +2 -3
- package/lib/oauthValidation-helper.js +1 -1
- package/lib/securityHandlers.js +1 -1
- package/package.json +24 -23
- package/test/ajvHelpers.test.js +159 -0
- package/test/baseHandlers.test.js +150 -0
- package/test/extract-helper.test.js +100 -0
- package/test/index-async.test.js +599 -0
- package/test/index-sync.test.js +282 -0
- package/test/oauthValidation-helper.test.js +136 -0
- package/test/securityHandlers.test.js +557 -0
- package/.nycrc +0 -4
package/index.js
CHANGED
|
@@ -44,7 +44,7 @@ import { securityLib } from './lib/securityHandlers.js';
|
|
|
44
44
|
* @module api-helper
|
|
45
45
|
* @example
|
|
46
46
|
* import apiHelper from '@mimik/api-helper';
|
|
47
|
-
* or
|
|
47
|
+
* // or
|
|
48
48
|
* import { apiSetup, securityLib, getAPIFile, validateSecuritySchemes, extractProperties, setupServerFiles } from '@mimik/api-helper';
|
|
49
49
|
*/
|
|
50
50
|
const EMPTY = 0;
|
|
@@ -59,13 +59,12 @@ const POSTFIX_INDEX = 3;
|
|
|
59
59
|
* Implement the security flows for the API.
|
|
60
60
|
*
|
|
61
61
|
* @function securityLib
|
|
62
|
-
* @category
|
|
62
|
+
* @category sync
|
|
63
63
|
* @requires @mimik/swagger-helper
|
|
64
64
|
* @requires jsonwebtoken
|
|
65
65
|
* @requires lodash
|
|
66
66
|
* @param {object} config - Configuration of the service.
|
|
67
|
-
*
|
|
68
|
-
* @throws {Promise} An error is thrown if the initiatilization failed.
|
|
67
|
+
* @return {object} An object containing `SystemSecurity`, `AdminSecurity`, `UserSecurity`, and `ApiKeySecurity` handlers.
|
|
69
68
|
*
|
|
70
69
|
* This function is used to setup the following security handlers for the API:
|
|
71
70
|
* - `SystemSecurity` - used for the system operations, like /system, /onbehalf
|
|
@@ -78,33 +77,29 @@ export { securityLib };
|
|
|
78
77
|
|
|
79
78
|
/**
|
|
80
79
|
*
|
|
81
|
-
* Setup the API to be
|
|
80
|
+
* Setup the API to be used for a service
|
|
82
81
|
*
|
|
83
82
|
* @function apiSetup
|
|
84
83
|
* @category async
|
|
85
84
|
* @requires @mimik/response-helper
|
|
86
85
|
* @requires @mimik/sumologic-winston-logger
|
|
87
|
-
* @requires
|
|
88
|
-
* @requires
|
|
89
|
-
* @
|
|
90
|
-
* @requires jsonwebtoken
|
|
91
|
-
* @requires lodash
|
|
92
|
-
* @param {object} setup - Object containing the apiFilename and the exisiting security schemes in the API definition.
|
|
86
|
+
* @requires lodash.difference
|
|
87
|
+
* @requires openapi-backend
|
|
88
|
+
* @param {object} setup - Object containing the apiFilename and the existing security schemes in the API definition.
|
|
93
89
|
* @param {object} registeredOperations - List of the operation to register for the API.
|
|
94
90
|
* @param {object} securityHandlers - List of the security handlers to add for the service.
|
|
95
|
-
* @param {object} extraFormats - list of the formats to add for
|
|
91
|
+
* @param {object} extraFormats - list of the formats to add for validating properties.
|
|
96
92
|
* @param {object} config - Configuration of the service.
|
|
97
|
-
* @param {UUID.<string>} correlationId - CorrelationId when logging
|
|
98
|
-
* @return {Promise}.
|
|
99
|
-
*
|
|
100
|
-
* @throws {Promise} An error is thrown if the initiatilization failed.
|
|
93
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activities.
|
|
94
|
+
* @return {Promise.<object>} The API file itself.
|
|
95
|
+
* @throws {Promise} An error is thrown if the initialization failed.
|
|
101
96
|
*
|
|
102
97
|
* The following scheme names are reserved: `SystemSecurity`, `AdminSecurity`, `UserSecurity`, `PeerSecurity`, `ApiKeySecurity`.
|
|
103
98
|
* The following security schemes can be defaulted: `SystemSecurity`, `AdminSecurity`, `UserSecurity`, `ApiKeySecurity`.
|
|
104
99
|
* The secOptions in the options property passed when using `init` allows the following operations:
|
|
105
100
|
* - introduce a customer security scheme, in this case secOptions contains: { newSecurityScheme: {function}newSecurityHandler },
|
|
106
101
|
* - disable a security scheme that is defined in the swagger API, in this case secOptions contains: { securitySchemeToDisable: { {boolean}notEnabled: true } },
|
|
107
|
-
* -
|
|
102
|
+
* - overwrite an existing security scheme, in this case secOptions contains: { securitySchemeToOverwrite: {function}newSecurityHandler }.
|
|
108
103
|
* If the secOptions is not present either to introduce, disable or overwrite a security scheme that is present in the swagger API file an error is generated.
|
|
109
104
|
* If the secOptions contains unused security schemes, an error is generated.
|
|
110
105
|
*
|
|
@@ -142,7 +137,7 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
|
|
|
142
137
|
}
|
|
143
138
|
const appliedSecurities = [];
|
|
144
139
|
const registerDefault = (securitySchemeName, securityHandler) => {
|
|
145
|
-
if (existingSecuritySchemes.includes(securitySchemeName) && (!securityHandlers ||
|
|
140
|
+
if (existingSecuritySchemes.includes(securitySchemeName) && (!securityHandlers || !securityHandlers[securitySchemeName])) {
|
|
146
141
|
api.registerSecurityHandler(securitySchemeName, securityHandler);
|
|
147
142
|
appliedSecurities.push(securitySchemeName);
|
|
148
143
|
}
|
|
@@ -161,7 +156,7 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
|
|
|
161
156
|
if (unusedSecuritySchemes.length !== EMPTY) throw getRichError('System', 'unused handlers for security schemes', { unusedSecuritySchemes });
|
|
162
157
|
|
|
163
158
|
remainingSecurities.forEach((securityScheme) => {
|
|
164
|
-
if (!securityHandlerNames.includes(securityScheme) && !securityHandlers[securityScheme]
|
|
159
|
+
if (!securityHandlerNames.includes(securityScheme) && !securityHandlers[securityScheme]?.notEnabled) {
|
|
165
160
|
throw getRichError('System', 'missing handler for security scheme', { securityScheme });
|
|
166
161
|
}
|
|
167
162
|
});
|
|
@@ -172,16 +167,72 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
|
|
|
172
167
|
});
|
|
173
168
|
}
|
|
174
169
|
else if (remainingSecurities.length !== EMPTY) throw getRichError('System', 'missing handlers for security schemes', { missingSecuritySchemes: remainingSecurities });
|
|
175
|
-
api.init()
|
|
170
|
+
return api.init()
|
|
176
171
|
.catch((err) => {
|
|
177
172
|
throw getRichError('System', 'could not initialize the api', { api }, err);
|
|
178
|
-
})
|
|
179
|
-
|
|
173
|
+
})
|
|
174
|
+
.then(() => api);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const swaggerOptions = spec => ({
|
|
178
|
+
spec,
|
|
179
|
+
allowMetaPatches: false,
|
|
180
|
+
skipNormalization: true,
|
|
181
|
+
mode: 'strict',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const saveResolvedSpec = (apiDefinitionResult, apiFilename, correlationId) => {
|
|
185
|
+
if (apiDefinitionResult.errors.length !== EMPTY) {
|
|
186
|
+
logger.error('errors while resolving definition', { errors: apiDefinitionResult.errors }, correlationId);
|
|
187
|
+
throw getRichError('Parameter', 'errors while resolving definition', { apiFilename, errors: apiDefinitionResult.errors });
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
fs.writeFileSync(apiFilename, JSON.stringify(apiDefinitionResult.spec, null, TAB));
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
throw getRichError('System', 'file system error', { apiFilename }, err);
|
|
194
|
+
}
|
|
195
|
+
return apiDefinitionResult.spec;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const buildProviderRequest = (params, apiInfo, apiFilename) => {
|
|
199
|
+
const provider = apiInfo.provider || BITBUCKET;
|
|
200
|
+
|
|
201
|
+
switch (provider) {
|
|
202
|
+
case SWAGGERHUB: {
|
|
203
|
+
const result = {
|
|
204
|
+
url: `${API_PROVIDER_SWAGGERHUB}/${params[CUSTOMER_INDEX]}/${params[API_NAME_INDEX]}/${params[API_VERSION_INDEX]}?${RESOLVED}`,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (apiInfo.apiApiKey) result.authorization = apiInfo.apiApiKey;
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
case BITBUCKET: {
|
|
211
|
+
if (!apiInfo.apiBasicAuth || !apiInfo.apiBasicAuth.username || !apiInfo.apiBasicAuth.password) {
|
|
212
|
+
throw getRichError('Parameter', 'missing username/password for accessing Bitbucket', { apiFilename });
|
|
213
|
+
}
|
|
214
|
+
if (apiInfo.apiBasicAuth.username === DEFAULT_BITBUCKET_USERNAME || apiInfo.apiBasicAuth.password === DEFAULT_BITBUCKET_PASSWORD) {
|
|
215
|
+
throw getRichError('Parameter', 'missing username/password for accessing Bitbucket', { apiFilename });
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
return {
|
|
219
|
+
url: `${API_PROVIDER_BITBUCKET}/${params[CUSTOMER_INDEX]}/${params[API_NAME_INDEX]}${API_SOURCE}/${params[API_VERSION_INDEX]}/${SWAGGER}${EXTENSION_YML}`,
|
|
220
|
+
authorization: `Basic ${Base64.encode(`${apiInfo.apiBasicAuth.username}:${apiInfo.apiBasicAuth.password}`)}`,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
throw getRichError('System', 'could not create basicAuth', { apiFilename }, err);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
default: {
|
|
228
|
+
throw getRichError('Parameter', 'invalid API provider', { provider, apiFilename });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
180
231
|
};
|
|
181
232
|
|
|
182
233
|
/**
|
|
183
234
|
*
|
|
184
|
-
* Gets the API file from swaggerhub and store it in the
|
|
235
|
+
* Gets the API file from swaggerhub and store it in the given PATH location.
|
|
185
236
|
*
|
|
186
237
|
* @function getAPIFile
|
|
187
238
|
* @category async
|
|
@@ -192,11 +243,10 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
|
|
|
192
243
|
* @requires js-yaml
|
|
193
244
|
* @requires path
|
|
194
245
|
* @param {PATH.<string>} apiFilename - Name of the file where the API file will be stored.
|
|
195
|
-
* @param {UUID.<string>} correlationId - CorrelationId when logging
|
|
246
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activities.
|
|
196
247
|
* @param {object} options - Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiInfo` to access the api file in the api provider.
|
|
197
|
-
* @return {Promise}.
|
|
198
|
-
*
|
|
199
|
-
* @throws {Promise} An error is thrown if the apiFilename resolution generates an error or the request to the API provider fails or the file connot be saved.
|
|
248
|
+
* @return {Promise.<object>} The API file itself.
|
|
249
|
+
* @throws {Promise} An error is thrown if the apiFilename resolution generates an error or the request to the API provider fails or the file cannot be saved.
|
|
200
250
|
*
|
|
201
251
|
* `apiInfo` options has the following format:
|
|
202
252
|
* ``` javascript
|
|
@@ -206,17 +256,11 @@ export const apiSetup = (setup, registeredOperations, securityHandlers, extraFor
|
|
|
206
256
|
* "username": "username for bitbucket",
|
|
207
257
|
* "password": "password for bitbucket"
|
|
208
258
|
* },
|
|
209
|
-
* "apiApiKey": "apiKey for access private API on swaggerhub, can be optional if the API is accessible
|
|
259
|
+
* "apiApiKey": "apiKey for access private API on swaggerhub, can be optional if the API is accessible publicly"
|
|
210
260
|
* }
|
|
261
|
+
* ```
|
|
211
262
|
*/
|
|
212
263
|
export const getAPIFile = (apiFilename, correlationId, options) => {
|
|
213
|
-
const swaggerOptions = spec => ({
|
|
214
|
-
spec,
|
|
215
|
-
allowMetaPatches: false,
|
|
216
|
-
skipNormalization: true,
|
|
217
|
-
mode: 'strict',
|
|
218
|
-
});
|
|
219
|
-
|
|
220
264
|
logger.info('getting API definition', correlationId);
|
|
221
265
|
let apiDefinition;
|
|
222
266
|
|
|
@@ -238,24 +282,12 @@ export const getAPIFile = (apiFilename, correlationId, options) => {
|
|
|
238
282
|
}
|
|
239
283
|
return SwaggerClient.resolve(swaggerOptions(apiDefinition))
|
|
240
284
|
.catch((err) => {
|
|
241
|
-
throw getRichError('System', 'could not resolve
|
|
285
|
+
throw getRichError('System', 'could not resolve apiDefinition', { apiFilename }, err);
|
|
242
286
|
})
|
|
243
|
-
.then(
|
|
244
|
-
if (apiDefinitionResult.errors.length !== EMPTY) {
|
|
245
|
-
logger.error('errors while resolving definition', { errors: apiDefinitionResult.errors }, correlationId);
|
|
246
|
-
throw getRichError('Parameter', 'errors while resolving definition', { apiFilename, errors: apiDefinitionResult.errors });
|
|
247
|
-
}
|
|
248
|
-
try {
|
|
249
|
-
fs.writeFileSync(apiFilename, JSON.stringify(apiDefinitionResult.spec, null, TAB));
|
|
250
|
-
}
|
|
251
|
-
catch (err) {
|
|
252
|
-
throw getRichError('System', 'file system error', { apiFilename }, err);
|
|
253
|
-
}
|
|
254
|
-
return apiDefinitionResult.spec;
|
|
255
|
-
});
|
|
287
|
+
.then(result => saveResolvedSpec(result, apiFilename, correlationId));
|
|
256
288
|
}
|
|
257
289
|
if (!options) {
|
|
258
|
-
return Promise.reject(getRichError('
|
|
290
|
+
return Promise.reject(getRichError('Parameter', 'no options', { apiFilename }));
|
|
259
291
|
}
|
|
260
292
|
const { apiInfo } = options;
|
|
261
293
|
|
|
@@ -287,56 +319,33 @@ export const getAPIFile = (apiFilename, correlationId, options) => {
|
|
|
287
319
|
catch (err) {
|
|
288
320
|
return Promise.reject(getRichError('System', 'file system error', { apiDirectory }, err));
|
|
289
321
|
}
|
|
322
|
+
let providerResult;
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
providerResult = buildProviderRequest(params, apiInfo, apiFilename);
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
return Promise.reject(err);
|
|
329
|
+
}
|
|
290
330
|
const opts = {
|
|
291
331
|
method: 'GET',
|
|
332
|
+
url: providerResult.url,
|
|
292
333
|
headers: {
|
|
293
334
|
'x-correlation-id': correlationId,
|
|
294
335
|
},
|
|
336
|
+
retry: {
|
|
337
|
+
logLevel: {
|
|
338
|
+
response: 'debug',
|
|
339
|
+
responseDetails: 'type',
|
|
340
|
+
request: 'debug',
|
|
341
|
+
},
|
|
342
|
+
},
|
|
295
343
|
};
|
|
296
|
-
const provider = apiInfo.provider || BITBUCKET;
|
|
297
344
|
|
|
298
|
-
|
|
299
|
-
switch (provider) {
|
|
300
|
-
case SWAGGERHUB: {
|
|
301
|
-
opts.url = `${API_PROVIDER_SWAGGERHUB}/${params[CUSTOMER_INDEX]}/${params[API_NAME_INDEX]}/${params[API_VERSION_INDEX]}?${RESOLVED}`;
|
|
302
|
-
if (apiInfo.apiApiKey) opts.headers.Authorization = apiInfo.apiApiKey;
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
case BITBUCKET: {
|
|
306
|
-
if (!apiInfo.apiBasicAuth || !apiInfo.apiBasicAuth.username || !apiInfo.apiBasicAuth.password) {
|
|
307
|
-
throw getRichError('Parameter', 'missing username/password for accessing Bitbucket', { apiFilename });
|
|
308
|
-
}
|
|
309
|
-
if (apiInfo.apiBasicAuth.username === DEFAULT_BITBUCKET_USERNAME || apiInfo.apiBasicAuth.password === DEFAULT_BITBUCKET_PASSWORD) {
|
|
310
|
-
throw getRichError('Parameter', 'missing username/password for accessing Bitbucket', { apiFilename });
|
|
311
|
-
}
|
|
312
|
-
try {
|
|
313
|
-
opts.headers.Authorization = `Basic ${Base64.encode(`${apiInfo.apiBasicAuth.username}:${apiInfo.apiBasicAuth.password}`)}`;
|
|
314
|
-
}
|
|
315
|
-
catch (err) {
|
|
316
|
-
throw getRichError('System', 'could not create basicAuth', { apiFilename }, err);
|
|
317
|
-
}
|
|
318
|
-
opts.url = `${API_PROVIDER_BITBUCKET}/${params[CUSTOMER_INDEX]}/${params[API_NAME_INDEX]}${API_SOURCE}/${params[API_VERSION_INDEX]}/${SWAGGER}${EXTENSION_YML}`;
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
default: {
|
|
322
|
-
throw getRichError('Parameter', 'invalid API provider', { provider, apiFilename });
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
catch (err) {
|
|
327
|
-
return Promise.reject(err);
|
|
328
|
-
}
|
|
345
|
+
if (providerResult.authorization) opts.headers.Authorization = providerResult.authorization;
|
|
329
346
|
if (options.metrics) {
|
|
330
|
-
opts.metrics = options.metrics;
|
|
331
|
-
opts.metrics.url = opts.url;
|
|
347
|
+
opts.metrics = { ...options.metrics, url: opts.url };
|
|
332
348
|
}
|
|
333
|
-
opts.retry = {
|
|
334
|
-
logLevel: {
|
|
335
|
-
response: 'debug',
|
|
336
|
-
responseDetails: 'type',
|
|
337
|
-
request: 'debug',
|
|
338
|
-
},
|
|
339
|
-
};
|
|
340
349
|
logger.debug('API file does not exist, retrieving it', { url: opts.url }, correlationId);
|
|
341
350
|
return rpRetry(opts)
|
|
342
351
|
.then((result) => {
|
|
@@ -349,21 +358,9 @@ export const getAPIFile = (apiFilename, correlationId, options) => {
|
|
|
349
358
|
if (err.statusCode) {
|
|
350
359
|
throw err;
|
|
351
360
|
}
|
|
352
|
-
throw getRichError('System', 'could not resolve
|
|
361
|
+
throw getRichError('System', 'could not resolve apiDefinition', { apiFilename }, err);
|
|
353
362
|
})
|
|
354
|
-
.then(
|
|
355
|
-
if (apiDefinitionResult.errors.length !== EMPTY) {
|
|
356
|
-
logger.error('errors while resolving definition', { errors: apiDefinitionResult.errors }, correlationId);
|
|
357
|
-
throw getRichError('Parameter', 'errors while resolving definition', { apiFilename, errors: apiDefinitionResult.errors });
|
|
358
|
-
}
|
|
359
|
-
try {
|
|
360
|
-
fs.writeFileSync(apiFilename, JSON.stringify(apiDefinitionResult.spec, null, TAB));
|
|
361
|
-
}
|
|
362
|
-
catch (err) {
|
|
363
|
-
throw getRichError('System', 'file system error', { apiFilename }, err);
|
|
364
|
-
}
|
|
365
|
-
return apiDefinitionResult.spec;
|
|
366
|
-
});
|
|
363
|
+
.then(result => saveResolvedSpec(result, apiFilename, correlationId));
|
|
367
364
|
};
|
|
368
365
|
|
|
369
366
|
/**
|
|
@@ -375,9 +372,9 @@ export const getAPIFile = (apiFilename, correlationId, options) => {
|
|
|
375
372
|
* @requires @mimik/sumologic-winston-logger
|
|
376
373
|
* @requires @mimik/response-helper
|
|
377
374
|
* @param {object} apiDefinition - JSON object containing the API definition.
|
|
378
|
-
* @param {UUID.<string>} correlationId - CorrelationId when logging
|
|
379
|
-
* @return An array of the known securitySchemes that are in the API definition.
|
|
380
|
-
* @throws An error is thrown
|
|
375
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activities.
|
|
376
|
+
* @return {Array.<string>} An array of the known securitySchemes that are in the API definition.
|
|
377
|
+
* @throws An error is thrown if a validation fails.
|
|
381
378
|
*/
|
|
382
379
|
export const validateSecuritySchemes = (apiDefinition, correlationId) => {
|
|
383
380
|
const existingSecuritySchemes = [];
|
|
@@ -397,7 +394,7 @@ export const validateSecuritySchemes = (apiDefinition, correlationId) => {
|
|
|
397
394
|
|
|
398
395
|
/**
|
|
399
396
|
*
|
|
400
|
-
* Extracts the properties from API
|
|
397
|
+
* Extracts the properties from API definition and creates a file binding the handler with the controller operations.
|
|
401
398
|
*
|
|
402
399
|
* @function extractProperties
|
|
403
400
|
* @category sync
|
|
@@ -407,9 +404,9 @@ export const validateSecuritySchemes = (apiDefinition, correlationId) => {
|
|
|
407
404
|
* @param {object} apiDefinition - JSON object containing the API definition.
|
|
408
405
|
* @param {PATH.<string>} controllersDirectory - Directory to find the controller files.
|
|
409
406
|
* @param {PATH.<string>} buildDirectory - Directory where the register file will be stored.
|
|
410
|
-
* @param {UUID.<string>} correlationId - CorrelationId when logging
|
|
411
|
-
* @return
|
|
412
|
-
* @throws An error is thrown for many reasons, like operationId does not exist in controllers, controller
|
|
407
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activities.
|
|
408
|
+
* @return {void}
|
|
409
|
+
* @throws An error is thrown for many reasons, like operationId does not exist in controllers, controller does not exist...
|
|
413
410
|
*/
|
|
414
411
|
export const extractProperties = (apiDefinition, controllersDirectory, buildDirectory, correlationId) => {
|
|
415
412
|
const result = {};
|
|
@@ -493,11 +490,10 @@ export const extractProperties = (apiDefinition, controllersDirectory, buildDire
|
|
|
493
490
|
* @param {PATH.<string>} apiFilename - Name of the file where the API file will be stored.
|
|
494
491
|
* @param {PATH.<string>} controllersDirectory - Directory to find the controller files.
|
|
495
492
|
* @param {PATH.<string>} buildDirectory - Directory where the register file will be stored.
|
|
496
|
-
* @param {UUID.<string>} correlationId - CorrelationId when logging
|
|
497
|
-
* @param {object} options - Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiKey
|
|
498
|
-
* @return {Promise}.
|
|
499
|
-
*
|
|
500
|
-
* @throws {Promise} An error is thrown for many reasons assocated with getAPIFile or validateSecuritySchemes or extractProperties.
|
|
493
|
+
* @param {UUID.<string>} correlationId - CorrelationId when logging activities.
|
|
494
|
+
* @param {object} options - Options associated with the call. Use to pass `metrics` to `rpRetry` and `apiKey` to access private API.
|
|
495
|
+
* @return {Promise.<object>} The API file, the API filename, the existing known security schemes and the defined security schemes.
|
|
496
|
+
* @throws {Promise} An error is thrown for many reasons associated with getAPIFile or validateSecuritySchemes or extractProperties.
|
|
501
497
|
*/
|
|
502
498
|
export const setupServerFiles = (apiFilename, controllersDirectory, buildDirectory, correlationId, options) => getAPIFile(apiFilename, correlationId, options)
|
|
503
499
|
.then((apiDefinition) => {
|
package/lib/ajvHelpers.js
CHANGED
|
@@ -14,7 +14,7 @@ const ajvFormats = origFormats => (ajv) => {
|
|
|
14
14
|
validate: /((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$))/u,
|
|
15
15
|
},
|
|
16
16
|
};
|
|
17
|
-
const libFormats = DEFAULT_FORMATS;
|
|
17
|
+
const libFormats = [...DEFAULT_FORMATS];
|
|
18
18
|
let formats = origFormats;
|
|
19
19
|
|
|
20
20
|
if (!formats) formats = {};
|
package/lib/baseHandlers.js
CHANGED
|
@@ -34,9 +34,8 @@ const unauthorizedHandler = (con, req, res) => {
|
|
|
34
34
|
let error = new Error('Unauthorized');
|
|
35
35
|
|
|
36
36
|
error.statusCode = UNAUTHORIZED_ERROR;
|
|
37
|
-
const schemes = Object.keys(con.security);
|
|
37
|
+
const schemes = Object.keys(con.security).filter(key => key !== 'authorized');
|
|
38
38
|
|
|
39
|
-
delete schemes.authorized;
|
|
40
39
|
schemes.forEach((scheme) => {
|
|
41
40
|
const { error: schemeError } = con.security[scheme] || {};
|
|
42
41
|
if (schemeError) {
|
|
@@ -49,7 +48,7 @@ const unauthorizedHandler = (con, req, res) => {
|
|
|
49
48
|
const notImplemented = (con, req, res) => {
|
|
50
49
|
const { method } = req;
|
|
51
50
|
const path = req.url;
|
|
52
|
-
const error = new Error(`${
|
|
51
|
+
const error = new Error(`${method} ${path} defined in Swagger specification, but not implemented`);
|
|
53
52
|
|
|
54
53
|
error.statusCode = NOT_IMPLEMENTED_ERROR;
|
|
55
54
|
error.info = {
|
|
@@ -13,7 +13,7 @@ const validateOauth2 = (securitySchemes, securityType, flow) => {
|
|
|
13
13
|
if (security.type !== OAUTH2) {
|
|
14
14
|
throw getRichError('System', `auth type is not ${OAUTH2}`, { securityType, receivedAuth: security.type, expectedAuth: OAUTH2 });
|
|
15
15
|
}
|
|
16
|
-
if (!security.flows[flow]) {
|
|
16
|
+
if (!security.flows || !security.flows[flow]) {
|
|
17
17
|
throw getRichError('System', 'no flow type available', { securityType, flow });
|
|
18
18
|
}
|
|
19
19
|
return securityType;
|
package/lib/securityHandlers.js
CHANGED
|
@@ -93,7 +93,7 @@ const checkHeaders = (headers) => {
|
|
|
93
93
|
|
|
94
94
|
const checkScopes = (tokenScopes, defScopes, definition) => {
|
|
95
95
|
if (!tokenScopes) {
|
|
96
|
-
throw
|
|
96
|
+
throw getError('no scope in authorization token', UNAUTHORIZED_ERROR);
|
|
97
97
|
}
|
|
98
98
|
let claims = [];
|
|
99
99
|
let onBehalf = false;
|
package/package.json
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mimik/api-helper",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "helper for openAPI backend and mimik service",
|
|
5
5
|
"main": "./index.js",
|
|
6
|
-
"type": "module",
|
|
6
|
+
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"lint": "eslint . --no-error-on-unmatched-pattern",
|
|
9
9
|
"docs": "jsdoc2md index.js > README.md",
|
|
10
|
-
"test": "
|
|
11
|
-
"test-ci": "
|
|
10
|
+
"test": "mocha test/ --recursive",
|
|
11
|
+
"test-ci": "c8 --reporter=lcov --reporter=text npm test",
|
|
12
12
|
"prepublishOnly": "npm run docs && npm run lint && npm run test-ci",
|
|
13
13
|
"commit-ready": "npm run docs && npm run lint && npm run test-ci"
|
|
14
14
|
},
|
|
15
|
-
"husky": {
|
|
16
|
-
"hooks": {
|
|
17
|
-
"pre-commit": "npm run commit-ready",
|
|
18
|
-
"pre-push": "npm run test"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
15
|
"keywords": [
|
|
22
16
|
"mimik",
|
|
23
17
|
"microservice",
|
|
@@ -25,33 +19,40 @@
|
|
|
25
19
|
],
|
|
26
20
|
"author": "mimik technology inc <support@mimik.com> (https://developer.mimik.com/)",
|
|
27
21
|
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=24"
|
|
24
|
+
},
|
|
28
25
|
"repository": {
|
|
29
26
|
"type": "git",
|
|
30
27
|
"url": "https://bitbucket.org/mimiktech/api-helper"
|
|
31
28
|
},
|
|
32
29
|
"dependencies": {
|
|
33
|
-
"@mimik/request-
|
|
34
|
-
"@mimik/
|
|
35
|
-
"@mimik/
|
|
36
|
-
"@mimik/
|
|
37
|
-
"@mimik/swagger-helper": "^5.0.2",
|
|
30
|
+
"@mimik/request-retry": "^4.0.9",
|
|
31
|
+
"@mimik/response-helper": "^4.0.10",
|
|
32
|
+
"@mimik/sumologic-winston-logger": "^2.1.13",
|
|
33
|
+
"@mimik/swagger-helper": "^5.0.3",
|
|
38
34
|
"ajv-formats": "3.0.1",
|
|
39
|
-
"js-base64": "3.7.
|
|
40
|
-
"js-yaml":"4.1.
|
|
41
|
-
"jsonwebtoken": "9.0.
|
|
42
|
-
"lodash.compact": "3.0.1",
|
|
35
|
+
"js-base64": "3.7.8",
|
|
36
|
+
"js-yaml": "4.1.1",
|
|
37
|
+
"jsonwebtoken": "9.0.3",
|
|
38
|
+
"lodash.compact": "3.0.1",
|
|
43
39
|
"lodash.difference": "4.5.0",
|
|
44
40
|
"lodash.intersection": "4.4.0",
|
|
45
|
-
"openapi-backend": "5.
|
|
46
|
-
"swagger-client": "3.
|
|
41
|
+
"openapi-backend": "5.16.1",
|
|
42
|
+
"swagger-client": "3.36.2"
|
|
47
43
|
},
|
|
48
44
|
"devDependencies": {
|
|
49
45
|
"@eslint/js": "9.32.0",
|
|
50
46
|
"@mimik/eslint-plugin-document-env": "^2.0.8",
|
|
51
|
-
"@stylistic/eslint-plugin": "5.
|
|
47
|
+
"@stylistic/eslint-plugin": "5.9.0",
|
|
48
|
+
"c8": "10.1.3",
|
|
49
|
+
"chai": "6.2.2",
|
|
52
50
|
"eslint": "9.32.0",
|
|
53
51
|
"eslint-plugin-import": "2.32.0",
|
|
52
|
+
"esmock": "2.7.3",
|
|
53
|
+
"globals": "17.3.0",
|
|
54
54
|
"husky": "9.1.7",
|
|
55
|
-
"jsdoc-to-markdown": "9.1.
|
|
55
|
+
"jsdoc-to-markdown": "9.1.3",
|
|
56
|
+
"mocha": "11.7.5"
|
|
56
57
|
}
|
|
57
58
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import esmock from 'esmock';
|
|
2
|
+
import { expect } from 'chai';
|
|
3
|
+
|
|
4
|
+
let ajvFormats;
|
|
5
|
+
let addedLibFormats;
|
|
6
|
+
let addedCustomFormats;
|
|
7
|
+
|
|
8
|
+
describe('ajvHelpers', () => {
|
|
9
|
+
before(async () => {
|
|
10
|
+
const mod = await esmock('../lib/ajvHelpers.js', {
|
|
11
|
+
'ajv-formats': {
|
|
12
|
+
default: (ajv, formats) => {
|
|
13
|
+
addedLibFormats = formats;
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
({ ajvFormats } = mod);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
addedLibFormats = null;
|
|
23
|
+
addedCustomFormats = {};
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const createMockAjv = () => ({
|
|
27
|
+
addFormat: (name, format) => {
|
|
28
|
+
addedCustomFormats[name] = format;
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('with no extra formats', () => {
|
|
33
|
+
it('should add default library formats', () => {
|
|
34
|
+
const ajv = createMockAjv();
|
|
35
|
+
const configurer = ajvFormats();
|
|
36
|
+
|
|
37
|
+
configurer(ajv);
|
|
38
|
+
expect(addedLibFormats).to.deep.equal(['date', 'time', 'date-time', 'byte', 'uuid', 'uri', 'email', 'ipv4', 'ipv6']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should add built-in semver and ip custom formats', () => {
|
|
42
|
+
const ajv = createMockAjv();
|
|
43
|
+
const configurer = ajvFormats();
|
|
44
|
+
|
|
45
|
+
configurer(ajv);
|
|
46
|
+
expect(addedCustomFormats).to.have.property('semver');
|
|
47
|
+
expect(addedCustomFormats.semver.type).to.equal('string');
|
|
48
|
+
expect(addedCustomFormats).to.have.property('ip');
|
|
49
|
+
expect(addedCustomFormats.ip.type).to.equal('string');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should return the ajv instance', () => {
|
|
53
|
+
const ajv = createMockAjv();
|
|
54
|
+
const configurer = ajvFormats();
|
|
55
|
+
const result = configurer(ajv);
|
|
56
|
+
|
|
57
|
+
expect(result).to.equal(ajv);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('with null extra formats', () => {
|
|
62
|
+
it('should treat null as no extra formats', () => {
|
|
63
|
+
const ajv = createMockAjv();
|
|
64
|
+
const configurer = ajvFormats(null);
|
|
65
|
+
|
|
66
|
+
configurer(ajv);
|
|
67
|
+
expect(addedLibFormats).to.deep.equal(['date', 'time', 'date-time', 'byte', 'uuid', 'uri', 'email', 'ipv4', 'ipv6']);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('with extra library formats (string-only)', () => {
|
|
72
|
+
it('should append format names to the library formats list', () => {
|
|
73
|
+
const ajv = createMockAjv();
|
|
74
|
+
const configurer = ajvFormats({ hostname: {} });
|
|
75
|
+
|
|
76
|
+
configurer(ajv);
|
|
77
|
+
expect(addedLibFormats).to.include('hostname');
|
|
78
|
+
expect(addedLibFormats).to.have.lengthOf(10);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('with extra custom formats (with type)', () => {
|
|
83
|
+
it('should add custom format via ajv.addFormat', () => {
|
|
84
|
+
const customFormat = { type: 'string', validate: /^[A-Z]+$/u };
|
|
85
|
+
const ajv = createMockAjv();
|
|
86
|
+
const configurer = ajvFormats({ uppercase: customFormat });
|
|
87
|
+
|
|
88
|
+
configurer(ajv);
|
|
89
|
+
expect(addedCustomFormats).to.have.property('uppercase');
|
|
90
|
+
expect(addedCustomFormats.uppercase).to.equal(customFormat);
|
|
91
|
+
expect(addedLibFormats).to.not.include('uppercase');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('semver validation', () => {
|
|
96
|
+
it('should accept valid semver strings', () => {
|
|
97
|
+
const ajv = createMockAjv();
|
|
98
|
+
|
|
99
|
+
ajvFormats()(ajv);
|
|
100
|
+
const { validate } = addedCustomFormats.semver;
|
|
101
|
+
|
|
102
|
+
expect(validate.test('1.0.0')).to.equal(true);
|
|
103
|
+
expect(validate.test('0.1.0')).to.equal(true);
|
|
104
|
+
expect(validate.test('1.2.3-alpha.1')).to.equal(true);
|
|
105
|
+
expect(validate.test('1.2.3+build.123')).to.equal(true);
|
|
106
|
+
expect(validate.test('1.2.3-beta+build')).to.equal(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should reject invalid semver strings', () => {
|
|
110
|
+
const ajv = createMockAjv();
|
|
111
|
+
|
|
112
|
+
ajvFormats()(ajv);
|
|
113
|
+
const { validate } = addedCustomFormats.semver;
|
|
114
|
+
|
|
115
|
+
expect(validate.test('1.0')).to.equal(false);
|
|
116
|
+
expect(validate.test('abc')).to.equal(false);
|
|
117
|
+
expect(validate.test('1.0.0.0')).to.equal(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('ip validation', () => {
|
|
122
|
+
it('should accept valid IPv4 addresses', () => {
|
|
123
|
+
const ajv = createMockAjv();
|
|
124
|
+
|
|
125
|
+
ajvFormats()(ajv);
|
|
126
|
+
const { validate } = addedCustomFormats.ip;
|
|
127
|
+
|
|
128
|
+
expect(validate.test('192.168.1.1')).to.equal(true);
|
|
129
|
+
expect(validate.test('10.0.0.1')).to.equal(true);
|
|
130
|
+
expect(validate.test('255.255.255.255')).to.equal(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should accept valid IPv6 addresses', () => {
|
|
134
|
+
const ajv = createMockAjv();
|
|
135
|
+
|
|
136
|
+
ajvFormats()(ajv);
|
|
137
|
+
const { validate } = addedCustomFormats.ip;
|
|
138
|
+
|
|
139
|
+
expect(validate.test('::1')).to.equal(true);
|
|
140
|
+
expect(validate.test('2001:0db8:85a3:0000:0000:8a2e:0370:7334')).to.equal(true);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('DEFAULT_FORMATS isolation', () => {
|
|
145
|
+
it('should not mutate DEFAULT_FORMATS between calls', () => {
|
|
146
|
+
const ajv1 = createMockAjv();
|
|
147
|
+
const ajv2 = createMockAjv();
|
|
148
|
+
|
|
149
|
+
ajvFormats({ extra1: {} })(ajv1);
|
|
150
|
+
const firstCallLength = addedLibFormats.length;
|
|
151
|
+
|
|
152
|
+
ajvFormats()(ajv2);
|
|
153
|
+
const secondCallLength = addedLibFormats.length;
|
|
154
|
+
|
|
155
|
+
expect(secondCallLength).to.equal(9);
|
|
156
|
+
expect(firstCallLength).to.equal(10);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|