@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.
@@ -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 ?? { ...allowAllChildProcess };
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: (_signal) => {
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
- driver.dispose();
356
- this._activeDrivers.delete(ctx.pid);
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.ptySetDiscipline(ctx.pid, 0, {
415
- canonical: !mode,
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 + '\n');
538
+ const data = new TextEncoder().encode(event.message);
441
539
  if (event.channel === 'stdout') {
442
540
  ctx.onStdout?.(data);
443
541
  proc.onStdout?.(data);
@@ -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;
@@ -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.overlayHostPathFor(virtualPath);
266
+ const hostPath = this.getOverlayHostPathCandidate(virtualPath);
217
267
  if (!hostPath) {
218
268
  return null;
219
269
  }
@@ -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 && isESM(sourceCode, filePath)) {
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 isESM(code);
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 = stdLibBrowser[moduleName];
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.1",
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.1",
103
- "@secure-exec/v8": "0.2.0-rc.1"
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",