@contentstack/datasync-manager 2.2.0 → 2.4.0-beta.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 +89 -1
- package/dist/core/index.js +14 -0
- package/dist/util/messages.js +3 -0
- 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;
|
|
@@ -105,6 +158,41 @@ const get = (req, RETRY = 1) => {
|
|
|
105
158
|
}, timeDelay);
|
|
106
159
|
}
|
|
107
160
|
else {
|
|
161
|
+
// Enhanced error handling for Error 141 (Invalid sync_token)
|
|
162
|
+
try {
|
|
163
|
+
const errorBody = JSON.parse(body);
|
|
164
|
+
// Validate error response structure and check for Error 141
|
|
165
|
+
if (errorBody && typeof errorBody === 'object' && errorBody.error_code === 141 && errorBody.errors && typeof errorBody.errors === 'object' && errorBody.errors.sync_token) {
|
|
166
|
+
debug('Error 141 detected: Invalid sync_token. Triggering auto-recovery with init=true');
|
|
167
|
+
// Ensure req.qs exists before modifying
|
|
168
|
+
if (!req.qs) {
|
|
169
|
+
req.qs = {};
|
|
170
|
+
}
|
|
171
|
+
// Clear the invalid token parameters and reinitialize
|
|
172
|
+
if (req.qs.sync_token) {
|
|
173
|
+
delete req.qs.sync_token;
|
|
174
|
+
}
|
|
175
|
+
if (req.qs.pagination_token) {
|
|
176
|
+
delete req.qs.pagination_token;
|
|
177
|
+
}
|
|
178
|
+
req.qs.init = true;
|
|
179
|
+
// Mark this as a recovery attempt to prevent infinite loops
|
|
180
|
+
if (!req._error141Recovery) {
|
|
181
|
+
req._error141Recovery = true;
|
|
182
|
+
debug('Retrying with init=true after Error 141');
|
|
183
|
+
return (0, exports.get)(req, 1) // Reset retry counter for fresh start
|
|
184
|
+
.then(resolve)
|
|
185
|
+
.catch(reject);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
debug('Error 141 recovery already attempted, failing to prevent infinite loop');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (parseError) {
|
|
193
|
+
// Body is not JSON or parsing failed, continue with normal error handling
|
|
194
|
+
debug('Error response parsing failed:', parseError);
|
|
195
|
+
}
|
|
108
196
|
debug(messages_1.MESSAGES.API.REQUEST_FAILED(options));
|
|
109
197
|
return reject(body);
|
|
110
198
|
}
|
package/dist/core/index.js
CHANGED
|
@@ -361,6 +361,20 @@ const fire = (req) => {
|
|
|
361
361
|
.catch(reject);
|
|
362
362
|
}).catch((error) => {
|
|
363
363
|
debug(messages_1.MESSAGES.SYNC_CORE.ERROR_FIRE, error);
|
|
364
|
+
// Check if this is an Error 141 (invalid token) - enhanced handling
|
|
365
|
+
try {
|
|
366
|
+
const parsedError = typeof error === 'string' ? JSON.parse(error) : error;
|
|
367
|
+
if (parsedError.error_code === 141) {
|
|
368
|
+
logger_1.logger.error('Error 141: Invalid sync_token detected. Token has been reset.');
|
|
369
|
+
logger_1.logger.info('System will automatically re-initialize with fresh token on next sync.');
|
|
370
|
+
// The error has already been handled in api.ts with init=true
|
|
371
|
+
// Just ensure we don't keep retrying with the bad token
|
|
372
|
+
flag.SQ = false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch (parseError) {
|
|
376
|
+
// Not a JSON error or not Error 141, continue with normal handling
|
|
377
|
+
}
|
|
364
378
|
if ((0, inet_1.netConnectivityIssues)(error)) {
|
|
365
379
|
flag.SQ = false;
|
|
366
380
|
}
|
package/dist/util/messages.js
CHANGED
|
@@ -38,6 +38,9 @@ exports.MESSAGES = {
|
|
|
38
38
|
REQUEST_TIMEOUT: (path) => `Request timeout for ${path || 'unknown'}`,
|
|
39
39
|
REQUEST_ERROR: (path, message, code) => `Request error for ${path || 'unknown'}: ${message || 'Unknown error'} (${code || 'NO_CODE'})`,
|
|
40
40
|
SOCKET_HANGUP_RETRY: (path, delay, attempt, max) => `Socket hang up detected. Retrying ${path || 'unknown'} with ${delay} ms delay (attempt ${attempt}/${max})`,
|
|
41
|
+
ERROR_141_DETECTED: 'Error 141: Invalid sync_token detected. Token is no longer valid.',
|
|
42
|
+
ERROR_141_RECOVERY: 'Attempting automatic recovery with init=true to get fresh token.',
|
|
43
|
+
ERROR_141_RETRY: 'Retrying sync operation with fresh initialization after Error 141.',
|
|
41
44
|
},
|
|
42
45
|
// Plugin messages (plugins.ts)
|
|
43
46
|
PLUGINS: {
|
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.4.0-beta.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",
|