seabox 0.1.0-beta.1 → 0.1.0-beta.3

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/README.md CHANGED
@@ -13,6 +13,11 @@ A reusable tool for building Node.js Single Executable Applications (SEA) with n
13
13
  - Automatic code signature removal before injection
14
14
  - Simple configuration via package.json
15
15
 
16
+ ## Use case
17
+ This tooling was created as an alternative to pkg, which is unfortunatly deprecated, and where forks were running foul of virus checkers. By using node's SEA, the executables are directly from nodejs's distribution source, and built using node's native Single Executable Application solution. Unfortunatly this does mean native modules embedded within the exe cannot run directly and must be extracted to a location on the disk on first run - This tooling automates that process for you, while providing arbitrary asset embedding. Embedded assets are _not_ extracted and access to them is handled by intercepting require and fs.
18
+
19
+ Note: **V8 snapshot includes and embedds the original source**, this is currently a limitation of Node's SEA tooling as far as I can tell; thus the snapshot is only useful for faster startup.
20
+
16
21
  ## Installation
17
22
 
18
23
  ```bash
@@ -63,7 +68,9 @@ Add a `sea` configuration to your `package.json`:
63
68
  - **useCodeCache**: Enable V8 code cache (default: false)
64
69
  - **encryptAssets**: Enable encryption for assets (default: false)
65
70
  - **encryptExclude**: Patterns to exclude from encryption (e.g., `['*.txt']`)
66
- - **exclude**: *(Deprecated)* Use `!` prefix in assets array instead
71
+ - **rebuild**: Automatically rebuild native modules for the target platform before building the SEA (default: false)
72
+ - **rcedit**: (Windows only) Customize executable icon and version information. See [rcedit options](#windows-executable-customization-rcedit)
73
+ - **cacheLocation**: Custom cache directory for extracted binaries (default: `'./.sea-cache'`). Supports environment variable expansion (e.g., `'%LOCALAPPDATA%\\myapp-cache'` on Windows or `'$HOME/.cache/myapp'` on Unix)
67
74
 
68
75
  ## Usage
69
76
 
@@ -94,8 +101,12 @@ npm run build:exe
94
101
  # Build using package.json configuration
95
102
  npx seabox
96
103
 
104
+ # Build using a standalone config file (alternative to package.json)
105
+ npx seabox --config sea-config.json
106
+
97
107
  # Verbose output
98
108
  npx seabox --verbose
109
+
99
110
  ```
100
111
 
101
112
  ### Programmatic API
@@ -126,7 +137,35 @@ Native modules (`.node`, `.dll`, `.so`, `.dylib`) are automatically:
126
137
  - Integrity-checked using SHA-256 hashes
127
138
  - Loaded via custom module resolution
128
139
 
129
- Cache location: `%LOCALAPPDATA%\.sea-cache\<appname>\<version>-<platform>-<arch>`
140
+ ### Cache Location
141
+
142
+ By default, binaries are extracted to: `./.sea-cache/<appname>/<version>-<platform>-<arch>`
143
+
144
+ You can customize the cache location in your configuration:
145
+
146
+ ```json
147
+ {
148
+ "sea": {
149
+ "cacheLocation": "%LOCALAPPDATA%\\myapp-cache"
150
+ }
151
+ }
152
+ ```
153
+
154
+ The cache location supports environment variable expansion:
155
+ - **Windows**: `%LOCALAPPDATA%`, `%APPDATA%`, `%TEMP%`, etc.
156
+ - **Unix/Linux/macOS**: `$HOME`, `$TMPDIR`, `${XDG_CACHE_HOME}`, etc.
157
+
158
+ **Override at runtime**: Set the `SEACACHE` environment variable to override the configured location:
159
+
160
+ ```bash
161
+ # Windows
162
+ set SEACACHE=C:\custom\cache\path
163
+ myapp.exe
164
+
165
+ # Unix/Linux/macOS
166
+ export SEACACHE=/custom/cache/path
167
+ ./myapp
168
+ ```
130
169
 
131
170
  ## Native Module Rebuilding
132
171
 
@@ -150,6 +189,52 @@ The rebuilder will:
150
189
 
151
190
  **Note**: Cross-compilation may require additional platform-specific build tools installed.
152
191
 
192
+ ## Windows Executable Customization (rcedit)
193
+
194
+ For Windows executables, you can customize the icon and version information using the `rcedit` configuration option:
195
+
196
+ ```json
197
+ {
198
+ "sea": {
199
+ "output": "myapp.exe",
200
+ "targets": ["node24.11.0-win32-x64"],
201
+ "rcedit": {
202
+ "icon": ".\\assets\\myapp.ico",
203
+ "file-version": "1.2.3.4",
204
+ "product-version": "1.2.3.4",
205
+ "version-string": {
206
+ "CompanyName": "My Company",
207
+ "FileDescription": "My Application",
208
+ "ProductName": "MyApp",
209
+ "InternalName": "myapp.exe",
210
+ "OriginalFilename": "myapp.exe",
211
+ "LegalCopyright": "Copyright (C) 2025 My Company"
212
+ }
213
+ }
214
+ }
215
+ }
216
+ ```
217
+
218
+ ### rcedit Options
219
+
220
+ - **icon**: Path to `.ico` file for the executable icon
221
+ - **file-version**: File version in `X.X.X.X` format
222
+ - **product-version**: Product version in `X.X.X.X` format
223
+ - **version-string**: Object containing version string properties:
224
+ - `CompanyName`: Company name
225
+ - `FileDescription`: Description of the file
226
+ - `ProductName`: Product name
227
+ - `InternalName`: Internal name
228
+ - `OriginalFilename`: Original filename
229
+ - `LegalCopyright`: Copyright notice
230
+ - `LegalTrademarks`: Trademark information (optional)
231
+ - `PrivateBuild`: Private build description (optional)
232
+ - `SpecialBuild`: Special build description (optional)
233
+
234
+ The rcedit step runs after signature removal and before the SEA blob injection. This only works for Windows (`win32`) targets.
235
+
236
+ For more details, see the [rcedit documentation](https://github.com/electron/rcedit).
237
+
153
238
  ## Asset Encryption
154
239
 
155
240
  seabox supports optional AES-256-GCM encryption of embedded assets to protect your application code and data:
@@ -169,33 +254,17 @@ seabox supports optional AES-256-GCM encryption of embedded assets to protect yo
169
254
  1. **Build Time**: A random 256-bit encryption key is generated
170
255
  2. **Asset Encryption**: Non-binary assets are encrypted using AES-256-GCM
171
256
  3. **Key Embedding**: The encryption key is obfuscated and embedded in the bootstrap code
172
- 4. **Snapshot Obfuscation**: When `useSnapshot: true`, the key becomes part of V8 bytecode
257
+ 4. **Key Obfuscation**: the bootstrap and key code are obfuscated, but not removed
173
258
  5. **Runtime Decryption**: Assets are transparently decrypted when accessed
174
259
 
175
- ### Security Considerations
260
+ ### Considerations
176
261
 
177
262
  - **Binary files** (`.node`, `.dll`, `.so`, `.dylib`) are **never encrypted** as they must be extracted as-is
178
263
  - The manifest (`sea-manifest.json`) is **not encrypted** to allow bootstrap initialization
264
+ - **V8 snapshot includes the original source**, this is currently a limitation of Node's SEA.
179
265
  - Encryption provides **obfuscation**, not cryptographic security against determined attackers
180
- - When combined with V8 snapshots, the encryption key is compiled into bytecode, making extraction significantly harder
181
- - Use `encryptExclude` to skip encryption for non-sensitive assets (e.g., public files, documentation)
182
-
183
- ### Best Practices
184
-
185
- ```json
186
- {
187
- "sea": {
188
- "encryptAssets": true,
189
- "useSnapshot": true, // Strongly recommended with encryption
190
- "assets": [
191
- "./out/**/*",
192
- "!**/*.md", // Exclude documentation
193
- "!**/public/**", // Exclude public assets
194
- "!**/LICENSE" // Exclude license files
195
- ]
196
- }
197
- }
198
- ```
266
+ - The bootloader code, that includes the encryption key, is obfuscated in the source embedded by Node's SEA
267
+
199
268
 
200
269
  ## Platform Support
201
270
 
package/bin/seabox.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * CLI entry point for SEA builder
@@ -32,7 +32,7 @@ async function main() {
32
32
  // Show help
33
33
  if (showHelp) {
34
34
  console.log(`
35
- SEA Builder - Node.js Single Executable Application Builder
35
+ SeaBox - Node.js Single Executable Application Builder
36
36
 
37
37
  Usage:
38
38
  seabox [options]
package/lib/bootstrap.js CHANGED
@@ -202,18 +202,56 @@ function getAsset(assetKey, encoding) {
202
202
  }
203
203
 
204
204
 
205
+ /**
206
+ * Resolve environment variables in a path string.
207
+ * Supports %VAR% on Windows and $VAR or ${VAR} on Unix-like systems.
208
+ * @param {string} pathStr - Path string that may contain environment variables
209
+ * @returns {string} - Path with environment variables expanded
210
+ */
211
+ function resolveEnvVars(pathStr) {
212
+ if (!pathStr) return pathStr;
213
+
214
+ // Replace Windows-style %VAR%
215
+ let resolved = pathStr.replace(/%([^%]+)%/g, (match, varName) => {
216
+ return process.env[varName] || match;
217
+ });
218
+
219
+ // Replace Unix-style $VAR and ${VAR}
220
+ resolved = resolved.replace(/\$\{([^}]+)\}/g, (match, varName) => {
221
+ return process.env[varName] || match;
222
+ });
223
+ resolved = resolved.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, varName) => {
224
+ return process.env[varName] || match;
225
+ });
226
+
227
+ return resolved;
228
+ }
229
+
205
230
  /**
206
231
  * Get the extraction cache directory for this application.
207
232
  * @param {string} appName
208
233
  * @param {string} appVersion
209
234
  * @param {string} platform
210
235
  * @param {string} arch
236
+ * @param {string} [configuredCacheLocation] - Optional configured cache location from manifest
211
237
  * @returns {string}
212
238
  */
213
- function getExtractionCacheDir(appName, appVersion, platform, arch) {
239
+ function getExtractionCacheDir(appName, appVersion, platform, arch, configuredCacheLocation) {
240
+ // Priority: SEACACHE env var > configured location > default
214
241
  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}`);
242
+
243
+ if (configuredCacheLocation) {
244
+ // Resolve environment variables in the configured path
245
+ const resolvedBase = resolveEnvVars(configuredCacheLocation);
246
+ // Make relative paths absolute (relative to cwd)
247
+ const absoluteBase = path.isAbsolute(resolvedBase) ? resolvedBase : path.resolve(process.cwd(), resolvedBase);
248
+ return path.join(absoluteBase, appName, `${appVersion}-${platform}-${arch}`);
249
+ }
250
+
251
+ // Default behavior
252
+ const defaultBase = './.sea-cache';
253
+ const absoluteBase = path.resolve(process.cwd(), defaultBase);
254
+ return path.join(absoluteBase, appName, `${appVersion}-${platform}-${arch}`);
217
255
  }
218
256
 
219
257
  /**
@@ -400,6 +438,9 @@ function bootstrap() {
400
438
  console.log(` App: ${manifest.appName} v${manifest.appVersion}`);
401
439
  console.log(` Platform: ${manifest.platform}-${manifest.arch}`);
402
440
  console.log(` Binaries to extract: ${manifest.binaries.length}`);
441
+ if (manifest.cacheLocation) {
442
+ console.log(` Configured cache location: ${manifest.cacheLocation}`);
443
+ }
403
444
  }
404
445
 
405
446
  // Filter binaries for current platform
@@ -415,7 +456,8 @@ function bootstrap() {
415
456
  manifest.appName,
416
457
  manifest.appVersion,
417
458
  manifest.platform,
418
- manifest.arch
459
+ manifest.arch,
460
+ manifest.cacheLocation
419
461
  );
420
462
 
421
463
  const nativeModuleMap = {};
package/lib/build.js CHANGED
@@ -249,7 +249,7 @@ const SEA_ENCRYPTED_ASSETS = new Set(${encryptedKeysJson});
249
249
 
250
250
  if (verbose) console.log('\n[8/8] Injecting SEA blob into Node binary');
251
251
  const outputExe = path.join(projectRoot, config.outputPath, config.output);
252
- await injectBlob(nodeBinary, blobOutputPath, outputExe, platform, verbose);
252
+ await injectBlob(nodeBinary, blobOutputPath, outputExe, platform, verbose, config.rcedit);
253
253
 
254
254
  if (!debug) {
255
255
  if (verbose) console.log('\nCleaning up temporary files');
@@ -0,0 +1,56 @@
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 CHANGED
@@ -22,6 +22,8 @@ const path = require('path');
22
22
  * @property {string[]} [encryptExclude] - Asset patterns to exclude from encryption
23
23
  * @property {boolean} [rebuild] - Automatically rebuild native modules for target platform (default: false)
24
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
+ * @property {Object} [rcedit] - Windows executable resource editor options (icon, version info, etc.)
25
27
  */
26
28
 
27
29
  /**
package/lib/inject.js CHANGED
@@ -17,9 +17,11 @@ const execFileAsync = promisify(execFile);
17
17
  * @param {string} blobPath - Path to the SEA blob file
18
18
  * @param {string} outputPath - Path for the output executable
19
19
  * @param {string} platform - Target platform (win32, linux, darwin)
20
+ * @param {boolean} verbose - Enable verbose logging
21
+ * @param {Object} [rceditOptions] - Optional rcedit configuration for Windows executables
20
22
  * @returns {Promise<void>}
21
23
  */
22
- async function injectBlob(nodeBinaryPath, blobPath, outputPath, platform, verbose) {
24
+ async function injectBlob(nodeBinaryPath, blobPath, outputPath, platform, verbose, rceditOptions) {
23
25
  // Copy node binary to output location
24
26
  fs.copyFileSync(nodeBinaryPath, outputPath);
25
27
 
@@ -27,6 +29,11 @@ async function injectBlob(nodeBinaryPath, blobPath, outputPath, platform, verbos
27
29
  // The downloaded Node.js binary is signed, and postject will corrupt this signature
28
30
  await removeSignature(outputPath, platform);
29
31
 
32
+ // Apply rcedit changes (Windows only, before postject)
33
+ if (platform === 'win32' && rceditOptions && typeof rceditOptions === 'object') {
34
+ await applyRcedit(outputPath, rceditOptions, verbose);
35
+ }
36
+
30
37
  // Prepare postject command
31
38
  const sentinel = 'NODE_SEA_BLOB';
32
39
  const sentinelFuse = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';
@@ -66,6 +73,31 @@ async function injectBlob(nodeBinaryPath, blobPath, outputPath, platform, verbos
66
73
  //console.log('\nNote: Executable is now ready for signing with your certificate');
67
74
  }
68
75
 
76
+ /**
77
+ * Apply rcedit to modify Windows executable resources.
78
+ * @param {string} exePath - Path to the executable
79
+ * @param {Object} options - rcedit options (icon, version-string, file-version, product-version, etc.)
80
+ * @param {boolean} verbose - Enable verbose logging
81
+ * @returns {Promise<void>}
82
+ */
83
+ async function applyRcedit(exePath, options, verbose) {
84
+ if (verbose) {
85
+ console.log('\nApplying rcedit to modify executable resources...');
86
+ console.log('Options:', JSON.stringify(options, null, 2));
87
+ }
88
+
89
+ const rcedit = require('rcedit');
90
+
91
+ try {
92
+ await rcedit(exePath, options);
93
+ if (verbose) {
94
+ console.log('✓ rcedit applied successfully');
95
+ }
96
+ } catch (error) {
97
+ throw new Error(`rcedit failed: ${error.message}`);
98
+ }
99
+ }
100
+
69
101
  /**
70
102
  * Resolve the postject executable path.
71
103
  * @returns {string}
@@ -77,5 +109,6 @@ function resolvePostject() {
77
109
 
78
110
  module.exports = {
79
111
  injectBlob,
80
- resolvePostject
112
+ resolvePostject,
113
+ applyRcedit
81
114
  };
package/lib/manifest.js CHANGED
@@ -23,6 +23,7 @@ const path = require('path');
23
23
  * @property {string} arch - Target architecture
24
24
  * @property {BinaryManifestEntry[]} binaries - Binary extraction rules
25
25
  * @property {string[]} allAssetKeys - All embedded asset keys
26
+ * @property {string} [cacheLocation] - Configured cache location (optional)
26
27
  */
27
28
 
28
29
  /**
@@ -48,7 +49,7 @@ function generateManifest(assets, config, targetPlatform, targetArch) {
48
49
  };
49
50
  });
50
51
 
51
- return {
52
+ const manifest = {
52
53
  appName: config._packageName || 'app',
53
54
  appVersion: config._packageVersion || '1.0.0',
54
55
  platform: targetPlatform,
@@ -56,6 +57,13 @@ function generateManifest(assets, config, targetPlatform, targetArch) {
56
57
  binaries,
57
58
  allAssetKeys: assets.map(a => a.assetKey)
58
59
  };
60
+
61
+ // Include cacheLocation if configured
62
+ if (config.cacheLocation) {
63
+ manifest.cacheLocation = config.cacheLocation;
64
+ }
65
+
66
+ return manifest;
59
67
  }
60
68
 
61
69
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seabox",
3
- "version": "0.1.0-beta.1",
3
+ "version": "0.1.0-beta.3",
4
4
  "description": "Node.js Single Executable Application (SEA) builder tool with native and library extraction",
5
5
  "main": "lib/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,10 +28,16 @@
28
28
  ],
29
29
  "author": "Meirion Hughes",
30
30
  "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/MeirionHughes/seabox"
34
+ },
35
+ "homepage": "https://github.com/MeirionHughes/seabox",
31
36
  "dependencies": {
32
37
  "adm-zip": "^0.5.16",
33
38
  "glob": "^10.0.0",
34
39
  "postject": "^1.0.0-alpha.6",
40
+ "rcedit": "^4.0.1",
35
41
  "tar": "^6.2.1"
36
42
  },
37
43
  "devDependencies": {