seabox 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,655 @@
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
+ console.log('[SEA Bootstrap] Running in SEA context');
136
+ } catch (err) {
137
+ // Not in SEA context - this is fine for development
138
+ console.log('[SEA Bootstrap] Not in SEA context (normal mode)');
139
+ }
140
+
141
+ /**
142
+ * Decrypt an encrypted asset.
143
+ * @param {Buffer} encryptedData - Encrypted data with IV and auth tag prepended
144
+ * @param {Buffer} key - 32-byte encryption key
145
+ * @returns {Buffer} - Decrypted data
146
+ */
147
+ function decryptAsset(encryptedData, key) {
148
+ // Extract IV, auth tag, and encrypted content
149
+ const iv = encryptedData.slice(0, 16);
150
+ const authTag = encryptedData.slice(16, 32);
151
+ const encrypted = encryptedData.slice(32);
152
+
153
+ // Create decipher
154
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
155
+ decipher.setAuthTag(authTag);
156
+
157
+ // Decrypt the data
158
+ const decrypted = Buffer.concat([
159
+ decipher.update(encrypted),
160
+ decipher.final()
161
+ ]);
162
+
163
+ return decrypted;
164
+ }
165
+
166
+ /**
167
+ * Get an asset from the SEA, decrypting if necessary.
168
+ * @param {string} assetKey - Asset key
169
+ * @param {string} [encoding] - Optional encoding (e.g., 'utf8')
170
+ * @returns {Buffer|string} - Asset content
171
+ */
172
+ function getAsset(assetKey, encoding) {
173
+ if (!sea) {
174
+ throw new Error('Cannot get asset: not in SEA context');
175
+ }
176
+
177
+ // Check if this asset is encrypted
178
+ const isEncrypted = typeof SEA_ENCRYPTED_ASSETS !== 'undefined' && SEA_ENCRYPTED_ASSETS.has(assetKey);
179
+
180
+ let content;
181
+ if (isEncrypted && typeof SEA_ENCRYPTION_KEY !== 'undefined') {
182
+ // Get raw encrypted data
183
+ const encryptedData = sea.getRawAsset(assetKey);
184
+ const buffer = Buffer.isBuffer(encryptedData) ? encryptedData : Buffer.from(encryptedData);
185
+
186
+ // Decrypt it
187
+ content = decryptAsset(buffer, SEA_ENCRYPTION_KEY);
188
+ } else {
189
+ // Get unencrypted asset
190
+ content = sea.getAsset(assetKey, encoding);
191
+ if (!encoding && !Buffer.isBuffer(content)) {
192
+ content = Buffer.from(content);
193
+ }
194
+ }
195
+
196
+ // Apply encoding if requested
197
+ if (encoding && Buffer.isBuffer(content)) {
198
+ return content.toString(encoding);
199
+ }
200
+
201
+ return content;
202
+ }
203
+
204
+
205
+ /**
206
+ * Get the extraction cache directory for this application.
207
+ * @param {string} appName
208
+ * @param {string} appVersion
209
+ * @param {string} platform
210
+ * @param {string} arch
211
+ * @returns {string}
212
+ */
213
+ function getExtractionCacheDir(appName, appVersion, platform, arch) {
214
+ if(process.env.SEACACHE) return process.env.SEACACHE;
215
+ const localAppData = process.env.LOCALAPPDATA || process.env.HOME || process.cwd();
216
+ return path.join(localAppData, '.sea-cache', appName, `${appVersion}-${platform}-${arch}`);
217
+ }
218
+
219
+ /**
220
+ * Extract a binary asset to the cache directory.
221
+ * @param {string} assetKey
222
+ * @param {string} targetPath
223
+ * @param {string} expectedHash
224
+ * @returns {boolean} - True if extracted or already valid
225
+ */
226
+ function extractBinary(assetKey, targetPath, expectedHash) {
227
+ // Check if already extracted and valid
228
+ if (fs.existsSync(targetPath)) {
229
+ const existingHash = hashFile(targetPath);
230
+ if (existingHash === expectedHash) {
231
+ return true; // Already valid
232
+ }
233
+ // Hash mismatch, re-extract
234
+ fs.unlinkSync(targetPath);
235
+ }
236
+
237
+ // Extract from SEA blob (binaries are never encrypted)
238
+ const assetBuffer = sea.getRawAsset(assetKey);
239
+ if (!assetBuffer) {
240
+ throw new Error(`Asset not found in SEA blob: ${assetKey}`);
241
+ }
242
+
243
+ // Convert ArrayBuffer to Buffer if needed
244
+ const buffer = Buffer.isBuffer(assetBuffer)
245
+ ? assetBuffer
246
+ : Buffer.from(assetBuffer);
247
+
248
+ // Verify hash before writing
249
+ const actualHash = crypto.createHash('sha256').update(buffer).digest('hex');
250
+ if (actualHash !== expectedHash) {
251
+ throw new Error(`Integrity check failed for ${assetKey}: expected ${expectedHash}, got ${actualHash}`);
252
+ }
253
+
254
+ // Write to cache
255
+ const dir = path.dirname(targetPath);
256
+ if (!fs.existsSync(dir)) {
257
+ fs.mkdirSync(dir, { recursive: true });
258
+ }
259
+
260
+ fs.writeFileSync(targetPath, buffer);
261
+
262
+ // Set executable permissions on Unix-like systems
263
+ if (process.platform !== 'win32') {
264
+ fs.chmodSync(targetPath, 0o755);
265
+ }
266
+
267
+ return true;
268
+ }
269
+
270
+ /**
271
+ * Compute SHA-256 hash of a file.
272
+ * @param {string} filePath
273
+ * @returns {string}
274
+ */
275
+ function hashFile(filePath) {
276
+ const content = fs.readFileSync(filePath);
277
+ return crypto.createHash('sha256').update(content).digest('hex');
278
+ }
279
+
280
+ /**
281
+ * Override fs module methods to intercept file access for extracted binaries.
282
+ * This provides a generic solution that works with any native module loading pattern
283
+ * (bindings package, direct require, etc.)
284
+ * @param {string} cacheDir - Cache directory where binaries are extracted
285
+ * @param {Object.<string, string>} assetPathMap - Map of asset key -> extracted path
286
+ */
287
+ /**
288
+ * Override Module._resolveFilename and Module._load to redirect native module loads to extracted cache.
289
+ * @param {Object.<string, string>} nativeModuleMap - Map of module name -> extracted path
290
+ */
291
+ function overrideModuleResolution(nativeModuleMap) {
292
+ const originalResolveFilename = Module._resolveFilename;
293
+ const originalLoad = Module._load;
294
+
295
+ // Override _resolveFilename for normal resolution
296
+ Module._resolveFilename = function (request, parent, isMain, options) {
297
+ // Normalize the request path
298
+ const normalized = path.normalize(request);
299
+
300
+ // Check direct match
301
+ if (nativeModuleMap[request]) {
302
+ return nativeModuleMap[request];
303
+ }
304
+
305
+ // Check for basename match
306
+ const basename = path.basename(request);
307
+ if (nativeModuleMap[basename]) {
308
+ return nativeModuleMap[basename];
309
+ }
310
+
311
+ // Check if the request includes any of our native modules
312
+ for (const [moduleName, extractedPath] of Object.entries(nativeModuleMap)) {
313
+ if (request.endsWith(moduleName) || request.includes(moduleName) ||
314
+ normalized.includes(moduleName.replace(/\//g, path.sep))) {
315
+ return extractedPath;
316
+ }
317
+ }
318
+
319
+ // Fall back to original resolution
320
+ return originalResolveFilename.call(this, request, parent, isMain, options);
321
+ };
322
+
323
+ // Override _load to intercept at load time (catches SEA embedderRequire)
324
+ Module._load = function (request, parent, isMain) {
325
+ // Normalize the request path
326
+ const normalized = path.normalize(request);
327
+
328
+ // Check direct match
329
+ if (nativeModuleMap[request]) {
330
+ return originalLoad.call(this, nativeModuleMap[request], parent, isMain);
331
+ }
332
+
333
+ // Check for basename match
334
+ const basename = path.basename(request);
335
+ if (nativeModuleMap[basename]) {
336
+ return originalLoad.call(this, nativeModuleMap[basename], parent, isMain);
337
+ }
338
+
339
+ // Check if the request includes any of our native modules
340
+ for (const [moduleName, extractedPath] of Object.entries(nativeModuleMap)) {
341
+ if (request.endsWith(moduleName) || request.includes(moduleName) ||
342
+ normalized.includes(moduleName.replace(/\//g, path.sep))) {
343
+ return originalLoad.call(this, extractedPath, parent, isMain);
344
+ }
345
+ }
346
+
347
+ // Fall back to original load
348
+ return originalLoad.call(this, request, parent, isMain);
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Bootstrap the SEA application.
354
+ * Extracts binaries and sets up module resolution.
355
+ */
356
+ function bootstrap() {
357
+ if (!sea) {
358
+ throw new Error('This script must run in a SEA (Single Executable Application) context');
359
+ }
360
+
361
+ // Patch sea.getAsset to handle decryption transparently
362
+ if (typeof SEA_ENCRYPTED_ASSETS !== 'undefined' && typeof SEA_ENCRYPTION_KEY !== 'undefined') {
363
+ const originalGetAsset = sea.getAsset.bind(sea);
364
+ const originalGetRawAsset = sea.getRawAsset.bind(sea);
365
+
366
+ sea.getAsset = function(assetKey, encoding) {
367
+ const isEncrypted = SEA_ENCRYPTED_ASSETS.has(assetKey);
368
+
369
+ if (isEncrypted) {
370
+ // Get raw encrypted data
371
+ const encryptedData = originalGetRawAsset(assetKey);
372
+ const buffer = Buffer.isBuffer(encryptedData) ? encryptedData : Buffer.from(encryptedData);
373
+
374
+ // Decrypt it
375
+ const decrypted = decryptAsset(buffer, SEA_ENCRYPTION_KEY);
376
+
377
+ // Apply encoding if requested
378
+ if (encoding) {
379
+ return decrypted.toString(encoding);
380
+ }
381
+ return decrypted;
382
+ } else {
383
+ // Not encrypted, use original method
384
+ return originalGetAsset(assetKey, encoding);
385
+ }
386
+ };
387
+ }
388
+
389
+ // Load the manifest from SEA assets (manifest is never encrypted)
390
+ const manifestJson = sea.getAsset('sea-manifest.json', 'utf8');
391
+ if (!manifestJson) {
392
+ throw new Error('SEA manifest not found in blob');
393
+ }
394
+
395
+ const manifest = JSON.parse(manifestJson);
396
+ const verbose = process.env.SEA_VERBOSE === 'true';
397
+
398
+ if (verbose) {
399
+ console.log('SEA Bootstrap:');
400
+ console.log(` App: ${manifest.appName} v${manifest.appVersion}`);
401
+ console.log(` Platform: ${manifest.platform}-${manifest.arch}`);
402
+ console.log(` Binaries to extract: ${manifest.binaries.length}`);
403
+ }
404
+
405
+ // Filter binaries for current platform
406
+ const platformBinaries = manifest.binaries.filter(b =>
407
+ (b.platform === '*' || b.platform === process.platform) &&
408
+ (b.arch === '*' || b.arch === process.arch)
409
+ );
410
+
411
+ // Sort by extraction order
412
+ platformBinaries.sort((a, b) => a.order - b.order);
413
+
414
+ const cacheDir = getExtractionCacheDir(
415
+ manifest.appName,
416
+ manifest.appVersion,
417
+ manifest.platform,
418
+ manifest.arch
419
+ );
420
+
421
+ const nativeModuleMap = {};
422
+
423
+ // Extract binaries
424
+ for (const binary of platformBinaries) {
425
+ const targetPath = path.join(cacheDir, binary.fileName);
426
+
427
+ if (verbose) {
428
+ console.log(` Extracting: ${binary.assetKey} -> ${targetPath}`);
429
+ }
430
+
431
+ extractBinary(binary.assetKey, targetPath, binary.hash);
432
+
433
+ // Map the module name to extracted path
434
+ nativeModuleMap[binary.fileName] = targetPath;
435
+
436
+ // Also map without extension for easier resolution
437
+ const nameWithoutExt = path.basename(binary.fileName, path.extname(binary.fileName));
438
+ nativeModuleMap[nameWithoutExt] = targetPath;
439
+
440
+ // PHASE 2: Populate global asset map for fs overrides
441
+ assetPathMapGlobal[binary.assetKey] = targetPath;
442
+ }
443
+
444
+ // PHASE 2: Set global cache directory
445
+ cacheDirGlobal = cacheDir;
446
+
447
+ if (verbose) {
448
+ console.log('✓ Binary extraction complete');
449
+ console.log(` Cache directory: ${cacheDir}`);
450
+ console.log(` Asset mappings: ${Object.keys(assetPathMapGlobal).length}`);
451
+ }
452
+
453
+ // On Windows, add the cache directory to DLL search path
454
+ if (process.platform === 'win32') {
455
+ // Add to PATH so that native addons can find dependent DLLs
456
+ process.env.PATH = `${cacheDir};${process.env.PATH}`;
457
+
458
+ if (verbose) {
459
+ console.log(` Added to PATH: ${cacheDir}`);
460
+ }
461
+
462
+ // Also try to add DLL directory using SetDllDirectory (if available)
463
+ try {
464
+ // Windows-specific: preload all DLLs to ensure they're in the process
465
+ platformBinaries
466
+ .filter(b => b.fileName.endsWith('.dll'))
467
+ .forEach(b => {
468
+ const dllPath = path.join(cacheDir, b.fileName);
469
+ if (verbose) {
470
+ console.log(` Preloading DLL: ${dllPath}`);
471
+ }
472
+ // Use process.dlopen to preload the DLL
473
+ try {
474
+ process.dlopen({ exports: {} }, dllPath);
475
+ } catch (err) {
476
+ // DLL might not be a valid node addon, which is fine
477
+ if (verbose) {
478
+ console.log(` (Non-addon DLL, will be loaded by OS loader)`);
479
+ }
480
+ }
481
+ });
482
+ } catch (err) {
483
+ console.warn('Warning: Could not preload DLLs:', err.message);
484
+ }
485
+ }
486
+
487
+ // Set environment variable for cache directory (used by applications)
488
+ process.env.SEA_CACHE_DIR = cacheDir;
489
+
490
+ // Export native module map and cache dir to global for require override in app code
491
+ global.__seaNativeModuleMap = nativeModuleMap;
492
+ global.__seaCacheDir = cacheDir;
493
+
494
+ // Add cache directory to module paths so bindings can find extracted .node files
495
+ if (!Module.globalPaths.includes(cacheDir)) {
496
+ Module.globalPaths.unshift(cacheDir);
497
+ }
498
+
499
+ // Override module resolution
500
+ overrideModuleResolution(nativeModuleMap);
501
+
502
+ // Provide a 'bindings' module shim for SEA compatibility
503
+ // This gets injected into the module cache so require('bindings') works
504
+ const bindingsShim = function(name) {
505
+ // Ensure .node extension
506
+ if (!name.endsWith('.node')) {
507
+ name += '.node';
508
+ }
509
+
510
+ if (verbose) {
511
+ console.log(`[bindings shim] Loading native module: ${name}`);
512
+ }
513
+
514
+ // Try to load from native module map
515
+ if (nativeModuleMap[name]) {
516
+ const exports = {};
517
+ process.dlopen({ exports }, nativeModuleMap[name]);
518
+ return exports;
519
+ }
520
+
521
+ // Try basename without .node
522
+ const baseName = path.basename(name, '.node');
523
+ if (nativeModuleMap[baseName]) {
524
+ const exports = {};
525
+ process.dlopen({ exports }, nativeModuleMap[baseName]);
526
+ return exports;
527
+ }
528
+
529
+ throw new Error(`Could not load native module "${name}" - not found in SEA cache`);
530
+ };
531
+
532
+ // Inject bindings into module cache
533
+ Module._cache['bindings'] = {
534
+ id: 'bindings',
535
+ exports: bindingsShim,
536
+ loaded: true
537
+ };
538
+
539
+ if (verbose) {
540
+ console.log('[SEA] Injected bindings shim into module cache');
541
+ }
542
+
543
+ // CRITICAL: Override the GLOBAL require function (not just Module.prototype.require)
544
+ // In SEA context, the bundled app code uses the global require which we CAN override
545
+ const originalGlobalRequire = global.require;
546
+
547
+ if (verbose) {
548
+ console.log('[SEA] About to override global.require');
549
+ console.log('[SEA] Original global.require type:', typeof originalGlobalRequire);
550
+ console.log('[SEA] Original global.require name:', originalGlobalRequire?.name);
551
+ }
552
+
553
+ global.require = function seaRequire(id) {
554
+ // Check if this is a .node file
555
+ if (typeof id === 'string' && id.endsWith('.node')) {
556
+ if (verbose) {
557
+ console.log(`[SEA require] Intercepted .node require: ${id}`);
558
+ }
559
+
560
+ // Get the basename
561
+ const basename = path.basename(id);
562
+
563
+ // Check if we have this in our native module map
564
+ if (nativeModuleMap[basename]) {
565
+ if (verbose) {
566
+ console.log(`[SEA require] Loading from cache via dlopen: ${nativeModuleMap[basename]}`);
567
+ }
568
+ const exports = {};
569
+ process.dlopen({ exports }, nativeModuleMap[basename]);
570
+ return exports;
571
+ }
572
+
573
+ // Check if absolute path is in cache
574
+ if (path.isAbsolute(id) && id.startsWith(cacheDir)) {
575
+ if (verbose) {
576
+ console.log(`[SEA require] Loading cache path via dlopen: ${id}`);
577
+ }
578
+ const exports = {};
579
+ process.dlopen({ exports }, id);
580
+ return exports;
581
+ }
582
+
583
+ // Check asset map for path matching
584
+ for (const [assetKey, extractedPath] of Object.entries(assetPathMapGlobal)) {
585
+ if (id.includes(assetKey) || basename === path.basename(assetKey)) {
586
+ if (verbose) {
587
+ console.log(`[SEA require] Loading from asset map via dlopen: ${extractedPath}`);
588
+ }
589
+ const exports = {};
590
+ process.dlopen({ exports }, extractedPath);
591
+ return exports;
592
+ }
593
+ }
594
+ }
595
+
596
+ // For all other requires, use the original
597
+ return originalGlobalRequire.call(this, id);
598
+ };
599
+
600
+ if (verbose) {
601
+ console.log('[SEA] global.require has been overridden');
602
+ console.log('[SEA] New global.require type:', typeof global.require);
603
+ console.log('[SEA] New global.require name:', global.require.name);
604
+ }
605
+
606
+ // Also override Module.prototype.require for completeness
607
+ const originalModuleRequire = Module.prototype.require;
608
+ Module.prototype.require = function(id) {
609
+ // Delegate to our global require override
610
+ return global.require(id);
611
+ };
612
+ }
613
+
614
+ // Run bootstrap if in SEA context
615
+ if (isSEA && sea) {
616
+ // Check if we're building a snapshot
617
+ let isBuildingSnapshot = false;
618
+ try {
619
+ const v8 = require('v8');
620
+ isBuildingSnapshot = v8.startupSnapshot && v8.startupSnapshot.isBuildingSnapshot && v8.startupSnapshot.isBuildingSnapshot();
621
+ } catch (err) {
622
+ // v8.startupSnapshot not available
623
+ }
624
+
625
+ if (isBuildingSnapshot) {
626
+ console.log('[SEA Bootstrap] Snapshot build mode - setting up deserialization callback');
627
+ // During snapshot build: set up callback to run bootstrap at runtime
628
+ const v8 = require('v8');
629
+ const originalCallback = v8.startupSnapshot.setDeserializeMainFunction;
630
+
631
+ // Intercept setDeserializeMainFunction to add bootstrap before app code
632
+ v8.startupSnapshot.setDeserializeMainFunction = function(callback) {
633
+ originalCallback.call(this, () => {
634
+ console.log('[SEA Bootstrap] Running at deserialization');
635
+ bootstrap();
636
+ // Then run the application callback
637
+ callback();
638
+ });
639
+ };
640
+ } else {
641
+ // Normal runtime: run bootstrap immediately
642
+ bootstrap();
643
+ }
644
+ } else {
645
+ console.log('[SEA Bootstrap] Skipping extraction (not in SEA)');
646
+ }
647
+
648
+ })(); // End bootstrap IIFE
649
+
650
+ // Export for testing (only accessible if loaded as module)
651
+ if (typeof module !== 'undefined' && module.exports) {
652
+ module.exports = {
653
+ // Empty exports - bootstrap runs automatically
654
+ };
655
+ }