seabox 0.1.2 → 0.3.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/lib/bootstrap.cjs CHANGED
@@ -1,756 +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
- // 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
- }
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
+ }