ti2 1.0.97 → 1.0.99

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.
@@ -1,6 +1,7 @@
1
1
  /* globals beforeAll describe it expect */
2
2
  const yaml = require('js-yaml');
3
- const fs = require('fs');
3
+ const fs = require('fs')
4
+ const R = require('ramda')
4
5
 
5
6
  // Load environment variables and API schema
6
7
  const { env: { adminKey } } = process;
@@ -315,28 +316,24 @@ describe('scheduled cronjob execution', () => {
315
316
  const url = require('url');
316
317
 
317
318
  server = http.createServer((req, res) => {
318
- let body = '';
319
- req.on('data', chunk => body += chunk);
319
+ let callbackPayload = '';
320
+ req.on('data', chunk => callbackPayload += chunk);
320
321
  req.on('end', () => {
321
322
  const requestUrl = url.parse(req.url, true);
322
323
  const receivedUniqueId = requestUrl.query.date;
324
+ const receivedJobId = R.path(['result', 'bullJobId'], JSON.parse(callbackPayload))
323
325
 
324
326
  if (receivedUniqueId === uniqueId) {
325
327
  // Check if response and response.bullJobId are available
326
- if (response && response.bullJobId) {
327
- res.writeHead(200, { 'Content-Type': 'application/json' });
328
- res.end(JSON.stringify({ success: true }));
329
- // server.close() will be handled in afterAll or if explicitly resolved/rejected
330
- resolve({ id: response.bullJobId });
331
- } else {
332
- // This case handles if callback arrives but job creation might have failed or response not processed
333
- res.writeHead(500, { 'Content-Type': 'application/json' });
334
- res.end(JSON.stringify({ success: false, message: 'Callback received but job details unavailable.' }));
335
- reject(new Error('Callback processed but original job details (response.bullJobId) were missing.'));
336
- }
328
+ res.writeHead(200, { 'Content-Type': 'application/json' });
329
+ res.end(JSON.stringify({ success: true }));
330
+ // Close the server after sending the response
331
+ server.close(() => {
332
+ resolve({ receivedJobId });
333
+ });
337
334
  } else {
338
335
  res.writeHead(400, { 'Content-Type': 'application/json' });
339
- res.end(JSON.stringify({ success: false, message: 'Callback ID mismatch' }));
336
+ return res.end(JSON.stringify({ success: false, message: 'Callback ID mismatch' }));
340
337
  // Do not resolve or reject here, let the test timeout if correct callback never comes.
341
338
  // Or, optionally, reject:
342
339
  // reject(new Error('Callback ID mismatch.'));
@@ -366,12 +363,12 @@ describe('scheduled cronjob execution', () => {
366
363
  });
367
364
 
368
365
  expect(response.bullJobId).toBeTruthy();
366
+ bullJobId = response.bullJobId;
369
367
  expect(response.id).toBeTruthy();
370
368
  cronJobIdToCleanup = response.id;
371
369
 
372
- const callBackInstance = await callBackServerPromise;
373
- expect(callBackInstance.id).toBe(response.bullJobId);
374
- bullJobId = response.bullJobId;
370
+ const { receivedJobId } = await callBackServerPromise;
371
+ expect(receivedJobId).toBe(bullJobId);
375
372
 
376
373
  }, 120e3); // Timeout for the cronjob to be executed
377
374
 
@@ -380,7 +377,7 @@ describe('scheduled cronjob execution', () => {
380
377
  expect(bullJobId).toBeTruthy();
381
378
  const { queue } = require('../../worker/queue');
382
379
  let jobInQueue;
383
- const maxRetries = 60;
380
+ const maxRetries = 120;
384
381
  const retryInterval = 1e3;
385
382
 
386
383
  for (let i = 0; i < maxRetries; i++) {
@@ -389,6 +386,6 @@ describe('scheduled cronjob execution', () => {
389
386
  await new Promise(resolveDelay => setTimeout(resolveDelay, retryInterval));
390
387
  }
391
388
  expect(jobInQueue).toBeNull();
392
- }, 60e3) // timeout for bull to remove the queue Job once executed (removeOnComplete)
389
+ }, 120e3) // timeout for bull to remove the queue Job once executed (removeOnComplete)
393
390
  });
394
391
  });
@@ -450,14 +450,19 @@ const createBooking = plugins => async (req, res, next) => {
450
450
  assert(userAppKeys, 'could not find the app key');
451
451
  const token = await userAppKeys.token;
452
452
  const func = (app.createBooking || app.addServiceToItinerary).bind(app);
453
- const results = await func({
454
- axios,
455
- token,
456
- payload,
457
- typeDefsAndQueries,
458
- requestId: req.requestId,
459
- });
460
-
453
+ let results;
454
+ if (payload.mock) {
455
+ results = { mock: true, success: true, bookingId: '1234567890' };
456
+ } else {
457
+ results = await func({
458
+ axios,
459
+ token,
460
+ payload,
461
+ typeDefsAndQueries,
462
+ requestId: req.requestId,
463
+ });
464
+ }
465
+ console.log(`emitting bookingsCreateBooking event for ${appKey}, user ${userId}, hint ${hint}, results: ${JSON.stringify(results)}`);
461
466
  app.events.emit('bookingsCreateBooking', {
462
467
  userId,
463
468
  hint,
package/index.js CHANGED
@@ -15,6 +15,7 @@ const EventEmitter = require('eventemitter2');
15
15
  const axios = require('axios');
16
16
  const curlirize = require('axios-curlirize');
17
17
  const { addJob } = require('./worker/queue');
18
+ const { configureAxiosSSL } = require('./lib/sslConfig');
18
19
 
19
20
  const cacheSettings = {
20
21
  '*': [
@@ -78,6 +79,10 @@ module.exports = async ({
78
79
  const nuName = attr.replace(/_/g, '-').replace(`ti2-${pluginName}-`, '');
79
80
  params[nuName] = value;
80
81
  });
82
+ // Create a new axios instance for this plugin and configure SSL if needed
83
+ const pluginAxios = axios.create();
84
+ configureAxiosSSL(pluginAxios, process.env.SSL_INSECURE_ALLOWED_DOMAINS);
85
+
81
86
  const pluginInstance = await new Plugin({
82
87
  cache: {
83
88
  drop: args => cache.drop({ ...args, pluginName }),
@@ -85,7 +90,7 @@ module.exports = async ({
85
90
  getOrExec: args => cache.getOrExec({ ...args, pluginName }),
86
91
  save: args => cache.save({ ...args, pluginName }),
87
92
  },
88
- axios,
93
+ axios: pluginAxios,
89
94
  events: ti2Events,
90
95
  name: pluginName,
91
96
  ...params,
@@ -0,0 +1,112 @@
1
+ const axios = require('axios');
2
+ const { configureAxiosSSL, isDomainAllowedInsecure } = require('../sslConfig');
3
+
4
+ describe('SSL Configuration', () => {
5
+ describe('isDomainAllowedInsecure', () => {
6
+ it('should return false when no domains are allowed', () => {
7
+ expect(isDomainAllowedInsecure('https://example.com', '')).toBe(false);
8
+ expect(isDomainAllowedInsecure('https://example.com', null)).toBe(false);
9
+ expect(isDomainAllowedInsecure('https://example.com', undefined)).toBe(false);
10
+ });
11
+
12
+ it('should match exact domain', () => {
13
+ const allowedDomains = 'localhost|staging.example.com';
14
+ expect(isDomainAllowedInsecure('https://localhost/api', allowedDomains)).toBe(true);
15
+ expect(isDomainAllowedInsecure('https://staging.example.com/api', allowedDomains)).toBe(true);
16
+ expect(isDomainAllowedInsecure('https://production.example.com/api', allowedDomains)).toBe(false);
17
+ });
18
+
19
+ it('should match subdomain', () => {
20
+ const allowedDomains = 'example.com';
21
+ expect(isDomainAllowedInsecure('https://api.example.com/endpoint', allowedDomains)).toBe(true);
22
+ expect(isDomainAllowedInsecure('https://staging.api.example.com/endpoint', allowedDomains)).toBe(true);
23
+ expect(isDomainAllowedInsecure('https://example.com/endpoint', allowedDomains)).toBe(true);
24
+ expect(isDomainAllowedInsecure('https://different.com/endpoint', allowedDomains)).toBe(false);
25
+ });
26
+
27
+ it('should handle IP addresses', () => {
28
+ const allowedDomains = '192.168.1.100|10.0.0.1';
29
+ expect(isDomainAllowedInsecure('https://192.168.1.100:8080/api', allowedDomains)).toBe(true);
30
+ expect(isDomainAllowedInsecure('https://10.0.0.1/api', allowedDomains)).toBe(true);
31
+ expect(isDomainAllowedInsecure('https://192.168.1.101/api', allowedDomains)).toBe(false);
32
+ });
33
+
34
+ it('should be case insensitive', () => {
35
+ const allowedDomains = 'EXAMPLE.COM|LocalHost';
36
+ expect(isDomainAllowedInsecure('https://example.com/api', allowedDomains)).toBe(true);
37
+ expect(isDomainAllowedInsecure('https://EXAMPLE.COM/api', allowedDomains)).toBe(true);
38
+ expect(isDomainAllowedInsecure('https://localhost/api', allowedDomains)).toBe(true);
39
+ expect(isDomainAllowedInsecure('https://LOCALHOST/api', allowedDomains)).toBe(true);
40
+ });
41
+
42
+ it('should handle invalid URLs gracefully', () => {
43
+ const allowedDomains = 'example.com';
44
+ expect(isDomainAllowedInsecure('not-a-url', allowedDomains)).toBe(false);
45
+ expect(isDomainAllowedInsecure('', allowedDomains)).toBe(false);
46
+ expect(isDomainAllowedInsecure(null, allowedDomains)).toBe(false);
47
+ });
48
+
49
+ it('should handle whitespace in domain list', () => {
50
+ const allowedDomains = ' localhost | staging.example.com | 192.168.1.100 ';
51
+ expect(isDomainAllowedInsecure('https://localhost/api', allowedDomains)).toBe(true);
52
+ expect(isDomainAllowedInsecure('https://staging.example.com/api', allowedDomains)).toBe(true);
53
+ expect(isDomainAllowedInsecure('https://192.168.1.100/api', allowedDomains)).toBe(true);
54
+ });
55
+ });
56
+
57
+ describe('configureAxiosSSL', () => {
58
+ it('should not modify axios when no domains are configured', () => {
59
+ const axiosInstance = axios.create();
60
+ const originalInterceptors = axiosInstance.interceptors.request.handlers.length;
61
+
62
+ configureAxiosSSL(axiosInstance, '');
63
+ expect(axiosInstance.interceptors.request.handlers.length).toBe(originalInterceptors);
64
+
65
+ configureAxiosSSL(axiosInstance, null);
66
+ expect(axiosInstance.interceptors.request.handlers.length).toBe(originalInterceptors);
67
+ });
68
+
69
+ it('should add request interceptor when domains are configured', () => {
70
+ const axiosInstance = axios.create();
71
+ const originalInterceptors = axiosInstance.interceptors.request.handlers.length;
72
+
73
+ configureAxiosSSL(axiosInstance, 'localhost|example.com');
74
+ expect(axiosInstance.interceptors.request.handlers.length).toBe(originalInterceptors + 1);
75
+ });
76
+
77
+ it('should set httpsAgent for allowed domains', async () => {
78
+ const axiosInstance = axios.create();
79
+ configureAxiosSSL(axiosInstance, 'localhost|staging.example.com');
80
+
81
+ // Test the interceptor by creating a mock config
82
+ const config = { url: 'https://localhost/api' };
83
+ const interceptor = axiosInstance.interceptors.request.handlers[0];
84
+
85
+ if (interceptor && interceptor.fulfilled) {
86
+ const modifiedConfig = await interceptor.fulfilled(config);
87
+ expect(modifiedConfig.httpsAgent).toBeDefined();
88
+ expect(modifiedConfig.httpsAgent.options.rejectUnauthorized).toBe(false);
89
+ }
90
+ });
91
+
92
+ it('should not set httpsAgent for non-allowed domains', async () => {
93
+ const axiosInstance = axios.create();
94
+ configureAxiosSSL(axiosInstance, 'localhost|staging.example.com');
95
+
96
+ // Test the interceptor by creating a mock config
97
+ const config = { url: 'https://production.example.com/api' };
98
+ const interceptor = axiosInstance.interceptors.request.handlers[0];
99
+
100
+ if (interceptor && interceptor.fulfilled) {
101
+ const modifiedConfig = await interceptor.fulfilled(config);
102
+ expect(modifiedConfig.httpsAgent).toBeUndefined();
103
+ }
104
+ });
105
+
106
+ it('should return the same axios instance', () => {
107
+ const axiosInstance = axios.create();
108
+ const result = configureAxiosSSL(axiosInstance, 'localhost');
109
+ expect(result).toBe(axiosInstance);
110
+ });
111
+ });
112
+ });
@@ -0,0 +1,63 @@
1
+ const https = require('https');
2
+ const { URL } = require('url');
3
+
4
+ /**
5
+ * Creates an HTTPS agent with SSL verification disabled
6
+ */
7
+ const createInsecureAgent = () => new https.Agent({
8
+ rejectUnauthorized: false,
9
+ });
10
+
11
+ /**
12
+ * Checks if a URL's domain is in the allowed insecure domains list
13
+ * @param {string} url - The URL to check
14
+ * @param {string} allowedDomains - Pipe-separated list of allowed domains
15
+ * @returns {boolean} True if the domain is allowed to skip SSL verification
16
+ */
17
+ const isDomainAllowedInsecure = (url, allowedDomains) => {
18
+ if (!allowedDomains || !url) return false;
19
+
20
+ try {
21
+ const parsedUrl = new URL(url);
22
+ const hostname = parsedUrl.hostname.toLowerCase();
23
+ const domains = allowedDomains.split('|').map(d => d.trim().toLowerCase()).filter(Boolean);
24
+
25
+ return domains.some(domain => {
26
+ // Exact match or subdomain match
27
+ return hostname === domain || hostname.endsWith(`.${domain}`);
28
+ });
29
+ } catch (error) {
30
+ // Invalid URL, don't allow insecure
31
+ return false;
32
+ }
33
+ };
34
+
35
+ /**
36
+ * Configures an axios instance to allow insecure SSL for specific domains
37
+ * @param {Object} axiosInstance - The axios instance to configure
38
+ * @param {string} allowedDomains - Pipe-separated list of allowed domains (from env var)
39
+ * @returns {Object} The configured axios instance
40
+ */
41
+ const configureAxiosSSL = (axiosInstance, allowedDomains) => {
42
+ if (!allowedDomains) return axiosInstance;
43
+
44
+ const insecureAgent = createInsecureAgent();
45
+
46
+ // Add request interceptor to conditionally apply insecure agent
47
+ axiosInstance.interceptors.request.use(
48
+ (config) => {
49
+ if (config.url && isDomainAllowedInsecure(config.url, allowedDomains)) {
50
+ config.httpsAgent = insecureAgent;
51
+ }
52
+ return config;
53
+ },
54
+ (error) => Promise.reject(error)
55
+ );
56
+
57
+ return axiosInstance;
58
+ };
59
+
60
+ module.exports = {
61
+ configureAxiosSSL,
62
+ isDomainAllowedInsecure,
63
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ti2",
3
- "version": "1.0.97",
3
+ "version": "1.0.99",
4
4
  "description": "Tourist Industry Exchange (TI2)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -29,6 +29,7 @@ A MySQl instance is required and the following environment variables are require
29
29
  - jwtKey (a key to encrypt user sessions request)
30
30
  - frontendKey (an additional optional key to validate the origin of the requests)
31
31
  - PORT (an optional port to run the http server, in case a port is not specified as a parameter)
32
+ - SSL_INSECURE_ALLOWED_DOMAINS (optional, pipe-separated list of domains where SSL certificate verification is skipped)
32
33
 
33
34
  ```
34
35
  DB_URL=mysql://root:@mysqlserver/ti2development
@@ -39,6 +40,25 @@ frontendKey=IpOiLahrGQxsQimwoK7Z
39
40
  adminKey=bCviyz7iKyuEBoOVlODn
40
41
  ```
41
42
 
43
+ #### SSL Configuration for Development/Testing
44
+
45
+ For development or testing environments where you need to connect to APIs with self-signed or invalid SSL certificates, you can use the `SSL_INSECURE_ALLOWED_DOMAINS` environment variable:
46
+
47
+ ```bash
48
+ # Skip SSL verification for specific domains
49
+ SSL_INSECURE_ALLOWED_DOMAINS="localhost|staging.api.com|192.168.1.100"
50
+
51
+ # Multiple domains separated by pipe
52
+ SSL_INSECURE_ALLOWED_DOMAINS="dev.example.com|test.example.com"
53
+ ```
54
+
55
+ This configuration will:
56
+ - Skip SSL certificate verification for the specified domains
57
+ - Apply only to plugin axios instances (external API calls)
58
+ - Maintain normal SSL verification for all other domains
59
+
60
+ **⚠️ Warning**: Only use this in development/testing environments. Never skip SSL verification in production for security reasons.
61
+
42
62
  ### Initialize a new NodeJS project
43
63
 
44
64
  We suggest creating a folder with your instance name+i2 (i.e acme-ti2); then initialize an node's npm package and install ti2 along with any plugins to be used.
package/worker/index.js CHANGED
@@ -4,6 +4,10 @@ const R = require('ramda');
4
4
  const { queue, saveResult } = require('./queue');
5
5
  const bookingsControllerFactory = require('../controllers/bookings');
6
6
  const mainAxios = require('axios'); // Generic axios instance for direct calls
7
+ const { configureAxiosSSL } = require('../lib/sslConfig');
8
+
9
+ // Configure SSL for the main axios instance
10
+ configureAxiosSSL(mainAxios, process.env.SSL_INSECURE_ALLOWED_DOMAINS);
7
11
 
8
12
  const workers = process.env.WEB_CONCURRENCY || 2;
9
13
  const maxJobsPerWorker = process.env.MAX_JOBS_PER_WORKER || 1;