@involvex/msix-packager-cli 1.4.1
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/README.md +360 -0
- package/package.json +57 -0
- package/src/certificates.js +320 -0
- package/src/cli.js +383 -0
- package/src/constants.js +140 -0
- package/src/index.js +414 -0
- package/src/manifest.js +389 -0
- package/src/package.js +909 -0
- package/src/sea-handler-new.js +301 -0
- package/src/sea-handler.js +1124 -0
- package/src/utils.js +292 -0
- package/src/validation.js +228 -0
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs-extra");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const { executeCommand } = require("./utils");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Single Executable Application (SEA) using Node.js built-in capabilities with NCC bundling
|
|
8
|
+
* @param {string} packageDir - Directory cont // If postject fails due to multiple sentinels and we haven't tried overwrite yet
|
|
9
|
+
if (postjectError.message.includes('Multiple occurences of sentinel')) {
|
|
10
|
+
console.log('🔧 Detected multiple sentinels, attempting overwrite recovery...');
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const overwriteCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --overwrite`;
|
|
14
|
+
executeCommand(overwriteCommand);
|
|
15
|
+
console.log('✅ SEA blob injection successful with overwrite');
|
|
16
|
+
} catch (overwriteError) {
|
|
17
|
+
console.error(`❌ SEA injection failed even with overwrite: ${overwriteError.message}`);
|
|
18
|
+
|
|
19
|
+
// If still failing, try a completely different approach - create a truly fresh binary
|
|
20
|
+
console.log('🔧 Attempting to create completely fresh binary...');
|
|
21
|
+
const superFreshPath = await createSuperFreshBinary();
|
|
22
|
+
if (superFreshPath && await fs.pathExists(superFreshPath)) {
|
|
23
|
+
console.log('📋 Copying super fresh binary...');
|
|
24
|
+
await fs.copyFile(superFreshPath, executablePath);
|
|
25
|
+
|
|
26
|
+
// Try injection one more time
|
|
27
|
+
try {
|
|
28
|
+
const finalCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`;
|
|
29
|
+
executeCommand(finalCommand);
|
|
30
|
+
console.log('✅ SEA blob injection successful with super fresh binary');
|
|
31
|
+
} catch (finalError) {
|
|
32
|
+
throw new Error(`All SEA injection attempts failed: ${finalError.message}`);
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error(`Could not create super fresh binary: ${overwriteError.message}`);
|
|
36
|
+
}
|
|
37
|
+
}ckage
|
|
38
|
+
* @param {Object} config - Configuration object
|
|
39
|
+
* @param {Object} packageJson - package.json content
|
|
40
|
+
* @returns {Promise<boolean>} Success status
|
|
41
|
+
*/
|
|
42
|
+
async function createSingleExecutableApp(packageDir, config, packageJson) {
|
|
43
|
+
try {
|
|
44
|
+
console.log(
|
|
45
|
+
"🔨 Creating Single Executable Application (SEA) with NCC bundling...",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const appName = config.appName || "app";
|
|
49
|
+
const executableName = `${appName.replace(/[^a-zA-Z0-9.-]/g, "_")}.exe`;
|
|
50
|
+
const executablePath = path.join(packageDir, executableName);
|
|
51
|
+
const tempNodeBinary = path.join(packageDir, "temp-node.exe");
|
|
52
|
+
|
|
53
|
+
// Step 1: Install NCC and TypeScript dependencies if needed
|
|
54
|
+
console.log("📦 Installing @vercel/ncc...");
|
|
55
|
+
try {
|
|
56
|
+
executeCommand("bun install @vercel/ncc --no-save", { cwd: packageDir });
|
|
57
|
+
} catch (nccError) {
|
|
58
|
+
console.log("Installing NCC globally...");
|
|
59
|
+
executeCommand("bun install -g @vercel/ncc", { cwd: packageDir });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Install TypeScript if TypeScript files are detected
|
|
63
|
+
const hasTypeScript =
|
|
64
|
+
(await fs.pathExists(path.join(packageDir, "tsconfig.json"))) ||
|
|
65
|
+
(await fs.readdir(packageDir)).some((file) => file.endsWith(".ts"));
|
|
66
|
+
|
|
67
|
+
if (hasTypeScript) {
|
|
68
|
+
console.log(
|
|
69
|
+
"📝 TypeScript detected - ensuring TypeScript compiler is available",
|
|
70
|
+
);
|
|
71
|
+
try {
|
|
72
|
+
executeCommand("bun install typescript --no-save", { cwd: packageDir });
|
|
73
|
+
} catch (tsError) {
|
|
74
|
+
console.log("Installing TypeScript globally...");
|
|
75
|
+
executeCommand("bun install -g typescript", { cwd: packageDir });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 2: Detect entry point and handle TypeScript
|
|
80
|
+
const mainScript = packageJson.main || "index.js";
|
|
81
|
+
const isTypeScript =
|
|
82
|
+
mainScript.endsWith(".ts") ||
|
|
83
|
+
(await fs.pathExists(path.join(packageDir, "tsconfig.json")));
|
|
84
|
+
|
|
85
|
+
// Auto-detect TypeScript entry point if main is JS but TS files exist
|
|
86
|
+
let entryScript = mainScript;
|
|
87
|
+
if (!isTypeScript && !mainScript.endsWith(".ts")) {
|
|
88
|
+
const tsAlternatives = [
|
|
89
|
+
mainScript.replace(/\.js$/, ".ts"),
|
|
90
|
+
"index.ts",
|
|
91
|
+
"src/index.ts",
|
|
92
|
+
"app.ts",
|
|
93
|
+
"src/app.ts",
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
for (const tsFile of tsAlternatives) {
|
|
97
|
+
if (await fs.pathExists(path.join(packageDir, tsFile))) {
|
|
98
|
+
entryScript = tsFile;
|
|
99
|
+
console.log(`📝 Detected TypeScript entry: ${tsFile}`);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const entryContent = `#!/usr/bin/env node
|
|
106
|
+
// SEA Entry Point for ${config.displayName || config.appName}
|
|
107
|
+
|
|
108
|
+
console.log('🚀 Starting ${config.displayName || config.appName}...');
|
|
109
|
+
|
|
110
|
+
// Check if running as SEA
|
|
111
|
+
let isSEA = false;
|
|
112
|
+
try {
|
|
113
|
+
const { sea } = require('node:sea');
|
|
114
|
+
isSEA = sea.isSea();
|
|
115
|
+
if (isSEA) {
|
|
116
|
+
console.log('Running in SEA mode');
|
|
117
|
+
// Set process title
|
|
118
|
+
process.title = '${config.displayName || config.appName}';
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
// SEA module not available, running in regular Node.js
|
|
122
|
+
console.log('Running in development mode');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Set working directory to executable directory
|
|
126
|
+
try {
|
|
127
|
+
const path = require('path');
|
|
128
|
+
const execDir = path.dirname(process.execPath);
|
|
129
|
+
process.chdir(execDir);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn('Could not change working directory:', err.message);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Load the main application
|
|
135
|
+
try {
|
|
136
|
+
require('./${entryScript}');
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('❌ Failed to start application:', error.message);
|
|
139
|
+
console.error(error.stack);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const entryPath = path.join(packageDir, "sea-entry.js");
|
|
145
|
+
await fs.writeFile(entryPath, entryContent);
|
|
146
|
+
|
|
147
|
+
// Step 3: Bundle with NCC (with TypeScript support)
|
|
148
|
+
console.log("📦 Bundling application with NCC...");
|
|
149
|
+
|
|
150
|
+
// NCC automatically handles TypeScript files - no additional config needed!
|
|
151
|
+
let bundleCommand = `npx ncc build sea-entry.js -o sea-dist --minify --no-source-map-register`;
|
|
152
|
+
|
|
153
|
+
// Add TypeScript-specific options if needed
|
|
154
|
+
if (isTypeScript || entryScript.endsWith(".ts")) {
|
|
155
|
+
console.log(
|
|
156
|
+
"📝 TypeScript detected - NCC will handle transpilation automatically",
|
|
157
|
+
);
|
|
158
|
+
// NCC will automatically detect and use tsconfig.json if present
|
|
159
|
+
bundleCommand += ` --target es2020`; // Ensure compatibility with Node.js SEA
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If there's a tsconfig.json, NCC will use it automatically
|
|
163
|
+
const tsconfigPath = path.join(packageDir, "tsconfig.json");
|
|
164
|
+
if (await fs.pathExists(tsconfigPath)) {
|
|
165
|
+
console.log("📄 Using existing tsconfig.json for TypeScript compilation");
|
|
166
|
+
} else if (isTypeScript || entryScript.endsWith(".ts")) {
|
|
167
|
+
// Create a basic tsconfig.json for SEA compatibility
|
|
168
|
+
console.log("📄 Creating basic tsconfig.json for SEA compatibility");
|
|
169
|
+
const basicTsConfig = {
|
|
170
|
+
compilerOptions: {
|
|
171
|
+
target: "ES2020",
|
|
172
|
+
module: "CommonJS",
|
|
173
|
+
lib: ["ES2020"],
|
|
174
|
+
outDir: "./dist",
|
|
175
|
+
rootDir: "./src",
|
|
176
|
+
strict: true,
|
|
177
|
+
esModuleInterop: true,
|
|
178
|
+
skipLibCheck: true,
|
|
179
|
+
forceConsistentCasingInFileNames: true,
|
|
180
|
+
moduleResolution: "node",
|
|
181
|
+
resolveJsonModule: true,
|
|
182
|
+
declaration: false,
|
|
183
|
+
declarationMap: false,
|
|
184
|
+
sourceMap: false,
|
|
185
|
+
},
|
|
186
|
+
include: ["**/*.ts"],
|
|
187
|
+
exclude: [
|
|
188
|
+
"node_modules",
|
|
189
|
+
"dist",
|
|
190
|
+
"sea-dist",
|
|
191
|
+
"**/*.test.ts",
|
|
192
|
+
"**/*.spec.ts",
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
await fs.writeJson(tsconfigPath, basicTsConfig, { spaces: 2 });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
executeCommand(bundleCommand, { cwd: packageDir });
|
|
199
|
+
|
|
200
|
+
const bundledPath = path.join(packageDir, "sea-dist", "index.js");
|
|
201
|
+
if (!(await fs.pathExists(bundledPath))) {
|
|
202
|
+
throw new Error("NCC bundling failed - bundled file not found");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Step 4: Create SEA configuration
|
|
206
|
+
const seaConfig = {
|
|
207
|
+
main: "sea-dist/index.js",
|
|
208
|
+
output: "sea-prep.blob",
|
|
209
|
+
disableExperimentalSEAWarning: true,
|
|
210
|
+
useSnapshot: false,
|
|
211
|
+
useCodeCache: true,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const seaConfigPath = path.join(packageDir, "sea-config.json");
|
|
215
|
+
await fs.writeJson(seaConfigPath, seaConfig, { spaces: 2 });
|
|
216
|
+
|
|
217
|
+
// Step 5: Generate SEA blob
|
|
218
|
+
console.log("🗜️ Generating SEA blob...");
|
|
219
|
+
const originalCwd = process.cwd();
|
|
220
|
+
process.chdir(packageDir);
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
executeCommand("node --experimental-sea-config sea-config.json");
|
|
224
|
+
|
|
225
|
+
const blobPath = path.join(packageDir, "sea-prep.blob");
|
|
226
|
+
if (!(await fs.pathExists(blobPath))) {
|
|
227
|
+
throw new Error("SEA blob generation failed");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log("✅ SEA blob created successfully");
|
|
231
|
+
|
|
232
|
+
// Step 6: Get a completely fresh Node.js executable (ensuring no SEA markers)
|
|
233
|
+
console.log("📄 Creating executable base with fresh Node.js binary...");
|
|
234
|
+
|
|
235
|
+
// Always get a fresh Node.js binary from alternative sources
|
|
236
|
+
const freshNodePath = await getFreshNodeBinary();
|
|
237
|
+
console.log(`📥 Using Node.js binary from: ${freshNodePath}`);
|
|
238
|
+
|
|
239
|
+
// Remove any existing executable to ensure clean state
|
|
240
|
+
if (await fs.pathExists(executablePath)) {
|
|
241
|
+
console.log(
|
|
242
|
+
"🗑️ Removing existing executable to ensure clean state...",
|
|
243
|
+
);
|
|
244
|
+
await fs.remove(executablePath);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Remove any temporary files
|
|
248
|
+
if (await fs.pathExists(tempNodeBinary)) {
|
|
249
|
+
await fs.remove(tempNodeBinary);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Copy the fresh Node.js binary to temporary location first
|
|
253
|
+
await fs.copyFile(freshNodePath, tempNodeBinary);
|
|
254
|
+
|
|
255
|
+
// Verify the temporary binary exists and is valid
|
|
256
|
+
if (!(await fs.pathExists(tempNodeBinary))) {
|
|
257
|
+
throw new Error("Failed to create temporary Node.js binary");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Test that it's a valid Node.js binary
|
|
261
|
+
try {
|
|
262
|
+
const testCommand = `"${tempNodeBinary}" --version`;
|
|
263
|
+
const version = executeCommand(testCommand, { silent: true });
|
|
264
|
+
console.log(`✅ Fresh Node.js binary verified: ${version.trim()}`);
|
|
265
|
+
} catch (testError) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Fresh Node.js binary is invalid: ${testError.message}`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Move to final executable name
|
|
272
|
+
await fs.move(tempNodeBinary, executablePath);
|
|
273
|
+
|
|
274
|
+
if (!(await fs.pathExists(executablePath))) {
|
|
275
|
+
throw new Error("Failed to create executable base");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
console.log(`✅ Fresh Node.js binary prepared: ${executableName}`);
|
|
279
|
+
|
|
280
|
+
// Verify this is truly a fresh binary (no SEA markers)
|
|
281
|
+
try {
|
|
282
|
+
const { executeCommand: execCmd } = require("./utils");
|
|
283
|
+
const checkCommand = `npx postject "${executableName}" NODE_SEA_BLOB --info 2>nul || echo "NO_SEA_MARKERS"`;
|
|
284
|
+
const checkResult = execCmd(checkCommand, { silent: true });
|
|
285
|
+
if (checkResult.includes("NO_SEA_MARKERS")) {
|
|
286
|
+
console.log("✅ Confirmed: Binary has no existing SEA markers");
|
|
287
|
+
} else {
|
|
288
|
+
console.warn("⚠️ Warning: Binary may have existing SEA markers");
|
|
289
|
+
}
|
|
290
|
+
} catch (checkError) {
|
|
291
|
+
// This is fine, just means postject check failed
|
|
292
|
+
console.log("✅ Binary check completed");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Step 7: Remove signature (Windows) to prepare for SEA injection
|
|
296
|
+
console.log("🔓 Removing executable signature...");
|
|
297
|
+
try {
|
|
298
|
+
const { findWindowsSDKTools } = require("./utils");
|
|
299
|
+
const tools = findWindowsSDKTools();
|
|
300
|
+
const removeSignCommand = `"${tools.signtool}" remove /s "${executableName}"`;
|
|
301
|
+
executeCommand(removeSignCommand);
|
|
302
|
+
console.log("✅ Signature removed successfully");
|
|
303
|
+
} catch (sigError) {
|
|
304
|
+
console.warn("⚠️ Could not remove signature (this is usually fine)");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Step 8: Inject blob with postject (with enhanced error handling)
|
|
308
|
+
console.log("💉 Injecting SEA blob into executable...");
|
|
309
|
+
|
|
310
|
+
// First, verify that our executable doesn't have multiple sentinels
|
|
311
|
+
let sentinelCount = 0;
|
|
312
|
+
try {
|
|
313
|
+
const binaryData = await fs.readFile(executablePath);
|
|
314
|
+
const sentinelPattern = Buffer.from(
|
|
315
|
+
"NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
|
|
316
|
+
"utf8",
|
|
317
|
+
);
|
|
318
|
+
let searchPos = 0;
|
|
319
|
+
while (true) {
|
|
320
|
+
const index = binaryData.indexOf(sentinelPattern, searchPos);
|
|
321
|
+
if (index === -1) break;
|
|
322
|
+
sentinelCount++;
|
|
323
|
+
searchPos = index + 1;
|
|
324
|
+
}
|
|
325
|
+
console.log(`🔍 Found ${sentinelCount} sentinel markers in executable`);
|
|
326
|
+
} catch (readError) {
|
|
327
|
+
console.warn("⚠️ Could not analyze binary for sentinels");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
let postjectCommand;
|
|
332
|
+
|
|
333
|
+
if (sentinelCount > 1) {
|
|
334
|
+
// Use overwrite flag immediately if multiple sentinels detected
|
|
335
|
+
console.log(
|
|
336
|
+
"⚠️ Multiple sentinels detected, using overwrite mode...",
|
|
337
|
+
);
|
|
338
|
+
postjectCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --overwrite`;
|
|
339
|
+
} else {
|
|
340
|
+
// Use normal injection for clean binaries
|
|
341
|
+
console.log(
|
|
342
|
+
`Start injection of NODE_SEA_BLOB in ${executableName}...`,
|
|
343
|
+
);
|
|
344
|
+
postjectCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
executeCommand(postjectCommand);
|
|
348
|
+
console.log("✅ SEA blob injection successful");
|
|
349
|
+
} catch (postjectError) {
|
|
350
|
+
console.error(`❌ SEA blob injection failed: ${postjectError.message}`);
|
|
351
|
+
|
|
352
|
+
// If postject fails due to multiple sentinels, this means our fresh binary isn't actually fresh
|
|
353
|
+
if (postjectError.message.includes("Multiple occurences of sentinel")) {
|
|
354
|
+
console.error(
|
|
355
|
+
"❌ Fresh binary still contains SEA markers - this should not happen!",
|
|
356
|
+
);
|
|
357
|
+
console.log("� Attempting emergency recovery with overwrite...");
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
// Try with overwrite flag as last resort
|
|
361
|
+
const overwriteCommand = `npx postject "${executableName}" NODE_SEA_BLOB sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --overwrite`;
|
|
362
|
+
executeCommand(overwriteCommand);
|
|
363
|
+
console.log("✅ SEA blob injection successful with overwrite");
|
|
364
|
+
} catch (overwriteError) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`SEA injection failed even with overwrite: ${overwriteError.message}`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
} else {
|
|
370
|
+
// For other postject errors, re-throw
|
|
371
|
+
throw postjectError;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Step 9: Sign the executable
|
|
376
|
+
await signSEAExecutable(executablePath, config);
|
|
377
|
+
|
|
378
|
+
// Step 10: Cleanup temporary files
|
|
379
|
+
await fs.remove(entryPath);
|
|
380
|
+
await fs.remove(path.join(packageDir, "sea-dist"));
|
|
381
|
+
await fs.remove(seaConfigPath);
|
|
382
|
+
await fs.remove(blobPath);
|
|
383
|
+
|
|
384
|
+
// Clean up any temporary Node.js binaries
|
|
385
|
+
if (await fs.pathExists(tempNodeBinary)) {
|
|
386
|
+
await fs.remove(tempNodeBinary);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Remove temporary tsconfig.json if we created it
|
|
390
|
+
const tsconfigPath = path.join(packageDir, "tsconfig.json");
|
|
391
|
+
if (
|
|
392
|
+
!(await fs.pathExists(tsconfigPath.replace(".json", ".original.json")))
|
|
393
|
+
) {
|
|
394
|
+
// Only remove if we didn't backup an existing one
|
|
395
|
+
const tsconfigContent = await fs
|
|
396
|
+
.readJson(tsconfigPath)
|
|
397
|
+
.catch(() => null);
|
|
398
|
+
if (
|
|
399
|
+
tsconfigContent &&
|
|
400
|
+
tsconfigContent.compilerOptions &&
|
|
401
|
+
tsconfigContent.compilerOptions.target === "ES2020"
|
|
402
|
+
) {
|
|
403
|
+
console.log("🧹 Cleaning up temporary tsconfig.json");
|
|
404
|
+
await fs.remove(tsconfigPath);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Update config
|
|
409
|
+
config.executable = executableName;
|
|
410
|
+
delete config.executableArgs;
|
|
411
|
+
|
|
412
|
+
console.log(`✅ SEA executable created: ${executableName}`);
|
|
413
|
+
return true;
|
|
414
|
+
} finally {
|
|
415
|
+
process.chdir(originalCwd);
|
|
416
|
+
}
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.warn(`⚠️ SEA creation failed: ${error.message}`);
|
|
419
|
+
console.log("Falling back to PKG...");
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Gets the Node.js binary path for the current platform
|
|
426
|
+
* @returns {Promise<string>} Path to Node.js binary
|
|
427
|
+
*/
|
|
428
|
+
async function getNodeBinaryPath() {
|
|
429
|
+
return process.execPath;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Gets a fresh Node.js binary path, trying alternative sources if needed
|
|
434
|
+
* @returns {Promise<string>} Path to a fresh Node.js binary
|
|
435
|
+
*/
|
|
436
|
+
async function getFreshNodeBinary() {
|
|
437
|
+
const { executeCommand } = require("./utils");
|
|
438
|
+
|
|
439
|
+
// Always try to get a completely fresh Node.js binary
|
|
440
|
+
const freshSources = [];
|
|
441
|
+
|
|
442
|
+
// 1. Try to find Node.js installation via where command (different from current process)
|
|
443
|
+
try {
|
|
444
|
+
const whereResult = executeCommand("where node", { silent: true });
|
|
445
|
+
const nodePaths = whereResult
|
|
446
|
+
.split("\n")
|
|
447
|
+
.map((p) => p.trim())
|
|
448
|
+
.filter((p) => p && p.includes("node.exe") && p !== process.execPath);
|
|
449
|
+
freshSources.push(...nodePaths);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
// Ignore where command failures
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 2. Try common Node.js installation paths (avoid current process path)
|
|
455
|
+
const commonPaths = [
|
|
456
|
+
"C:\\Program Files\\nodejs\\node.exe",
|
|
457
|
+
"C:\\Program Files (x86)\\nodejs\\node.exe",
|
|
458
|
+
process.env.PROGRAMFILES
|
|
459
|
+
? path.join(process.env.PROGRAMFILES, "nodejs", "node.exe")
|
|
460
|
+
: null,
|
|
461
|
+
process.env["PROGRAMFILES(X86)"]
|
|
462
|
+
? path.join(process.env["PROGRAMFILES(X86)"], "nodejs", "node.exe")
|
|
463
|
+
: null,
|
|
464
|
+
// Try appdata paths
|
|
465
|
+
process.env.APPDATA
|
|
466
|
+
? path.join(process.env.APPDATA, "npm", "node.exe")
|
|
467
|
+
: null,
|
|
468
|
+
process.env.LOCALAPPDATA
|
|
469
|
+
? path.join(process.env.LOCALAPPDATA, "npm", "node.exe")
|
|
470
|
+
: null,
|
|
471
|
+
]
|
|
472
|
+
.filter(Boolean)
|
|
473
|
+
.filter((p) => p !== process.execPath);
|
|
474
|
+
|
|
475
|
+
freshSources.push(...commonPaths);
|
|
476
|
+
|
|
477
|
+
// 3. Check if we can find Node.js through PowerShell Get-Command
|
|
478
|
+
try {
|
|
479
|
+
const psResult = executeCommand(
|
|
480
|
+
'powershell -Command "Get-Command node -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source"',
|
|
481
|
+
{ silent: true },
|
|
482
|
+
);
|
|
483
|
+
const psPath = psResult.trim();
|
|
484
|
+
if (psPath && psPath !== process.execPath && psPath.includes("node.exe")) {
|
|
485
|
+
freshSources.push(psPath);
|
|
486
|
+
}
|
|
487
|
+
} catch (err) {
|
|
488
|
+
// Ignore PowerShell command failures
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Remove duplicates and current process path
|
|
492
|
+
const uniquePaths = [...new Set(freshSources)].filter(
|
|
493
|
+
(p) => p !== process.execPath,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
console.log(
|
|
497
|
+
`🔍 Found ${uniquePaths.length} alternative Node.js binary sources`,
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// Test each path to find a working one WITHOUT existing SEA markers
|
|
501
|
+
for (const nodePath of uniquePaths) {
|
|
502
|
+
try {
|
|
503
|
+
if (await fs.pathExists(nodePath)) {
|
|
504
|
+
// Verify it's a valid Node.js executable by checking version
|
|
505
|
+
const versionResult = executeCommand(`"${nodePath}" --version`, {
|
|
506
|
+
silent: true,
|
|
507
|
+
});
|
|
508
|
+
if (versionResult.includes("v")) {
|
|
509
|
+
// Check if this binary already has SEA markers
|
|
510
|
+
const hasSeaMarkers = await checkForSeaMarkers(nodePath);
|
|
511
|
+
if (!hasSeaMarkers) {
|
|
512
|
+
console.log(
|
|
513
|
+
`✅ Found clean Node.js binary (no SEA markers): ${nodePath}`,
|
|
514
|
+
);
|
|
515
|
+
return nodePath;
|
|
516
|
+
} else {
|
|
517
|
+
console.log(
|
|
518
|
+
`⚠️ Skipping binary with existing SEA markers: ${nodePath}`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
} catch (err) {
|
|
524
|
+
// Skip invalid binaries
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// If no clean alternative found, create a fresh copy by downloading immediately
|
|
530
|
+
console.log(
|
|
531
|
+
"🔧 No clean Node.js binary found locally, downloading fresh copy...",
|
|
532
|
+
);
|
|
533
|
+
const superFreshPath = await createSuperFreshBinary();
|
|
534
|
+
if (superFreshPath) {
|
|
535
|
+
return superFreshPath;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Absolute fallback - at least log the issue
|
|
539
|
+
console.error(
|
|
540
|
+
"❌ Unable to create fresh Node.js binary - using current process (may have SEA markers)",
|
|
541
|
+
);
|
|
542
|
+
throw new Error(
|
|
543
|
+
"Could not obtain a fresh Node.js binary without SEA markers",
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Checks if a Node.js binary already contains SEA markers
|
|
549
|
+
* @param {string} binaryPath - Path to the Node.js binary
|
|
550
|
+
* @returns {Promise<boolean>} True if SEA markers are found
|
|
551
|
+
*/
|
|
552
|
+
async function checkForSeaMarkers(binaryPath) {
|
|
553
|
+
try {
|
|
554
|
+
// Method 1: Use postject to check for existing SEA blob
|
|
555
|
+
const { executeCommand } = require("./utils");
|
|
556
|
+
try {
|
|
557
|
+
const checkCommand = `npx postject "${binaryPath}" NODE_SEA_BLOB --info`;
|
|
558
|
+
const result = executeCommand(checkCommand, { silent: true });
|
|
559
|
+
|
|
560
|
+
// If postject finds info about SEA blob, it means markers exist
|
|
561
|
+
if (
|
|
562
|
+
result.includes("NODE_SEA_BLOB") ||
|
|
563
|
+
result.includes("NODE_SEA_FUSE")
|
|
564
|
+
) {
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
} catch (postjectError) {
|
|
568
|
+
// Postject error usually means no SEA markers, but let's do binary check too
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Method 2: Direct binary content check for SEA markers
|
|
572
|
+
const binaryData = await fs.readFile(binaryPath);
|
|
573
|
+
const seaFusePattern = Buffer.from(
|
|
574
|
+
"NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
|
|
575
|
+
"utf8",
|
|
576
|
+
);
|
|
577
|
+
const seaBlobPattern = Buffer.from("NODE_SEA_BLOB", "utf8");
|
|
578
|
+
|
|
579
|
+
// Check for any occurrence of SEA markers
|
|
580
|
+
if (
|
|
581
|
+
binaryData.indexOf(seaFusePattern) !== -1 ||
|
|
582
|
+
binaryData.indexOf(seaBlobPattern) !== -1
|
|
583
|
+
) {
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return false;
|
|
588
|
+
} catch (error) {
|
|
589
|
+
// If we can't check, assume no SEA markers (safer for injection)
|
|
590
|
+
console.warn(`⚠️ Could not check for SEA markers: ${error.message}`);
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Creates a guaranteed fresh Node.js binary by downloading or extracting from system
|
|
597
|
+
* @returns {Promise<string>} Path to the fresh binary
|
|
598
|
+
*/
|
|
599
|
+
async function createFreshNodeBinary() {
|
|
600
|
+
const os = require("os");
|
|
601
|
+
const crypto = require("crypto");
|
|
602
|
+
|
|
603
|
+
// Create a unique temporary path for our fresh binary
|
|
604
|
+
const tempDir = os.tmpdir();
|
|
605
|
+
const uniqueId = crypto.randomBytes(8).toString("hex");
|
|
606
|
+
const freshBinaryPath = path.join(tempDir, `node-fresh-${uniqueId}.exe`);
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
// Strategy 1: Try to extract Node.js from an MSI installer if available
|
|
610
|
+
const msiPath = await findNodeMsiInstaller();
|
|
611
|
+
if (msiPath) {
|
|
612
|
+
console.log("📦 Extracting fresh Node.js from MSI installer...");
|
|
613
|
+
const extractedPath = await extractNodeFromMsi(msiPath, freshBinaryPath);
|
|
614
|
+
if (extractedPath) {
|
|
615
|
+
return extractedPath;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Strategy 2: Copy from current process but strip SEA markers
|
|
620
|
+
console.log(
|
|
621
|
+
"🔧 Creating fresh copy by stripping SEA markers from current binary...",
|
|
622
|
+
);
|
|
623
|
+
await fs.copyFile(process.execPath, freshBinaryPath);
|
|
624
|
+
|
|
625
|
+
// Remove any existing SEA markers using binary manipulation
|
|
626
|
+
const cleaned = await stripSeaMarkers(freshBinaryPath);
|
|
627
|
+
if (cleaned) {
|
|
628
|
+
console.log("✅ Successfully created clean Node.js binary");
|
|
629
|
+
return freshBinaryPath;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Strategy 3: As absolute last resort, download Node.js
|
|
633
|
+
console.log("⬇️ Downloading fresh Node.js binary...");
|
|
634
|
+
const downloadedPath = await downloadFreshNodeBinary(freshBinaryPath);
|
|
635
|
+
if (downloadedPath) {
|
|
636
|
+
return downloadedPath;
|
|
637
|
+
}
|
|
638
|
+
} catch (error) {
|
|
639
|
+
console.warn(`⚠️ Failed to create fresh binary: ${error.message}`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Absolute fallback - at least log the issue
|
|
643
|
+
console.error(
|
|
644
|
+
"❌ Unable to create fresh Node.js binary - using current process (may have SEA markers)",
|
|
645
|
+
);
|
|
646
|
+
throw new Error(
|
|
647
|
+
"Could not obtain a fresh Node.js binary without SEA markers",
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Strips SEA markers from a Node.js binary using multiple methods
|
|
653
|
+
* @param {string} binaryPath - Path to the binary to clean
|
|
654
|
+
* @returns {Promise<boolean>} True if successful
|
|
655
|
+
*/
|
|
656
|
+
async function stripSeaMarkers(binaryPath) {
|
|
657
|
+
try {
|
|
658
|
+
console.log(`🧹 Attempting to strip SEA markers from: ${binaryPath}`);
|
|
659
|
+
|
|
660
|
+
// Read the binary file
|
|
661
|
+
const binaryData = await fs.readFile(binaryPath);
|
|
662
|
+
const originalSize = binaryData.length;
|
|
663
|
+
|
|
664
|
+
// Define all possible SEA-related patterns to remove
|
|
665
|
+
const patterns = [
|
|
666
|
+
"NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
|
|
667
|
+
"NODE_SEA_BLOB",
|
|
668
|
+
"NODE_SEA_FUSE",
|
|
669
|
+
"sea_blob_size",
|
|
670
|
+
"SEA_BLOB_START",
|
|
671
|
+
"SEA_BLOB_END",
|
|
672
|
+
];
|
|
673
|
+
|
|
674
|
+
let modified = false;
|
|
675
|
+
let totalReplacements = 0;
|
|
676
|
+
|
|
677
|
+
// Replace all patterns with null bytes
|
|
678
|
+
for (const patternStr of patterns) {
|
|
679
|
+
const pattern = Buffer.from(patternStr, "utf8");
|
|
680
|
+
const nullReplacement = Buffer.alloc(pattern.length, 0);
|
|
681
|
+
let searchStart = 0;
|
|
682
|
+
let patternReplacements = 0;
|
|
683
|
+
|
|
684
|
+
while (true) {
|
|
685
|
+
const index = binaryData.indexOf(pattern, searchStart);
|
|
686
|
+
if (index === -1) break;
|
|
687
|
+
|
|
688
|
+
console.log(`🧹 Removing "${patternStr}" marker at offset ${index}`);
|
|
689
|
+
binaryData.set(nullReplacement, index);
|
|
690
|
+
modified = true;
|
|
691
|
+
patternReplacements++;
|
|
692
|
+
totalReplacements++;
|
|
693
|
+
searchStart = index + pattern.length;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (patternReplacements > 0) {
|
|
697
|
+
console.log(
|
|
698
|
+
` ✅ Removed ${patternReplacements} instances of "${patternStr}"`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (modified) {
|
|
704
|
+
// Create backup before modifying
|
|
705
|
+
const backupPath = `${binaryPath}.backup`;
|
|
706
|
+
await fs.copyFile(binaryPath, backupPath);
|
|
707
|
+
console.log(`📋 Created backup: ${backupPath}`);
|
|
708
|
+
|
|
709
|
+
// Write the cleaned binary back
|
|
710
|
+
await fs.writeFile(binaryPath, binaryData);
|
|
711
|
+
console.log(
|
|
712
|
+
`✅ Successfully stripped ${totalReplacements} SEA markers from binary`,
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
// Verify the binary still works
|
|
716
|
+
const { executeCommand } = require("./utils");
|
|
717
|
+
try {
|
|
718
|
+
const versionResult = executeCommand(`"${binaryPath}" --version`, {
|
|
719
|
+
silent: true,
|
|
720
|
+
});
|
|
721
|
+
if (versionResult.includes("v")) {
|
|
722
|
+
console.log(`✅ Cleaned binary verified: ${versionResult.trim()}`);
|
|
723
|
+
|
|
724
|
+
// Clean up backup if verification successful
|
|
725
|
+
await fs.remove(backupPath);
|
|
726
|
+
|
|
727
|
+
return true;
|
|
728
|
+
} else {
|
|
729
|
+
throw new Error("Binary verification failed");
|
|
730
|
+
}
|
|
731
|
+
} catch (verifyError) {
|
|
732
|
+
console.warn(
|
|
733
|
+
`⚠️ Binary verification failed, restoring backup: ${verifyError.message}`,
|
|
734
|
+
);
|
|
735
|
+
await fs.copyFile(backupPath, binaryPath);
|
|
736
|
+
await fs.remove(backupPath);
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
console.log(
|
|
741
|
+
`✅ No SEA markers found in binary (size: ${originalSize} bytes)`,
|
|
742
|
+
);
|
|
743
|
+
return true; // No markers to strip is success
|
|
744
|
+
}
|
|
745
|
+
} catch (error) {
|
|
746
|
+
console.warn(`⚠️ Failed to strip SEA markers: ${error.message}`);
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Finds Node.js MSI installer files on the system
|
|
753
|
+
* @returns {Promise<string|null>} Path to MSI installer or null
|
|
754
|
+
*/
|
|
755
|
+
async function findNodeMsiInstaller() {
|
|
756
|
+
// Common locations where Node.js MSI installers might be found
|
|
757
|
+
const searchPaths = [
|
|
758
|
+
path.join(os.homedir(), "Downloads"),
|
|
759
|
+
path.join(os.homedir(), "Desktop"),
|
|
760
|
+
"C:\\Temp",
|
|
761
|
+
"C:\\Downloads",
|
|
762
|
+
];
|
|
763
|
+
|
|
764
|
+
for (const searchPath of searchPaths) {
|
|
765
|
+
try {
|
|
766
|
+
if (await fs.pathExists(searchPath)) {
|
|
767
|
+
const files = await fs.readdir(searchPath);
|
|
768
|
+
const msiFile = files.find(
|
|
769
|
+
(file) =>
|
|
770
|
+
file.toLowerCase().includes("node") &&
|
|
771
|
+
file.toLowerCase().endsWith(".msi"),
|
|
772
|
+
);
|
|
773
|
+
if (msiFile) {
|
|
774
|
+
return path.join(searchPath, msiFile);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
} catch (error) {
|
|
778
|
+
// Ignore search errors
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Extracts Node.js binary from MSI installer
|
|
787
|
+
* @param {string} msiPath - Path to MSI installer
|
|
788
|
+
* @param {string} outputPath - Where to extract the binary
|
|
789
|
+
* @returns {Promise<string|null>} Path to extracted binary or null
|
|
790
|
+
*/
|
|
791
|
+
async function extractNodeFromMsi(msiPath, outputPath) {
|
|
792
|
+
try {
|
|
793
|
+
const { executeCommand } = require("./utils");
|
|
794
|
+
const tempExtractDir = path.join(os.tmpdir(), `node-extract-${Date.now()}`);
|
|
795
|
+
|
|
796
|
+
// Extract MSI contents
|
|
797
|
+
const extractCommand = `msiexec /a "${msiPath}" /qn TARGETDIR="${tempExtractDir}"`;
|
|
798
|
+
executeCommand(extractCommand, { silent: true });
|
|
799
|
+
|
|
800
|
+
// Find node.exe in extracted contents
|
|
801
|
+
const findNodePath = async (dir) => {
|
|
802
|
+
const items = await fs.readdir(dir);
|
|
803
|
+
for (const item of items) {
|
|
804
|
+
const itemPath = path.join(dir, item);
|
|
805
|
+
const stat = await fs.stat(itemPath);
|
|
806
|
+
if (stat.isDirectory()) {
|
|
807
|
+
const found = await findNodePath(itemPath);
|
|
808
|
+
if (found) return found;
|
|
809
|
+
} else if (item === "node.exe") {
|
|
810
|
+
return itemPath;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return null;
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const extractedNodePath = await findNodePath(tempExtractDir);
|
|
817
|
+
if (extractedNodePath) {
|
|
818
|
+
await fs.copyFile(extractedNodePath, outputPath);
|
|
819
|
+
await fs.remove(tempExtractDir);
|
|
820
|
+
return outputPath;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
await fs.remove(tempExtractDir);
|
|
824
|
+
return null;
|
|
825
|
+
} catch (error) {
|
|
826
|
+
console.warn(`⚠️ MSI extraction failed: ${error.message}`);
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Downloads a fresh Node.js binary from the official website
|
|
833
|
+
* @param {string} outputPath - Where to save the downloaded binary
|
|
834
|
+
* @returns {Promise<string|null>} Path to downloaded binary or null
|
|
835
|
+
*/
|
|
836
|
+
async function downloadFreshNodeBinary(outputPath) {
|
|
837
|
+
try {
|
|
838
|
+
const https = require("https");
|
|
839
|
+
const { executeCommand } = require("./utils");
|
|
840
|
+
|
|
841
|
+
// Get current Node.js version to download the same version
|
|
842
|
+
const currentVersion = process.version; // e.g., v18.17.0
|
|
843
|
+
|
|
844
|
+
// Download URL for Windows x64 Node.js binary
|
|
845
|
+
const downloadUrl = `https://nodejs.org/dist/${currentVersion}/win-x64/node.exe`;
|
|
846
|
+
|
|
847
|
+
console.log(
|
|
848
|
+
`📥 Downloading Node.js ${currentVersion} from official source...`,
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
// Use curl or PowerShell to download
|
|
852
|
+
try {
|
|
853
|
+
const curlCommand = `curl -L -o "${outputPath}" "${downloadUrl}"`;
|
|
854
|
+
executeCommand(curlCommand, { silent: true });
|
|
855
|
+
} catch (curlError) {
|
|
856
|
+
// Fallback to PowerShell
|
|
857
|
+
const psCommand = `powershell -Command "Invoke-WebRequest -Uri '${downloadUrl}' -OutFile '${outputPath}'"`;
|
|
858
|
+
executeCommand(psCommand, { silent: true });
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// Verify the download
|
|
862
|
+
if (await fs.pathExists(outputPath)) {
|
|
863
|
+
const versionResult = executeCommand(`"${outputPath}" --version`, {
|
|
864
|
+
silent: true,
|
|
865
|
+
});
|
|
866
|
+
if (versionResult.includes("v")) {
|
|
867
|
+
console.log(
|
|
868
|
+
`✅ Downloaded fresh Node.js binary: ${versionResult.trim()}`,
|
|
869
|
+
);
|
|
870
|
+
return outputPath;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return null;
|
|
875
|
+
} catch (error) {
|
|
876
|
+
console.warn(`⚠️ Download failed: ${error.message}`);
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Creates a super fresh Node.js binary using direct download
|
|
883
|
+
* @returns {Promise<string|null>} Path to the super fresh binary or null
|
|
884
|
+
*/
|
|
885
|
+
async function createSuperFreshBinary() {
|
|
886
|
+
try {
|
|
887
|
+
const os = require("os");
|
|
888
|
+
const crypto = require("crypto");
|
|
889
|
+
const https = require("https");
|
|
890
|
+
|
|
891
|
+
// Create a unique temporary path for our super fresh binary
|
|
892
|
+
const tempDir = os.tmpdir();
|
|
893
|
+
const uniqueId = crypto.randomBytes(8).toString("hex");
|
|
894
|
+
const superFreshBinaryPath = path.join(
|
|
895
|
+
tempDir,
|
|
896
|
+
`node-super-fresh-${uniqueId}.exe`,
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
// Get current Node.js version to download the same version
|
|
900
|
+
const currentVersion = process.version; // e.g., v18.17.0
|
|
901
|
+
const downloadUrl = `https://nodejs.org/dist/${currentVersion}/win-x64/node.exe`;
|
|
902
|
+
|
|
903
|
+
console.log(
|
|
904
|
+
`📥 Downloading guaranteed fresh Node.js ${currentVersion} binary from official source...`,
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
// Use Node.js built-in https to download (most reliable method)
|
|
908
|
+
await new Promise((resolve, reject) => {
|
|
909
|
+
const file = fs.createWriteStream(superFreshBinaryPath);
|
|
910
|
+
|
|
911
|
+
https
|
|
912
|
+
.get(downloadUrl, (response) => {
|
|
913
|
+
if (response.statusCode === 200) {
|
|
914
|
+
response.pipe(file);
|
|
915
|
+
file.on("finish", () => {
|
|
916
|
+
file.close();
|
|
917
|
+
resolve();
|
|
918
|
+
});
|
|
919
|
+
} else if (
|
|
920
|
+
response.statusCode === 302 ||
|
|
921
|
+
response.statusCode === 301
|
|
922
|
+
) {
|
|
923
|
+
// Handle redirects
|
|
924
|
+
file.close();
|
|
925
|
+
fs.unlink(superFreshBinaryPath, () => {});
|
|
926
|
+
https
|
|
927
|
+
.get(response.headers.location, (redirectResponse) => {
|
|
928
|
+
if (redirectResponse.statusCode === 200) {
|
|
929
|
+
const redirectFile =
|
|
930
|
+
fs.createWriteStream(superFreshBinaryPath);
|
|
931
|
+
redirectResponse.pipe(redirectFile);
|
|
932
|
+
redirectFile.on("finish", () => {
|
|
933
|
+
redirectFile.close();
|
|
934
|
+
resolve();
|
|
935
|
+
});
|
|
936
|
+
} else {
|
|
937
|
+
reject(
|
|
938
|
+
new Error(
|
|
939
|
+
`Failed to download after redirect: HTTP ${redirectResponse.statusCode}`,
|
|
940
|
+
),
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
})
|
|
944
|
+
.on("error", reject);
|
|
945
|
+
} else {
|
|
946
|
+
reject(
|
|
947
|
+
new Error(
|
|
948
|
+
`Failed to download Node.js: HTTP ${response.statusCode}`,
|
|
949
|
+
),
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
})
|
|
953
|
+
.on("error", (err) => {
|
|
954
|
+
fs.unlink(superFreshBinaryPath, () => {});
|
|
955
|
+
reject(err);
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
// Verify the download
|
|
960
|
+
if (await fs.pathExists(superFreshBinaryPath)) {
|
|
961
|
+
const { executeCommand } = require("./utils");
|
|
962
|
+
const versionResult = executeCommand(
|
|
963
|
+
`"${superFreshBinaryPath}" --version`,
|
|
964
|
+
{ silent: true },
|
|
965
|
+
);
|
|
966
|
+
if (versionResult.includes("v")) {
|
|
967
|
+
console.log(
|
|
968
|
+
`✅ Downloaded super fresh Node.js binary: ${versionResult.trim()}`,
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// Verify it has no SEA markers
|
|
972
|
+
const hasSeaMarkers = await checkForSeaMarkers(superFreshBinaryPath);
|
|
973
|
+
if (!hasSeaMarkers) {
|
|
974
|
+
console.log("✅ Confirmed: Super fresh binary has no SEA markers");
|
|
975
|
+
return superFreshBinaryPath;
|
|
976
|
+
} else {
|
|
977
|
+
console.warn(
|
|
978
|
+
"⚠️ Warning: Even downloaded binary has SEA markers (very unusual)",
|
|
979
|
+
);
|
|
980
|
+
return superFreshBinaryPath; // Still return it, might work with overwrite
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
return null;
|
|
986
|
+
} catch (error) {
|
|
987
|
+
console.warn(`⚠️ Super fresh binary download failed: ${error.message}`);
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Signs the SEA executable
|
|
994
|
+
* @param {string} executablePath - Path to executable
|
|
995
|
+
* @param {Object} config - Configuration object
|
|
996
|
+
*/
|
|
997
|
+
async function signSEAExecutable(executablePath, config) {
|
|
998
|
+
try {
|
|
999
|
+
const { determineSigningMethod } = require("./certificates");
|
|
1000
|
+
const { findWindowsSDKTools } = require("./utils");
|
|
1001
|
+
|
|
1002
|
+
const signingMethod = await determineSigningMethod(config);
|
|
1003
|
+
if (!signingMethod) {
|
|
1004
|
+
console.log("No signing configuration found, skipping...");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const tools = findWindowsSDKTools();
|
|
1009
|
+
let signCommand;
|
|
1010
|
+
|
|
1011
|
+
if (signingMethod.method === "pfx") {
|
|
1012
|
+
const { pfxPath, password, timestampUrl } = signingMethod.parameters;
|
|
1013
|
+
signCommand = `"${tools.signtool}" sign /f "${pfxPath}" /p "${password}" /tr "${timestampUrl}" /td SHA256 /fd SHA256 "${executablePath}"`;
|
|
1014
|
+
} else if (signingMethod.method === "store") {
|
|
1015
|
+
const { thumbprint, store, timestampUrl } = signingMethod.parameters;
|
|
1016
|
+
const storeLocation = store.toLowerCase() === "localmachine" ? "/sm" : "";
|
|
1017
|
+
signCommand = `"${tools.signtool}" sign /sha1 "${thumbprint}" /s "My" ${storeLocation} /fd SHA256 "${executablePath}"`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (signCommand) {
|
|
1021
|
+
executeCommand(signCommand);
|
|
1022
|
+
console.log("✅ SEA executable signed successfully");
|
|
1023
|
+
}
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
console.warn(`Warning: Could not sign SEA executable: ${error.message}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Creates a PKG fallback executable
|
|
1031
|
+
* @param {string} packageDir - Package directory path
|
|
1032
|
+
* @param {Object} config - Configuration object
|
|
1033
|
+
* @param {Object} packageJson - package.json content
|
|
1034
|
+
*/
|
|
1035
|
+
async function createFallbackLauncher(packageDir, config, packageJson) {
|
|
1036
|
+
try {
|
|
1037
|
+
console.log("📦 Creating PKG fallback executable...");
|
|
1038
|
+
|
|
1039
|
+
const executableName = `${config.appName || "app"}.exe`;
|
|
1040
|
+
const executablePath = path.join(packageDir, executableName);
|
|
1041
|
+
const mainScript = packageJson.main || "index.js";
|
|
1042
|
+
|
|
1043
|
+
// Create launcher script with proper directory handling
|
|
1044
|
+
const launcherContent = `#!/usr/bin/env node
|
|
1045
|
+
// PKG Launcher for ${config.displayName || config.appName}
|
|
1046
|
+
|
|
1047
|
+
console.log('🚀 Starting ${config.displayName || config.appName}...');
|
|
1048
|
+
|
|
1049
|
+
// Set working directory - handle PKG snapshot case
|
|
1050
|
+
try {
|
|
1051
|
+
if (__dirname.includes('snapshot')) {
|
|
1052
|
+
// Running from PKG, use the executable's directory
|
|
1053
|
+
const path = require('path');
|
|
1054
|
+
const execDir = path.dirname(process.execPath);
|
|
1055
|
+
process.chdir(execDir);
|
|
1056
|
+
} else {
|
|
1057
|
+
// Running in development mode
|
|
1058
|
+
process.chdir(__dirname);
|
|
1059
|
+
}
|
|
1060
|
+
} catch (err) {
|
|
1061
|
+
console.warn('Could not change working directory:', err.message);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Load the main application
|
|
1065
|
+
try {
|
|
1066
|
+
require('./${mainScript}');
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
console.error('❌ Failed to start application:', error.message);
|
|
1069
|
+
console.error(error.stack);
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
1072
|
+
`;
|
|
1073
|
+
|
|
1074
|
+
const launcherScript = path.join(packageDir, "launcher.js");
|
|
1075
|
+
await fs.writeFile(launcherScript, launcherContent);
|
|
1076
|
+
|
|
1077
|
+
// Create PKG configuration
|
|
1078
|
+
const pkgConfig = {
|
|
1079
|
+
name: config.appName || "app",
|
|
1080
|
+
version: packageJson.version || "1.0.0",
|
|
1081
|
+
main: "launcher.js",
|
|
1082
|
+
bin: "launcher.js",
|
|
1083
|
+
pkg: {
|
|
1084
|
+
scripts: [mainScript],
|
|
1085
|
+
targets: ["node18-win-x64"],
|
|
1086
|
+
outputPath: ".",
|
|
1087
|
+
},
|
|
1088
|
+
dependencies: packageJson.dependencies || {},
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
const pkgConfigPath = path.join(packageDir, "pkg-config.json");
|
|
1092
|
+
await fs.writeJson(pkgConfigPath, pkgConfig, { spaces: 2 });
|
|
1093
|
+
|
|
1094
|
+
// Create executable with PKG
|
|
1095
|
+
const finalExeName = path.basename(executablePath, ".exe");
|
|
1096
|
+
const pkgCommand = `npx pkg launcher.js --target node18-win-x64 --output "${finalExeName}" --config pkg-config.json`;
|
|
1097
|
+
|
|
1098
|
+
executeCommand(pkgCommand, { cwd: packageDir });
|
|
1099
|
+
|
|
1100
|
+
// Verify executable was created
|
|
1101
|
+
if (await fs.pathExists(executablePath)) {
|
|
1102
|
+
console.log(`✅ Created executable with PKG: ${executableName}`);
|
|
1103
|
+
|
|
1104
|
+
// Clean up temporary files
|
|
1105
|
+
await fs.remove(launcherScript);
|
|
1106
|
+
await fs.remove(pkgConfigPath);
|
|
1107
|
+
|
|
1108
|
+
// Update config
|
|
1109
|
+
config.executable = executableName;
|
|
1110
|
+
delete config.executableArgs;
|
|
1111
|
+
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
} catch (pkgError) {
|
|
1115
|
+
console.warn(`⚠️ PKG fallback failed: ${pkgError.message}`);
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
module.exports = {
|
|
1121
|
+
createSingleExecutableApp,
|
|
1122
|
+
createFallbackLauncher,
|
|
1123
|
+
getFreshNodeBinary,
|
|
1124
|
+
};
|