seabox 0.1.0-beta.2 → 0.1.0-beta.4

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 DELETED
@@ -1,283 +0,0 @@
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 };
@@ -1,56 +0,0 @@
1
- /*MIT License
2
-
3
- Copyright (c) 2018 Osama Abbas
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.*/
22
-
23
- module.exports.fixBytecode = function (bytecodeBuffer) {
24
- if (!isBuffer(bytecodeBuffer)) {
25
- throw new Error('bytecodeBuffer must be a buffer object.');
26
- }
27
-
28
- const dummyBytecode = compileCode('"ಠ_ಠ"');
29
- const version = parseFloat(process.version.slice(1, 5));
30
-
31
- if (process.version.startsWith('v8.8') || process.version.startsWith('v8.9')) {
32
- // Node is v8.8.x or v8.9.x
33
- dummyBytecode.subarray(16, 20).copy(bytecodeBuffer, 16);
34
- dummyBytecode.subarray(20, 24).copy(bytecodeBuffer, 20);
35
- } else if (version >= 12 && version <= 23) {
36
- dummyBytecode.subarray(12, 16).copy(bytecodeBuffer, 12);
37
- } else {
38
- dummyBytecode.subarray(12, 16).copy(bytecodeBuffer, 12);
39
- dummyBytecode.subarray(16, 20).copy(bytecodeBuffer, 16);
40
- }
41
- };
42
-
43
- module.exports.readSourceHash = function (bytecodeBuffer) {
44
- if (!isBuffer(bytecodeBuffer)) {
45
- throw new Error('bytecodeBuffer must be a buffer object.');
46
- }
47
-
48
- if (process.version.startsWith('v8.8') || process.version.startsWith('v8.9')) {
49
- // Node is v8.8.x or v8.9.x
50
- // eslint-disable-next-line no-return-assign
51
- return bytecodeBuffer.subarray(12, 16).reduce((sum, number, power) => sum += number * Math.pow(256, power), 0);
52
- } else {
53
- // eslint-disable-next-line no-return-assign
54
- return bytecodeBuffer.subarray(8, 12).reduce((sum, number, power) => sum += number * Math.pow(256, power), 0);
55
- }
56
- };
package/lib/config.js DELETED
@@ -1,118 +0,0 @@
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
- * @property {string} [cacheLocation] - Cache directory for extracted binaries (default: './.sea-cache', supports env vars like '%LOCALAPPDATA%\\path')
26
- */
27
-
28
- /**
29
- * Load SEA configuration from package.json or a standalone file.
30
- * @param {string} [configPath] - Optional path to a standalone config file
31
- * @param {string} [projectRoot] - Project root directory (defaults to cwd)
32
- * @returns {SeaConfig}
33
- */
34
- function loadConfig(configPath, projectRoot = process.cwd()) {
35
- let config;
36
-
37
- if (configPath) {
38
- // Load from standalone config file
39
- const fullPath = path.resolve(projectRoot, configPath);
40
- if (!fs.existsSync(fullPath)) {
41
- throw new Error(`Config file not found: ${fullPath}`);
42
- }
43
- config = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
44
- } else {
45
- // Load from package.json
46
- const pkgPath = path.join(projectRoot, 'package.json');
47
- if (!fs.existsSync(pkgPath)) {
48
- throw new Error(`package.json not found in: ${projectRoot}`);
49
- }
50
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
51
-
52
- // Check for 'sea' field first, fall back to 'pkg' for migration compatibility
53
- config = pkg.seabox || pkg.pkg;
54
-
55
- if (!config) {
56
- throw new Error('No "seabox" or "pkg" configuration found in package.json');
57
- }
58
-
59
- // Attach package metadata for manifest
60
- config._packageName = pkg.name;
61
- config._packageVersion = pkg.version;
62
- }
63
-
64
- validateConfig(config);
65
- return config;
66
- }
67
-
68
- /**
69
- * Validate required configuration fields.
70
- * @param {SeaConfig} config
71
- */
72
- function validateConfig(config) {
73
- const required = ['entry', 'assets', 'targets', 'output', 'outputPath'];
74
- const missing = required.filter(field => !config[field]);
75
-
76
- if (missing.length > 0) {
77
- throw new Error(`Missing required SEA config fields: ${missing.join(', ')}`);
78
- }
79
-
80
- if (!Array.isArray(config.assets) || config.assets.length === 0) {
81
- throw new Error('SEA config "assets" must be a non-empty array');
82
- }
83
-
84
- if (!Array.isArray(config.targets) || config.targets.length === 0) {
85
- throw new Error('SEA config "targets" must be a non-empty array');
86
- }
87
-
88
- // Validate target format (e.g., "node24.11.0-win-x64")
89
- const targetPattern = /^node\d+\.\d+\.\d+-\w+-\w+$/;
90
- config.targets.forEach(target => {
91
- if (!targetPattern.test(target)) {
92
- throw new Error(`Invalid target format: "${target}". Expected format: "nodeX.Y.Z-platform-arch"`);
93
- }
94
- });
95
- }
96
-
97
- /**
98
- * Parse a target string into components.
99
- * @param {string} target - e.g., "node24.11.0-win-x64"
100
- * @returns {{nodeVersion: string, platform: string, arch: string}}
101
- */
102
- function parseTarget(target) {
103
- const match = target.match(/^node(\d+\.\d+\.\d+)-(\w+)-(\w+)$/);
104
- if (!match) {
105
- throw new Error(`Cannot parse target: ${target}`);
106
- }
107
- return {
108
- nodeVersion: match[1],
109
- platform: match[2],
110
- arch: match[3]
111
- };
112
- }
113
-
114
- module.exports = {
115
- loadConfig,
116
- validateConfig,
117
- parseTarget
118
- };
package/lib/index.js DELETED
@@ -1,27 +0,0 @@
1
- /**
2
- * index.js
3
- * Main entry point for seabox
4
- */
5
-
6
- const { build } = require('./build');
7
- const { loadConfig, parseTarget } = require('./config');
8
- const { scanAssets, groupAssets } = require('./scanner');
9
- const { generateManifest, serializeManifest } = require('./manifest');
10
- const { createSeaConfig, writeSeaConfigJson, generateBlob } = require('./blob');
11
- const { fetchNodeBinary } = require('./fetch-node');
12
- const { injectBlob } = require('./inject');
13
-
14
- module.exports = {
15
- build,
16
- loadConfig,
17
- parseTarget,
18
- scanAssets,
19
- groupAssets,
20
- generateManifest,
21
- serializeManifest,
22
- createSeaConfig,
23
- writeSeaConfigJson,
24
- generateBlob,
25
- fetchNodeBinary,
26
- injectBlob
27
- };
package/lib/inject.js DELETED
@@ -1,81 +0,0 @@
1
- /**
2
- * inject.js
3
- * Inject SEA blob into Node binary using postject.
4
- */
5
-
6
- const fs = require('fs');
7
- const path = require('path');
8
- const { execFile } = require('child_process');
9
- const { promisify } = require('util');
10
- const { removeSignature } = require('./unsign');
11
-
12
- const execFileAsync = promisify(execFile);
13
-
14
- /**
15
- * Inject a SEA blob into a Node.js binary using postject.
16
- * @param {string} nodeBinaryPath - Path to the source Node binary
17
- * @param {string} blobPath - Path to the SEA blob file
18
- * @param {string} outputPath - Path for the output executable
19
- * @param {string} platform - Target platform (win32, linux, darwin)
20
- * @returns {Promise<void>}
21
- */
22
- async function injectBlob(nodeBinaryPath, blobPath, outputPath, platform, verbose) {
23
- // Copy node binary to output location
24
- fs.copyFileSync(nodeBinaryPath, outputPath);
25
-
26
- // Remove existing signature before postject injection
27
- // The downloaded Node.js binary is signed, and postject will corrupt this signature
28
- await removeSignature(outputPath, platform);
29
-
30
- // Prepare postject command
31
- const sentinel = 'NODE_SEA_BLOB';
32
- const sentinelFuse = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';
33
-
34
- const args = [
35
- outputPath,
36
- sentinel,
37
- blobPath,
38
- '--sentinel-fuse', sentinelFuse
39
- ];
40
-
41
- // Platform-specific postject options
42
- if (platform === 'darwin') {
43
- args.push('--macho-segment-name', 'NODE_SEA');
44
- }
45
-
46
- console.log(`Injecting SEA blob into: ${outputPath}`);
47
-
48
- // Use cmd.exe on Windows to run npx
49
- const isWindows = process.platform === 'win32';
50
- const command = isWindows ? 'cmd.exe' : 'npx';
51
- const cmdArgs = isWindows
52
- ? ['/c', 'npx', 'postject', ...args]
53
- : ['postject', ...args];
54
-
55
- //console.log(`Command: ${command} ${cmdArgs.join(' ')}`);
56
-
57
- try {
58
- const { stdout, stderr } = await execFileAsync(command, cmdArgs);
59
- if (stdout && verbose) console.log(stdout);
60
- if (stderr && verbose) console.error(stderr);
61
- console.log('✓ SEA blob injected successfully');
62
- } catch (error) {
63
- throw new Error(`Postject injection failed: ${error.message}`);
64
- }
65
-
66
- //console.log('\nNote: Executable is now ready for signing with your certificate');
67
- }
68
-
69
- /**
70
- * Resolve the postject executable path.
71
- * @returns {string}
72
- */
73
- function resolvePostject() {
74
- // Use npx to run postject
75
- return 'npx';
76
- }
77
-
78
- module.exports = {
79
- injectBlob,
80
- resolvePostject
81
- };