@secure-exec/nodejs 0.2.0-rc.1 → 0.2.0-rc.2
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/dist/bridge/child-process.js +47 -1
- package/dist/bridge/fs.d.ts +6 -1
- package/dist/bridge/fs.js +12 -3
- package/dist/bridge/index.d.ts +2 -2
- package/dist/bridge/index.js +2 -2
- package/dist/bridge/network.d.ts +35 -1
- package/dist/bridge/network.js +471 -153
- package/dist/bridge/os.js +9 -3
- package/dist/bridge/polyfills.d.ts +6 -9
- package/dist/bridge/polyfills.js +616 -14
- package/dist/bridge/process.d.ts +3 -2
- package/dist/bridge/process.js +288 -61
- package/dist/bridge-contract.d.ts +21 -3
- package/dist/bridge-contract.js +2 -0
- package/dist/bridge-handlers.d.ts +10 -0
- package/dist/bridge-handlers.js +515 -179
- package/dist/bridge.js +1531 -855
- package/dist/builtin-modules.js +6 -0
- package/dist/esm-compiler.d.ts +1 -0
- package/dist/esm-compiler.js +29 -11
- package/dist/execution-driver.js +362 -10
- package/dist/host-network-adapter.js +10 -6
- package/dist/isolate-bootstrap.d.ts +6 -0
- package/dist/kernel-runtime.d.ts +3 -1
- package/dist/kernel-runtime.js +109 -11
- package/dist/module-access.d.ts +3 -0
- package/dist/module-access.js +52 -2
- package/dist/module-resolver.js +3 -3
- package/dist/module-source.d.ts +5 -0
- package/dist/module-source.js +224 -0
- package/dist/polyfills.js +27 -1
- package/package.json +5 -3
package/dist/kernel-runtime.js
CHANGED
|
@@ -11,8 +11,41 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
11
11
|
import * as fsPromises from 'node:fs/promises';
|
|
12
12
|
import { dirname, join, resolve } from 'node:path';
|
|
13
13
|
import { NodeExecutionDriver } from './execution-driver.js';
|
|
14
|
-
import { createNodeDriver } from './driver.js';
|
|
14
|
+
import { createDefaultNetworkAdapter, createNodeDriver } from './driver.js';
|
|
15
15
|
import { allowAllChildProcess, allowAllFs, createProcessScopedFileSystem, } from '@secure-exec/core';
|
|
16
|
+
const allowKernelProcSelfRead = {
|
|
17
|
+
fs: (request) => {
|
|
18
|
+
const rawPath = typeof request?.path === 'string' ? request.path : '';
|
|
19
|
+
const normalized = rawPath.length > 1 && rawPath.endsWith('/')
|
|
20
|
+
? rawPath.slice(0, -1)
|
|
21
|
+
: rawPath || '/';
|
|
22
|
+
switch (request?.op) {
|
|
23
|
+
case 'read':
|
|
24
|
+
case 'readdir':
|
|
25
|
+
case 'readlink':
|
|
26
|
+
case 'stat':
|
|
27
|
+
case 'exists':
|
|
28
|
+
break;
|
|
29
|
+
default:
|
|
30
|
+
return {
|
|
31
|
+
allow: false,
|
|
32
|
+
reason: 'kernel procfs metadata is read-only',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (normalized === '/proc' ||
|
|
36
|
+
normalized === '/proc/self' ||
|
|
37
|
+
normalized.startsWith('/proc/self/') ||
|
|
38
|
+
normalized === '/proc/sys' ||
|
|
39
|
+
normalized === '/proc/sys/kernel' ||
|
|
40
|
+
normalized === '/proc/sys/kernel/hostname') {
|
|
41
|
+
return { allow: true };
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
allow: false,
|
|
45
|
+
reason: 'kernel-mounted Node only allows read-only /proc/self metadata by default',
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
};
|
|
16
49
|
/**
|
|
17
50
|
* Create a Node.js RuntimeDriver that can be mounted into the kernel.
|
|
18
51
|
*/
|
|
@@ -288,7 +321,10 @@ class NodeRuntimeDriver {
|
|
|
288
321
|
_activeDrivers = new Map();
|
|
289
322
|
constructor(options) {
|
|
290
323
|
this._memoryLimit = options?.memoryLimit ?? 128;
|
|
291
|
-
this._permissions = options?.permissions ?? {
|
|
324
|
+
this._permissions = options?.permissions ?? {
|
|
325
|
+
...allowAllChildProcess,
|
|
326
|
+
...allowKernelProcSelfRead,
|
|
327
|
+
};
|
|
292
328
|
this._bindings = options?.bindings;
|
|
293
329
|
}
|
|
294
330
|
async init(kernel) {
|
|
@@ -309,6 +345,16 @@ class NodeRuntimeDriver {
|
|
|
309
345
|
resolve(code);
|
|
310
346
|
};
|
|
311
347
|
});
|
|
348
|
+
let killedSignal = null;
|
|
349
|
+
let killExitReported = false;
|
|
350
|
+
const reportKilledExit = (signal) => {
|
|
351
|
+
if (killExitReported)
|
|
352
|
+
return;
|
|
353
|
+
killExitReported = true;
|
|
354
|
+
const exitCode = 128 + signal;
|
|
355
|
+
resolveExit(exitCode);
|
|
356
|
+
proc.onExit?.(exitCode);
|
|
357
|
+
};
|
|
312
358
|
// Stdin buffering — writeStdin collects data, closeStdin resolves the promise
|
|
313
359
|
const stdinChunks = [];
|
|
314
360
|
let stdinResolve = null;
|
|
@@ -349,17 +395,31 @@ class NodeRuntimeDriver {
|
|
|
349
395
|
stdinResolve = null;
|
|
350
396
|
}
|
|
351
397
|
},
|
|
352
|
-
kill: (
|
|
398
|
+
kill: (signal) => {
|
|
399
|
+
if (exitResolved)
|
|
400
|
+
return;
|
|
401
|
+
const normalizedSignal = signal > 0 ? signal : 15;
|
|
402
|
+
killedSignal = normalizedSignal;
|
|
353
403
|
const driver = this._activeDrivers.get(ctx.pid);
|
|
354
|
-
if (driver) {
|
|
355
|
-
|
|
356
|
-
|
|
404
|
+
if (!driver) {
|
|
405
|
+
reportKilledExit(normalizedSignal);
|
|
406
|
+
return;
|
|
357
407
|
}
|
|
408
|
+
this._activeDrivers.delete(ctx.pid);
|
|
409
|
+
void driver
|
|
410
|
+
.terminate()
|
|
411
|
+
.catch(() => {
|
|
412
|
+
// Best effort: disposal still clears local resource tracking.
|
|
413
|
+
driver.dispose();
|
|
414
|
+
})
|
|
415
|
+
.finally(() => {
|
|
416
|
+
reportKilledExit(normalizedSignal);
|
|
417
|
+
});
|
|
358
418
|
},
|
|
359
419
|
wait: () => exitPromise,
|
|
360
420
|
};
|
|
361
421
|
// Launch async — spawn() returns synchronously per RuntimeDriver contract
|
|
362
|
-
this._executeAsync(command, args, ctx, proc, resolveExit, stdinPromise);
|
|
422
|
+
this._executeAsync(command, args, ctx, proc, resolveExit, stdinPromise, () => killedSignal);
|
|
363
423
|
return proc;
|
|
364
424
|
}
|
|
365
425
|
async dispose() {
|
|
@@ -375,13 +435,16 @@ class NodeRuntimeDriver {
|
|
|
375
435
|
// -------------------------------------------------------------------------
|
|
376
436
|
// Async execution
|
|
377
437
|
// -------------------------------------------------------------------------
|
|
378
|
-
async _executeAsync(command, args, ctx, proc, resolveExit, stdinPromise) {
|
|
438
|
+
async _executeAsync(command, args, ctx, proc, resolveExit, stdinPromise, getKilledSignal) {
|
|
379
439
|
const kernel = this._kernel;
|
|
380
440
|
try {
|
|
381
441
|
// Resolve the code to execute
|
|
382
442
|
const { code, filePath } = await this._resolveEntry(command, args, kernel);
|
|
383
443
|
// Wait for stdin data (resolves immediately if no writeStdin called)
|
|
384
444
|
const stdinData = await stdinPromise;
|
|
445
|
+
if (getKilledSignal() !== null) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
385
448
|
// Build kernel-backed system driver
|
|
386
449
|
const commandExecutor = createKernelCommandExecutor(kernel, ctx.pid);
|
|
387
450
|
let filesystem = createProcessScopedFileSystem(createKernelVfsAdapter(kernel.vfs), ctx.pid);
|
|
@@ -397,6 +460,10 @@ class NodeRuntimeDriver {
|
|
|
397
460
|
const stderrIsTTY = ctx.stderrIsTTY ?? false;
|
|
398
461
|
const systemDriver = createNodeDriver({
|
|
399
462
|
filesystem,
|
|
463
|
+
moduleAccess: { cwd: ctx.cwd },
|
|
464
|
+
networkAdapter: kernel.socketTable.hasHostNetworkAdapter()
|
|
465
|
+
? createDefaultNetworkAdapter()
|
|
466
|
+
: undefined,
|
|
400
467
|
commandExecutor,
|
|
401
468
|
permissions,
|
|
402
469
|
processConfig: {
|
|
@@ -407,16 +474,35 @@ class NodeRuntimeDriver {
|
|
|
407
474
|
stdoutIsTTY,
|
|
408
475
|
stderrIsTTY,
|
|
409
476
|
},
|
|
477
|
+
osConfig: {
|
|
478
|
+
homedir: ctx.env.HOME || '/root',
|
|
479
|
+
tmpdir: ctx.env.TMPDIR || '/tmp',
|
|
480
|
+
},
|
|
410
481
|
});
|
|
411
482
|
// Wire PTY raw mode callback when stdin is a terminal
|
|
412
483
|
const onPtySetRawMode = stdinIsTTY
|
|
413
484
|
? (mode) => {
|
|
414
|
-
kernel.
|
|
415
|
-
|
|
485
|
+
kernel.tcsetattr(ctx.pid, 0, {
|
|
486
|
+
icanon: !mode,
|
|
416
487
|
echo: !mode,
|
|
488
|
+
isig: !mode,
|
|
489
|
+
icrnl: !mode,
|
|
417
490
|
});
|
|
418
491
|
}
|
|
419
492
|
: undefined;
|
|
493
|
+
const liveStdinSource = stdinIsTTY
|
|
494
|
+
? {
|
|
495
|
+
async read() {
|
|
496
|
+
try {
|
|
497
|
+
const chunk = await kernel.fdRead(ctx.pid, 0, 4096);
|
|
498
|
+
return chunk.length === 0 ? null : chunk;
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
}
|
|
505
|
+
: undefined;
|
|
420
506
|
// Create a per-process isolate with kernel socket routing
|
|
421
507
|
const executionDriver = new NodeExecutionDriver({
|
|
422
508
|
system: systemDriver,
|
|
@@ -428,8 +514,20 @@ class NodeRuntimeDriver {
|
|
|
428
514
|
processTable: kernel.processTable,
|
|
429
515
|
timerTable: kernel.timerTable,
|
|
430
516
|
pid: ctx.pid,
|
|
517
|
+
liveStdinSource,
|
|
431
518
|
});
|
|
432
519
|
this._activeDrivers.set(ctx.pid, executionDriver);
|
|
520
|
+
const killedSignal = getKilledSignal();
|
|
521
|
+
if (killedSignal !== null) {
|
|
522
|
+
this._activeDrivers.delete(ctx.pid);
|
|
523
|
+
try {
|
|
524
|
+
await executionDriver.terminate();
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
executionDriver.dispose();
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
433
531
|
// Execute with stdout/stderr capture and stdin data
|
|
434
532
|
const result = await executionDriver.exec(code, {
|
|
435
533
|
filePath,
|
|
@@ -437,7 +535,7 @@ class NodeRuntimeDriver {
|
|
|
437
535
|
cwd: ctx.cwd,
|
|
438
536
|
stdin: stdinData,
|
|
439
537
|
onStdio: (event) => {
|
|
440
|
-
const data = new TextEncoder().encode(event.message
|
|
538
|
+
const data = new TextEncoder().encode(event.message);
|
|
441
539
|
if (event.channel === 'stdout') {
|
|
442
540
|
ctx.onStdout?.(data);
|
|
443
541
|
proc.onStdout?.(data);
|
package/dist/module-access.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface ModuleAccessOptions {
|
|
|
20
20
|
*/
|
|
21
21
|
export declare class ModuleAccessFileSystem implements VirtualFileSystem {
|
|
22
22
|
private readonly baseFileSystem?;
|
|
23
|
+
private readonly configuredNodeModulesRoot;
|
|
23
24
|
private readonly hostNodeModulesRoot;
|
|
24
25
|
private readonly overlayAllowedRoots;
|
|
25
26
|
constructor(baseFileSystem: VirtualFileSystem | undefined, options: ModuleAccessOptions);
|
|
@@ -29,6 +30,8 @@ export declare class ModuleAccessFileSystem implements VirtualFileSystem {
|
|
|
29
30
|
private isReadOnlyProjectionPath;
|
|
30
31
|
private shouldMergeBase;
|
|
31
32
|
private overlayHostPathFor;
|
|
33
|
+
private isProjectedHostPath;
|
|
34
|
+
private getOverlayHostPathCandidate;
|
|
32
35
|
prepareOpenSync(pathValue: string, flags: number): boolean;
|
|
33
36
|
/** Translate a sandbox path to the corresponding host path (for sync module resolution). */
|
|
34
37
|
toHostPath(sandboxPath: string): string | null;
|
package/dist/module-access.js
CHANGED
|
@@ -69,16 +69,39 @@ function isNativeAddonPath(pathValue) {
|
|
|
69
69
|
function collectOverlayAllowedRoots(hostNodeModulesRoot) {
|
|
70
70
|
const roots = new Set([hostNodeModulesRoot]);
|
|
71
71
|
const symlinkScanRoots = [hostNodeModulesRoot, path.join(hostNodeModulesRoot, ".pnpm", "node_modules")];
|
|
72
|
+
const scannedSymlinkDirs = new Set();
|
|
73
|
+
const findNearestNodeModulesAncestor = (targetPath) => {
|
|
74
|
+
let current = path.resolve(targetPath);
|
|
75
|
+
while (true) {
|
|
76
|
+
if (path.basename(current) === "node_modules") {
|
|
77
|
+
return current;
|
|
78
|
+
}
|
|
79
|
+
const parent = path.dirname(current);
|
|
80
|
+
if (parent === current) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
current = parent;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
72
86
|
const addSymlinkTarget = (entryPath) => {
|
|
73
87
|
try {
|
|
74
88
|
const target = fsSync.realpathSync(entryPath);
|
|
75
89
|
roots.add(target);
|
|
90
|
+
const packageNodeModulesRoot = findNearestNodeModulesAncestor(target);
|
|
91
|
+
if (packageNodeModulesRoot) {
|
|
92
|
+
roots.add(packageNodeModulesRoot);
|
|
93
|
+
scanDirForSymlinks(packageNodeModulesRoot);
|
|
94
|
+
}
|
|
76
95
|
}
|
|
77
96
|
catch {
|
|
78
97
|
// Ignore broken symlinks.
|
|
79
98
|
}
|
|
80
99
|
};
|
|
81
100
|
const scanDirForSymlinks = (scanRoot) => {
|
|
101
|
+
if (scannedSymlinkDirs.has(scanRoot)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
scannedSymlinkDirs.add(scanRoot);
|
|
82
105
|
let entries = [];
|
|
83
106
|
try {
|
|
84
107
|
entries = fsSync.readdirSync(scanRoot, { withFileTypes: true });
|
|
@@ -122,6 +145,7 @@ function collectOverlayAllowedRoots(hostNodeModulesRoot) {
|
|
|
122
145
|
*/
|
|
123
146
|
export class ModuleAccessFileSystem {
|
|
124
147
|
baseFileSystem;
|
|
148
|
+
configuredNodeModulesRoot;
|
|
125
149
|
hostNodeModulesRoot;
|
|
126
150
|
overlayAllowedRoots;
|
|
127
151
|
constructor(baseFileSystem, options) {
|
|
@@ -132,6 +156,7 @@ export class ModuleAccessFileSystem {
|
|
|
132
156
|
}
|
|
133
157
|
const cwd = path.resolve(cwdInput);
|
|
134
158
|
const nodeModulesPath = path.join(cwd, "node_modules");
|
|
159
|
+
this.configuredNodeModulesRoot = nodeModulesPath;
|
|
135
160
|
try {
|
|
136
161
|
this.hostNodeModulesRoot = fsSync.realpathSync(nodeModulesPath);
|
|
137
162
|
this.overlayAllowedRoots = collectOverlayAllowedRoots(this.hostNodeModulesRoot);
|
|
@@ -164,7 +189,8 @@ export class ModuleAccessFileSystem {
|
|
|
164
189
|
return entries;
|
|
165
190
|
}
|
|
166
191
|
isReadOnlyProjectionPath(virtualPath) {
|
|
167
|
-
return startsWithPath(virtualPath, SANDBOX_NODE_MODULES_ROOT)
|
|
192
|
+
return (startsWithPath(virtualPath, SANDBOX_NODE_MODULES_ROOT) ||
|
|
193
|
+
this.isProjectedHostPath(virtualPath));
|
|
168
194
|
}
|
|
169
195
|
shouldMergeBase(pathValue) {
|
|
170
196
|
return (pathValue === "/" ||
|
|
@@ -189,6 +215,30 @@ export class ModuleAccessFileSystem {
|
|
|
189
215
|
}
|
|
190
216
|
return path.join(this.hostNodeModulesRoot, ...relative.split("/"));
|
|
191
217
|
}
|
|
218
|
+
isProjectedHostPath(pathValue) {
|
|
219
|
+
if (!path.isAbsolute(pathValue)) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
const resolved = path.resolve(pathValue);
|
|
223
|
+
if (isWithinPath(resolved, this.configuredNodeModulesRoot)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
if (this.hostNodeModulesRoot &&
|
|
227
|
+
isWithinPath(resolved, this.hostNodeModulesRoot)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
return this.overlayAllowedRoots.some((root) => isWithinPath(resolved, root));
|
|
231
|
+
}
|
|
232
|
+
getOverlayHostPathCandidate(pathValue) {
|
|
233
|
+
const overlayPath = this.overlayHostPathFor(pathValue);
|
|
234
|
+
if (overlayPath) {
|
|
235
|
+
return overlayPath;
|
|
236
|
+
}
|
|
237
|
+
if (!this.isProjectedHostPath(pathValue)) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return path.resolve(pathValue);
|
|
241
|
+
}
|
|
192
242
|
prepareOpenSync(pathValue, flags) {
|
|
193
243
|
const virtualPath = normalizeOverlayPath(pathValue);
|
|
194
244
|
if (this.isReadOnlyProjectionPath(virtualPath)) {
|
|
@@ -213,7 +263,7 @@ export class ModuleAccessFileSystem {
|
|
|
213
263
|
if (isNativeAddonPath(virtualPath)) {
|
|
214
264
|
throw createModuleAccessError(MODULE_ACCESS_NATIVE_ADDON, `native addon '${virtualPath}' is not supported for module overlay`);
|
|
215
265
|
}
|
|
216
|
-
const hostPath = this.
|
|
266
|
+
const hostPath = this.getOverlayHostPathCandidate(virtualPath);
|
|
217
267
|
if (!hostPath) {
|
|
218
268
|
return null;
|
|
219
269
|
}
|
package/dist/module-resolver.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { normalizeBuiltinSpecifier, getPathDir, } from "./builtin-modules.js";
|
|
2
2
|
import { resolveModule } from "./package-bundler.js";
|
|
3
|
-
import { isESM } from "@secure-exec/core/internal/shared/esm-utils";
|
|
4
3
|
import { parseJsonWithLimit } from "./isolate-bootstrap.js";
|
|
4
|
+
import { sourceHasModuleSyntax } from "./module-source.js";
|
|
5
5
|
export async function getNearestPackageType(deps, filePath) {
|
|
6
6
|
let currentDir = getPathDir(filePath);
|
|
7
7
|
const visitedDirs = [];
|
|
@@ -72,7 +72,7 @@ export async function getModuleFormat(deps, filePath, sourceCode) {
|
|
|
72
72
|
else if (packageType === "commonjs") {
|
|
73
73
|
format = "cjs";
|
|
74
74
|
}
|
|
75
|
-
else if (sourceCode &&
|
|
75
|
+
else if (sourceCode && await sourceHasModuleSyntax(sourceCode, filePath)) {
|
|
76
76
|
// Some package managers/projected filesystems omit package.json.
|
|
77
77
|
// Fall back to syntax-based detection for plain .js modules.
|
|
78
78
|
format = "esm";
|
|
@@ -90,7 +90,7 @@ export async function getModuleFormat(deps, filePath, sourceCode) {
|
|
|
90
90
|
export async function shouldRunAsESM(deps, code, filePath) {
|
|
91
91
|
// Keep heuristic mode for string-only snippets without file metadata.
|
|
92
92
|
if (!filePath) {
|
|
93
|
-
return
|
|
93
|
+
return sourceHasModuleSyntax(code);
|
|
94
94
|
}
|
|
95
95
|
return (await getModuleFormat(deps, filePath)) === "esm";
|
|
96
96
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function sourceHasModuleSyntax(source: string, filePath?: string): Promise<boolean>;
|
|
2
|
+
export declare function transformSourceForRequireSync(source: string, filePath: string): string;
|
|
3
|
+
export declare function transformSourceForRequire(source: string, filePath: string): Promise<string>;
|
|
4
|
+
export declare function transformSourceForImport(source: string, filePath: string): Promise<string>;
|
|
5
|
+
export declare function transformSourceForImportSync(source: string, filePath: string, formatPath?: string): string;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname as pathDirname, join as pathJoin } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { transform, transformSync } from "esbuild";
|
|
5
|
+
import { initSync as initCjsLexerSync, parse as parseCjsExports } from "cjs-module-lexer";
|
|
6
|
+
import { init, initSync, parse } from "es-module-lexer";
|
|
7
|
+
const REQUIRE_TRANSFORM_MARKER = "/*__secure_exec_require_esm__*/";
|
|
8
|
+
const IMPORT_META_URL_HELPER = "__secureExecImportMetaUrl__";
|
|
9
|
+
const IMPORT_META_RESOLVE_HELPER = "__secureExecImportMetaResolve__";
|
|
10
|
+
const UNICODE_SET_REGEX_MARKER = "/v";
|
|
11
|
+
const CJS_IMPORT_DEFAULT_HELPER = "__secureExecImportedCjsModule__";
|
|
12
|
+
function isJavaScriptLikePath(filePath) {
|
|
13
|
+
return filePath === undefined || /\.[cm]?[jt]sx?$/.test(filePath);
|
|
14
|
+
}
|
|
15
|
+
function normalizeJavaScriptSource(source) {
|
|
16
|
+
const bomPrefix = source.charCodeAt(0) === 0xfeff ? "\uFEFF" : "";
|
|
17
|
+
const shebangOffset = bomPrefix.length;
|
|
18
|
+
if (!source.startsWith("#!", shebangOffset)) {
|
|
19
|
+
return source;
|
|
20
|
+
}
|
|
21
|
+
return (bomPrefix +
|
|
22
|
+
"//" +
|
|
23
|
+
source.slice(shebangOffset + 2));
|
|
24
|
+
}
|
|
25
|
+
function parseSourceSyntax(source, filePath) {
|
|
26
|
+
const [imports, , , hasModuleSyntax] = parse(source, filePath);
|
|
27
|
+
const hasDynamicImport = imports.some((specifier) => specifier.d >= 0);
|
|
28
|
+
const hasImportMeta = imports.some((specifier) => specifier.d === -2);
|
|
29
|
+
return { hasModuleSyntax, hasDynamicImport, hasImportMeta };
|
|
30
|
+
}
|
|
31
|
+
function isValidIdentifier(value) {
|
|
32
|
+
return /^[$A-Z_][0-9A-Z_$]*$/i.test(value);
|
|
33
|
+
}
|
|
34
|
+
function getNearestPackageTypeSync(filePath) {
|
|
35
|
+
let currentDir = pathDirname(filePath);
|
|
36
|
+
while (true) {
|
|
37
|
+
const packageJsonPath = pathJoin(currentDir, "package.json");
|
|
38
|
+
if (existsSync(packageJsonPath)) {
|
|
39
|
+
try {
|
|
40
|
+
const pkgJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
41
|
+
return pkgJson.type === "module" || pkgJson.type === "commonjs"
|
|
42
|
+
? pkgJson.type
|
|
43
|
+
: null;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const parentDir = pathDirname(currentDir);
|
|
50
|
+
if (parentDir === currentDir) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
currentDir = parentDir;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isCommonJsModuleForImportSync(source, formatPath) {
|
|
57
|
+
if (!isJavaScriptLikePath(formatPath)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (formatPath.endsWith(".cjs")) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (formatPath.endsWith(".mjs")) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
if (formatPath.endsWith(".js")) {
|
|
67
|
+
const packageType = getNearestPackageTypeSync(formatPath);
|
|
68
|
+
if (packageType === "module") {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (packageType === "commonjs") {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
initSync();
|
|
75
|
+
return !parseSourceSyntax(source, formatPath).hasModuleSyntax;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
function buildCommonJsImportWrapper(source, filePath) {
|
|
80
|
+
initCjsLexerSync();
|
|
81
|
+
const { exports } = parseCjsExports(source);
|
|
82
|
+
const namedExports = Array.from(new Set(exports.filter((name) => name !== "default" &&
|
|
83
|
+
name !== "__esModule" &&
|
|
84
|
+
isValidIdentifier(name))));
|
|
85
|
+
const lines = [
|
|
86
|
+
`const ${CJS_IMPORT_DEFAULT_HELPER} = globalThis._requireFrom(${JSON.stringify(filePath)}, "/");`,
|
|
87
|
+
`export default ${CJS_IMPORT_DEFAULT_HELPER};`,
|
|
88
|
+
...namedExports.map((name) => `export const ${name} = ${CJS_IMPORT_DEFAULT_HELPER} == null ? undefined : ${CJS_IMPORT_DEFAULT_HELPER}[${JSON.stringify(name)}];`),
|
|
89
|
+
];
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
function getRequireTransformOptions(filePath, syntax) {
|
|
93
|
+
const requiresEsmWrapper = syntax.hasModuleSyntax || syntax.hasImportMeta;
|
|
94
|
+
const bannerLines = requiresEsmWrapper ? [REQUIRE_TRANSFORM_MARKER] : [];
|
|
95
|
+
if (syntax.hasImportMeta) {
|
|
96
|
+
bannerLines.push(`const ${IMPORT_META_URL_HELPER} = require("node:url").pathToFileURL(__secureExecFilename).href;`);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
banner: bannerLines.length > 0 ? bannerLines.join("\n") : undefined,
|
|
100
|
+
define: syntax.hasImportMeta
|
|
101
|
+
? {
|
|
102
|
+
"import.meta.url": IMPORT_META_URL_HELPER,
|
|
103
|
+
}
|
|
104
|
+
: undefined,
|
|
105
|
+
format: "cjs",
|
|
106
|
+
loader: "js",
|
|
107
|
+
platform: "node",
|
|
108
|
+
sourcefile: filePath,
|
|
109
|
+
supported: {
|
|
110
|
+
"dynamic-import": false,
|
|
111
|
+
},
|
|
112
|
+
target: "node22",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function getImportTransformOptions(filePath, syntax) {
|
|
116
|
+
const bannerLines = [];
|
|
117
|
+
if (syntax.hasImportMeta) {
|
|
118
|
+
bannerLines.push(`const ${IMPORT_META_URL_HELPER} = ${JSON.stringify(pathToFileURL(filePath).href)};`, `const ${IMPORT_META_RESOLVE_HELPER} = (specifier) => globalThis.__importMetaResolve(specifier, ${JSON.stringify(filePath)});`);
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
banner: bannerLines.length > 0 ? bannerLines.join("\n") : undefined,
|
|
122
|
+
define: syntax.hasImportMeta
|
|
123
|
+
? {
|
|
124
|
+
"import.meta.url": IMPORT_META_URL_HELPER,
|
|
125
|
+
"import.meta.resolve": IMPORT_META_RESOLVE_HELPER,
|
|
126
|
+
}
|
|
127
|
+
: undefined,
|
|
128
|
+
format: "esm",
|
|
129
|
+
loader: "js",
|
|
130
|
+
platform: "node",
|
|
131
|
+
sourcefile: filePath,
|
|
132
|
+
target: "es2020",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export async function sourceHasModuleSyntax(source, filePath) {
|
|
136
|
+
const normalizedSource = normalizeJavaScriptSource(source);
|
|
137
|
+
if (filePath?.endsWith(".mjs")) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (filePath?.endsWith(".cjs")) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
await init;
|
|
144
|
+
return parseSourceSyntax(normalizedSource, filePath).hasModuleSyntax;
|
|
145
|
+
}
|
|
146
|
+
export function transformSourceForRequireSync(source, filePath) {
|
|
147
|
+
if (!isJavaScriptLikePath(filePath)) {
|
|
148
|
+
return source;
|
|
149
|
+
}
|
|
150
|
+
const normalizedSource = normalizeJavaScriptSource(source);
|
|
151
|
+
initSync();
|
|
152
|
+
const syntax = parseSourceSyntax(normalizedSource, filePath);
|
|
153
|
+
if (!(syntax.hasModuleSyntax || syntax.hasDynamicImport || syntax.hasImportMeta)) {
|
|
154
|
+
return normalizedSource;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return transformSync(normalizedSource, getRequireTransformOptions(filePath, syntax)).code;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return normalizedSource;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function transformSourceForRequire(source, filePath) {
|
|
164
|
+
if (!isJavaScriptLikePath(filePath)) {
|
|
165
|
+
return source;
|
|
166
|
+
}
|
|
167
|
+
const normalizedSource = normalizeJavaScriptSource(source);
|
|
168
|
+
await init;
|
|
169
|
+
const syntax = parseSourceSyntax(normalizedSource, filePath);
|
|
170
|
+
if (!(syntax.hasModuleSyntax || syntax.hasDynamicImport || syntax.hasImportMeta)) {
|
|
171
|
+
return normalizedSource;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
return (await transform(normalizedSource, getRequireTransformOptions(filePath, syntax))).code;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return normalizedSource;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export async function transformSourceForImport(source, filePath) {
|
|
181
|
+
if (!isJavaScriptLikePath(filePath)) {
|
|
182
|
+
return source;
|
|
183
|
+
}
|
|
184
|
+
const normalizedSource = normalizeJavaScriptSource(source);
|
|
185
|
+
await init;
|
|
186
|
+
const syntax = parseSourceSyntax(normalizedSource, filePath);
|
|
187
|
+
const needsTransform = normalizedSource.includes(UNICODE_SET_REGEX_MARKER) || syntax.hasImportMeta;
|
|
188
|
+
if (!(syntax.hasModuleSyntax || syntax.hasDynamicImport || syntax.hasImportMeta)) {
|
|
189
|
+
return normalizedSource;
|
|
190
|
+
}
|
|
191
|
+
if (!needsTransform) {
|
|
192
|
+
return normalizedSource;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
return (await transform(normalizedSource, getImportTransformOptions(filePath, syntax))).code;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return normalizedSource;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export function transformSourceForImportSync(source, filePath, formatPath = filePath) {
|
|
202
|
+
if (!isJavaScriptLikePath(filePath)) {
|
|
203
|
+
return source;
|
|
204
|
+
}
|
|
205
|
+
const normalizedSource = normalizeJavaScriptSource(source);
|
|
206
|
+
if (isCommonJsModuleForImportSync(normalizedSource, formatPath)) {
|
|
207
|
+
return buildCommonJsImportWrapper(normalizedSource, filePath);
|
|
208
|
+
}
|
|
209
|
+
initSync();
|
|
210
|
+
const syntax = parseSourceSyntax(normalizedSource, filePath);
|
|
211
|
+
const needsTransform = normalizedSource.includes(UNICODE_SET_REGEX_MARKER) || syntax.hasImportMeta;
|
|
212
|
+
if (!(syntax.hasModuleSyntax || syntax.hasDynamicImport || syntax.hasImportMeta)) {
|
|
213
|
+
return normalizedSource;
|
|
214
|
+
}
|
|
215
|
+
if (!needsTransform) {
|
|
216
|
+
return normalizedSource;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
return transformSync(normalizedSource, getImportTransformOptions(filePath, syntax)).code;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return normalizedSource;
|
|
223
|
+
}
|
|
224
|
+
}
|
package/dist/polyfills.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import * as esbuild from "esbuild";
|
|
2
2
|
import stdLibBrowser from "node-stdlib-browser";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
// Cache bundled polyfills
|
|
4
5
|
const polyfillCache = new Map();
|
|
6
|
+
function resolveCustomPolyfillSource(fileName) {
|
|
7
|
+
return fileURLToPath(new URL(`../src/polyfills/${fileName}`, import.meta.url));
|
|
8
|
+
}
|
|
9
|
+
const WEB_STREAMS_PONYFILL_PATH = fileURLToPath(new URL("../../../node_modules/.pnpm/node_modules/web-streams-polyfill/dist/ponyfill.js", import.meta.url));
|
|
10
|
+
const CUSTOM_POLYFILL_ENTRY_POINTS = new Map([
|
|
11
|
+
["crypto", resolveCustomPolyfillSource("crypto.js")],
|
|
12
|
+
["stream/web", resolveCustomPolyfillSource("stream-web.js")],
|
|
13
|
+
["util/types", resolveCustomPolyfillSource("util-types.js")],
|
|
14
|
+
["internal/webstreams/util", resolveCustomPolyfillSource("internal-webstreams-util.js")],
|
|
15
|
+
["internal/webstreams/adapters", resolveCustomPolyfillSource("internal-webstreams-adapters.js")],
|
|
16
|
+
["internal/webstreams/readablestream", resolveCustomPolyfillSource("internal-webstreams-readablestream.js")],
|
|
17
|
+
["internal/webstreams/writablestream", resolveCustomPolyfillSource("internal-webstreams-writablestream.js")],
|
|
18
|
+
["internal/webstreams/transformstream", resolveCustomPolyfillSource("internal-webstreams-transformstream.js")],
|
|
19
|
+
["internal/worker/js_transferable", resolveCustomPolyfillSource("internal-worker-js-transferable.js")],
|
|
20
|
+
["internal/test/binding", resolveCustomPolyfillSource("internal-test-binding.js")],
|
|
21
|
+
["internal/mime", resolveCustomPolyfillSource("internal-mime.js")],
|
|
22
|
+
]);
|
|
5
23
|
// node-stdlib-browser provides the mapping from Node.js stdlib to polyfill paths
|
|
6
24
|
// e.g., { path: "/path/to/path-browserify/index.js", fs: null, ... }
|
|
7
25
|
// We use this mapping instead of maintaining our own
|
|
@@ -13,7 +31,8 @@ export async function bundlePolyfill(moduleName) {
|
|
|
13
31
|
if (cached)
|
|
14
32
|
return cached;
|
|
15
33
|
// Get the polyfill entry point from node-stdlib-browser
|
|
16
|
-
const entryPoint =
|
|
34
|
+
const entryPoint = CUSTOM_POLYFILL_ENTRY_POINTS.get(moduleName) ??
|
|
35
|
+
stdLibBrowser[moduleName];
|
|
17
36
|
if (!entryPoint) {
|
|
18
37
|
throw new Error(`No polyfill available for module: ${moduleName}`);
|
|
19
38
|
}
|
|
@@ -26,6 +45,10 @@ export async function bundlePolyfill(moduleName) {
|
|
|
26
45
|
alias[`node:${name}`] = path;
|
|
27
46
|
}
|
|
28
47
|
}
|
|
48
|
+
if (typeof stdLibBrowser.crypto === "string") {
|
|
49
|
+
alias.__secure_exec_crypto_browserify__ = stdLibBrowser.crypto;
|
|
50
|
+
}
|
|
51
|
+
alias["web-streams-polyfill/dist/ponyfill.js"] = WEB_STREAMS_PONYFILL_PATH;
|
|
29
52
|
// Bundle using esbuild with CommonJS format
|
|
30
53
|
// This ensures proper module.exports handling for all module types including JSON
|
|
31
54
|
const result = await esbuild.build({
|
|
@@ -84,6 +107,9 @@ export function getAvailableStdlib() {
|
|
|
84
107
|
export function hasPolyfill(moduleName) {
|
|
85
108
|
// Strip node: prefix
|
|
86
109
|
const name = moduleName.replace(/^node:/, "");
|
|
110
|
+
if (CUSTOM_POLYFILL_ENTRY_POINTS.has(name)) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
87
113
|
const polyfill = stdLibBrowser[name];
|
|
88
114
|
return polyfill !== undefined && polyfill !== null;
|
|
89
115
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@secure-exec/nodejs",
|
|
3
|
-
"version": "0.2.0-rc.
|
|
3
|
+
"version": "0.2.0-rc.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -97,10 +97,12 @@
|
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
+
"cjs-module-lexer": "^2.1.0",
|
|
101
|
+
"es-module-lexer": "^1.7.0",
|
|
100
102
|
"esbuild": "^0.27.1",
|
|
101
103
|
"node-stdlib-browser": "^1.3.1",
|
|
102
|
-
"@secure-exec/core": "0.2.0-rc.
|
|
103
|
-
"@secure-exec/v8": "0.2.0-rc.
|
|
104
|
+
"@secure-exec/core": "0.2.0-rc.2",
|
|
105
|
+
"@secure-exec/v8": "0.2.0-rc.2"
|
|
104
106
|
},
|
|
105
107
|
"devDependencies": {
|
|
106
108
|
"@types/node": "^22.10.2",
|