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.
package/lib/build.js ADDED
@@ -0,0 +1,283 @@
1
+ /**
2
+ * build.js
3
+ * Main orchestrator for SEA build pipeline.
4
+ * Usage: node scripts/sea/build.js [--config <path>] [--verbose]
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const { loadConfig, parseTarget } = require('./config');
10
+ const { scanAssets, groupAssets } = require('./scanner');
11
+ const { generateManifest, serializeManifest } = require('./manifest');
12
+ const { createSeaConfig, writeSeaConfigJson, generateBlob } = require('./blob');
13
+ const { fetchNodeBinary } = require('./fetch-node');
14
+ const { injectBlob } = require('./inject');
15
+ const { generateEncryptionKey, encryptAssets, keyToObfuscatedCode } = require('./crypto-assets');
16
+ const { obfuscateBootstrap } = require('./obfuscate');
17
+ const { execSync } = require('child_process');
18
+
19
+ /**
20
+ * Main build orchestrator.
21
+ * @param {Object} options - Build options
22
+ * @param {string} [options.configPath] - Path to config file
23
+ * @param {boolean} [options.verbose] - Enable verbose logging
24
+ * @param {boolean} [options.debug] - Keep temporary build files
25
+ * @param {string} [options.projectRoot] - Project root directory
26
+ */
27
+ async function build(options = {}) {
28
+ // Support both options object and legacy process.argv parsing
29
+ let configPath = options.configPath;
30
+ let verbose = options.verbose;
31
+ let debug = options.debug;
32
+ let projectRoot = options.projectRoot || process.cwd();
33
+
34
+ // Fallback to process.argv if called without options
35
+ if (!options.configPath && !options.verbose && !options.debug) {
36
+ const args = process.argv.slice(2);
37
+ configPath = args.includes('--config') ? args[args.indexOf('--config') + 1] : null;
38
+ verbose = args.includes('--verbose');
39
+ debug = args.includes('--debug');
40
+ }
41
+
42
+ if (!verbose) {
43
+ console.log('Building SEA...');
44
+ }
45
+
46
+ const config = loadConfig(configPath, projectRoot);
47
+
48
+ if (verbose) {
49
+ console.log('\n[1/8] Loading configuration');
50
+ console.log('Config:', JSON.stringify(config, null, 2));
51
+ }
52
+
53
+ const target = config.targets[0];
54
+ const { nodeVersion, platform, arch } = parseTarget(target);
55
+
56
+ if (verbose) {
57
+ console.log(`Target: Node ${nodeVersion} on ${platform}-${arch}`);
58
+ }
59
+
60
+ if (config.rebuild) {
61
+ if (verbose) console.log('\n[1b] Rebuilding native modules');
62
+ const rebuildScript = path.join(__dirname, '..', 'bin', 'seabox-rebuild.js');
63
+ try {
64
+ execSync(`node "${rebuildScript}" --target ${target} "${projectRoot}"`, {
65
+ stdio: verbose ? 'inherit' : 'ignore'
66
+ });
67
+ if (verbose) console.log('✓ Native modules rebuilt');
68
+ } catch (error) {
69
+ console.error('✗ Native module rebuild failed:', error.message);
70
+ throw new Error('Native module rebuild failed. See output above for details.');
71
+ }
72
+ }
73
+
74
+ const assets = await scanAssets(
75
+ config.assets,
76
+ config.binaries || [],
77
+ config.exclude || [],
78
+ projectRoot
79
+ );
80
+
81
+ const { binaries, nonBinaries } = groupAssets(assets);
82
+
83
+ if (verbose) {
84
+ console.log(`\n[2/8] Scanning assets`);
85
+ console.log(`Found ${assets.length} assets (${binaries.length} binaries, ${nonBinaries.length} non-binaries)`);
86
+ console.log('Binaries:', binaries.map(b => b.assetKey));
87
+ const nonBinariesList = assets.filter(a => !a.isBinary);
88
+ console.log('Non-binary assets:', nonBinariesList.map(a => a.assetKey));
89
+ }
90
+
91
+ let encryptionKey = null;
92
+ let encryptedAssetKeys = new Set();
93
+
94
+ if (config.encryptAssets) {
95
+ if (verbose) console.log('\n[2b] Encrypting assets');
96
+ encryptionKey = generateEncryptionKey();
97
+
98
+ const excludeFromEncryption = [
99
+ 'sea-manifest.json',
100
+ ...(config.encryptExclude || [])
101
+ ];
102
+
103
+ const encryptedMap = encryptAssets(assets, encryptionKey, excludeFromEncryption);
104
+
105
+ for (const asset of assets) {
106
+ if (encryptedMap.has(asset.assetKey)) {
107
+ asset.content = encryptedMap.get(asset.assetKey);
108
+ asset.encrypted = true;
109
+ encryptedAssetKeys.add(asset.assetKey);
110
+ }
111
+ }
112
+
113
+ if (verbose) {
114
+ console.log(`Encrypted ${encryptedAssetKeys.size} assets`);
115
+ console.log('Encrypted assets:', Array.from(encryptedAssetKeys));
116
+ }
117
+ }
118
+
119
+ if (verbose) console.log('\n[3/8] Generating manifest');
120
+ const manifest = generateManifest(assets, config, platform, arch);
121
+ const manifestJson = serializeManifest(manifest);
122
+
123
+ const manifestAsset = {
124
+ sourcePath: null,
125
+ assetKey: 'sea-manifest.json',
126
+ isBinary: false,
127
+ content: Buffer.from(manifestJson, 'utf8')
128
+ };
129
+ assets.push(manifestAsset);
130
+
131
+ if (verbose) console.log('\n[4/8] Preparing bootstrap');
132
+ const bootstrapPath = path.join(__dirname, 'bootstrap.js');
133
+ const bootstrapContent = fs.readFileSync(bootstrapPath, 'utf8');
134
+
135
+ let augmentedBootstrap = bootstrapContent;
136
+ if (config.encryptAssets && encryptionKey && encryptedAssetKeys.size > 0) {
137
+ const keyCode = keyToObfuscatedCode(encryptionKey);
138
+ const encryptedKeysJson = JSON.stringify(Array.from(encryptedAssetKeys));
139
+
140
+ const encryptionSetup = `
141
+ const SEA_ENCRYPTION_KEY = ${keyCode};
142
+ const SEA_ENCRYPTED_ASSETS = new Set(${encryptedKeysJson});
143
+ `;
144
+
145
+ augmentedBootstrap = bootstrapContent.replace(
146
+ " 'use strict';",
147
+ " 'use strict';\n" + encryptionSetup
148
+ );
149
+
150
+ if (verbose) console.log('Injecting encryption key and obfuscating bootstrap');
151
+ augmentedBootstrap = obfuscateBootstrap(augmentedBootstrap);
152
+ }
153
+
154
+ const entryPath = path.resolve(projectRoot, config.entry);
155
+ let entryContent = fs.readFileSync(entryPath, 'utf8');
156
+
157
+ // Strip shebang if present (e.g., #!/usr/bin/env node) - handle both Unix and Windows line endings
158
+ entryContent = entryContent.replace(/^#!.*(\r?\n)/, '');
159
+
160
+ // When using snapshot, wrap the application in v8.startupSnapshot.setDeserializeMainFunction
161
+ let bundledEntry;
162
+ if (config.useSnapshot) {
163
+ // Snapshot mode: Bootstrap runs at build time to set up deserialization callback
164
+ // Application code runs at runtime inside the callback
165
+
166
+ bundledEntry = `${augmentedBootstrap}\n\n`;
167
+ bundledEntry += `(function() {\n`;
168
+ bundledEntry += ` const v8 = require('v8');\n`;
169
+ bundledEntry += ` if (v8.startupSnapshot && v8.startupSnapshot.isBuildingSnapshot()) {\n`;
170
+ bundledEntry += ` v8.startupSnapshot.setDeserializeMainFunction(() => {\n`;
171
+ bundledEntry += entryContent + '\n';
172
+ bundledEntry += ` });\n`;
173
+ bundledEntry += ` } else {\n`;
174
+ bundledEntry += entryContent + '\n';
175
+ bundledEntry += ` }\n`;
176
+ bundledEntry += `})();\n`;
177
+ } else {
178
+ const requireOverride = `
179
+ (function() {
180
+ const originalRequire = require;
181
+ const __seaNativeModuleMap = typeof global.__seaNativeModuleMap !== 'undefined' ? global.__seaNativeModuleMap : {};
182
+ const __seaCacheDir = typeof global.__seaCacheDir !== 'undefined' ? global.__seaCacheDir : '';
183
+
184
+ require = function(id) {
185
+ if (typeof id === 'string' && id.endsWith('.node')) {
186
+ const path = originalRequire('path');
187
+ const basename = path.basename(id);
188
+
189
+ if (__seaNativeModuleMap[basename]) {
190
+ const exports = {};
191
+ process.dlopen({ exports }, __seaNativeModuleMap[basename]);
192
+ return exports;
193
+ }
194
+
195
+ if (path.isAbsolute(id) && id.startsWith(__seaCacheDir)) {
196
+ const exports = {};
197
+ process.dlopen({ exports }, id);
198
+ return exports;
199
+ }
200
+ }
201
+
202
+ if (id === 'bindings') {
203
+ return function(name) {
204
+ if (!name.endsWith('.node')) {
205
+ name += '.node';
206
+ }
207
+ if (__seaNativeModuleMap[name]) {
208
+ const exports = {};
209
+ process.dlopen({ exports }, __seaNativeModuleMap[name]);
210
+ return exports;
211
+ }
212
+ throw new Error('Could not load native module "' + name + '" - not found in SEA cache');
213
+ };
214
+ }
215
+
216
+ return originalRequire.call(this, id);
217
+ };
218
+ })();
219
+ `;
220
+ bundledEntry = `${augmentedBootstrap}\n\n${requireOverride}\n${entryContent}`;
221
+ }
222
+
223
+ const bundledEntryPath = path.join(projectRoot, 'out', '_sea-entry.js');
224
+
225
+ fs.mkdirSync(path.dirname(bundledEntryPath), { recursive: true });
226
+ fs.writeFileSync(bundledEntryPath, bundledEntry, 'utf8');
227
+
228
+ if (verbose) console.log(`Bundled entry: ${bundledEntryPath}`);
229
+
230
+ if (verbose) console.log('\n[5/8] Creating SEA blob configuration');
231
+ const blobOutputPath = path.join(projectRoot, config.outputPath, 'sea-blob.bin');
232
+ const seaConfig = createSeaConfig(bundledEntryPath, blobOutputPath, assets, config);
233
+
234
+ const seaConfigPath = path.join(projectRoot, config.outputPath, 'sea-config.json');
235
+ const tempDir = path.join(projectRoot, config.outputPath, '.sea-temp');
236
+ fs.mkdirSync(path.dirname(seaConfigPath), { recursive: true });
237
+ fs.mkdirSync(tempDir, { recursive: true });
238
+ writeSeaConfigJson(seaConfig, seaConfigPath, assets, tempDir);
239
+
240
+ if (verbose) {
241
+ console.log(`SEA config: ${seaConfigPath}`);
242
+ console.log('\n[6/8] Generating SEA blob');
243
+ }
244
+ await generateBlob(seaConfigPath);
245
+
246
+ if (verbose) console.log('\n[7/8] Fetching Node.js binary');
247
+ const cacheDir = path.join(projectRoot, 'node_modules', '.cache', 'sea-node-binaries');
248
+ const nodeBinary = await fetchNodeBinary(nodeVersion, platform, arch, cacheDir);
249
+
250
+ if (verbose) console.log('\n[8/8] Injecting SEA blob into Node binary');
251
+ const outputExe = path.join(projectRoot, config.outputPath, config.output);
252
+ await injectBlob(nodeBinary, blobOutputPath, outputExe, platform, verbose);
253
+
254
+ if (!debug) {
255
+ if (verbose) console.log('\nCleaning up temporary files');
256
+ const filesToClean = [seaConfigPath, blobOutputPath];
257
+ for (const file of filesToClean) {
258
+ if (fs.existsSync(file)) {
259
+ fs.unlinkSync(file);
260
+ }
261
+ }
262
+ if (fs.existsSync(tempDir)) {
263
+ fs.rmSync(tempDir, { recursive: true, force: true });
264
+ }
265
+ }
266
+
267
+ const sizeMB = (fs.statSync(outputExe).size / 1024 / 1024).toFixed(2);
268
+ console.log(`\n✓ Build complete: ${config.output} (${sizeMB} MB)`);
269
+ if (verbose) console.log(`Output path: ${outputExe}`);
270
+ }
271
+
272
+ // Run if called directly
273
+ if (require.main === module) {
274
+ build().catch(error => {
275
+ console.error('Build failed:', error.message);
276
+ if (process.argv.includes('--verbose')) {
277
+ console.error(error.stack);
278
+ }
279
+ process.exit(1);
280
+ });
281
+ }
282
+
283
+ module.exports = { build };
package/lib/config.js ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * config.js
3
+ * Load and validate SEA configuration from package.json or standalone config file.
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * @typedef {Object} SeaConfig
11
+ * @property {string} entry - Path to the bundled application entry script
12
+ * @property {string[]} assets - Array of glob patterns or paths to include in SEA blob (supports '!' prefix for exclusions)
13
+ * @property {string[]} targets - Array of target specifiers (e.g., "node24.11.0-win-x64")
14
+ * @property {string} output - Output executable filename
15
+ * @property {string} outputPath - Directory for the final executable
16
+ * @property {string[]} [binaries] - Array of binary filename patterns to extract at runtime
17
+ * @property {boolean} [disableExperimentalSEAWarning] - Suppress SEA experimental warning
18
+ * @property {boolean} [useSnapshot] - Enable V8 snapshot
19
+ * @property {boolean} [useCodeCache] - Enable V8 code cache
20
+ * @property {string[]} [exclude] - (Legacy) Glob patterns to exclude from assets - use '!' prefix in assets instead
21
+ * @property {boolean} [encryptAssets] - Enable asset encryption (default: false)
22
+ * @property {string[]} [encryptExclude] - Asset patterns to exclude from encryption
23
+ * @property {boolean} [rebuild] - Automatically rebuild native modules for target platform (default: false)
24
+ * @property {boolean} [verbose] - Enable diagnostic logging
25
+ */
26
+
27
+ /**
28
+ * Load SEA configuration from package.json or a standalone file.
29
+ * @param {string} [configPath] - Optional path to a standalone config file
30
+ * @param {string} [projectRoot] - Project root directory (defaults to cwd)
31
+ * @returns {SeaConfig}
32
+ */
33
+ function loadConfig(configPath, projectRoot = process.cwd()) {
34
+ let config;
35
+
36
+ if (configPath) {
37
+ // Load from standalone config file
38
+ const fullPath = path.resolve(projectRoot, configPath);
39
+ if (!fs.existsSync(fullPath)) {
40
+ throw new Error(`Config file not found: ${fullPath}`);
41
+ }
42
+ config = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
43
+ } else {
44
+ // Load from package.json
45
+ const pkgPath = path.join(projectRoot, 'package.json');
46
+ if (!fs.existsSync(pkgPath)) {
47
+ throw new Error(`package.json not found in: ${projectRoot}`);
48
+ }
49
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
50
+
51
+ // Check for 'sea' field first, fall back to 'pkg' for migration compatibility
52
+ config = pkg.seabox || pkg.pkg;
53
+
54
+ if (!config) {
55
+ throw new Error('No "seabox" or "pkg" configuration found in package.json');
56
+ }
57
+
58
+ // Attach package metadata for manifest
59
+ config._packageName = pkg.name;
60
+ config._packageVersion = pkg.version;
61
+ }
62
+
63
+ validateConfig(config);
64
+ return config;
65
+ }
66
+
67
+ /**
68
+ * Validate required configuration fields.
69
+ * @param {SeaConfig} config
70
+ */
71
+ function validateConfig(config) {
72
+ const required = ['entry', 'assets', 'targets', 'output', 'outputPath'];
73
+ const missing = required.filter(field => !config[field]);
74
+
75
+ if (missing.length > 0) {
76
+ throw new Error(`Missing required SEA config fields: ${missing.join(', ')}`);
77
+ }
78
+
79
+ if (!Array.isArray(config.assets) || config.assets.length === 0) {
80
+ throw new Error('SEA config "assets" must be a non-empty array');
81
+ }
82
+
83
+ if (!Array.isArray(config.targets) || config.targets.length === 0) {
84
+ throw new Error('SEA config "targets" must be a non-empty array');
85
+ }
86
+
87
+ // Validate target format (e.g., "node24.11.0-win-x64")
88
+ const targetPattern = /^node\d+\.\d+\.\d+-\w+-\w+$/;
89
+ config.targets.forEach(target => {
90
+ if (!targetPattern.test(target)) {
91
+ throw new Error(`Invalid target format: "${target}". Expected format: "nodeX.Y.Z-platform-arch"`);
92
+ }
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Parse a target string into components.
98
+ * @param {string} target - e.g., "node24.11.0-win-x64"
99
+ * @returns {{nodeVersion: string, platform: string, arch: string}}
100
+ */
101
+ function parseTarget(target) {
102
+ const match = target.match(/^node(\d+\.\d+\.\d+)-(\w+)-(\w+)$/);
103
+ if (!match) {
104
+ throw new Error(`Cannot parse target: ${target}`);
105
+ }
106
+ return {
107
+ nodeVersion: match[1],
108
+ platform: match[2],
109
+ arch: match[3]
110
+ };
111
+ }
112
+
113
+ module.exports = {
114
+ loadConfig,
115
+ validateConfig,
116
+ parseTarget
117
+ };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * crypto-assets.js
3
+ * Asset encryption/decryption utilities for SEA applications.
4
+ * The encryption key is embedded in the bootstrap code and, when using snapshots,
5
+ * becomes part of the V8 bytecode, providing obfuscation.
6
+ */
7
+
8
+ const crypto = require('crypto');
9
+
10
+ /**
11
+ * Generate a random encryption key.
12
+ * @returns {Buffer} - 32-byte key for AES-256
13
+ */
14
+ function generateEncryptionKey() {
15
+ return crypto.randomBytes(32);
16
+ }
17
+
18
+ /**
19
+ * Encrypt asset data using AES-256-GCM.
20
+ * @param {Buffer} data - Asset data to encrypt
21
+ * @param {Buffer} key - 32-byte encryption key
22
+ * @returns {Buffer} - Encrypted data with IV and auth tag prepended
23
+ */
24
+ function encryptAsset(data, key) {
25
+ // Generate a random IV for this encryption
26
+ const iv = crypto.randomBytes(16);
27
+
28
+ // Create cipher
29
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
30
+
31
+ // Encrypt the data
32
+ const encrypted = Buffer.concat([
33
+ cipher.update(data),
34
+ cipher.final()
35
+ ]);
36
+
37
+ // Get the authentication tag
38
+ const authTag = cipher.getAuthTag();
39
+
40
+ // Format: [IV (16 bytes)] + [Auth Tag (16 bytes)] + [Encrypted Data]
41
+ return Buffer.concat([iv, authTag, encrypted]);
42
+ }
43
+
44
+ /**
45
+ * Decrypt asset data using AES-256-GCM.
46
+ * @param {Buffer} encryptedData - Encrypted data with IV and auth tag prepended
47
+ * @param {Buffer} key - 32-byte encryption key
48
+ * @returns {Buffer} - Decrypted data
49
+ */
50
+ function decryptAsset(encryptedData, key) {
51
+ // Extract IV, auth tag, and encrypted content
52
+ const iv = encryptedData.slice(0, 16);
53
+ const authTag = encryptedData.slice(16, 32);
54
+ const encrypted = encryptedData.slice(32);
55
+
56
+ // Create decipher
57
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
58
+ decipher.setAuthTag(authTag);
59
+
60
+ // Decrypt the data
61
+ const decrypted = Buffer.concat([
62
+ decipher.update(encrypted),
63
+ decipher.final()
64
+ ]);
65
+
66
+ return decrypted;
67
+ }
68
+
69
+ /**
70
+ * Encrypt multiple assets.
71
+ * @param {import('./scanner').AssetEntry[]} assets - Assets to encrypt
72
+ * @param {Buffer} key - Encryption key
73
+ * @param {string[]} [excludePatterns] - Patterns to exclude from encryption (e.g., ['*.txt'])
74
+ * @returns {Map<string, Buffer>} - Map of asset key to encrypted content
75
+ */
76
+ function encryptAssets(assets, key, excludePatterns = []) {
77
+ const encrypted = new Map();
78
+
79
+ for (const asset of assets) {
80
+ // Skip binaries - they need to be extracted as-is
81
+ if (asset.isBinary) {
82
+ continue;
83
+ }
84
+
85
+ // Check if this asset should be excluded from encryption
86
+ let shouldExclude = false;
87
+ for (const pattern of excludePatterns) {
88
+ if (asset.assetKey.includes(pattern) || asset.assetKey.endsWith(pattern)) {
89
+ shouldExclude = true;
90
+ break;
91
+ }
92
+ }
93
+
94
+ if (shouldExclude) {
95
+ continue;
96
+ }
97
+
98
+ // Get asset content
99
+ const content = asset.content || require('fs').readFileSync(asset.sourcePath);
100
+
101
+ // Encrypt it
102
+ const encryptedContent = encryptAsset(content, key);
103
+ encrypted.set(asset.assetKey, encryptedContent);
104
+ }
105
+
106
+ return encrypted;
107
+ }
108
+
109
+ /**
110
+ * Generate the encryption key as a code string for embedding in bootstrap.
111
+ * Returns a string that evaluates to a Buffer containing the key.
112
+ * @param {Buffer} key - Encryption key
113
+ * @returns {string} - JavaScript code that creates the key Buffer
114
+ */
115
+ function keyToCode(key) {
116
+ // Convert key to hex string for embedding
117
+ const hexKey = key.toString('hex');
118
+
119
+ // Return code that reconstructs the Buffer
120
+ // Using multiple obfuscation techniques:
121
+ // 1. Split the hex string
122
+ // 2. Use Array.from with character codes
123
+ // 3. XOR with a simple constant (adds one more layer)
124
+
125
+ const parts = [];
126
+ for (let i = 0; i < hexKey.length; i += 8) {
127
+ parts.push(hexKey.slice(i, i + 8));
128
+ }
129
+
130
+ return `Buffer.from('${hexKey}', 'hex')`;
131
+ }
132
+
133
+ /**
134
+ * Generate a more obfuscated version of the key embedding code.
135
+ * This version splits the key and uses character code manipulation.
136
+ * @param {Buffer} key - Encryption key
137
+ * @returns {string} - Obfuscated JavaScript code
138
+ */
139
+ function keyToObfuscatedCode(key) {
140
+ // Convert to array of byte values
141
+ const bytes = Array.from(key);
142
+
143
+ // Create a simple XOR mask
144
+ const xorMask = 0x5A;
145
+
146
+ // XOR each byte with the mask
147
+ const masked = bytes.map(b => b ^ xorMask);
148
+
149
+ // Return code that reconstructs the key
150
+ return `Buffer.from([${masked.join(',')}].map(b => b ^ 0x${xorMask.toString(16)}))`;
151
+ }
152
+
153
+ module.exports = {
154
+ generateEncryptionKey,
155
+ encryptAsset,
156
+ decryptAsset,
157
+ encryptAssets,
158
+ keyToCode,
159
+ keyToObfuscatedCode
160
+ };