seabox 0.1.0-beta.3 → 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/README.md +342 -146
- package/bin/seabox-rebuild.mjs +95 -0
- package/bin/seabox.mjs +135 -0
- package/lib/{blob.js → blob.mjs} +12 -18
- package/lib/bootstrap.cjs +753 -0
- package/lib/build-cache.mjs +199 -0
- package/lib/build.mjs +75 -0
- package/lib/config.mjs +234 -0
- package/lib/{crypto-assets.js → crypto-assets.mjs} +12 -47
- package/lib/entry-bundler.mjs +64 -0
- package/lib/{fetch-node.js → fetch-node.mjs} +10 -16
- package/lib/index.mjs +26 -0
- package/lib/{inject.js → inject.mjs} +24 -27
- package/lib/{manifest.js → manifest.mjs} +5 -11
- package/lib/multi-target-builder.mjs +620 -0
- package/lib/native-scanner.mjs +210 -0
- package/lib/{obfuscate.js → obfuscate.mjs} +9 -31
- package/lib/require-shim.mjs +111 -0
- package/lib/rolldown-bundler.mjs +415 -0
- package/lib/{unsign.js → unsign.cjs} +7 -32
- package/package.json +9 -5
- package/bin/seabox-rebuild.js +0 -395
- package/bin/seabox.js +0 -81
- package/lib/bindings.js +0 -31
- package/lib/bootstrap.js +0 -697
- package/lib/build.js +0 -283
- package/lib/bytenode-hack.js +0 -56
- package/lib/config.js +0 -119
- package/lib/index.js +0 -27
- package/lib/scanner.js +0 -153
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* native-scanner.mjs
|
|
3
|
+
* Deep scanning of node_modules for native modules and automatic detection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import fsSync from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} NativeModuleMetadata
|
|
12
|
+
* @property {string} name - Module name
|
|
13
|
+
* @property {string} path - Absolute path to module root
|
|
14
|
+
* @property {string} version - Module version
|
|
15
|
+
* @property {boolean} hasBindingGyp - Whether binding.gyp exists
|
|
16
|
+
* @property {string[]} binaryFiles - Detected .node files
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Scan node_modules directory for native modules
|
|
21
|
+
* @param {string} projectRoot - Project root directory
|
|
22
|
+
* @param {boolean} verbose - Enable verbose logging
|
|
23
|
+
* @returns {Promise<NativeModuleMetadata[]>}
|
|
24
|
+
*/
|
|
25
|
+
export async function scanDependenciesForNativeModules(projectRoot, verbose = false) {
|
|
26
|
+
const nativeModules = [];
|
|
27
|
+
const nodeModulesPath = path.join(projectRoot, 'node_modules');
|
|
28
|
+
|
|
29
|
+
if (!fsSync.existsSync(nodeModulesPath)) {
|
|
30
|
+
if (verbose) {
|
|
31
|
+
console.log('[NativeScanner] No node_modules directory found');
|
|
32
|
+
}
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (verbose) {
|
|
37
|
+
console.log('[NativeScanner] Scanning node_modules for native modules');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Recursively scan a directory for packages
|
|
42
|
+
*/
|
|
43
|
+
async function scanDir(dir, isScoped = false) {
|
|
44
|
+
try {
|
|
45
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
46
|
+
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
// Skip hidden directories and bin
|
|
49
|
+
if (entry.name.startsWith('.') || entry.name === '.bin') {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fullPath = path.join(dir, entry.name);
|
|
54
|
+
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
const pkgPath = path.join(fullPath, 'package.json');
|
|
57
|
+
|
|
58
|
+
// Check if this is a scoped package directory
|
|
59
|
+
if (entry.name.startsWith('@')) {
|
|
60
|
+
await scanDir(fullPath, true);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check if package.json exists
|
|
65
|
+
if (fsSync.existsSync(pkgPath)) {
|
|
66
|
+
const moduleInfo = await analyzePackage(fullPath, pkgPath);
|
|
67
|
+
if (moduleInfo) {
|
|
68
|
+
nativeModules.push(moduleInfo);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (verbose) {
|
|
75
|
+
console.warn('[NativeScanner] Error scanning directory:', dir, err.message);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Analyze a package to determine if it's a native module
|
|
82
|
+
*/
|
|
83
|
+
async function analyzePackage(modulePath, pkgPath) {
|
|
84
|
+
try {
|
|
85
|
+
const pkgContent = await fs.readFile(pkgPath, 'utf8');
|
|
86
|
+
const pkg = JSON.parse(pkgContent);
|
|
87
|
+
|
|
88
|
+
// Check for native module indicators
|
|
89
|
+
const hasBindingGyp = fsSync.existsSync(path.join(modulePath, 'binding.gyp'));
|
|
90
|
+
const hasGypfile = pkg.gypfile === true;
|
|
91
|
+
const hasBinaryField = pkg.binary != null;
|
|
92
|
+
|
|
93
|
+
if (!hasBindingGyp && !hasGypfile && !hasBinaryField) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find .node files
|
|
98
|
+
const binaryFiles = await findNodeFiles(modulePath);
|
|
99
|
+
|
|
100
|
+
if (hasBindingGyp || hasGypfile || binaryFiles.length > 0) {
|
|
101
|
+
return {
|
|
102
|
+
name: pkg.name,
|
|
103
|
+
path: modulePath,
|
|
104
|
+
version: pkg.version,
|
|
105
|
+
hasBindingGyp: hasBindingGyp,
|
|
106
|
+
binaryFiles: binaryFiles
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return null;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
// Ignore packages with invalid package.json
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Find all .node files in a directory tree
|
|
119
|
+
*/
|
|
120
|
+
async function findNodeFiles(dir, maxDepth = 5, currentDepth = 0) {
|
|
121
|
+
const nodeFiles = [];
|
|
122
|
+
|
|
123
|
+
if (currentDepth > maxDepth) {
|
|
124
|
+
return nodeFiles;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
129
|
+
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
const fullPath = path.join(dir, entry.name);
|
|
132
|
+
|
|
133
|
+
if (entry.isDirectory()) {
|
|
134
|
+
// Skip node_modules subdirectories
|
|
135
|
+
if (entry.name === 'node_modules') {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Recurse into subdirectories
|
|
140
|
+
const subFiles = await findNodeFiles(fullPath, maxDepth, currentDepth + 1);
|
|
141
|
+
nodeFiles.push(...subFiles);
|
|
142
|
+
} else if (entry.name.endsWith('.node')) {
|
|
143
|
+
nodeFiles.push(fullPath);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// Ignore read errors
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return nodeFiles;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
await scanDir(nodeModulesPath);
|
|
154
|
+
|
|
155
|
+
if (verbose) {
|
|
156
|
+
console.log(`[NativeScanner] Found ${nativeModules.length} native modules`);
|
|
157
|
+
for (const mod of nativeModules) {
|
|
158
|
+
console.log(` - ${mod.name}@${mod.version} (${mod.binaryFiles.length} binaries)`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return nativeModules;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Find the build output path for a native module
|
|
167
|
+
* @param {string} moduleRoot - Root directory of the native module
|
|
168
|
+
* @param {string} target - Build target (e.g., node24.11.0-win32-x64)
|
|
169
|
+
* @returns {Promise<string|null>}
|
|
170
|
+
*/
|
|
171
|
+
export async function findNativeModuleBuildPath(moduleRoot, target) {
|
|
172
|
+
const { platform, arch } = parseTarget(target);
|
|
173
|
+
|
|
174
|
+
// Common build output locations
|
|
175
|
+
const searchPaths = [
|
|
176
|
+
path.join(moduleRoot, 'build/Release'),
|
|
177
|
+
path.join(moduleRoot, 'build/Debug'),
|
|
178
|
+
path.join(moduleRoot, 'lib/binding', `${platform}-${arch}`),
|
|
179
|
+
path.join(moduleRoot, 'prebuilds', `${platform}-${arch}`)
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
for (const searchPath of searchPaths) {
|
|
183
|
+
if (fsSync.existsSync(searchPath)) {
|
|
184
|
+
// Look for .node files
|
|
185
|
+
const files = await fs.readdir(searchPath);
|
|
186
|
+
const nodeFile = files.find(f => f.endsWith('.node'));
|
|
187
|
+
|
|
188
|
+
if (nodeFile) {
|
|
189
|
+
return path.join(searchPath, nodeFile);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Parse a target string into components
|
|
199
|
+
*/
|
|
200
|
+
function parseTarget(target) {
|
|
201
|
+
const match = target.match(/^node(\d+\.\d+\.\d+)-(\w+)-(\w+)$/);
|
|
202
|
+
if (!match) {
|
|
203
|
+
throw new Error(`Cannot parse target: ${target}`);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
nodeVersion: match[1],
|
|
207
|
+
platform: match[2],
|
|
208
|
+
arch: match[3]
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* obfuscate.mjs
|
|
3
|
+
* Obfuscate bootstrap code to protect encryption keys and decryption logic
|
|
3
4
|
*/
|
|
4
5
|
|
|
6
|
+
import Module from 'module';
|
|
7
|
+
|
|
8
|
+
const require = Module.createRequire(import.meta.url);
|
|
5
9
|
const JavaScriptObfuscator = require('javascript-obfuscator');
|
|
6
10
|
|
|
7
11
|
/**
|
|
@@ -10,11 +14,9 @@ const JavaScriptObfuscator = require('javascript-obfuscator');
|
|
|
10
14
|
* @param {string} bootstrapCode - The bootstrap JavaScript code to obfuscate
|
|
11
15
|
* @returns {string} Obfuscated JavaScript code
|
|
12
16
|
*/
|
|
13
|
-
function obfuscateBootstrap(bootstrapCode) {
|
|
17
|
+
export function obfuscateBootstrap(bootstrapCode) {
|
|
14
18
|
const obfuscationResult = JavaScriptObfuscator.obfuscate(bootstrapCode, {
|
|
15
19
|
// Maximum protection settings for encryption key and decryption logic
|
|
16
|
-
|
|
17
|
-
// String encoding
|
|
18
20
|
stringArray: true,
|
|
19
21
|
stringArrayThreshold: 1,
|
|
20
22
|
stringArrayEncoding: ['rc4'],
|
|
@@ -25,49 +27,25 @@ function obfuscateBootstrap(bootstrapCode) {
|
|
|
25
27
|
stringArrayWrappersChainedCalls: true,
|
|
26
28
|
stringArrayWrappersParametersMaxCount: 5,
|
|
27
29
|
stringArrayWrappersType: 'function',
|
|
28
|
-
|
|
29
|
-
// Control flow
|
|
30
30
|
controlFlowFlattening: true,
|
|
31
31
|
controlFlowFlatteningThreshold: 1,
|
|
32
32
|
deadCodeInjection: true,
|
|
33
33
|
deadCodeInjectionThreshold: 0.4,
|
|
34
|
-
|
|
35
|
-
// Code transformations
|
|
36
34
|
transformObjectKeys: true,
|
|
37
35
|
splitStrings: true,
|
|
38
36
|
splitStringsChunkLength: 10,
|
|
39
|
-
|
|
40
|
-
// Identifiers
|
|
41
37
|
identifierNamesGenerator: 'hexadecimal',
|
|
42
38
|
identifiersPrefix: '',
|
|
43
|
-
renameGlobals: false,
|
|
44
|
-
renameProperties: false,
|
|
45
|
-
|
|
46
|
-
// Self-defending
|
|
39
|
+
renameGlobals: false,
|
|
40
|
+
renameProperties: false,
|
|
47
41
|
selfDefending: true,
|
|
48
|
-
|
|
49
|
-
// Compact output
|
|
50
42
|
compact: true,
|
|
51
|
-
|
|
52
|
-
// Additional obfuscation
|
|
53
43
|
numbersToExpressions: true,
|
|
54
44
|
simplify: true,
|
|
55
|
-
|
|
56
|
-
// Disable source maps (we don't want them)
|
|
57
|
-
sourceMap: false,
|
|
58
|
-
|
|
59
|
-
// Performance vs protection tradeoff
|
|
60
|
-
// (these settings prioritize protection over performance)
|
|
61
45
|
target: 'node',
|
|
62
46
|
ignoreImports: true,
|
|
63
|
-
|
|
64
|
-
// Comments removal
|
|
65
|
-
// (handled automatically by compact: true)
|
|
47
|
+
sourceMap: false
|
|
66
48
|
});
|
|
67
49
|
|
|
68
50
|
return obfuscationResult.getObfuscatedCode();
|
|
69
51
|
}
|
|
70
|
-
|
|
71
|
-
module.exports = {
|
|
72
|
-
obfuscateBootstrap
|
|
73
|
-
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* require-shim.js
|
|
3
|
+
* SEA-aware require replacement that intercepts .node module loads
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate the __requireSeabox shim code
|
|
8
|
+
* @returns {string} - The shim code to inject
|
|
9
|
+
*/
|
|
10
|
+
export function generateRequireShim() {
|
|
11
|
+
return `
|
|
12
|
+
// SEA-aware require replacement
|
|
13
|
+
const __originalRequire = require;
|
|
14
|
+
function __requireSeabox(id) {
|
|
15
|
+
// Check if this is a native module request (either .node extension or asset key)
|
|
16
|
+
if (typeof id === 'string' && (id.endsWith('.node') || id.startsWith('native/'))) {
|
|
17
|
+
// Check if we're in SEA mode
|
|
18
|
+
let isSEA = false;
|
|
19
|
+
try {
|
|
20
|
+
const sea = __originalRequire('node:sea');
|
|
21
|
+
isSEA = sea.isSea();
|
|
22
|
+
} catch (e) {
|
|
23
|
+
// Not in SEA mode
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (isSEA && global.__seaNativeModuleMap) {
|
|
27
|
+
const path = __originalRequire('path');
|
|
28
|
+
|
|
29
|
+
// Try multiple resolution strategies
|
|
30
|
+
const basename = path.basename(id);
|
|
31
|
+
const nameWithoutExt = basename.replace(/\\.node$/, '');
|
|
32
|
+
|
|
33
|
+
// 1. Try the ID as-is (asset key)
|
|
34
|
+
let resolvedPath = global.__seaNativeModuleMap[id];
|
|
35
|
+
|
|
36
|
+
// 2. Try basename
|
|
37
|
+
if (!resolvedPath) {
|
|
38
|
+
resolvedPath = global.__seaNativeModuleMap[basename];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 3. Try name without extension
|
|
42
|
+
if (!resolvedPath) {
|
|
43
|
+
resolvedPath = global.__seaNativeModuleMap[nameWithoutExt];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 4. Try searching for matching keys
|
|
47
|
+
if (!resolvedPath) {
|
|
48
|
+
for (const [key, value] of Object.entries(global.__seaNativeModuleMap)) {
|
|
49
|
+
if (key.endsWith(basename) || key.endsWith(nameWithoutExt)) {
|
|
50
|
+
resolvedPath = value;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (resolvedPath) {
|
|
57
|
+
const module = { exports: {} };
|
|
58
|
+
process.dlopen(module, resolvedPath);
|
|
59
|
+
return module.exports;
|
|
60
|
+
} else {
|
|
61
|
+
console.error('[__requireSeabox] ✗ Native module not found in map');
|
|
62
|
+
console.error('[__requireSeabox] Requested:', id);
|
|
63
|
+
console.error('[__requireSeabox] Available:', Object.keys(global.__seaNativeModuleMap));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle bindings module - return a shim that uses our native module map
|
|
69
|
+
if (id === 'bindings') {
|
|
70
|
+
return function(name) {
|
|
71
|
+
if (!name.endsWith('.node')) {
|
|
72
|
+
name += '.node';
|
|
73
|
+
}
|
|
74
|
+
return __requireSeabox(name);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Fall back to original require
|
|
79
|
+
return __originalRequire(id);
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Replace all require() calls with __requireSeabox() in source code
|
|
86
|
+
* @param {string} sourceCode - The source code to transform
|
|
87
|
+
* @param {boolean} verbose - Enable verbose logging
|
|
88
|
+
* @returns {Object} - { code: string, count: number }
|
|
89
|
+
*/
|
|
90
|
+
export function replaceRequireCalls(sourceCode, verbose = false) {
|
|
91
|
+
if (verbose) console.log('Replacing require() calls with __requireSeabox()');
|
|
92
|
+
|
|
93
|
+
const requirePattern = /\brequire\s*\(/g;
|
|
94
|
+
let replacementCount = 0;
|
|
95
|
+
|
|
96
|
+
const transformedCode = sourceCode.replace(requirePattern, (match) => {
|
|
97
|
+
replacementCount++;
|
|
98
|
+
return '__requireSeabox(';
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (verbose) console.log('Replaced ' + replacementCount + ' require() calls');
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
code: transformedCode,
|
|
105
|
+
count: replacementCount
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|