seabox 0.1.1 → 0.2.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/.mocharc.json +6 -6
- package/LICENSE.MD +21 -21
- package/README.md +310 -310
- package/bin/seabox-rebuild.mjs +88 -88
- package/bin/seabox.mjs +150 -147
- package/lib/blob.mjs +104 -104
- package/lib/bootstrap.cjs +756 -753
- package/lib/build-cache.mjs +199 -199
- package/lib/build.mjs +77 -77
- package/lib/config.mjs +243 -243
- package/lib/crypto-assets.mjs +125 -125
- package/lib/diagnostics.mjs +203 -203
- package/lib/entry-bundler.mjs +64 -64
- package/lib/fetch-node.mjs +172 -172
- package/lib/index.mjs +26 -26
- package/lib/inject.mjs +106 -106
- package/lib/manifest.mjs +100 -100
- package/lib/multi-target-builder.mjs +697 -705
- package/lib/native-scanner.mjs +203 -203
- package/lib/obfuscate.mjs +51 -51
- package/lib/require-shim.mjs +113 -113
- package/lib/rolldown-bundler.mjs +411 -411
- package/lib/unsign.cjs +197 -169
- package/package.json +61 -61
package/lib/bootstrap.cjs
CHANGED
|
@@ -1,753 +1,756 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* bootstrap.js
|
|
3
|
-
* Runtime template for SEA applications.
|
|
4
|
-
* Handles binary extraction and module resolution override.
|
|
5
|
-
*
|
|
6
|
-
* This file is prepended to the application entry bundle.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
(function () {
|
|
10
|
-
'use strict';
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const crypto = require('crypto');
|
|
15
|
-
const Module = require('module');
|
|
16
|
-
|
|
17
|
-
// PHASE 1: Override fs methods IMMEDIATELY (before app code captures them)
|
|
18
|
-
// We'll populate the redirect maps in Phase 2 after extraction
|
|
19
|
-
const assetPathMapGlobal = {};
|
|
20
|
-
let cacheDirGlobal = '';
|
|
21
|
-
|
|
22
|
-
const originalExistsSync = fs.existsSync;
|
|
23
|
-
const originalReadFileSync = fs.readFileSync;
|
|
24
|
-
const originalStatSync = fs.statSync;
|
|
25
|
-
const originalLstatSync = fs.lstatSync;
|
|
26
|
-
const originalRealpathSync = fs.realpathSync;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Check if a requested path should be redirected to cache.
|
|
30
|
-
* @param {string} requestedPath
|
|
31
|
-
* @returns {string|null} - Redirected path or null
|
|
32
|
-
*/
|
|
33
|
-
function getRedirectedPath(requestedPath) {
|
|
34
|
-
if (!requestedPath) return null;
|
|
35
|
-
|
|
36
|
-
// Normalize the requested path
|
|
37
|
-
const normalized = path.normalize(requestedPath);
|
|
38
|
-
|
|
39
|
-
// Check direct match against asset keys
|
|
40
|
-
for (const [assetKey, extractedPath] of Object.entries(assetPathMapGlobal)) {
|
|
41
|
-
// Try exact match
|
|
42
|
-
if (normalized.endsWith(assetKey)) {
|
|
43
|
-
return extractedPath;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Try basename match
|
|
47
|
-
const assetBasename = path.basename(assetKey);
|
|
48
|
-
if (path.basename(normalized) === assetBasename) {
|
|
49
|
-
return extractedPath;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Try matching the path components
|
|
53
|
-
if (normalized.includes(assetKey.replace(/\//g, path.sep))) {
|
|
54
|
-
return extractedPath;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Override existsSync immediately
|
|
62
|
-
fs.existsSync = function (filePath) {
|
|
63
|
-
if (!filePath) return originalExistsSync(filePath);
|
|
64
|
-
|
|
65
|
-
// If this path should be redirected, check if the REDIRECTED path exists
|
|
66
|
-
const redirected = getRedirectedPath(filePath);
|
|
67
|
-
if (redirected) {
|
|
68
|
-
return originalExistsSync(redirected);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// For .node files, ONLY allow cache paths for our managed binaries
|
|
72
|
-
if (filePath.endsWith('.node') && cacheDirGlobal) {
|
|
73
|
-
const basename = path.basename(filePath);
|
|
74
|
-
|
|
75
|
-
// Check if this is one of our managed binaries
|
|
76
|
-
for (const assetKey of Object.keys(assetPathMapGlobal)) {
|
|
77
|
-
if (path.basename(assetKey) === basename) {
|
|
78
|
-
// This is one of our binaries
|
|
79
|
-
// Only return true if the path is in the cache directory
|
|
80
|
-
if (filePath.startsWith(cacheDirGlobal)) {
|
|
81
|
-
return originalExistsSync(filePath);
|
|
82
|
-
} else {
|
|
83
|
-
// Block all non-cache paths for our binaries
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return originalExistsSync(filePath);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// Override readFileSync immediately
|
|
94
|
-
fs.readFileSync = function (filePath, options) {
|
|
95
|
-
const redirected = getRedirectedPath(filePath);
|
|
96
|
-
if (redirected) {
|
|
97
|
-
return originalReadFileSync(redirected, options);
|
|
98
|
-
}
|
|
99
|
-
return originalReadFileSync(filePath, options);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// Override statSync immediately
|
|
103
|
-
fs.statSync = function (filePath, options) {
|
|
104
|
-
const redirected = getRedirectedPath(filePath);
|
|
105
|
-
if (redirected) {
|
|
106
|
-
return originalStatSync(redirected, options);
|
|
107
|
-
}
|
|
108
|
-
return originalStatSync(filePath, options);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
// Override lstatSync immediately
|
|
112
|
-
fs.lstatSync = function (filePath, options) {
|
|
113
|
-
const redirected = getRedirectedPath(filePath);
|
|
114
|
-
if (redirected) {
|
|
115
|
-
return originalLstatSync(redirected, options);
|
|
116
|
-
}
|
|
117
|
-
return originalLstatSync(filePath, options);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
// Override realpathSync immediately
|
|
121
|
-
fs.realpathSync = function (filePath, options) {
|
|
122
|
-
const redirected = getRedirectedPath(filePath);
|
|
123
|
-
if (redirected) {
|
|
124
|
-
return redirected; // Return the redirected path as the "real" path
|
|
125
|
-
}
|
|
126
|
-
return originalRealpathSync(filePath, options);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// SEA API - check if we're in an SEA by trying to load the sea module
|
|
130
|
-
let sea = null;
|
|
131
|
-
let isSEA = false;
|
|
132
|
-
try {
|
|
133
|
-
sea = require('node:sea');
|
|
134
|
-
isSEA = true;
|
|
135
|
-
} catch (err) {
|
|
136
|
-
// Not in SEA context - this is fine for development
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Decrypt an encrypted asset.
|
|
141
|
-
* @param {Buffer} encryptedData - Encrypted data with IV and auth tag prepended
|
|
142
|
-
* @param {Buffer} key - 32-byte encryption key
|
|
143
|
-
* @returns {Buffer} - Decrypted data
|
|
144
|
-
*/
|
|
145
|
-
function decryptAsset(encryptedData, key) {
|
|
146
|
-
// Extract IV, auth tag, and encrypted content
|
|
147
|
-
const iv = encryptedData.slice(0, 16);
|
|
148
|
-
const authTag = encryptedData.slice(16, 32);
|
|
149
|
-
const encrypted = encryptedData.slice(32);
|
|
150
|
-
|
|
151
|
-
// Create decipher
|
|
152
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
153
|
-
decipher.setAuthTag(authTag);
|
|
154
|
-
|
|
155
|
-
// Decrypt the data
|
|
156
|
-
const decrypted = Buffer.concat([
|
|
157
|
-
decipher.update(encrypted),
|
|
158
|
-
decipher.final()
|
|
159
|
-
]);
|
|
160
|
-
|
|
161
|
-
return decrypted;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Get an asset from the SEA, decrypting if necessary.
|
|
166
|
-
* @param {string} assetKey - Asset key
|
|
167
|
-
* @param {string} [encoding] - Optional encoding (e.g., 'utf8')
|
|
168
|
-
* @returns {Buffer|string} - Asset content
|
|
169
|
-
*/
|
|
170
|
-
function getAsset(assetKey, encoding) {
|
|
171
|
-
if (!sea) {
|
|
172
|
-
throw new Error('Cannot get asset: not in SEA context');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Check if this asset is encrypted
|
|
176
|
-
const isEncrypted = typeof SEA_ENCRYPTED_ASSETS !== 'undefined' && SEA_ENCRYPTED_ASSETS.has(assetKey);
|
|
177
|
-
|
|
178
|
-
let content;
|
|
179
|
-
if (isEncrypted && typeof SEA_ENCRYPTION_KEY !== 'undefined') {
|
|
180
|
-
// Get raw encrypted data
|
|
181
|
-
const encryptedData = sea.getRawAsset(assetKey);
|
|
182
|
-
const buffer = Buffer.isBuffer(encryptedData) ? encryptedData : Buffer.from(encryptedData);
|
|
183
|
-
|
|
184
|
-
// Decrypt it
|
|
185
|
-
content = decryptAsset(buffer, SEA_ENCRYPTION_KEY);
|
|
186
|
-
} else {
|
|
187
|
-
// Get unencrypted asset
|
|
188
|
-
content = sea.getAsset(assetKey, encoding);
|
|
189
|
-
if (!encoding && !Buffer.isBuffer(content)) {
|
|
190
|
-
content = Buffer.from(content);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Apply encoding if requested
|
|
195
|
-
if (encoding && Buffer.isBuffer(content)) {
|
|
196
|
-
return content.toString(encoding);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return content;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Resolve environment variables in a path string.
|
|
205
|
-
* Supports %VAR% on Windows and $VAR or ${VAR} on Unix-like systems.
|
|
206
|
-
* @param {string} pathStr - Path string that may contain environment variables
|
|
207
|
-
* @returns {string} - Path with environment variables expanded
|
|
208
|
-
*/
|
|
209
|
-
function resolveEnvVars(pathStr) {
|
|
210
|
-
if (!pathStr) return pathStr;
|
|
211
|
-
|
|
212
|
-
// Replace Windows-style %VAR%
|
|
213
|
-
let resolved = pathStr.replace(/%([^%]+)%/g, (match, varName) => {
|
|
214
|
-
return process.env[varName] || match;
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Replace Unix-style $VAR and ${VAR}
|
|
218
|
-
resolved = resolved.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
219
|
-
return process.env[varName] || match;
|
|
220
|
-
});
|
|
221
|
-
resolved = resolved.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, varName) => {
|
|
222
|
-
return process.env[varName] || match;
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
return resolved;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Get the extraction cache directory for this application.
|
|
230
|
-
* @param {string} appName
|
|
231
|
-
* @param {string} appVersion
|
|
232
|
-
* @param {string} platform
|
|
233
|
-
* @param {string} arch
|
|
234
|
-
* @param {string} [configuredCacheLocation] - Optional configured cache location from manifest
|
|
235
|
-
* @returns {string}
|
|
236
|
-
*/
|
|
237
|
-
function getExtractionCacheDir(appName, appVersion, platform, arch, configuredCacheLocation) {
|
|
238
|
-
// Priority: SEACACHE env var > configured location > default
|
|
239
|
-
if (process.env.SEACACHE) return process.env.SEACACHE;
|
|
240
|
-
|
|
241
|
-
if (configuredCacheLocation) {
|
|
242
|
-
// Resolve environment variables in the configured path
|
|
243
|
-
const resolvedBase = resolveEnvVars(configuredCacheLocation);
|
|
244
|
-
// Make relative paths absolute (relative to cwd)
|
|
245
|
-
const absoluteBase = path.isAbsolute(resolvedBase) ? resolvedBase : path.resolve(process.cwd(), resolvedBase);
|
|
246
|
-
return path.join(absoluteBase, appName, `${appVersion}-${platform}-${arch}`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Default behavior
|
|
250
|
-
const localAppData = process.env.LOCALAPPDATA || process.env.HOME || process.cwd();
|
|
251
|
-
return path.join(localAppData, '.sea-cache', appName, `${appVersion}-${platform}-${arch}`);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Extract a binary asset to the cache directory.
|
|
256
|
-
* @param {string} assetKey
|
|
257
|
-
* @param {string} targetPath
|
|
258
|
-
* @param {string} expectedHash
|
|
259
|
-
* @returns {boolean} - True if extracted or already valid
|
|
260
|
-
*/
|
|
261
|
-
function extractBinary(assetKey, targetPath, expectedHash) {
|
|
262
|
-
// Check if already extracted and valid
|
|
263
|
-
if (fs.existsSync(targetPath)) {
|
|
264
|
-
const existingHash = hashFile(targetPath);
|
|
265
|
-
if (existingHash === expectedHash) {
|
|
266
|
-
return true; // Already valid
|
|
267
|
-
}
|
|
268
|
-
// Hash mismatch, re-extract
|
|
269
|
-
fs.unlinkSync(targetPath);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Extract from SEA blob (binaries are never encrypted)
|
|
273
|
-
const assetBuffer = sea.getRawAsset(assetKey);
|
|
274
|
-
if (!assetBuffer) {
|
|
275
|
-
throw new Error(`Asset not found in SEA blob: ${assetKey}`);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Convert ArrayBuffer to Buffer if needed
|
|
279
|
-
const buffer = Buffer.isBuffer(assetBuffer)
|
|
280
|
-
? assetBuffer
|
|
281
|
-
: Buffer.from(assetBuffer);
|
|
282
|
-
|
|
283
|
-
// Verify hash before writing
|
|
284
|
-
const actualHash = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
285
|
-
if (actualHash !== expectedHash) {
|
|
286
|
-
throw new Error(`Integrity check failed for ${assetKey}: expected ${expectedHash}, got ${actualHash}`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Write to cache
|
|
290
|
-
const dir = path.dirname(targetPath);
|
|
291
|
-
if (!fs.existsSync(dir)) {
|
|
292
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
fs.writeFileSync(targetPath, buffer);
|
|
296
|
-
|
|
297
|
-
// Set executable permissions on Unix-like systems
|
|
298
|
-
if (process.platform !== 'win32') {
|
|
299
|
-
fs.chmodSync(targetPath, 0o755);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Compute SHA-256 hash of a file.
|
|
307
|
-
* @param {string} filePath
|
|
308
|
-
* @returns {string}
|
|
309
|
-
*/
|
|
310
|
-
function hashFile(filePath) {
|
|
311
|
-
const content = fs.readFileSync(filePath);
|
|
312
|
-
return crypto.createHash('sha256').update(content).digest('hex');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Override fs module methods to intercept file access for extracted binaries.
|
|
317
|
-
* This provides a generic solution that works with any native module loading pattern
|
|
318
|
-
* (bindings package, direct require, etc.)
|
|
319
|
-
* @param {string} cacheDir - Cache directory where binaries are extracted
|
|
320
|
-
* @param {Object.<string, string>} assetPathMap - Map of asset key -> extracted path
|
|
321
|
-
*/
|
|
322
|
-
/**
|
|
323
|
-
* Override Module._resolveFilename and Module._load to redirect native module loads to extracted cache.
|
|
324
|
-
* @param {Object.<string, string>} nativeModuleMap - Map of module name -> extracted path
|
|
325
|
-
*/
|
|
326
|
-
function overrideModuleResolution(nativeModuleMap) {
|
|
327
|
-
const originalResolveFilename = Module._resolveFilename;
|
|
328
|
-
const originalLoad = Module._load;
|
|
329
|
-
|
|
330
|
-
// Override _resolveFilename for normal resolution
|
|
331
|
-
Module._resolveFilename = function (request, parent, isMain, options) {
|
|
332
|
-
// Normalize the request path
|
|
333
|
-
const normalized = path.normalize(request);
|
|
334
|
-
|
|
335
|
-
// Check direct match
|
|
336
|
-
if (nativeModuleMap[request]) {
|
|
337
|
-
return nativeModuleMap[request];
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Check for basename match
|
|
341
|
-
const basename = path.basename(request);
|
|
342
|
-
if (nativeModuleMap[basename]) {
|
|
343
|
-
return nativeModuleMap[basename];
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Check if the request includes any of our native modules
|
|
347
|
-
for (const [moduleName, extractedPath] of Object.entries(nativeModuleMap)) {
|
|
348
|
-
if (request.endsWith(moduleName) || request.includes(moduleName) ||
|
|
349
|
-
normalized.includes(moduleName.replace(/\//g, path.sep))) {
|
|
350
|
-
return extractedPath;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Fall back to original resolution
|
|
355
|
-
return originalResolveFilename.call(this, request, parent, isMain, options);
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
// Override _load to intercept at load time (catches SEA embedderRequire)
|
|
359
|
-
Module._load = function (request, parent, isMain) {
|
|
360
|
-
// Normalize the request path
|
|
361
|
-
const normalized = path.normalize(request);
|
|
362
|
-
|
|
363
|
-
// Check direct match
|
|
364
|
-
if (nativeModuleMap[request]) {
|
|
365
|
-
return originalLoad.call(this, nativeModuleMap[request], parent, isMain);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Check for basename match
|
|
369
|
-
const basename = path.basename(request);
|
|
370
|
-
if (nativeModuleMap[basename]) {
|
|
371
|
-
return originalLoad.call(this, nativeModuleMap[basename], parent, isMain);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Check if the request includes any of our native modules
|
|
375
|
-
for (const [moduleName, extractedPath] of Object.entries(nativeModuleMap)) {
|
|
376
|
-
if (request.endsWith(moduleName) || request.includes(moduleName) ||
|
|
377
|
-
normalized.includes(moduleName.replace(/\//g, path.sep))) {
|
|
378
|
-
return originalLoad.call(this, extractedPath, parent, isMain);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Fall back to original load
|
|
383
|
-
return originalLoad.call(this, request, parent, isMain);
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Bootstrap the SEA application.
|
|
389
|
-
* Extracts binaries and sets up module resolution.
|
|
390
|
-
*/
|
|
391
|
-
function bootstrap() {
|
|
392
|
-
if (!sea) {
|
|
393
|
-
throw new Error('This script must run in a SEA (Single Executable Application) context');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Patch sea.getAsset to handle decryption transparently
|
|
397
|
-
if (typeof SEA_ENCRYPTED_ASSETS !== 'undefined' && typeof SEA_ENCRYPTION_KEY !== 'undefined') {
|
|
398
|
-
const originalGetAsset = sea.getAsset.bind(sea);
|
|
399
|
-
const originalGetRawAsset = sea.getRawAsset.bind(sea);
|
|
400
|
-
|
|
401
|
-
sea.getAsset = function (assetKey, encoding) {
|
|
402
|
-
const isEncrypted = SEA_ENCRYPTED_ASSETS.has(assetKey);
|
|
403
|
-
|
|
404
|
-
if (isEncrypted) {
|
|
405
|
-
// Get raw encrypted data
|
|
406
|
-
const encryptedData = originalGetRawAsset(assetKey);
|
|
407
|
-
const buffer = Buffer.isBuffer(encryptedData) ? encryptedData : Buffer.from(encryptedData);
|
|
408
|
-
|
|
409
|
-
// Decrypt it
|
|
410
|
-
const decrypted = decryptAsset(buffer, SEA_ENCRYPTION_KEY);
|
|
411
|
-
|
|
412
|
-
// Apply encoding if requested
|
|
413
|
-
if (encoding) {
|
|
414
|
-
return decrypted.toString(encoding);
|
|
415
|
-
}
|
|
416
|
-
return decrypted;
|
|
417
|
-
} else {
|
|
418
|
-
// Not encrypted, use original method
|
|
419
|
-
return originalGetAsset(assetKey, encoding);
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// Load the manifest from SEA assets (manifest is never encrypted)
|
|
425
|
-
const manifestJson = sea.getAsset('sea-manifest.json', 'utf8');
|
|
426
|
-
if (!manifestJson) {
|
|
427
|
-
throw new Error('SEA manifest not found in blob');
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const manifest = JSON.parse(manifestJson);
|
|
431
|
-
const verbose = process.env.SEA_VERBOSE === 'true';
|
|
432
|
-
|
|
433
|
-
if (verbose) {
|
|
434
|
-
console.log('=== SEA Bootstrap START ===');
|
|
435
|
-
console.log('SEA Bootstrap:');
|
|
436
|
-
console.log(` App: ${manifest.appName} v${manifest.appVersion}`);
|
|
437
|
-
console.log(` Platform: ${manifest.platform}-${manifest.arch}`);
|
|
438
|
-
console.log(` Binaries in manifest: ${manifest.binaries.length}`);
|
|
439
|
-
console.log(` All manifest binaries:`, manifest.binaries.map(b => b.fileName));
|
|
440
|
-
if (manifest.cacheLocation) {
|
|
441
|
-
console.log(` Configured cache location: ${manifest.cacheLocation}`);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Filter binaries for current platform
|
|
446
|
-
const platformBinaries = manifest.binaries.filter(b =>
|
|
447
|
-
(b.platform === '*' || b.platform === process.platform) &&
|
|
448
|
-
(b.arch === '*' || b.arch === process.arch)
|
|
449
|
-
);
|
|
450
|
-
|
|
451
|
-
// Sort by extraction order
|
|
452
|
-
platformBinaries.sort((a, b) => a.order - b.order);
|
|
453
|
-
|
|
454
|
-
const cacheDir = getExtractionCacheDir(
|
|
455
|
-
manifest.appName,
|
|
456
|
-
manifest.appVersion,
|
|
457
|
-
manifest.platform,
|
|
458
|
-
manifest.arch,
|
|
459
|
-
manifest.cacheLocation
|
|
460
|
-
);
|
|
461
|
-
|
|
462
|
-
const nativeModuleMap = {};
|
|
463
|
-
|
|
464
|
-
// Extract binaries
|
|
465
|
-
for (const binary of platformBinaries) {
|
|
466
|
-
const targetPath = path.join(cacheDir, binary.fileName);
|
|
467
|
-
|
|
468
|
-
if (verbose) {
|
|
469
|
-
console.log(` Extracting: ${binary.assetKey} -> ${targetPath}`);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
extractBinary(binary.assetKey, targetPath, binary.hash);
|
|
473
|
-
|
|
474
|
-
// Map the module name to extracted path
|
|
475
|
-
nativeModuleMap[binary.fileName] = targetPath;
|
|
476
|
-
|
|
477
|
-
// Also map without extension for easier resolution
|
|
478
|
-
const nameWithoutExt = path.basename(binary.fileName, path.extname(binary.fileName));
|
|
479
|
-
nativeModuleMap[nameWithoutExt] = targetPath;
|
|
480
|
-
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
//
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
//
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
v8.startupSnapshot.setDeserializeMainFunction
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1
|
+
/**
|
|
2
|
+
* bootstrap.js
|
|
3
|
+
* Runtime template for SEA applications.
|
|
4
|
+
* Handles binary extraction and module resolution override.
|
|
5
|
+
*
|
|
6
|
+
* This file is prepended to the application entry bundle.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
(function () {
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const Module = require('module');
|
|
16
|
+
|
|
17
|
+
// PHASE 1: Override fs methods IMMEDIATELY (before app code captures them)
|
|
18
|
+
// We'll populate the redirect maps in Phase 2 after extraction
|
|
19
|
+
const assetPathMapGlobal = {};
|
|
20
|
+
let cacheDirGlobal = '';
|
|
21
|
+
|
|
22
|
+
const originalExistsSync = fs.existsSync;
|
|
23
|
+
const originalReadFileSync = fs.readFileSync;
|
|
24
|
+
const originalStatSync = fs.statSync;
|
|
25
|
+
const originalLstatSync = fs.lstatSync;
|
|
26
|
+
const originalRealpathSync = fs.realpathSync;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a requested path should be redirected to cache.
|
|
30
|
+
* @param {string} requestedPath
|
|
31
|
+
* @returns {string|null} - Redirected path or null
|
|
32
|
+
*/
|
|
33
|
+
function getRedirectedPath(requestedPath) {
|
|
34
|
+
if (!requestedPath) return null;
|
|
35
|
+
|
|
36
|
+
// Normalize the requested path
|
|
37
|
+
const normalized = path.normalize(requestedPath);
|
|
38
|
+
|
|
39
|
+
// Check direct match against asset keys
|
|
40
|
+
for (const [assetKey, extractedPath] of Object.entries(assetPathMapGlobal)) {
|
|
41
|
+
// Try exact match
|
|
42
|
+
if (normalized.endsWith(assetKey)) {
|
|
43
|
+
return extractedPath;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Try basename match
|
|
47
|
+
const assetBasename = path.basename(assetKey);
|
|
48
|
+
if (path.basename(normalized) === assetBasename) {
|
|
49
|
+
return extractedPath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Try matching the path components
|
|
53
|
+
if (normalized.includes(assetKey.replace(/\//g, path.sep))) {
|
|
54
|
+
return extractedPath;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Override existsSync immediately
|
|
62
|
+
fs.existsSync = function (filePath) {
|
|
63
|
+
if (!filePath) return originalExistsSync(filePath);
|
|
64
|
+
|
|
65
|
+
// If this path should be redirected, check if the REDIRECTED path exists
|
|
66
|
+
const redirected = getRedirectedPath(filePath);
|
|
67
|
+
if (redirected) {
|
|
68
|
+
return originalExistsSync(redirected);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// For .node files, ONLY allow cache paths for our managed binaries
|
|
72
|
+
if (filePath.endsWith('.node') && cacheDirGlobal) {
|
|
73
|
+
const basename = path.basename(filePath);
|
|
74
|
+
|
|
75
|
+
// Check if this is one of our managed binaries
|
|
76
|
+
for (const assetKey of Object.keys(assetPathMapGlobal)) {
|
|
77
|
+
if (path.basename(assetKey) === basename) {
|
|
78
|
+
// This is one of our binaries
|
|
79
|
+
// Only return true if the path is in the cache directory
|
|
80
|
+
if (filePath.startsWith(cacheDirGlobal)) {
|
|
81
|
+
return originalExistsSync(filePath);
|
|
82
|
+
} else {
|
|
83
|
+
// Block all non-cache paths for our binaries
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return originalExistsSync(filePath);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Override readFileSync immediately
|
|
94
|
+
fs.readFileSync = function (filePath, options) {
|
|
95
|
+
const redirected = getRedirectedPath(filePath);
|
|
96
|
+
if (redirected) {
|
|
97
|
+
return originalReadFileSync(redirected, options);
|
|
98
|
+
}
|
|
99
|
+
return originalReadFileSync(filePath, options);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Override statSync immediately
|
|
103
|
+
fs.statSync = function (filePath, options) {
|
|
104
|
+
const redirected = getRedirectedPath(filePath);
|
|
105
|
+
if (redirected) {
|
|
106
|
+
return originalStatSync(redirected, options);
|
|
107
|
+
}
|
|
108
|
+
return originalStatSync(filePath, options);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Override lstatSync immediately
|
|
112
|
+
fs.lstatSync = function (filePath, options) {
|
|
113
|
+
const redirected = getRedirectedPath(filePath);
|
|
114
|
+
if (redirected) {
|
|
115
|
+
return originalLstatSync(redirected, options);
|
|
116
|
+
}
|
|
117
|
+
return originalLstatSync(filePath, options);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Override realpathSync immediately
|
|
121
|
+
fs.realpathSync = function (filePath, options) {
|
|
122
|
+
const redirected = getRedirectedPath(filePath);
|
|
123
|
+
if (redirected) {
|
|
124
|
+
return redirected; // Return the redirected path as the "real" path
|
|
125
|
+
}
|
|
126
|
+
return originalRealpathSync(filePath, options);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// SEA API - check if we're in an SEA by trying to load the sea module
|
|
130
|
+
let sea = null;
|
|
131
|
+
let isSEA = false;
|
|
132
|
+
try {
|
|
133
|
+
sea = require('node:sea');
|
|
134
|
+
isSEA = true;
|
|
135
|
+
} catch (err) {
|
|
136
|
+
// Not in SEA context - this is fine for development
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Decrypt an encrypted asset.
|
|
141
|
+
* @param {Buffer} encryptedData - Encrypted data with IV and auth tag prepended
|
|
142
|
+
* @param {Buffer} key - 32-byte encryption key
|
|
143
|
+
* @returns {Buffer} - Decrypted data
|
|
144
|
+
*/
|
|
145
|
+
function decryptAsset(encryptedData, key) {
|
|
146
|
+
// Extract IV, auth tag, and encrypted content
|
|
147
|
+
const iv = encryptedData.slice(0, 16);
|
|
148
|
+
const authTag = encryptedData.slice(16, 32);
|
|
149
|
+
const encrypted = encryptedData.slice(32);
|
|
150
|
+
|
|
151
|
+
// Create decipher
|
|
152
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
153
|
+
decipher.setAuthTag(authTag);
|
|
154
|
+
|
|
155
|
+
// Decrypt the data
|
|
156
|
+
const decrypted = Buffer.concat([
|
|
157
|
+
decipher.update(encrypted),
|
|
158
|
+
decipher.final()
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
return decrypted;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get an asset from the SEA, decrypting if necessary.
|
|
166
|
+
* @param {string} assetKey - Asset key
|
|
167
|
+
* @param {string} [encoding] - Optional encoding (e.g., 'utf8')
|
|
168
|
+
* @returns {Buffer|string} - Asset content
|
|
169
|
+
*/
|
|
170
|
+
function getAsset(assetKey, encoding) {
|
|
171
|
+
if (!sea) {
|
|
172
|
+
throw new Error('Cannot get asset: not in SEA context');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check if this asset is encrypted
|
|
176
|
+
const isEncrypted = typeof SEA_ENCRYPTED_ASSETS !== 'undefined' && SEA_ENCRYPTED_ASSETS.has(assetKey);
|
|
177
|
+
|
|
178
|
+
let content;
|
|
179
|
+
if (isEncrypted && typeof SEA_ENCRYPTION_KEY !== 'undefined') {
|
|
180
|
+
// Get raw encrypted data
|
|
181
|
+
const encryptedData = sea.getRawAsset(assetKey);
|
|
182
|
+
const buffer = Buffer.isBuffer(encryptedData) ? encryptedData : Buffer.from(encryptedData);
|
|
183
|
+
|
|
184
|
+
// Decrypt it
|
|
185
|
+
content = decryptAsset(buffer, SEA_ENCRYPTION_KEY);
|
|
186
|
+
} else {
|
|
187
|
+
// Get unencrypted asset
|
|
188
|
+
content = sea.getAsset(assetKey, encoding);
|
|
189
|
+
if (!encoding && !Buffer.isBuffer(content)) {
|
|
190
|
+
content = Buffer.from(content);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Apply encoding if requested
|
|
195
|
+
if (encoding && Buffer.isBuffer(content)) {
|
|
196
|
+
return content.toString(encoding);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return content;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve environment variables in a path string.
|
|
205
|
+
* Supports %VAR% on Windows and $VAR or ${VAR} on Unix-like systems.
|
|
206
|
+
* @param {string} pathStr - Path string that may contain environment variables
|
|
207
|
+
* @returns {string} - Path with environment variables expanded
|
|
208
|
+
*/
|
|
209
|
+
function resolveEnvVars(pathStr) {
|
|
210
|
+
if (!pathStr) return pathStr;
|
|
211
|
+
|
|
212
|
+
// Replace Windows-style %VAR%
|
|
213
|
+
let resolved = pathStr.replace(/%([^%]+)%/g, (match, varName) => {
|
|
214
|
+
return process.env[varName] || match;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Replace Unix-style $VAR and ${VAR}
|
|
218
|
+
resolved = resolved.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
|
219
|
+
return process.env[varName] || match;
|
|
220
|
+
});
|
|
221
|
+
resolved = resolved.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, varName) => {
|
|
222
|
+
return process.env[varName] || match;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return resolved;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get the extraction cache directory for this application.
|
|
230
|
+
* @param {string} appName
|
|
231
|
+
* @param {string} appVersion
|
|
232
|
+
* @param {string} platform
|
|
233
|
+
* @param {string} arch
|
|
234
|
+
* @param {string} [configuredCacheLocation] - Optional configured cache location from manifest
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
function getExtractionCacheDir(appName, appVersion, platform, arch, configuredCacheLocation) {
|
|
238
|
+
// Priority: SEACACHE env var > configured location > default
|
|
239
|
+
if (process.env.SEACACHE) return process.env.SEACACHE;
|
|
240
|
+
|
|
241
|
+
if (configuredCacheLocation) {
|
|
242
|
+
// Resolve environment variables in the configured path
|
|
243
|
+
const resolvedBase = resolveEnvVars(configuredCacheLocation);
|
|
244
|
+
// Make relative paths absolute (relative to cwd)
|
|
245
|
+
const absoluteBase = path.isAbsolute(resolvedBase) ? resolvedBase : path.resolve(process.cwd(), resolvedBase);
|
|
246
|
+
return path.join(absoluteBase, appName, `${appVersion}-${platform}-${arch}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Default behavior
|
|
250
|
+
const localAppData = process.env.LOCALAPPDATA || process.env.HOME || process.cwd();
|
|
251
|
+
return path.join(localAppData, '.sea-cache', appName, `${appVersion}-${platform}-${arch}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Extract a binary asset to the cache directory.
|
|
256
|
+
* @param {string} assetKey
|
|
257
|
+
* @param {string} targetPath
|
|
258
|
+
* @param {string} expectedHash
|
|
259
|
+
* @returns {boolean} - True if extracted or already valid
|
|
260
|
+
*/
|
|
261
|
+
function extractBinary(assetKey, targetPath, expectedHash) {
|
|
262
|
+
// Check if already extracted and valid
|
|
263
|
+
if (fs.existsSync(targetPath)) {
|
|
264
|
+
const existingHash = hashFile(targetPath);
|
|
265
|
+
if (existingHash === expectedHash) {
|
|
266
|
+
return true; // Already valid
|
|
267
|
+
}
|
|
268
|
+
// Hash mismatch, re-extract
|
|
269
|
+
fs.unlinkSync(targetPath);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Extract from SEA blob (binaries are never encrypted)
|
|
273
|
+
const assetBuffer = sea.getRawAsset(assetKey);
|
|
274
|
+
if (!assetBuffer) {
|
|
275
|
+
throw new Error(`Asset not found in SEA blob: ${assetKey}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Convert ArrayBuffer to Buffer if needed
|
|
279
|
+
const buffer = Buffer.isBuffer(assetBuffer)
|
|
280
|
+
? assetBuffer
|
|
281
|
+
: Buffer.from(assetBuffer);
|
|
282
|
+
|
|
283
|
+
// Verify hash before writing
|
|
284
|
+
const actualHash = crypto.createHash('sha256').update(buffer).digest('hex');
|
|
285
|
+
if (actualHash !== expectedHash) {
|
|
286
|
+
throw new Error(`Integrity check failed for ${assetKey}: expected ${expectedHash}, got ${actualHash}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Write to cache
|
|
290
|
+
const dir = path.dirname(targetPath);
|
|
291
|
+
if (!fs.existsSync(dir)) {
|
|
292
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fs.writeFileSync(targetPath, buffer);
|
|
296
|
+
|
|
297
|
+
// Set executable permissions on Unix-like systems
|
|
298
|
+
if (process.platform !== 'win32') {
|
|
299
|
+
fs.chmodSync(targetPath, 0o755);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Compute SHA-256 hash of a file.
|
|
307
|
+
* @param {string} filePath
|
|
308
|
+
* @returns {string}
|
|
309
|
+
*/
|
|
310
|
+
function hashFile(filePath) {
|
|
311
|
+
const content = fs.readFileSync(filePath);
|
|
312
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Override fs module methods to intercept file access for extracted binaries.
|
|
317
|
+
* This provides a generic solution that works with any native module loading pattern
|
|
318
|
+
* (bindings package, direct require, etc.)
|
|
319
|
+
* @param {string} cacheDir - Cache directory where binaries are extracted
|
|
320
|
+
* @param {Object.<string, string>} assetPathMap - Map of asset key -> extracted path
|
|
321
|
+
*/
|
|
322
|
+
/**
|
|
323
|
+
* Override Module._resolveFilename and Module._load to redirect native module loads to extracted cache.
|
|
324
|
+
* @param {Object.<string, string>} nativeModuleMap - Map of module name -> extracted path
|
|
325
|
+
*/
|
|
326
|
+
function overrideModuleResolution(nativeModuleMap) {
|
|
327
|
+
const originalResolveFilename = Module._resolveFilename;
|
|
328
|
+
const originalLoad = Module._load;
|
|
329
|
+
|
|
330
|
+
// Override _resolveFilename for normal resolution
|
|
331
|
+
Module._resolveFilename = function (request, parent, isMain, options) {
|
|
332
|
+
// Normalize the request path
|
|
333
|
+
const normalized = path.normalize(request);
|
|
334
|
+
|
|
335
|
+
// Check direct match
|
|
336
|
+
if (nativeModuleMap[request]) {
|
|
337
|
+
return nativeModuleMap[request];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Check for basename match
|
|
341
|
+
const basename = path.basename(request);
|
|
342
|
+
if (nativeModuleMap[basename]) {
|
|
343
|
+
return nativeModuleMap[basename];
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check if the request includes any of our native modules
|
|
347
|
+
for (const [moduleName, extractedPath] of Object.entries(nativeModuleMap)) {
|
|
348
|
+
if (request.endsWith(moduleName) || request.includes(moduleName) ||
|
|
349
|
+
normalized.includes(moduleName.replace(/\//g, path.sep))) {
|
|
350
|
+
return extractedPath;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Fall back to original resolution
|
|
355
|
+
return originalResolveFilename.call(this, request, parent, isMain, options);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
// Override _load to intercept at load time (catches SEA embedderRequire)
|
|
359
|
+
Module._load = function (request, parent, isMain) {
|
|
360
|
+
// Normalize the request path
|
|
361
|
+
const normalized = path.normalize(request);
|
|
362
|
+
|
|
363
|
+
// Check direct match
|
|
364
|
+
if (nativeModuleMap[request]) {
|
|
365
|
+
return originalLoad.call(this, nativeModuleMap[request], parent, isMain);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Check for basename match
|
|
369
|
+
const basename = path.basename(request);
|
|
370
|
+
if (nativeModuleMap[basename]) {
|
|
371
|
+
return originalLoad.call(this, nativeModuleMap[basename], parent, isMain);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Check if the request includes any of our native modules
|
|
375
|
+
for (const [moduleName, extractedPath] of Object.entries(nativeModuleMap)) {
|
|
376
|
+
if (request.endsWith(moduleName) || request.includes(moduleName) ||
|
|
377
|
+
normalized.includes(moduleName.replace(/\//g, path.sep))) {
|
|
378
|
+
return originalLoad.call(this, extractedPath, parent, isMain);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Fall back to original load
|
|
383
|
+
return originalLoad.call(this, request, parent, isMain);
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Bootstrap the SEA application.
|
|
389
|
+
* Extracts binaries and sets up module resolution.
|
|
390
|
+
*/
|
|
391
|
+
function bootstrap() {
|
|
392
|
+
if (!sea) {
|
|
393
|
+
throw new Error('This script must run in a SEA (Single Executable Application) context');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Patch sea.getAsset to handle decryption transparently
|
|
397
|
+
if (typeof SEA_ENCRYPTED_ASSETS !== 'undefined' && typeof SEA_ENCRYPTION_KEY !== 'undefined') {
|
|
398
|
+
const originalGetAsset = sea.getAsset.bind(sea);
|
|
399
|
+
const originalGetRawAsset = sea.getRawAsset.bind(sea);
|
|
400
|
+
|
|
401
|
+
sea.getAsset = function (assetKey, encoding) {
|
|
402
|
+
const isEncrypted = SEA_ENCRYPTED_ASSETS.has(assetKey);
|
|
403
|
+
|
|
404
|
+
if (isEncrypted) {
|
|
405
|
+
// Get raw encrypted data
|
|
406
|
+
const encryptedData = originalGetRawAsset(assetKey);
|
|
407
|
+
const buffer = Buffer.isBuffer(encryptedData) ? encryptedData : Buffer.from(encryptedData);
|
|
408
|
+
|
|
409
|
+
// Decrypt it
|
|
410
|
+
const decrypted = decryptAsset(buffer, SEA_ENCRYPTION_KEY);
|
|
411
|
+
|
|
412
|
+
// Apply encoding if requested
|
|
413
|
+
if (encoding) {
|
|
414
|
+
return decrypted.toString(encoding);
|
|
415
|
+
}
|
|
416
|
+
return decrypted;
|
|
417
|
+
} else {
|
|
418
|
+
// Not encrypted, use original method
|
|
419
|
+
return originalGetAsset(assetKey, encoding);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Load the manifest from SEA assets (manifest is never encrypted)
|
|
425
|
+
const manifestJson = sea.getAsset('sea-manifest.json', 'utf8');
|
|
426
|
+
if (!manifestJson) {
|
|
427
|
+
throw new Error('SEA manifest not found in blob');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const manifest = JSON.parse(manifestJson);
|
|
431
|
+
const verbose = process.env.SEA_VERBOSE === 'true';
|
|
432
|
+
|
|
433
|
+
if (verbose) {
|
|
434
|
+
console.log('=== SEA Bootstrap START ===');
|
|
435
|
+
console.log('SEA Bootstrap:');
|
|
436
|
+
console.log(` App: ${manifest.appName} v${manifest.appVersion}`);
|
|
437
|
+
console.log(` Platform: ${manifest.platform}-${manifest.arch}`);
|
|
438
|
+
console.log(` Binaries in manifest: ${manifest.binaries.length}`);
|
|
439
|
+
console.log(` All manifest binaries:`, manifest.binaries.map(b => b.fileName));
|
|
440
|
+
if (manifest.cacheLocation) {
|
|
441
|
+
console.log(` Configured cache location: ${manifest.cacheLocation}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Filter binaries for current platform
|
|
446
|
+
const platformBinaries = manifest.binaries.filter(b =>
|
|
447
|
+
(b.platform === '*' || b.platform === process.platform) &&
|
|
448
|
+
(b.arch === '*' || b.arch === process.arch)
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// Sort by extraction order
|
|
452
|
+
platformBinaries.sort((a, b) => a.order - b.order);
|
|
453
|
+
|
|
454
|
+
const cacheDir = getExtractionCacheDir(
|
|
455
|
+
manifest.appName,
|
|
456
|
+
manifest.appVersion,
|
|
457
|
+
manifest.platform,
|
|
458
|
+
manifest.arch,
|
|
459
|
+
manifest.cacheLocation
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const nativeModuleMap = {};
|
|
463
|
+
|
|
464
|
+
// Extract binaries
|
|
465
|
+
for (const binary of platformBinaries) {
|
|
466
|
+
const targetPath = path.join(cacheDir, binary.fileName);
|
|
467
|
+
|
|
468
|
+
if (verbose) {
|
|
469
|
+
console.log(` Extracting: ${binary.assetKey} -> ${targetPath}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
extractBinary(binary.assetKey, targetPath, binary.hash);
|
|
473
|
+
|
|
474
|
+
// Map the module name to extracted path
|
|
475
|
+
nativeModuleMap[binary.fileName] = targetPath;
|
|
476
|
+
|
|
477
|
+
// Also map without extension for easier resolution
|
|
478
|
+
const nameWithoutExt = path.basename(binary.fileName, path.extname(binary.fileName));
|
|
479
|
+
nativeModuleMap[nameWithoutExt] = targetPath;
|
|
480
|
+
|
|
481
|
+
// CRITICAL: Also map the asset key (the path used in requires)
|
|
482
|
+
nativeModuleMap[binary.assetKey] = targetPath;
|
|
483
|
+
|
|
484
|
+
// PHASE 2: Populate global asset map for fs overrides
|
|
485
|
+
assetPathMapGlobal[binary.assetKey] = targetPath;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// PHASE 2: Set global cache directory
|
|
489
|
+
cacheDirGlobal = cacheDir;
|
|
490
|
+
|
|
491
|
+
if (verbose) {
|
|
492
|
+
console.log('✓ Binary extraction complete');
|
|
493
|
+
console.log(` Cache directory: ${cacheDir}`);
|
|
494
|
+
console.log(` Asset mappings: ${Object.keys(assetPathMapGlobal).length}`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// On Windows, add the cache directory to DLL search path
|
|
498
|
+
if (process.platform === 'win32') {
|
|
499
|
+
// Add to PATH so that native addons can find dependent DLLs
|
|
500
|
+
process.env.PATH = `${cacheDir};${process.env.PATH}`;
|
|
501
|
+
|
|
502
|
+
if (verbose) {
|
|
503
|
+
console.log(` Added to PATH: ${cacheDir}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Also try to add DLL directory using SetDllDirectory (if available)
|
|
507
|
+
try {
|
|
508
|
+
// Windows-specific: preload all DLLs to ensure they're in the process
|
|
509
|
+
platformBinaries
|
|
510
|
+
.filter(b => b.fileName.endsWith('.dll'))
|
|
511
|
+
.forEach(b => {
|
|
512
|
+
const dllPath = path.join(cacheDir, b.fileName);
|
|
513
|
+
if (verbose) {
|
|
514
|
+
console.log(` Preloading DLL: ${dllPath}`);
|
|
515
|
+
}
|
|
516
|
+
// Use process.dlopen to preload the DLL
|
|
517
|
+
try {
|
|
518
|
+
process.dlopen({ exports: {} }, dllPath);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
// DLL might not be a valid node addon, which is fine
|
|
521
|
+
if (verbose) {
|
|
522
|
+
console.log(` (Non-addon DLL, will be loaded by OS loader)`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
} catch (err) {
|
|
527
|
+
if (verbose) {
|
|
528
|
+
console.warn('Warning: Could not preload DLLs:', err.message);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Set environment variable for cache directory (used by applications)
|
|
534
|
+
process.env.SEA_CACHE_DIR = cacheDir;
|
|
535
|
+
|
|
536
|
+
// Export native module map and cache dir to global for require override in app code
|
|
537
|
+
global.__seaNativeModuleMap = nativeModuleMap;
|
|
538
|
+
global.__seaCacheDir = cacheDir;
|
|
539
|
+
|
|
540
|
+
// Add cache directory to module paths so bindings can find extracted .node files
|
|
541
|
+
if (!Module.globalPaths.includes(cacheDir)) {
|
|
542
|
+
Module.globalPaths.unshift(cacheDir);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Override module resolution
|
|
546
|
+
overrideModuleResolution(nativeModuleMap);
|
|
547
|
+
|
|
548
|
+
// Provide a 'bindings' module shim for SEA compatibility
|
|
549
|
+
// This gets injected into the module cache so require('bindings') works
|
|
550
|
+
const bindingsShim = function (name) {
|
|
551
|
+
// Ensure .node extension
|
|
552
|
+
if (!name.endsWith('.node')) {
|
|
553
|
+
name += '.node';
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (verbose) {
|
|
557
|
+
console.log(`[bindings shim] Loading native module: ${name}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Try to load from native module map
|
|
561
|
+
if (nativeModuleMap[name]) {
|
|
562
|
+
const exports = {};
|
|
563
|
+
process.dlopen({ exports }, nativeModuleMap[name]);
|
|
564
|
+
return exports;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Try basename without .node
|
|
568
|
+
const baseName = path.basename(name, '.node');
|
|
569
|
+
if (nativeModuleMap[baseName]) {
|
|
570
|
+
const exports = {};
|
|
571
|
+
process.dlopen({ exports }, nativeModuleMap[baseName]);
|
|
572
|
+
return exports;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
throw new Error(`Could not load native module "${name}" - not found in SEA cache`);
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
// Inject bindings into module cache
|
|
579
|
+
Module._cache['bindings'] = {
|
|
580
|
+
id: 'bindings',
|
|
581
|
+
exports: bindingsShim,
|
|
582
|
+
loaded: true
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
if (verbose) {
|
|
586
|
+
console.log('[SEA] Injected bindings shim into module cache');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Set up the .node extension handler to lazy-load native modules
|
|
592
|
+
* This needs to run during snapshot build so it's baked into the snapshot
|
|
593
|
+
*/
|
|
594
|
+
function setupNodeExtensionHandler() {
|
|
595
|
+
const Module = require('module');
|
|
596
|
+
const originalResolveFilename = Module._resolveFilename;
|
|
597
|
+
const originalNodeHandler = Module._extensions['.node'];
|
|
598
|
+
|
|
599
|
+
// CRITICAL: Override Module._resolveFilename FIRST to intercept path resolution
|
|
600
|
+
// This catches requires BEFORE they try to load, including snapshot requires
|
|
601
|
+
Module._resolveFilename = function (request, parent, isMain, options) {
|
|
602
|
+
// Only intercept .node files
|
|
603
|
+
if (request.endsWith('.node')) {
|
|
604
|
+
const basename = path.basename(request);
|
|
605
|
+
const nameWithoutExt = path.basename(request, '.node');
|
|
606
|
+
|
|
607
|
+
if (global.__seaNativeModuleMap) {
|
|
608
|
+
const resolvedPath = global.__seaNativeModuleMap[basename] ||
|
|
609
|
+
global.__seaNativeModuleMap[nameWithoutExt] ||
|
|
610
|
+
global.__seaNativeModuleMap[request];
|
|
611
|
+
|
|
612
|
+
if (resolvedPath) {
|
|
613
|
+
return resolvedPath;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Fall back to original resolution
|
|
619
|
+
return originalResolveFilename.call(this, request, parent, isMain, options);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// Override the .node extension handler
|
|
623
|
+
// During snapshot: creates a lazy-loading placeholder
|
|
624
|
+
// During runtime: loads from the cache using populated globals
|
|
625
|
+
Module._extensions['.node'] = function (module, filename) {
|
|
626
|
+
const basename = path.basename(filename);
|
|
627
|
+
const nameWithoutExt = path.basename(filename, '.node');
|
|
628
|
+
|
|
629
|
+
// Check if we have the native module map (only available after bootstrap at runtime)
|
|
630
|
+
if (global.__seaNativeModuleMap) {
|
|
631
|
+
const resolvedPath = global.__seaNativeModuleMap[basename] ||
|
|
632
|
+
global.__seaNativeModuleMap[nameWithoutExt];
|
|
633
|
+
|
|
634
|
+
if (resolvedPath) {
|
|
635
|
+
process.dlopen(module, resolvedPath);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// If we get here, we're during snapshot creation or the module wasn't found
|
|
641
|
+
// During snapshot creation, we create a lazy-loading proxy
|
|
642
|
+
const moduleKey = basename;
|
|
643
|
+
|
|
644
|
+
// Create a lazy-loading exports object
|
|
645
|
+
// When any property is accessed, it will try to load the real module
|
|
646
|
+
const lazyExports = {};
|
|
647
|
+
let realModuleLoaded = false;
|
|
648
|
+
let realExports = null;
|
|
649
|
+
|
|
650
|
+
// Use defineProperty to intercept all property access
|
|
651
|
+
const handler = {
|
|
652
|
+
get(target, prop) {
|
|
653
|
+
// If we haven't loaded the real module yet, try now
|
|
654
|
+
if (!realModuleLoaded) {
|
|
655
|
+
if (global.__seaNativeModuleMap) {
|
|
656
|
+
const resolvedPath = global.__seaNativeModuleMap[moduleKey] ||
|
|
657
|
+
global.__seaNativeModuleMap[nameWithoutExt];
|
|
658
|
+
|
|
659
|
+
if (resolvedPath) {
|
|
660
|
+
const tempModule = { exports: {} };
|
|
661
|
+
process.dlopen(tempModule, resolvedPath);
|
|
662
|
+
realExports = tempModule.exports;
|
|
663
|
+
realModuleLoaded = true;
|
|
664
|
+
|
|
665
|
+
// Copy all properties to the lazy exports
|
|
666
|
+
Object.assign(lazyExports, realExports);
|
|
667
|
+
|
|
668
|
+
return realExports[prop];
|
|
669
|
+
} else {
|
|
670
|
+
throw new Error(`Native module ${moduleKey} not found in SEA cache`);
|
|
671
|
+
}
|
|
672
|
+
} else {
|
|
673
|
+
throw new Error(`Native module loading attempted before bootstrap completed: ${moduleKey}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return realExports[prop];
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
set(target, prop, value) {
|
|
681
|
+
if (realExports) {
|
|
682
|
+
realExports[prop] = value;
|
|
683
|
+
} else {
|
|
684
|
+
lazyExports[prop] = value;
|
|
685
|
+
}
|
|
686
|
+
return true;
|
|
687
|
+
},
|
|
688
|
+
|
|
689
|
+
has(target, prop) {
|
|
690
|
+
if (realExports) {
|
|
691
|
+
return prop in realExports;
|
|
692
|
+
}
|
|
693
|
+
return prop in lazyExports;
|
|
694
|
+
},
|
|
695
|
+
|
|
696
|
+
ownKeys(target) {
|
|
697
|
+
if (realExports) {
|
|
698
|
+
return Object.keys(realExports);
|
|
699
|
+
}
|
|
700
|
+
return Object.keys(lazyExports);
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
704
|
+
if (realExports) {
|
|
705
|
+
return Object.getOwnPropertyDescriptor(realExports, prop);
|
|
706
|
+
}
|
|
707
|
+
return Object.getOwnPropertyDescriptor(lazyExports, prop);
|
|
708
|
+
}
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
module.exports = new Proxy(lazyExports, handler);
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Run bootstrap if in SEA context
|
|
716
|
+
if (isSEA && sea) {
|
|
717
|
+
// Check if we're building a snapshot
|
|
718
|
+
let isBuildingSnapshot = false;
|
|
719
|
+
try {
|
|
720
|
+
const v8 = require('v8');
|
|
721
|
+
isBuildingSnapshot = v8.startupSnapshot && v8.startupSnapshot.isBuildingSnapshot && v8.startupSnapshot.isBuildingSnapshot();
|
|
722
|
+
} catch (err) {
|
|
723
|
+
// v8.startupSnapshot not available
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (isBuildingSnapshot) {
|
|
727
|
+
setupNodeExtensionHandler();
|
|
728
|
+
|
|
729
|
+
// During snapshot build: set up callback to run bootstrap at runtime
|
|
730
|
+
const v8 = require('v8');
|
|
731
|
+
const originalCallback = v8.startupSnapshot.setDeserializeMainFunction;
|
|
732
|
+
|
|
733
|
+
// Intercept setDeserializeMainFunction to add bootstrap before app code
|
|
734
|
+
v8.startupSnapshot.setDeserializeMainFunction = function (callback) {
|
|
735
|
+
originalCallback.call(this, () => {
|
|
736
|
+
// Run bootstrap to extract binaries and populate globals
|
|
737
|
+
bootstrap();
|
|
738
|
+
|
|
739
|
+
// Then run the application callback
|
|
740
|
+
callback();
|
|
741
|
+
});
|
|
742
|
+
};
|
|
743
|
+
} else {
|
|
744
|
+
// Normal runtime: run bootstrap immediately
|
|
745
|
+
bootstrap();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
})(); // End bootstrap IIFE
|
|
750
|
+
|
|
751
|
+
// Export for testing (only accessible if loaded as module)
|
|
752
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
753
|
+
module.exports = {
|
|
754
|
+
// Empty exports - bootstrap runs automatically
|
|
755
|
+
};
|
|
756
|
+
}
|