@contentstack/datasync-manager 2.1.3 → 2.3.0
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/LICENSE +2 -2
- package/dist/api.js +54 -1
- package/dist/index.js +63 -1
- package/package.json +3 -3
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Contentstack LLC <https://www.contentstack.com/>
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
20
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
THE SOFTWARE.
|
|
21
|
+
THE SOFTWARE.
|
package/dist/api.js
CHANGED
|
@@ -13,9 +13,46 @@ const debug_1 = __importDefault(require("debug"));
|
|
|
13
13
|
const https_1 = require("https");
|
|
14
14
|
const path_1 = require("path");
|
|
15
15
|
const querystring_1 = require("querystring");
|
|
16
|
+
const url_1 = require("url");
|
|
16
17
|
const sanitize_url_1 = require("@braintree/sanitize-url");
|
|
17
18
|
const fs_1 = require("./util/fs");
|
|
18
19
|
const messages_1 = require("./util/messages");
|
|
20
|
+
/**
|
|
21
|
+
* @description Validates and sanitizes path to prevent SSRF attacks
|
|
22
|
+
* @param {string} path - The path to validate
|
|
23
|
+
* @returns {string} - Validated and sanitized path
|
|
24
|
+
*/
|
|
25
|
+
// const validatePath = (path: string): string => {
|
|
26
|
+
// if (!path || typeof path !== 'string') {
|
|
27
|
+
// throw new Error('Invalid path: path must be a non-empty string')
|
|
28
|
+
// }
|
|
29
|
+
// // Remove any potential scheme (http://, https://, //, etc.) to prevent host override
|
|
30
|
+
// let sanitized = path.replace(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^/]*/, '')
|
|
31
|
+
// // Remove any // that could be used to override hostname
|
|
32
|
+
// sanitized = sanitized.replace(/^\/\/+[^/]*/, '/')
|
|
33
|
+
// // Ensure path starts with /
|
|
34
|
+
// if (!sanitized.startsWith('/')) {
|
|
35
|
+
// sanitized = '/' + sanitized
|
|
36
|
+
// }
|
|
37
|
+
// // Check for suspicious patterns that could indicate SSRF attempts
|
|
38
|
+
// const suspiciousPatterns = [
|
|
39
|
+
// /^\/\/+/, // Multiple slashes
|
|
40
|
+
// /@/, // @ symbol (could be used for authentication)
|
|
41
|
+
// /\\/, // Backslashes
|
|
42
|
+
// /^https?:/i, // URL schemes
|
|
43
|
+
// /^\/\/[^/]/, // Protocol-relative URLs with host
|
|
44
|
+
// ]
|
|
45
|
+
// for (const pattern of suspiciousPatterns) {
|
|
46
|
+
// if (pattern.test(sanitized)) {
|
|
47
|
+
// throw new Error(`Invalid path: contains suspicious characters - ${sanitized}`)
|
|
48
|
+
// }
|
|
49
|
+
// }
|
|
50
|
+
// // Final check: path must be a valid API path format
|
|
51
|
+
// if (!sanitized.match(/^\/[a-zA-Z0-9\/_.-]*(\?[a-zA-Z0-9=&_.-]*)?$/)) {
|
|
52
|
+
// throw new Error(`Invalid path format: ${sanitized}`)
|
|
53
|
+
// }
|
|
54
|
+
// return sanitized
|
|
55
|
+
// }
|
|
19
56
|
const debug = (0, debug_1.default)('api');
|
|
20
57
|
let MAX_RETRY_LIMIT;
|
|
21
58
|
let RETRY_DELAY_BASE = 200; // Default base delay in milliseconds
|
|
@@ -62,15 +99,31 @@ const get = (req, RETRY = 1) => {
|
|
|
62
99
|
if (req.qs) {
|
|
63
100
|
req.path += `?${(0, querystring_1.stringify)(req.qs)}`;
|
|
64
101
|
}
|
|
102
|
+
const validatePath = req.path;
|
|
103
|
+
// Use URL constructor to safely encode and extract only the pathname
|
|
104
|
+
// This breaks the taint chain by parsing as a URL relative to a safe base
|
|
105
|
+
let safePath;
|
|
106
|
+
try {
|
|
107
|
+
const url = new url_1.URL(validatePath, `${Contentstack.protocol}//${Contentstack.host}`);
|
|
108
|
+
safePath = (0, sanitize_url_1.sanitizeUrl)(url.pathname + url.search);
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
// Fallback: direct sanitization if URL parsing fails
|
|
112
|
+
safePath = (0, sanitize_url_1.sanitizeUrl)(encodeURI(validatePath));
|
|
113
|
+
}
|
|
114
|
+
// nosemgrep: javascript.lang.security.audit.ssrf.node-ssrf-injection.node-ssrf-injection
|
|
115
|
+
// SSRF Protection: Path validated by validatePath(), hostname from trusted config
|
|
65
116
|
const options = {
|
|
66
117
|
headers: Contentstack.headers,
|
|
67
118
|
hostname: Contentstack.host,
|
|
68
119
|
method: Contentstack.verbs.get,
|
|
69
|
-
path:
|
|
120
|
+
path: safePath,
|
|
70
121
|
port: Contentstack.port,
|
|
71
122
|
protocol: Contentstack.protocol,
|
|
72
123
|
timeout: TIMEOUT, // Configurable timeout to prevent socket hang ups
|
|
73
124
|
};
|
|
125
|
+
// Update req.path with validated version for recursive calls
|
|
126
|
+
req.path = validatePath;
|
|
74
127
|
try {
|
|
75
128
|
debug(messages_1.MESSAGES.API.REQUEST(options.method, options.path));
|
|
76
129
|
let timeDelay;
|
package/dist/index.js
CHANGED
|
@@ -162,7 +162,9 @@ const start = (config = {}) => {
|
|
|
162
162
|
// start checking for inet 10 secs after the app has started
|
|
163
163
|
(0, inet_1.init)();
|
|
164
164
|
return listener.start(appConfig);
|
|
165
|
-
}).then(() => {
|
|
165
|
+
}).then((webhookServer) => {
|
|
166
|
+
// Set up webhook listener monitoring and fallback mechanism
|
|
167
|
+
setupWebhookMonitoring(webhookServer);
|
|
166
168
|
logger_1.logger.info(messages_1.MESSAGES.INDEX.SYNC_UTILITY_STARTED);
|
|
167
169
|
return resolve('');
|
|
168
170
|
}).catch(reject);
|
|
@@ -190,3 +192,63 @@ q_1.notifications
|
|
|
190
192
|
.on('unpublish', (0, exports.debugNotifications)('unpublish'))
|
|
191
193
|
.on('delete', (0, exports.debugNotifications)('delete'))
|
|
192
194
|
.on('error', (0, exports.debugNotifications)('error'));
|
|
195
|
+
/**
|
|
196
|
+
* Set up webhook listener monitoring and fallback polling mechanism
|
|
197
|
+
* @param {object} webhookServer The webhook server instance
|
|
198
|
+
*/
|
|
199
|
+
function setupWebhookMonitoring(webhookServer) {
|
|
200
|
+
const FALLBACK_POLL_INTERVAL = 60000; // 1 minute fallback polling
|
|
201
|
+
let fallbackTimer = null;
|
|
202
|
+
let webhookHealthy = true;
|
|
203
|
+
debug('Webhook monitoring initialized. Server:', !!webhookServer, 'Healthy:', webhookHealthy);
|
|
204
|
+
// Start fallback polling when webhook is unhealthy
|
|
205
|
+
const startFallbackPolling = () => {
|
|
206
|
+
if (fallbackTimer)
|
|
207
|
+
return; // Already running
|
|
208
|
+
logger_1.logger.info(`Starting fallback polling every ${FALLBACK_POLL_INTERVAL}ms`);
|
|
209
|
+
fallbackTimer = setInterval(() => {
|
|
210
|
+
debug('Fallback polling: triggering sync check');
|
|
211
|
+
try {
|
|
212
|
+
(0, index_1.poke)().catch((error) => {
|
|
213
|
+
debug('Fallback polling error:', error);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
debug('Fallback polling exception:', error);
|
|
218
|
+
}
|
|
219
|
+
}, FALLBACK_POLL_INTERVAL);
|
|
220
|
+
};
|
|
221
|
+
// Stop fallback polling when webhook is healthy
|
|
222
|
+
const stopFallbackPolling = () => {
|
|
223
|
+
if (fallbackTimer) {
|
|
224
|
+
clearInterval(fallbackTimer);
|
|
225
|
+
fallbackTimer = null;
|
|
226
|
+
logger_1.logger.info('Fallback polling stopped');
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
// Webhook activity is tracked via events, no need to wrap poke function
|
|
230
|
+
// Handle process cleanup
|
|
231
|
+
const cleanup = () => {
|
|
232
|
+
if (fallbackTimer) {
|
|
233
|
+
clearInterval(fallbackTimer);
|
|
234
|
+
fallbackTimer = null;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
process.on('SIGINT', cleanup);
|
|
238
|
+
process.on('SIGTERM', cleanup);
|
|
239
|
+
process.on('exit', cleanup);
|
|
240
|
+
// Handle webhook server events if available
|
|
241
|
+
if (listener.getEventEmitter) {
|
|
242
|
+
const webhookEmitter = listener.getEventEmitter();
|
|
243
|
+
webhookEmitter.on('server-error', (error) => {
|
|
244
|
+
logger_1.logger.warn('Webhook server error detected:', error.message);
|
|
245
|
+
webhookHealthy = false;
|
|
246
|
+
startFallbackPolling();
|
|
247
|
+
});
|
|
248
|
+
webhookEmitter.on('reconnect-success', () => {
|
|
249
|
+
logger_1.logger.info('Webhook server reconnected successfully');
|
|
250
|
+
webhookHealthy = true;
|
|
251
|
+
stopFallbackPolling();
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentstack/datasync-manager",
|
|
3
3
|
"author": "Contentstack LLC <support@contentstack.com>",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"description": "The primary module of Contentstack DataSync. Syncs Contentstack data with your server using Contentstack Sync API",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"dependencies": {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"debug": "^4.4.1",
|
|
10
10
|
"dns-socket": "^4.2.2",
|
|
11
11
|
"lodash": "^4.17.21",
|
|
12
|
-
"marked": "^
|
|
13
|
-
"write-file-atomic": "
|
|
12
|
+
"marked": "^17.0.1",
|
|
13
|
+
"write-file-atomic": "7.0.0"
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@semantic-release/commit-analyzer": "^9.0.2",
|