@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright (c) 2025 Contentstack LLC <https://www.contentstack.com/>
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: (0, sanitize_url_1.sanitizeUrl)(encodeURI(req.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
  }
@@ -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
  }
@@ -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.2.0",
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": "^4.3.0",
13
- "write-file-atomic": "4.0.2"
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",