@magentrix-corp/magentrix-cli 1.3.16 → 1.3.17

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.
Files changed (68) hide show
  1. package/LICENSE +25 -25
  2. package/README.md +1166 -1166
  3. package/actions/autopublish.old.js +293 -293
  4. package/actions/config.js +182 -182
  5. package/actions/create.js +466 -466
  6. package/actions/help.js +164 -164
  7. package/actions/iris/buildStage.js +874 -874
  8. package/actions/iris/delete.js +256 -256
  9. package/actions/iris/dev.js +391 -391
  10. package/actions/iris/index.js +6 -6
  11. package/actions/iris/link.js +375 -375
  12. package/actions/iris/recover.js +268 -268
  13. package/actions/main.js +80 -80
  14. package/actions/publish.js +1420 -1420
  15. package/actions/pull.js +684 -684
  16. package/actions/setup.js +148 -148
  17. package/actions/status.js +17 -17
  18. package/actions/update.js +248 -248
  19. package/bin/magentrix.js +393 -393
  20. package/package.json +55 -55
  21. package/utils/assetPaths.js +158 -158
  22. package/utils/autopublishLock.js +77 -77
  23. package/utils/cacher.js +206 -206
  24. package/utils/cli/checkInstanceUrl.js +76 -74
  25. package/utils/cli/helpers/compare.js +282 -282
  26. package/utils/cli/helpers/ensureApiKey.js +63 -63
  27. package/utils/cli/helpers/ensureCredentials.js +68 -68
  28. package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
  29. package/utils/cli/writeRecords.js +262 -262
  30. package/utils/compare.js +135 -135
  31. package/utils/compress.js +17 -17
  32. package/utils/config.js +527 -527
  33. package/utils/debug.js +144 -144
  34. package/utils/diagnostics/testPublishLogic.js +96 -96
  35. package/utils/diff.js +49 -49
  36. package/utils/downloadAssets.js +291 -291
  37. package/utils/filetag.js +115 -115
  38. package/utils/hash.js +14 -14
  39. package/utils/iris/backup.js +411 -411
  40. package/utils/iris/builder.js +541 -541
  41. package/utils/iris/config-reader.js +664 -664
  42. package/utils/iris/deleteHelper.js +150 -150
  43. package/utils/iris/errors.js +537 -537
  44. package/utils/iris/linker.js +601 -601
  45. package/utils/iris/lock.js +360 -360
  46. package/utils/iris/validation.js +360 -360
  47. package/utils/iris/validator.js +281 -281
  48. package/utils/iris/zipper.js +248 -248
  49. package/utils/logger.js +291 -291
  50. package/utils/magentrix/api/assets.js +220 -220
  51. package/utils/magentrix/api/auth.js +107 -107
  52. package/utils/magentrix/api/createEntity.js +61 -61
  53. package/utils/magentrix/api/deleteEntity.js +55 -55
  54. package/utils/magentrix/api/iris.js +251 -251
  55. package/utils/magentrix/api/meqlQuery.js +36 -36
  56. package/utils/magentrix/api/retrieveEntity.js +86 -86
  57. package/utils/magentrix/api/updateEntity.js +66 -66
  58. package/utils/magentrix/fetch.js +168 -168
  59. package/utils/merge.js +22 -22
  60. package/utils/permissionError.js +70 -70
  61. package/utils/preferences.js +40 -40
  62. package/utils/progress.js +469 -469
  63. package/utils/spinner.js +43 -43
  64. package/utils/template.js +52 -52
  65. package/utils/updateFileBase.js +121 -121
  66. package/utils/workspaces.js +108 -108
  67. package/vars/config.js +11 -11
  68. package/vars/global.js +50 -50
package/utils/cacher.js CHANGED
@@ -1,206 +1,206 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import Config from './config.js';
4
- import { findFileByTag, getFileTag, isPathLinkedToTagByLastKnownPath, setFileTag } from './filetag.js';
5
- import { compressString } from './compress.js';
6
- import { sha256 } from './hash.js';
7
- import { EXPORT_ROOT, ALLOWED_SRC_DIRS, IRIS_APPS_DIR } from '../vars/global.js';
8
-
9
- const config = new Config();
10
-
11
- // System/hidden files that should never be synced
12
- const IGNORED_FILES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
13
-
14
- /**
15
- * Recursively caches all files in a directory with tagging and content snapshotting
16
- * OPTIMIZED: Parallel processing + mtime checks for maximum speed
17
- * @param {string} dir - Directory to cache
18
- * @returns {Promise<void>}
19
- */
20
- export const cacheDir = async (dir) => {
21
- if (!fs.existsSync(dir)) return;
22
-
23
- const absDir = path.resolve(dir);
24
-
25
- // Only walk whitelisted code entity directories (exclude Assets and iris-apps, handled separately)
26
- const codeEntityDirs = ALLOWED_SRC_DIRS.filter(d => d !== 'Assets' && d !== IRIS_APPS_DIR);
27
- const fileArrays = await Promise.all(
28
- codeEntityDirs.map(d => {
29
- const dirPath = path.join(absDir, d);
30
- return fs.existsSync(dirPath) ? walkFiles(dirPath) : Promise.resolve([]);
31
- })
32
- );
33
- const files = fileArrays.flat();
34
-
35
- const cache = config.read('cachedFiles', { global: false, filename: 'fileCache.json' }) || {};
36
-
37
- // Process files in parallel batches
38
- const BATCH_SIZE = 50;
39
- const updates = [];
40
-
41
- for (let i = 0; i < files.length; i += BATCH_SIZE) {
42
- const batch = files.slice(i, i + BATCH_SIZE);
43
-
44
- // Process batch in parallel
45
- const batchResults = await Promise.all(
46
- batch.map(async (file) => {
47
- try {
48
- const stats = fs.statSync(file);
49
- if (!stats.isFile()) return null;
50
-
51
- const checkFileTag = async (retry = true) => {
52
- const tag = await getFileTag(file);
53
- if (!tag) {
54
- const dirLinkedTag = isPathLinkedToTagByLastKnownPath(file);
55
- if (dirLinkedTag && retry) {
56
- await setFileTag(file, dirLinkedTag);
57
- return await checkFileTag(false);
58
- }
59
- return null;
60
- }
61
- return tag;
62
- }
63
-
64
- const tag = await checkFileTag();
65
- if (!tag) return null;
66
-
67
- const objectId = tag;
68
-
69
- // OPTIMIZATION: Only read/hash/compress if file changed (mtime check)
70
- const existingCache = cache[objectId];
71
- if (existingCache &&
72
- existingCache.mtimeMs === stats.mtimeMs &&
73
- existingCache.size === stats.size &&
74
- existingCache.lastKnownPath === file) {
75
- // File hasn't changed, skip expensive operations
76
- return null;
77
- }
78
-
79
- // File is new or changed - do the expensive operations
80
- const content = fs.readFileSync(file, 'utf8');
81
-
82
- return {
83
- objectId,
84
- data: {
85
- tag,
86
- lastKnownPath: file,
87
- contentHash: sha256(content),
88
- compressedContent: compressString(content),
89
- size: stats.size,
90
- mtimeMs: stats.mtimeMs,
91
- dev: stats.dev,
92
- ino: stats.ino
93
- }
94
- };
95
- } catch (err) {
96
- return null;
97
- }
98
- })
99
- );
100
-
101
- // Collect updates
102
- updates.push(...batchResults.filter(r => r !== null));
103
- }
104
-
105
- // Apply all updates to cache
106
- if (updates.length > 0) {
107
- for (const { objectId, data } of updates) {
108
- cache[objectId] = data;
109
- }
110
- config.save('cachedFiles', cache, { global: false, filename: 'fileCache.json' });
111
- }
112
- };
113
-
114
- /**
115
- * Recaches file ID index by walking files and updating tags
116
- * OPTIMIZED: Parallel processing with batching for speed
117
- * @param {string} dir - Directory to recache
118
- * @returns {Promise<void>}
119
- */
120
- export const recacheFileIdIndex = async (dir) => {
121
- const absDir = path.resolve(dir);
122
-
123
- // Only walk whitelisted code entity directories (exclude Assets and iris-apps, handled separately)
124
- const codeEntityDirs = ALLOWED_SRC_DIRS.filter(d => d !== 'Assets' && d !== IRIS_APPS_DIR);
125
- const fileArrays = await Promise.all(
126
- codeEntityDirs.map(d => {
127
- const dirPath = path.join(absDir, d);
128
- return fs.existsSync(dirPath) ? walkFiles(dirPath) : Promise.resolve([]);
129
- })
130
- );
131
- const files = fileArrays.flat();
132
- if (!files || files?.length < 1) return;
133
-
134
- // Process files in parallel batches of 50 for speed
135
- const BATCH_SIZE = 50;
136
- const updates = [];
137
-
138
- for (let i = 0; i < files.length; i += BATCH_SIZE) {
139
- const batch = files.slice(i, i + BATCH_SIZE);
140
-
141
- // Process batch in parallel
142
- const batchResults = await Promise.all(
143
- batch.map(async (file) => {
144
- try {
145
- const tag = await getFileTag(file);
146
- if (!tag) return null;
147
-
148
- // Check if path changed
149
- const lastKnownPath = findFileByTag(tag);
150
- if (lastKnownPath !== file) {
151
- return { file, tag };
152
- }
153
- return null;
154
- } catch (err) {
155
- return null;
156
- }
157
- })
158
- );
159
-
160
- // Collect updates
161
- updates.push(...batchResults.filter(r => r !== null));
162
- }
163
-
164
- // Apply all updates at once
165
- for (const { file, tag } of updates) {
166
- await setFileTag(file, tag);
167
- }
168
- }
169
-
170
- /**
171
- * Recursively walks a directory and returns all file paths
172
- * @param {string} dir
173
- * @returns {Promise<string[]>}
174
- */
175
- export async function walkFiles(dir, settings) {
176
- const ignore = settings?.ignore || [];
177
-
178
- if (!fs.existsSync(dir)) return [];
179
- let entries = fs.readdirSync(dir, { withFileTypes: true });
180
- let paths = [];
181
-
182
- for (const entry of entries) {
183
- const fullPath = path.join(dir, entry.name);
184
-
185
- // Skip OS-generated system files (.DS_Store, Thumbs.db, etc.)
186
- if (IGNORED_FILES.has(entry.name)) {
187
- continue;
188
- }
189
-
190
- // Check if this path should be ignored
191
- if (ignore.find(p => fullPath.startsWith(p) || fullPath === p)) {
192
- continue;
193
- }
194
-
195
- if (entry.isDirectory()) {
196
- const subPaths = await walkFiles(fullPath, settings);
197
- if (subPaths && subPaths.length > 0) {
198
- paths.push(...subPaths);
199
- }
200
- } else if (entry.isFile()) {
201
- paths.push(fullPath);
202
- }
203
- }
204
-
205
- return paths;
206
- }
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import Config from './config.js';
4
+ import { findFileByTag, getFileTag, isPathLinkedToTagByLastKnownPath, setFileTag } from './filetag.js';
5
+ import { compressString } from './compress.js';
6
+ import { sha256 } from './hash.js';
7
+ import { EXPORT_ROOT, ALLOWED_SRC_DIRS, IRIS_APPS_DIR } from '../vars/global.js';
8
+
9
+ const config = new Config();
10
+
11
+ // System/hidden files that should never be synced
12
+ const IGNORED_FILES = new Set(['.DS_Store', 'Thumbs.db', 'desktop.ini']);
13
+
14
+ /**
15
+ * Recursively caches all files in a directory with tagging and content snapshotting
16
+ * OPTIMIZED: Parallel processing + mtime checks for maximum speed
17
+ * @param {string} dir - Directory to cache
18
+ * @returns {Promise<void>}
19
+ */
20
+ export const cacheDir = async (dir) => {
21
+ if (!fs.existsSync(dir)) return;
22
+
23
+ const absDir = path.resolve(dir);
24
+
25
+ // Only walk whitelisted code entity directories (exclude Assets and iris-apps, handled separately)
26
+ const codeEntityDirs = ALLOWED_SRC_DIRS.filter(d => d !== 'Assets' && d !== IRIS_APPS_DIR);
27
+ const fileArrays = await Promise.all(
28
+ codeEntityDirs.map(d => {
29
+ const dirPath = path.join(absDir, d);
30
+ return fs.existsSync(dirPath) ? walkFiles(dirPath) : Promise.resolve([]);
31
+ })
32
+ );
33
+ const files = fileArrays.flat();
34
+
35
+ const cache = config.read('cachedFiles', { global: false, filename: 'fileCache.json' }) || {};
36
+
37
+ // Process files in parallel batches
38
+ const BATCH_SIZE = 50;
39
+ const updates = [];
40
+
41
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
42
+ const batch = files.slice(i, i + BATCH_SIZE);
43
+
44
+ // Process batch in parallel
45
+ const batchResults = await Promise.all(
46
+ batch.map(async (file) => {
47
+ try {
48
+ const stats = fs.statSync(file);
49
+ if (!stats.isFile()) return null;
50
+
51
+ const checkFileTag = async (retry = true) => {
52
+ const tag = await getFileTag(file);
53
+ if (!tag) {
54
+ const dirLinkedTag = isPathLinkedToTagByLastKnownPath(file);
55
+ if (dirLinkedTag && retry) {
56
+ await setFileTag(file, dirLinkedTag);
57
+ return await checkFileTag(false);
58
+ }
59
+ return null;
60
+ }
61
+ return tag;
62
+ }
63
+
64
+ const tag = await checkFileTag();
65
+ if (!tag) return null;
66
+
67
+ const objectId = tag;
68
+
69
+ // OPTIMIZATION: Only read/hash/compress if file changed (mtime check)
70
+ const existingCache = cache[objectId];
71
+ if (existingCache &&
72
+ existingCache.mtimeMs === stats.mtimeMs &&
73
+ existingCache.size === stats.size &&
74
+ existingCache.lastKnownPath === file) {
75
+ // File hasn't changed, skip expensive operations
76
+ return null;
77
+ }
78
+
79
+ // File is new or changed - do the expensive operations
80
+ const content = fs.readFileSync(file, 'utf8');
81
+
82
+ return {
83
+ objectId,
84
+ data: {
85
+ tag,
86
+ lastKnownPath: file,
87
+ contentHash: sha256(content),
88
+ compressedContent: compressString(content),
89
+ size: stats.size,
90
+ mtimeMs: stats.mtimeMs,
91
+ dev: stats.dev,
92
+ ino: stats.ino
93
+ }
94
+ };
95
+ } catch (err) {
96
+ return null;
97
+ }
98
+ })
99
+ );
100
+
101
+ // Collect updates
102
+ updates.push(...batchResults.filter(r => r !== null));
103
+ }
104
+
105
+ // Apply all updates to cache
106
+ if (updates.length > 0) {
107
+ for (const { objectId, data } of updates) {
108
+ cache[objectId] = data;
109
+ }
110
+ config.save('cachedFiles', cache, { global: false, filename: 'fileCache.json' });
111
+ }
112
+ };
113
+
114
+ /**
115
+ * Recaches file ID index by walking files and updating tags
116
+ * OPTIMIZED: Parallel processing with batching for speed
117
+ * @param {string} dir - Directory to recache
118
+ * @returns {Promise<void>}
119
+ */
120
+ export const recacheFileIdIndex = async (dir) => {
121
+ const absDir = path.resolve(dir);
122
+
123
+ // Only walk whitelisted code entity directories (exclude Assets and iris-apps, handled separately)
124
+ const codeEntityDirs = ALLOWED_SRC_DIRS.filter(d => d !== 'Assets' && d !== IRIS_APPS_DIR);
125
+ const fileArrays = await Promise.all(
126
+ codeEntityDirs.map(d => {
127
+ const dirPath = path.join(absDir, d);
128
+ return fs.existsSync(dirPath) ? walkFiles(dirPath) : Promise.resolve([]);
129
+ })
130
+ );
131
+ const files = fileArrays.flat();
132
+ if (!files || files?.length < 1) return;
133
+
134
+ // Process files in parallel batches of 50 for speed
135
+ const BATCH_SIZE = 50;
136
+ const updates = [];
137
+
138
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
139
+ const batch = files.slice(i, i + BATCH_SIZE);
140
+
141
+ // Process batch in parallel
142
+ const batchResults = await Promise.all(
143
+ batch.map(async (file) => {
144
+ try {
145
+ const tag = await getFileTag(file);
146
+ if (!tag) return null;
147
+
148
+ // Check if path changed
149
+ const lastKnownPath = findFileByTag(tag);
150
+ if (lastKnownPath !== file) {
151
+ return { file, tag };
152
+ }
153
+ return null;
154
+ } catch (err) {
155
+ return null;
156
+ }
157
+ })
158
+ );
159
+
160
+ // Collect updates
161
+ updates.push(...batchResults.filter(r => r !== null));
162
+ }
163
+
164
+ // Apply all updates at once
165
+ for (const { file, tag } of updates) {
166
+ await setFileTag(file, tag);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Recursively walks a directory and returns all file paths
172
+ * @param {string} dir
173
+ * @returns {Promise<string[]>}
174
+ */
175
+ export async function walkFiles(dir, settings) {
176
+ const ignore = settings?.ignore || [];
177
+
178
+ if (!fs.existsSync(dir)) return [];
179
+ let entries = fs.readdirSync(dir, { withFileTypes: true });
180
+ let paths = [];
181
+
182
+ for (const entry of entries) {
183
+ const fullPath = path.join(dir, entry.name);
184
+
185
+ // Skip OS-generated system files (.DS_Store, Thumbs.db, etc.)
186
+ if (IGNORED_FILES.has(entry.name)) {
187
+ continue;
188
+ }
189
+
190
+ // Check if this path should be ignored
191
+ if (ignore.find(p => fullPath.startsWith(p) || fullPath === p)) {
192
+ continue;
193
+ }
194
+
195
+ if (entry.isDirectory()) {
196
+ const subPaths = await walkFiles(fullPath, settings);
197
+ if (subPaths && subPaths.length > 0) {
198
+ paths.push(...subPaths);
199
+ }
200
+ } else if (entry.isFile()) {
201
+ paths.push(fullPath);
202
+ }
203
+ }
204
+
205
+ return paths;
206
+ }
@@ -1,74 +1,76 @@
1
- import { lookup } from 'node:dns/promises';
2
- import debug from '../debug.js';
3
-
4
- /**
5
- * Checks if the provided Magentrix instance URL is reachable by making a GET request.
6
- * Throws an error if the response status is not 200 (OK), or if there is a network error.
7
- *
8
- * @async
9
- * @param {string} instanceUrl - The https://subdomain.magentrixcloud.com URL to check.
10
- * @throws {Error} Throws if the instance is unreachable or does not return 200 OK.
11
- * @returns {Promise<void>} Resolves if the instance is reachable (status 200); otherwise throws.
12
- */
13
- export const checkInstanceUrl = async (instanceUrl) => {
14
- debug.log('URL-CHECK', `Checking reachability: GET ${instanceUrl}`);
15
-
16
- // Resolve DNS first to help diagnose hosts file / local server issues
17
- const hostname = new URL(instanceUrl).hostname;
18
- try {
19
- const { address, family } = await lookup(hostname);
20
- debug.log('URL-CHECK', `DNS resolved: ${hostname} -> ${address} (IPv${family})`);
21
- } catch (dnsErr) {
22
- debug.log('URL-CHECK', `DNS lookup failed for ${hostname}: ${dnsErr.code || dnsErr.message}`);
23
- }
24
-
25
- const controller = new AbortController();
26
- const timeout = setTimeout(() => {
27
- debug.log('URL-CHECK', `Timeout after 15s — aborting request to ${instanceUrl}. If this is a local server (hosts file), check: (1) IIS has an HTTPS binding on port 443 for this hostname, (2) the SSL certificate is valid/trusted for "${hostname}", (3) Node.js trusts the certificate (self-signed certs require NODE_EXTRA_CA_CERTS or NODE_TLS_REJECT_UNAUTHORIZED=0)`);
28
- controller.abort();
29
- }, 15000);
30
- try {
31
- const response = await fetch(instanceUrl, {
32
- method: "GET",
33
- signal: controller.signal,
34
- });
35
-
36
- debug.log('URL-CHECK', `Response: ${response.status} ${response.statusText}`);
37
-
38
- if (response.status !== 200) {
39
- throw new Error(
40
- `Instance URL responded with status ${response.status} (${response.statusText}). Expected 200 OK.`
41
- );
42
- }
43
- } catch (err) {
44
- // Node's fetch wraps the real error in err.cause (e.g. ENOTFOUND, ECONNREFUSED, ETIMEDOUT)
45
- const cause = err.cause || {};
46
- const rootCode = cause.code || '';
47
- const rootMsg = cause.message || '';
48
- const isTimeout = err.name === 'AbortError' || rootCode === 'ABORT_ERR';
49
- const isTLS = rootCode === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' || rootCode === 'CERT_HAS_EXPIRED'
50
- || rootCode === 'DEPTH_ZERO_SELF_SIGNED_CERT' || rootCode === 'ERR_TLS_CERT_ALTNAME_INVALID'
51
- || rootCode === 'SELF_SIGNED_CERT_IN_CHAIN' || (rootMsg && rootMsg.includes('certificate'));
52
-
53
- let detail;
54
- if (isTimeout) {
55
- detail = `Connection timed out after 15s. The server at "${hostname}" did not respond. Check the debug log for DNS resolution and troubleshooting hints.`;
56
- } else if (isTLS) {
57
- detail = `SSL/TLS error (${rootCode}). If using a local server with a self-signed certificate, set NODE_TLS_REJECT_UNAUTHORIZED=0 or use NODE_EXTRA_CA_CERTS. Detail: ${rootMsg}`;
58
- } else {
59
- detail = rootCode ? `${rootCode}: ${rootMsg}` : (err.message || String(err));
60
- }
61
-
62
- debug.log('URL-CHECK', `Failed: ${detail}`);
63
- if (cause.code) debug.log('URL-CHECK', `Error code: ${cause.code}`);
64
- if (cause.hostname) debug.log('URL-CHECK', `Hostname: ${cause.hostname}`);
65
- if (err.stack) debug.log('URL-CHECK', `Stack: ${err.stack}`);
66
-
67
- // Wrap and re-throw to provide a clear error message.
68
- throw new Error(
69
- `Failed to reach instance URL "${instanceUrl}": ${detail}`
70
- );
71
- } finally {
72
- clearTimeout(timeout);
73
- }
74
- };
1
+ import https from 'node:https';
2
+ import { lookup } from 'node:dns/promises';
3
+ import debug from '../debug.js';
4
+
5
+ /**
6
+ * Checks if the provided Magentrix instance URL is reachable by making a GET request.
7
+ * Uses Node's https module instead of fetch/undici, which hangs on loopback addresses.
8
+ * Accepts any 2xx or 3xx status as "reachable".
9
+ *
10
+ * @async
11
+ * @param {string} instanceUrl - The https://subdomain.magentrixcloud.com URL to check.
12
+ * @throws {Error} Throws if the instance is unreachable or returns a 4xx/5xx status.
13
+ * @returns {Promise<void>} Resolves if the instance is reachable; otherwise throws.
14
+ */
15
+ export const checkInstanceUrl = async (instanceUrl) => {
16
+ debug.log('URL-CHECK', `Checking reachability: GET ${instanceUrl}`);
17
+
18
+ // Resolve DNS first to help diagnose hosts file / local server issues
19
+ const hostname = new URL(instanceUrl).hostname;
20
+ try {
21
+ const { address, family } = await lookup(hostname);
22
+ debug.log('URL-CHECK', `DNS resolved: ${hostname} -> ${address} (IPv${family})`);
23
+ } catch (dnsErr) {
24
+ debug.log('URL-CHECK', `DNS lookup failed for ${hostname}: ${dnsErr.code || dnsErr.message}`);
25
+ }
26
+
27
+ await new Promise((resolve, reject) => {
28
+ const timer = setTimeout(() => {
29
+ req.destroy();
30
+ debug.log('URL-CHECK', `Timeout after 15s — aborting request to ${instanceUrl}`);
31
+ reject(new Error(
32
+ `Connection timed out after 15s. The server at "${hostname}" did not respond.`
33
+ ));
34
+ }, 15000);
35
+
36
+ const req = https.get(instanceUrl, { rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0' }, (res) => {
37
+ clearTimeout(timer);
38
+ // Consume body so socket is freed
39
+ res.resume();
40
+
41
+ debug.log('URL-CHECK', `Response: ${res.statusCode} ${res.statusMessage}`);
42
+
43
+ if (res.statusCode >= 400) {
44
+ reject(new Error(
45
+ `Instance URL responded with status ${res.statusCode} (${res.statusMessage}).`
46
+ ));
47
+ } else {
48
+ resolve();
49
+ }
50
+ });
51
+
52
+ req.on('error', (err) => {
53
+ clearTimeout(timer);
54
+ const code = err.code || '';
55
+ const msg = err.message || String(err);
56
+ const isTLS = code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' || code === 'CERT_HAS_EXPIRED'
57
+ || code === 'DEPTH_ZERO_SELF_SIGNED_CERT' || code === 'ERR_TLS_CERT_ALTNAME_INVALID'
58
+ || code === 'SELF_SIGNED_CERT_IN_CHAIN' || msg.includes('certificate');
59
+
60
+ let detail;
61
+ if (isTLS) {
62
+ detail = `SSL/TLS error (${code}). If using a local server with a self-signed certificate, set NODE_TLS_REJECT_UNAUTHORIZED=0 or use NODE_EXTRA_CA_CERTS. Detail: ${msg}`;
63
+ } else {
64
+ detail = code ? `${code}: ${msg}` : msg;
65
+ }
66
+
67
+ debug.log('URL-CHECK', `Failed: ${detail}`);
68
+ if (err.code) debug.log('URL-CHECK', `Error code: ${err.code}`);
69
+ if (err.stack) debug.log('URL-CHECK', `Stack: ${err.stack}`);
70
+
71
+ reject(new Error(
72
+ `Failed to reach instance URL "${instanceUrl}": ${detail}`
73
+ ));
74
+ });
75
+ });
76
+ };