diff --git a/docs/doctoring/commonjs-runtime-authority.md b/docs/doctoring/commonjs-runtime-authority.md new file mode 100644 index 00000000..71957f93 --- /dev/null +++ b/docs/doctoring/commonjs-runtime-authority.md @@ -0,0 +1,37 @@ +# CommonJS Runtime-Authority Verification + +Status: active-PR evidence for PR #290. This note does not describe protected-main shipped behavior until the PR is merged. + +## Decision boundary + +Inkspan release verification treats executable module lookup or loading authority left in a packed JavaScript artifact as a packaging failure. The verifier parses emitted JavaScript with the TypeScript AST and classifies only statically recognizable syntax. It does not execute artifact code, resolve arbitrary aliases, evaluate receiver expressions, or infer dynamic computed member names. + +The current active-PR scanner covers static imports/re-exports, dynamic `import(...)`, direct and statically indirect CommonJS `require` forms, `module.require`, `require.main.require`, `require.resolve`, `.call`, `.apply`, `Reflect.apply`, retained `.bind` capabilities, statically composed `.bind` invocations, constructor invocation through `new`, `Reflect.construct` when the target is already a recognized CommonJS loader or resolver, and Node `process.getBuiltinModule(...)` / `globalThis.process.getBuiltinModule(...)` / `global.process.getBuiltinModule(...)` authority through direct calls, comma-indirected calls whose right operand is one of those exact ambient loader expressions, exact-receiver `.call(...)` / `.apply(...)`, `Reflect.apply(...)`, retained `.bind(...)` capabilities, and immediately invoked bound capabilities. A directly recognizable bound CommonJS or Node built-in loader/resolver is therefore rejected when it is retained for later use, before later invocation occurs. Literal module specifiers are retained as actionable evidence; computed or missing specifiers remain unknown rather than being guessed. Static string-literal element access may identify an already-supported member spelling. Ordinary object methods merely named `require`, `resolve`, or `getBuiltinModule` remain outside the authority model. + +The `process.getBuiltinModule(...)` classification is intentionally narrow: it recognizes only the ambient `process` spelling and the explicit `globalThis.process` / `global.process` spellings whose `process` member resolves to Node's ambient process object, plus syntax-only comma indirection that preserves one of those exact expressions as the right operand, and standard invocation/retention syntax when the receiver/target is one of those exact loader expressions. It does not resolve aliases, arbitrary receivers, or dynamic computed member names, and it does not infer that a similarly named method on another object has Node module authority. + +This is a release-evidence boundary, not a runtime loader. It adds no filesystem, network, credential, persistence, deployment, model, or host authority to Inkspan. + +## Standards basis + +Node.js documents `require()` as the CommonJS module loader, `require.main` as the entry-module reference for CommonJS entry points, and `require.resolve()` as using the internal `require()` resolution machinery without loading the resolved module. These semantics make both loading and resolution relevant executable authority in an artifact expected to be self-contained. + +Node.js also documents `process.getBuiltinModule(id)` as a globally available synchronous way to load a Node built-in module. In supported Node runtimes, `process`, `globalThis.process`, and `global.process` refer to the same ambient process object; the branch regression executes the `global.process.getBuiltinModule(...)` spelling to keep that equivalence machine-checked rather than inferred from arbitrary object structure. Because the packed Markdown artifact is required to be self-contained and free of executable runtime module authority, leaving that authority in the artifact is a packaging failure even though the target is a Node built-in rather than an external package. + +ECMA-262 defines `Function.prototype.bind` as producing a bound function whose target and leading arguments are retained for subsequent calls. That retained callable is already executable loader/resolver authority before later invocation, so a packed artifact that stores a directly recognizable `require.bind(...)`, `require.resolve.bind(...)`, or ambient `process.getBuiltinModule.bind(...)` capability has not eliminated the authority merely because the call happens later. ECMA-262 also defines `Function.prototype.call` and `Function.prototype.apply` as invoking their callable receiver with explicit receiver/argument data, `Reflect.apply(target, thisArgument, argumentsList)` as invoking a callable target with an argument list, ordinary `new` expressions as constructor invocation, and `Reflect.construct(target, argumentsList, newTarget)` as construction through an explicitly supplied target. Therefore, a verifier that recognizes only direct loader calls can miss equivalent statically recognizable authority invoked through standard `call`/`apply`/`Reflect.apply` or retained/composed through standard `bind` forms. + +The verifier intentionally remains syntax-bounded. ECMAScript permits arbitrary aliasing and computation; attempting whole-program resolution in this release check would enlarge the trusted implementation and risk unsound guesses. Unknown computed module arguments are therefore reported as executable authority without an invented specifier when the authority surface itself is statically recognizable. + +## Assurance implications + +The regression suite must include positive cases for each supported syntax family, including retained bound CommonJS loader/resolver capabilities before later invocation and Node built-in loading through all three exact ambient process spellings (`process`, `globalThis.process`, and `global.process`), direct and comma-indirected calls, exact-receiver `.call(...)` / `.apply(...)`, `Reflect.apply(...)`, retained `.bind(...)`, and immediate bound invocation, plus negative controls for ordinary objects with similarly named methods. Invalid emitted JavaScript fails closed. Any extension to the recognized syntax must be test-first and must preserve the no-execution/no-alias-evaluation invariant. + +Release acceptance must use the exact packed artifact produced from the exact candidate head. Passing source tests on a predecessor, a different checkout SHA, or a status-only/model-only signal is not evidence that the packed artifact is free of runtime module authority. + +## References + +Ecma International. (2026). *ECMA-262: ECMAScript® 2026 language specification* (17th ed.). https://262.ecma-international.org/ + +Node.js contributors. (2026). *Modules: CommonJS modules* (Node.js v26.5.1 documentation). Node.js. https://nodejs.org/api/modules.html + +Node.js contributors. (2026). *Process* (Node.js v26.7.0 documentation). Node.js. https://nodejs.org/api/process.html diff --git a/scripts/javascript-runtime-authority.mjs b/scripts/javascript-runtime-authority.mjs index 8ee68338..d6268149 100644 --- a/scripts/javascript-runtime-authority.mjs +++ b/scripts/javascript-runtime-authority.mjs @@ -5,11 +5,12 @@ import ts from 'typescript'; * * Parsing the artifact instead of scanning raw text deliberately ignores comments, * string literals, and template text that merely mention `require()` or `import()`. - * Actual static imports/re-exports, bare CommonJS `require()` calls, and dynamic - * `import()` calls remain fail-closed findings. Literal module specifiers are - * preserved in diagnostics so a verifier failure identifies the dependency edge - * that escaped bundling; computed specifiers remain `undefined` and therefore do - * not acquire an invented interpretation. + * Actual static imports/re-exports, statically recognizable CommonJS loader and + * resolver calls, Node built-in module loads, and dynamic `import()` calls remain + * fail-closed findings. Literal module specifiers are preserved in diagnostics so a + * verifier failure identifies the dependency edge that escaped bundling; computed + * specifiers remain `undefined` and therefore do not acquire an invented + * interpretation. * * @param {string} source JavaScript source emitted into the packed artifact. * @param {string} [filename='bundle.js'] Diagnostic filename for parse failures. @@ -34,6 +35,7 @@ export function findRuntimeModuleAuthority(source, filename = 'bundle.js') { } const findings = []; + const consumedBoundCalls = new WeakSet(); /** Return a literal module specifier without evaluating computed expressions. */ function literalSpecifier(expression) { @@ -42,6 +44,487 @@ export function findRuntimeModuleAuthority(source, filename = 'bundle.js') { : undefined; } + /** Remove syntax-only parentheses without evaluating the expression. */ + function unwrapParentheses(expression) { + let current = expression; + while (ts.isParenthesizedExpression(current)) { + current = current.expression; + } + return current; + } + + /** Return a statically written member name without evaluating property input. */ + function staticMemberName(expression) { + if (ts.isPropertyAccessExpression(expression)) { + return expression.name.text; + } + if ( + ts.isElementAccessExpression(expression) && + expression.argumentExpression && + ts.isStringLiteralLike(expression.argumentExpression) + ) { + return expression.argumentExpression.text; + } + return undefined; + } + + /** Identify Node's ambient synchronous built-in module loader syntax. */ + function isNodeBuiltinModuleExpression(expression) { + const current = unwrapParentheses(expression); + if ( + ts.isBinaryExpression(current) && + current.operatorToken.kind === ts.SyntaxKind.CommaToken + ) { + return isNodeBuiltinModuleExpression(current.right); + } + if ( + (!ts.isPropertyAccessExpression(current) && + !ts.isElementAccessExpression(current)) || + staticMemberName(current) !== 'getBuiltinModule' + ) { + return false; + } + + const receiver = unwrapParentheses(current.expression); + if (ts.isIdentifier(receiver) && receiver.text === 'process') { + return true; + } + if ( + (ts.isPropertyAccessExpression(receiver) || + ts.isElementAccessExpression(receiver)) && + staticMemberName(receiver) === 'process' + ) { + const root = unwrapParentheses(receiver.expression); + return ( + ts.isIdentifier(root) && + (root.text === 'globalThis' || root.text === 'global') + ); + } + return false; + } + + /** Return the module argument for recognizable ambient built-in loading. */ + function nodeBuiltinModuleInvocation(node) { + if (isNodeBuiltinModuleExpression(node.expression)) { + return { argument: node.arguments[0] }; + } + + const boundInvocation = commonJsBoundInvocation( + node, + isNodeBuiltinModuleExpression, + ); + if (boundInvocation) { + return boundInvocation; + } + + if (isReflectApplyExpression(node.expression)) { + const target = node.arguments[0]; + if (target && isNodeBuiltinModuleExpression(target)) { + return { argument: commonJsApplyArgument(node, 2) }; + } + if (target) { + const boundTarget = commonJsBoundExpression( + target, + isNodeBuiltinModuleExpression, + ); + if (boundTarget) { + return { + argument: commonJsBoundArgument( + boundTarget, + commonJsApplyArgument(node, 2), + ), + }; + } + } + } + + const callee = unwrapParentheses(node.expression); + if ( + ts.isPropertyAccessExpression(callee) || + ts.isElementAccessExpression(callee) + ) { + const invocationMethod = staticMemberName(callee); + if (invocationMethod === 'call' || invocationMethod === 'apply') { + const fallbackArgument = + invocationMethod === 'call' + ? node.arguments[1] + : commonJsApplyArgument(node); + if (isNodeBuiltinModuleExpression(callee.expression)) { + return { argument: fallbackArgument }; + } + const boundReceiver = commonJsBoundExpression( + callee.expression, + isNodeBuiltinModuleExpression, + ); + if (boundReceiver) { + return { + argument: commonJsBoundArgument( + boundReceiver, + fallbackArgument, + ), + }; + } + } + } + return null; + } + + /** Identify direct CommonJS loader values without resolving aliases or scope. */ + function isCommonJsLoaderExpression(expression) { + const current = unwrapParentheses(expression); + if (ts.isIdentifier(current)) { + return current.text === 'require'; + } + if ( + ts.isBinaryExpression(current) && + current.operatorToken.kind === ts.SyntaxKind.CommaToken + ) { + return isCommonJsLoaderExpression(current.right); + } + if ( + (ts.isPropertyAccessExpression(current) || + ts.isElementAccessExpression(current)) && + staticMemberName(current) === 'require' + ) { + const receiver = unwrapParentheses(current.expression); + if (ts.isIdentifier(receiver) && receiver.text === 'module') { + return true; + } + return ( + (ts.isPropertyAccessExpression(receiver) || + ts.isElementAccessExpression(receiver)) && + staticMemberName(receiver) === 'main' && + isCommonJsLoaderExpression(receiver.expression) + ); + } + return false; + } + + /** Identify statically recognizable CommonJS resolver values. */ + function isCommonJsResolverExpression(expression) { + const current = unwrapParentheses(expression); + if ( + ts.isBinaryExpression(current) && + current.operatorToken.kind === ts.SyntaxKind.CommaToken + ) { + return isCommonJsResolverExpression(current.right); + } + return ( + (ts.isPropertyAccessExpression(current) || + ts.isElementAccessExpression(current)) && + staticMemberName(current) === 'resolve' && + isCommonJsLoaderExpression(current.expression) + ); + } + + /** Identify the built-in `Reflect.apply` without resolving aliases or receivers. */ + function isReflectApplyExpression(expression) { + const current = unwrapParentheses(expression); + if ( + !ts.isPropertyAccessExpression(current) && + !ts.isElementAccessExpression(current) + ) { + return false; + } + const receiver = unwrapParentheses(current.expression); + return ( + staticMemberName(current) === 'apply' && + ts.isIdentifier(receiver) && + receiver.text === 'Reflect' + ); + } + + /** Identify the built-in `Reflect.construct` without resolving aliases. */ + function isReflectConstructExpression(expression) { + const current = unwrapParentheses(expression); + if ( + !ts.isPropertyAccessExpression(current) && + !ts.isElementAccessExpression(current) + ) { + return false; + } + const receiver = unwrapParentheses(current.expression); + return ( + staticMemberName(current) === 'construct' && + ts.isIdentifier(receiver) && + receiver.text === 'Reflect' + ); + } + + /** + * Return the first statically written apply payload argument. + * Non-array, computed, missing, and spread argument lists still identify + * executable authority but deliberately yield an unknown module specifier. + */ + function commonJsApplyArgument(node, argumentListIndex = 1) { + const argumentList = node.arguments[argumentListIndex]; + if (!argumentList) { + return undefined; + } + const current = unwrapParentheses(argumentList); + if (!ts.isArrayLiteralExpression(current)) { + return undefined; + } + const first = current.elements[0]; + return first && !ts.isSpreadElement(first) ? first : undefined; + } + + /** + * Recognize a statically written `.bind` expression whose receiver is already + * known module authority. The bound receiver and arguments are never evaluated. + */ + function commonJsBoundExpression(expression, authorityPredicate) { + const bindCall = unwrapParentheses(expression); + if (!ts.isCallExpression(bindCall)) { + return null; + } + const bindCallee = unwrapParentheses(bindCall.expression); + if ( + (!ts.isPropertyAccessExpression(bindCallee) && + !ts.isElementAccessExpression(bindCallee)) || + staticMemberName(bindCallee) !== 'bind' || + !authorityPredicate(bindCallee.expression) + ) { + return null; + } + return { + bindCall, + hasArgument: bindCall.arguments.length > 1, + argument: bindCall.arguments[1], + }; + } + + /** Preserve an explicitly bound package argument even when it is computed. */ + function commonJsBoundArgument(boundExpression, fallbackArgument) { + consumedBoundCalls.add(boundExpression.bindCall); + return boundExpression.hasArgument + ? boundExpression.argument + : fallbackArgument; + } + + /** + * Recognize an immediately invoked statically written `.bind` call whose + * receiver is already known module authority. A package argument bound after + * `thisArg` takes precedence over the first invocation argument. + */ + function commonJsBoundInvocation(node, authorityPredicate) { + const boundExpression = commonJsBoundExpression( + node.expression, + authorityPredicate, + ); + return boundExpression + ? { + argument: commonJsBoundArgument( + boundExpression, + node.arguments[0], + ), + } + : null; + } + + /** + * Recognize statically written constructor use of known CommonJS authority. + * The constructor target and package argument are inspected syntactically only; + * aliases, arbitrary receivers, and computed property names remain unresolved. + */ + function commonJsConstructorInvocation(node, authorityPredicate) { + if (authorityPredicate(node.expression)) { + return { argument: node.arguments?.[0] }; + } + const boundExpression = commonJsBoundExpression( + node.expression, + authorityPredicate, + ); + return boundExpression + ? { + argument: commonJsBoundArgument( + boundExpression, + node.arguments?.[0], + ), + } + : null; + } + + /** + * Recognize a `Reflect.construct` call whose constructor target is already + * known module authority. The argument-list array is inspected statically only. + */ + function commonJsReflectConstructInvocation(node, authorityPredicate) { + if (!isReflectConstructExpression(node.expression)) { + return null; + } + const target = node.arguments[0]; + if (target && authorityPredicate(target)) { + return { argument: commonJsApplyArgument(node, 1) }; + } + if (!target) { + return null; + } + const boundTarget = commonJsBoundExpression(target, authorityPredicate); + return boundTarget + ? { + argument: commonJsBoundArgument( + boundTarget, + commonJsApplyArgument(node, 1), + ), + } + : null; + } + + /** + * Return the package argument for one recognizable CommonJS resolver call. + * Arbitrary object methods named `resolve` remain outside this authority model. + */ + function commonJsResolverInvocation(node) { + if (isCommonJsResolverExpression(node.expression)) { + return { argument: node.arguments[0] }; + } + + const boundInvocation = commonJsBoundInvocation( + node, + isCommonJsResolverExpression, + ); + if (boundInvocation) { + return boundInvocation; + } + + const reflectConstructInvocation = commonJsReflectConstructInvocation( + node, + isCommonJsResolverExpression, + ); + if (reflectConstructInvocation) { + return reflectConstructInvocation; + } + + if (isReflectApplyExpression(node.expression)) { + const target = node.arguments[0]; + if (target && isCommonJsResolverExpression(target)) { + return { argument: commonJsApplyArgument(node, 2) }; + } + if (target) { + const boundTarget = commonJsBoundExpression( + target, + isCommonJsResolverExpression, + ); + if (boundTarget) { + return { + argument: commonJsBoundArgument( + boundTarget, + commonJsApplyArgument(node, 2), + ), + }; + } + } + } + + const callee = unwrapParentheses(node.expression); + if ( + ts.isPropertyAccessExpression(callee) || + ts.isElementAccessExpression(callee) + ) { + const invocationMethod = staticMemberName(callee); + if (invocationMethod === 'call' || invocationMethod === 'apply') { + const fallbackArgument = + invocationMethod === 'call' + ? node.arguments[1] + : commonJsApplyArgument(node); + if (isCommonJsResolverExpression(callee.expression)) { + return { argument: fallbackArgument }; + } + const boundReceiver = commonJsBoundExpression( + callee.expression, + isCommonJsResolverExpression, + ); + if (boundReceiver) { + return { + argument: commonJsBoundArgument( + boundReceiver, + fallbackArgument, + ), + }; + } + } + } + return null; + } + + /** + * Return the package argument for one recognizable CommonJS loader call. + * A wrapper object distinguishes a computed or missing argument from no match. + */ + function commonJsInvocation(node) { + if (isCommonJsLoaderExpression(node.expression)) { + return { argument: node.arguments[0] }; + } + + const boundInvocation = commonJsBoundInvocation( + node, + isCommonJsLoaderExpression, + ); + if (boundInvocation) { + return boundInvocation; + } + + const reflectConstructInvocation = commonJsReflectConstructInvocation( + node, + isCommonJsLoaderExpression, + ); + if (reflectConstructInvocation) { + return reflectConstructInvocation; + } + + if (isReflectApplyExpression(node.expression)) { + const target = node.arguments[0]; + if (target && isCommonJsLoaderExpression(target)) { + return { argument: commonJsApplyArgument(node, 2) }; + } + if (target) { + const boundTarget = commonJsBoundExpression( + target, + isCommonJsLoaderExpression, + ); + if (boundTarget) { + return { + argument: commonJsBoundArgument( + boundTarget, + commonJsApplyArgument(node, 2), + ), + }; + } + } + } + + const callee = unwrapParentheses(node.expression); + if ( + ts.isPropertyAccessExpression(callee) || + ts.isElementAccessExpression(callee) + ) { + const invocationMethod = staticMemberName(callee); + if (invocationMethod === 'call' || invocationMethod === 'apply') { + const fallbackArgument = + invocationMethod === 'call' + ? node.arguments[1] + : commonJsApplyArgument(node); + if (isCommonJsLoaderExpression(callee.expression)) { + return { argument: fallbackArgument }; + } + const boundReceiver = commonJsBoundExpression( + callee.expression, + isCommonJsLoaderExpression, + ); + if (boundReceiver) { + return { + argument: commonJsBoundArgument( + boundReceiver, + fallbackArgument, + ), + }; + } + } + } + return null; + } + /** @param {import('typescript').Node} node Parsed JavaScript node. */ function visit(node) { if (ts.isImportDeclaration(node) && node.moduleSpecifier) { @@ -56,22 +539,112 @@ export function findRuntimeModuleAuthority(source, filename = 'bundle.js') { offset: node.getStart(sourceFile), specifier: literalSpecifier(node.moduleSpecifier), }); - } else if (ts.isCallExpression(node)) { - if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { + } else if (ts.isNewExpression(node)) { + const resolverInvocation = commonJsConstructorInvocation( + node, + isCommonJsResolverExpression, + ); + if (resolverInvocation) { findings.push({ - kind: 'dynamic-import', + kind: 'commonjs-resolve', offset: node.getStart(sourceFile), - specifier: literalSpecifier(node.arguments[0]), + specifier: literalSpecifier(resolverInvocation.argument), }); - } else if ( - ts.isIdentifier(node.expression) && - node.expression.text === 'require' - ) { + } else { + const invocation = commonJsConstructorInvocation( + node, + isCommonJsLoaderExpression, + ); + if (invocation) { + findings.push({ + kind: 'commonjs-require', + offset: node.getStart(sourceFile), + specifier: literalSpecifier(invocation.argument), + }); + } + } + } else if (ts.isCallExpression(node)) { + if (node.expression.kind === ts.SyntaxKind.ImportKeyword) { findings.push({ - kind: 'commonjs-require', + kind: 'dynamic-import', offset: node.getStart(sourceFile), specifier: literalSpecifier(node.arguments[0]), }); + } else { + const nodeBuiltinInvocation = nodeBuiltinModuleInvocation(node); + if (nodeBuiltinInvocation) { + findings.push({ + kind: 'node-builtin-module', + offset: node.getStart(sourceFile), + specifier: literalSpecifier(nodeBuiltinInvocation.argument), + }); + } else { + const resolverInvocation = commonJsResolverInvocation(node); + if (resolverInvocation) { + findings.push({ + kind: 'commonjs-resolve', + offset: node.getStart(sourceFile), + specifier: literalSpecifier(resolverInvocation.argument), + }); + } else { + const invocation = commonJsInvocation(node); + if (invocation) { + findings.push({ + kind: 'commonjs-require', + offset: node.getStart(sourceFile), + specifier: literalSpecifier(invocation.argument), + }); + } else if (!consumedBoundCalls.has(node)) { + const boundNodeBuiltin = commonJsBoundExpression( + node, + isNodeBuiltinModuleExpression, + ); + if (boundNodeBuiltin) { + findings.push({ + kind: 'node-builtin-module', + offset: node.getStart(sourceFile), + specifier: literalSpecifier( + boundNodeBuiltin.hasArgument + ? boundNodeBuiltin.argument + : undefined, + ), + }); + } else { + const boundResolver = commonJsBoundExpression( + node, + isCommonJsResolverExpression, + ); + if (boundResolver) { + findings.push({ + kind: 'commonjs-resolve', + offset: node.getStart(sourceFile), + specifier: literalSpecifier( + boundResolver.hasArgument + ? boundResolver.argument + : undefined, + ), + }); + } else { + const boundLoader = commonJsBoundExpression( + node, + isCommonJsLoaderExpression, + ); + if (boundLoader) { + findings.push({ + kind: 'commonjs-require', + offset: node.getStart(sourceFile), + specifier: literalSpecifier( + boundLoader.hasArgument + ? boundLoader.argument + : undefined, + ), + }); + } + } + } + } + } + } } } diff --git a/scripts/javascript-runtime-authority.test.mjs b/scripts/javascript-runtime-authority.test.mjs index 7ca3e2a3..7c205540 100644 --- a/scripts/javascript-runtime-authority.test.mjs +++ b/scripts/javascript-runtime-authority.test.mjs @@ -41,6 +41,187 @@ test('reports executable module authority with actionable specifiers', () => { ); }); +test('reports statically recognizable indirect CommonJS loader invocations', () => { + const source = [ + "const comma = (0, require)('comma-package');", + "const called = require.call(undefined, 'call-package');", + "const moduleRequired = module.require('module-package');", + "const elementRequired = module['require']('element-package');", + 'const computed = module.require(runtimePackageName);', + 'const object = { require() { return "method-only"; } };', + 'const benign = object.require("not-the-commonjs-loader");', + 'void [comma, called, moduleRequired, elementRequired, computed, benign];', + ].join('\n'); + + assert.deepEqual( + findRuntimeModuleAuthority(source, 'indirect-authority.js').map( + ({ kind, specifier }) => ({ kind, specifier }), + ), + [ + { kind: 'commonjs-require', specifier: 'comma-package' }, + { kind: 'commonjs-require', specifier: 'call-package' }, + { kind: 'commonjs-require', specifier: 'module-package' }, + { kind: 'commonjs-require', specifier: 'element-package' }, + { kind: 'commonjs-require', specifier: undefined }, + ], + ); +}); + +test('reports CommonJS loader authority reached through require.main', () => { + const source = [ + "const direct = require.main.require('main-package');", + "const element = require['main']['require']('element-main-package');", + "const called = require.main.require.call(undefined, 'call-main-package');", + 'const computed = require.main.require(runtimePackageName);', + 'const object = { main: { require() { return "method-only"; } } };', + 'const benign = object.main.require("not-the-commonjs-loader");', + 'void [direct, element, called, computed, benign];', + ].join('\n'); + + assert.deepEqual( + findRuntimeModuleAuthority(source, 'require-main-authority.js').map( + ({ kind, specifier }) => ({ kind, specifier }), + ), + [ + { kind: 'commonjs-require', specifier: 'main-package' }, + { kind: 'commonjs-require', specifier: 'element-main-package' }, + { kind: 'commonjs-require', specifier: 'call-main-package' }, + { kind: 'commonjs-require', specifier: undefined }, + ], + ); +}); + +test('reports statically recognizable CommonJS resolver authority', () => { + const source = [ + "const direct = require.resolve('resolve-package');", + "const element = require['resolve']('element-resolve-package');", + 'const computed = require.resolve(runtimePackageName);', + 'const object = { resolve() { return "method-only"; } };', + 'const benign = object.resolve("not-the-commonjs-resolver");', + 'void [direct, element, computed, benign];', + ].join('\n'); + + assert.deepEqual( + findRuntimeModuleAuthority(source, 'resolver-authority.js').map( + ({ kind, specifier }) => ({ kind, specifier }), + ), + [ + { kind: 'commonjs-resolve', specifier: 'resolve-package' }, + { kind: 'commonjs-resolve', specifier: 'element-resolve-package' }, + { kind: 'commonjs-resolve', specifier: undefined }, + ], + ); +}); + +test('reports statically recognizable indirect CommonJS resolver invocations', () => { + const source = [ + "const comma = (0, require.resolve)('comma-resolve-package');", + "const called = require.resolve.call(undefined, 'call-resolve-package');", + "const elementCalled = require['resolve']['call'](undefined, 'element-call-resolve-package');", + 'const computed = require.resolve.call(undefined, runtimePackageName);', + 'const object = { resolve() { return "method-only"; } };', + 'const benignComma = (0, object.resolve)("not-the-commonjs-resolver");', + 'const benignCall = object.resolve.call(undefined, "also-not-the-commonjs-resolver");', + 'void [comma, called, elementCalled, computed, benignComma, benignCall];', + ].join('\n'); + + assert.deepEqual( + findRuntimeModuleAuthority(source, 'indirect-resolver-authority.js').map( + ({ kind, specifier }) => ({ kind, specifier }), + ), + [ + { kind: 'commonjs-resolve', specifier: 'comma-resolve-package' }, + { kind: 'commonjs-resolve', specifier: 'call-resolve-package' }, + { + kind: 'commonjs-resolve', + specifier: 'element-call-resolve-package', + }, + { kind: 'commonjs-resolve', specifier: undefined }, + ], + ); +}); + +test('reports statically recognizable CommonJS apply invocations', () => { + const source = [ + "const required = require.apply(undefined, ['apply-package']);", + "const moduleRequired = module['require']['apply'](undefined, ['module-apply-package']);", + 'const mainRequired = require.main.require.apply(undefined, [runtimePackageName]);', + "const resolved = require.resolve.apply(undefined, ['apply-resolve-package']);", + 'const computedResolved = require[\'resolve\'][\'apply\'](undefined, resolverArguments);', + 'const object = { require() {}, resolve() {} };', + "const benignRequire = object.require.apply(undefined, ['not-commonjs']);", + "const benignResolve = object.resolve.apply(undefined, ['not-commonjs-resolve']);", + 'void [required, moduleRequired, mainRequired, resolved, computedResolved, benignRequire, benignResolve];', + ].join('\n'); + + assert.deepEqual( + findRuntimeModuleAuthority(source, 'apply-authority.js').map( + ({ kind, specifier }) => ({ kind, specifier }), + ), + [ + { kind: 'commonjs-require', specifier: 'apply-package' }, + { kind: 'commonjs-require', specifier: 'module-apply-package' }, + { kind: 'commonjs-require', specifier: undefined }, + { kind: 'commonjs-resolve', specifier: 'apply-resolve-package' }, + { kind: 'commonjs-resolve', specifier: undefined }, + ], + ); +}); + +test('reports CommonJS authority invoked through Reflect.apply', () => { + const source = [ + "const required = Reflect.apply(require, undefined, ['reflect-package']);", + "const moduleRequired = Reflect['apply'](module.require, module, ['reflect-module-package']);", + 'const mainRequired = Reflect.apply(require.main.require, require.main, [runtimePackageName]);', + "const resolved = Reflect.apply(require.resolve, require, ['reflect-resolve-package']);", + 'const computedResolved = Reflect.apply(require[\'resolve\'], require, resolverArguments);', + 'const object = { require() {}, resolve() {} };', + "const benignRequire = Reflect.apply(object.require, object, ['not-commonjs']);", + "const benignResolve = Reflect.apply(object.resolve, object, ['not-commonjs-resolve']);", + 'void [required, moduleRequired, mainRequired, resolved, computedResolved, benignRequire, benignResolve];', + ].join('\n'); + + assert.deepEqual( + findRuntimeModuleAuthority(source, 'reflect-apply-authority.js').map( + ({ kind, specifier }) => ({ kind, specifier }), + ), + [ + { kind: 'commonjs-require', specifier: 'reflect-package' }, + { kind: 'commonjs-require', specifier: 'reflect-module-package' }, + { kind: 'commonjs-require', specifier: undefined }, + { kind: 'commonjs-resolve', specifier: 'reflect-resolve-package' }, + { kind: 'commonjs-resolve', specifier: undefined }, + ], + ); +}); + +test('reports bound CommonJS authority composed through call, apply, and Reflect.apply', () => { + const source = [ + "const called = require.bind(undefined, 'bound-call-package').call(undefined);", + "const applied = module.require.bind(module).apply(module, ['bound-apply-package']);", + "const reflected = Reflect.apply(require.main.require.bind(require.main, 'reflect-bound-package'), require.main, []);", + "const resolved = require.resolve.bind(require, 'bound-resolve-package').call(undefined);", + "const reflectedResolved = Reflect.apply(require['resolve'].bind(require), require, [runtimePackageName]);", + 'const object = { require() {}, resolve() {} };', + "const benignCalled = object.require.bind(object, 'not-commonjs').call(object);", + "const benignReflected = Reflect.apply(object.resolve.bind(object, 'not-commonjs-resolve'), object, []);", + 'void [called, applied, reflected, resolved, reflectedResolved, benignCalled, benignReflected];', + ].join('\n'); + + assert.deepEqual( + findRuntimeModuleAuthority(source, 'bound-composition-authority.js').map( + ({ kind, specifier }) => ({ kind, specifier }), + ), + [ + { kind: 'commonjs-require', specifier: 'bound-call-package' }, + { kind: 'commonjs-require', specifier: 'bound-apply-package' }, + { kind: 'commonjs-require', specifier: 'reflect-bound-package' }, + { kind: 'commonjs-resolve', specifier: 'bound-resolve-package' }, + { kind: 'commonjs-resolve', specifier: undefined }, + ], + ); +}); + test('fails closed when emitted JavaScript is syntactically invalid', () => { assert.throws( () => findRuntimeModuleAuthority('const = ;', 'broken.js'), diff --git a/src/javascriptRuntimeAuthorityBind.test.ts b/src/javascriptRuntimeAuthorityBind.test.ts new file mode 100644 index 00000000..36d3c441 --- /dev/null +++ b/src/javascriptRuntimeAuthorityBind.test.ts @@ -0,0 +1,46 @@ +import { execFileSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +function scanWithRepositoryAuthority(source: string): unknown { + const scannerUrl = pathToFileURL( + resolve(process.cwd(), 'scripts/javascript-runtime-authority.mjs'), + ).href; + const program = [ + `import { findRuntimeModuleAuthority } from ${JSON.stringify(scannerUrl)};`, + `const source = ${JSON.stringify(source)};`, + "const findings = findRuntimeModuleAuthority(source, 'bind-authority.js').map(({ kind, specifier }) => ({ kind, specifier }));", + 'process.stdout.write(JSON.stringify(findings));', + ].join('\n'); + return JSON.parse( + execFileSync(process.execPath, ['--input-type=module', '--eval', program], { + encoding: 'utf8', + }), + ) as unknown; +} + +describe('packed JavaScript CommonJS bind authority', () => { + it('reports statically recognizable bound CommonJS loader and resolver invocations', () => { + const source = [ + "const required = require.bind(undefined)('bind-package');", + "const prebound = module.require.bind(module, 'prebound-module-package')();", + 'const mainRequired = require.main.require.bind(require.main)(runtimePackageName);', + "const resolved = require.resolve.bind(require)('bind-resolve-package');", + "const computedResolved = require['resolve']['bind'](require)(runtimePackageName);", + 'const object = { require() {}, resolve() {} };', + "const benignRequire = object.require.bind(object)('not-commonjs');", + "const benignResolve = object.resolve.bind(object)('not-commonjs-resolve');", + 'void [required, prebound, mainRequired, resolved, computedResolved, benignRequire, benignResolve];', + ].join('\n'); + + expect(scanWithRepositoryAuthority(source)).toEqual([ + { kind: 'commonjs-require', specifier: 'bind-package' }, + { kind: 'commonjs-require', specifier: 'prebound-module-package' }, + { kind: 'commonjs-require', specifier: undefined }, + { kind: 'commonjs-resolve', specifier: 'bind-resolve-package' }, + { kind: 'commonjs-resolve', specifier: undefined }, + ]); + }); +}); diff --git a/src/javascriptRuntimeAuthorityBindCreation.test.ts b/src/javascriptRuntimeAuthorityBindCreation.test.ts new file mode 100644 index 00000000..a97af331 --- /dev/null +++ b/src/javascriptRuntimeAuthorityBindCreation.test.ts @@ -0,0 +1,58 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +/** Run the repository runtime-authority scanner against one emitted JS fixture. */ +function scanWithRepositoryAuthority(source: string): unknown { + const scannerUrl = pathToFileURL( + resolve(process.cwd(), 'scripts/javascript-runtime-authority.mjs'), + ).href; + const program = [ + `import { findRuntimeModuleAuthority } from ${JSON.stringify(scannerUrl)};`, + `const source = ${JSON.stringify(source)};`, + "const findings = findRuntimeModuleAuthority(source, 'bound-capability.js').map(({ kind, specifier }) => ({ kind, specifier }));", + 'process.stdout.write(JSON.stringify(findings));', + ].join('\n'); + return JSON.parse( + execFileSync(process.execPath, ['--input-type=module', '--eval', program], { + encoding: 'utf8', + }), + ) as unknown; +} + +describe('packed JavaScript retained CommonJS bind authority', () => { + it('reports directly recognizable bound loader and resolver capabilities before invocation', () => { + const source = [ + "const direct = require.bind(undefined, 'bound-package');", + "const resolver = require.resolve.bind(require, 'bound-resolve-package');", + 'const computed = module.require.bind(module, runtimePackageName);', + "const main = require.main.require.bind(require.main, 'main-bound-package');", + 'const late = require.bind(undefined);', + 'const object = { require() {}, resolve() {} };', + "const benignLoader = object.require.bind(object, 'not-commonjs');", + "const benignResolver = object.resolve.bind(object, 'not-commonjs-resolve');", + 'void [direct, resolver, computed, main, late, benignLoader, benignResolver];', + ].join('\n'); + + expect(scanWithRepositoryAuthority(source)).toEqual([ + { kind: 'commonjs-require', specifier: 'bound-package' }, + { kind: 'commonjs-resolve', specifier: 'bound-resolve-package' }, + { kind: 'commonjs-require', specifier: undefined }, + { kind: 'commonjs-require', specifier: 'main-bound-package' }, + { kind: 'commonjs-require', specifier: undefined }, + ]); + }); + + it('keeps doctoring aligned with retained bound capabilities', () => { + const doctoring = readFileSync( + resolve(process.cwd(), 'docs/doctoring/commonjs-runtime-authority.md'), + 'utf8', + ); + + expect(doctoring).toContain('retained `.bind` capabilities'); + expect(doctoring).toContain('before later invocation'); + }); +}); diff --git a/src/javascriptRuntimeAuthorityConstruct.test.ts b/src/javascriptRuntimeAuthorityConstruct.test.ts new file mode 100644 index 00000000..c468bebf --- /dev/null +++ b/src/javascriptRuntimeAuthorityConstruct.test.ts @@ -0,0 +1,68 @@ +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +function scanWithRepositoryAuthority(source: string): unknown { + const scannerUrl = pathToFileURL( + resolve(process.cwd(), 'scripts/javascript-runtime-authority.mjs'), + ).href; + const program = [ + `import { findRuntimeModuleAuthority } from ${JSON.stringify(scannerUrl)};`, + `const source = ${JSON.stringify(source)};`, + "const findings = findRuntimeModuleAuthority(source, 'construct-authority.js').map(({ kind, specifier }) => ({ kind, specifier }));", + 'process.stdout.write(JSON.stringify(findings));', + ].join('\n'); + return JSON.parse( + execFileSync(process.execPath, ['--input-type=module', '--eval', program], { + encoding: 'utf8', + }), + ) as unknown; +} + +describe('packed JavaScript CommonJS constructor authority', () => { + it('reports statically recognizable CommonJS loaders invoked through new', () => { + const source = [ + "const direct = new require('construct-package');", + "const parenthesized = new (require)('parenthesized-construct-package');", + "const comma = new (0, require)('comma-construct-package');", + 'const computed = new require(runtimePackageName);', + 'const object = { require() { return {}; } };', + "const benign = new object.require('not-commonjs');", + 'void [direct, parenthesized, comma, computed, benign];', + ].join('\n'); + + expect(scanWithRepositoryAuthority(source)).toEqual([ + { kind: 'commonjs-require', specifier: 'construct-package' }, + { kind: 'commonjs-require', specifier: 'parenthesized-construct-package' }, + { kind: 'commonjs-require', specifier: 'comma-construct-package' }, + { kind: 'commonjs-require', specifier: undefined }, + ]); + }); + + it('proves the supported Node CommonJS require function can load through new', () => { + const result = execFileSync( + process.execPath, + [ + '--eval', + "const loaded = new require('node:path'); process.stdout.write(String(loaded === require('node:path')));", + ], + { encoding: 'utf8' }, + ); + + expect(result).toBe('true'); + }); + + it('keeps doctoring aligned with constructor and Reflect.construct authority', () => { + const doctoring = readFileSync( + resolve(process.cwd(), 'docs/doctoring/commonjs-runtime-authority.md'), + 'utf8', + ); + + expect(doctoring).toContain('constructor invocation'); + expect(doctoring).toContain('Reflect.construct'); + expect(doctoring).toContain('no-execution/no-alias-evaluation invariant'); + }); +}); diff --git a/src/javascriptRuntimeAuthorityModuleMain.test.ts b/src/javascriptRuntimeAuthorityModuleMain.test.ts new file mode 100644 index 00000000..9663c725 --- /dev/null +++ b/src/javascriptRuntimeAuthorityModuleMain.test.ts @@ -0,0 +1,42 @@ +import { execFileSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +function scanWithRepositoryAuthority(source: string): unknown { + const scannerUrl = pathToFileURL( + resolve(process.cwd(), 'scripts/javascript-runtime-authority.mjs'), + ).href; + const program = [ + `import { findRuntimeModuleAuthority } from ${JSON.stringify(scannerUrl)};`, + `const source = ${JSON.stringify(source)};`, + "const findings = findRuntimeModuleAuthority(source, 'main-module-authority.js').map(({ kind, specifier }) => ({ kind, specifier }));", + 'process.stdout.write(JSON.stringify(findings));', + ].join('\n'); + return JSON.parse( + execFileSync(process.execPath, ['--input-type=module', '--eval', program], { + encoding: 'utf8', + }), + ) as unknown; +} + +describe('packed JavaScript CommonJS main-module authority', () => { + it('recognizes require.main loaders without inventing a module.main authority surface', () => { + const source = [ + "const direct = require.main.require('require-main-package');", + "const element = require['main']['require']('require-main-element-package');", + "const called = require.main.require.call(require.main, 'require-main-call-package');", + "const nonexistentNodeSurface = module.main.require('not-node-commonjs-authority');", + 'const object = { main: { require() {} } };', + "const benign = object.main.require('not-commonjs');", + 'void [direct, element, called, nonexistentNodeSurface, benign];', + ].join('\n'); + + expect(scanWithRepositoryAuthority(source)).toEqual([ + { kind: 'commonjs-require', specifier: 'require-main-package' }, + { kind: 'commonjs-require', specifier: 'require-main-element-package' }, + { kind: 'commonjs-require', specifier: 'require-main-call-package' }, + ]); + }); +}); diff --git a/src/javascriptRuntimeAuthorityNodeBuiltin.test.ts b/src/javascriptRuntimeAuthorityNodeBuiltin.test.ts new file mode 100644 index 00000000..150f9b2a --- /dev/null +++ b/src/javascriptRuntimeAuthorityNodeBuiltin.test.ts @@ -0,0 +1,128 @@ +import { execFileSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +function scanWithRepositoryAuthority( + source: string, + filename: string, +): unknown { + const scannerUrl = pathToFileURL( + resolve(process.cwd(), 'scripts/javascript-runtime-authority.mjs'), + ).href; + const program = [ + `import { findRuntimeModuleAuthority } from ${JSON.stringify(scannerUrl)};`, + `const source = ${JSON.stringify(source)};`, + `const findings = findRuntimeModuleAuthority(source, ${JSON.stringify(filename)}).map(({ kind, specifier }) => ({ kind, specifier }));`, + 'process.stdout.write(JSON.stringify(findings));', + ].join('\n'); + return JSON.parse( + execFileSync(process.execPath, ['--input-type=module', '--eval', program], { + encoding: 'utf8', + }), + ) as unknown; +} + +describe('Node built-in runtime module authority', () => { + it('reports statically recognizable process.getBuiltinModule calls', () => { + const source = [ + "const fs = process.getBuiltinModule('node:fs');", + "const path = globalThis.process.getBuiltinModule('node:path');", + 'const unknown = process.getBuiltinModule(runtimeBuiltinName);', + 'const object = { getBuiltinModule() { return "method-only"; } };', + 'const benign = object.getBuiltinModule("node:crypto");', + 'void [fs, path, unknown, benign];', + ].join('\n'); + + expect(scanWithRepositoryAuthority(source, 'node-builtins.js')).toEqual([ + { kind: 'node-builtin-module', specifier: 'node:fs' }, + { kind: 'node-builtin-module', specifier: 'node:path' }, + { kind: 'node-builtin-module', specifier: undefined }, + ]); + }); + + it('reports comma-indirected ambient built-in loaders without promoting ordinary object methods', () => { + const source = [ + "const fs = (0, process.getBuiltinModule)('node:fs');", + "const path = (0, globalThis.process.getBuiltinModule)('node:path');", + 'const unknown = (0, process.getBuiltinModule)(runtimeBuiltinName);', + "const util = (0, globalThis['process']['getBuiltinModule'])('node:util');", + 'const object = { getBuiltinModule() { return "method-only"; } };', + 'const benign = (0, object.getBuiltinModule)("node:crypto");', + 'void [fs, path, unknown, util, benign];', + ].join('\n'); + + expect( + scanWithRepositoryAuthority(source, 'node-builtins-comma-indirect.js'), + ).toEqual([ + { kind: 'node-builtin-module', specifier: 'node:fs' }, + { kind: 'node-builtin-module', specifier: 'node:path' }, + { kind: 'node-builtin-module', specifier: undefined }, + { kind: 'node-builtin-module', specifier: 'node:util' }, + ]); + }); + + it('proves the supported Node runtime can load through comma indirection', () => { + const result = execFileSync( + process.execPath, + [ + '--eval', + "const loaded = (0, process.getBuiltinModule)('node:path'); process.stdout.write(String(loaded === require('node:path')));", + ], + { encoding: 'utf8' }, + ); + + expect(result).toBe('true'); + }); + + it('reports Function.prototype call/apply and Reflect.apply invocation of the ambient loader', () => { + const source = [ + "const fs = process.getBuiltinModule.call(undefined, 'node:fs');", + 'const path = globalThis.process.getBuiltinModule.call(null, runtimeBuiltinName);', + "const util = process['getBuiltinModule']['call'](undefined, 'node:util');", + "const os = globalThis['process']['getBuiltinModule'].call(null, 'node:os');", + "const crypto = process.getBuiltinModule.apply(undefined, ['node:crypto']);", + 'const unknownApply = globalThis.process.getBuiltinModule.apply(null, [runtimeBuiltinName]);', + "const url = Reflect.apply(process.getBuiltinModule, undefined, ['node:url']);", + 'const unknownReflect = Reflect.apply(globalThis.process.getBuiltinModule, null, runtimeBuiltinArgs);', + 'const object = { getBuiltinModule() { return "method-only"; } };', + 'const benignCall = object.getBuiltinModule.call(null, "node:buffer");', + 'const benignApply = object.getBuiltinModule.apply(null, ["node:assert"]);', + 'const benignReflect = Reflect.apply(object.getBuiltinModule, null, ["node:zlib"]);', + 'void [fs, path, util, os, crypto, unknownApply, url, unknownReflect, benignCall, benignApply, benignReflect];', + ].join('\n'); + + expect( + scanWithRepositoryAuthority(source, 'node-builtins-indirect.js'), + ).toEqual([ + { kind: 'node-builtin-module', specifier: 'node:fs' }, + { kind: 'node-builtin-module', specifier: undefined }, + { kind: 'node-builtin-module', specifier: 'node:util' }, + { kind: 'node-builtin-module', specifier: 'node:os' }, + { kind: 'node-builtin-module', specifier: 'node:crypto' }, + { kind: 'node-builtin-module', specifier: undefined }, + { kind: 'node-builtin-module', specifier: 'node:url' }, + { kind: 'node-builtin-module', specifier: undefined }, + ]); + }); + + it('reports retained and immediately invoked bound ambient loaders', () => { + const source = [ + "const fsLoader = process.getBuiltinModule.bind(undefined, 'node:fs');", + 'const unknownLoader = globalThis.process.getBuiltinModule.bind(null, runtimeBuiltinName);', + "const path = process.getBuiltinModule.bind(undefined)('node:path');", + 'const unknown = globalThis.process.getBuiltinModule.bind(null)(runtimeBuiltinName);', + 'const object = { getBuiltinModule() { return "method-only"; } };', + 'const benign = object.getBuiltinModule.bind(null, "node:crypto");', + 'void [fsLoader, unknownLoader, path, unknown, benign];', + ].join('\n'); + + expect(scanWithRepositoryAuthority(source, 'node-builtins-bind.js')).toEqual([ + { kind: 'node-builtin-module', specifier: 'node:fs' }, + { kind: 'node-builtin-module', specifier: undefined }, + { kind: 'node-builtin-module', specifier: 'node:path' }, + { kind: 'node-builtin-module', specifier: undefined }, + ]); + }); +}); diff --git a/src/javascriptRuntimeAuthorityNodeBuiltinGlobal.test.ts b/src/javascriptRuntimeAuthorityNodeBuiltinGlobal.test.ts new file mode 100644 index 00000000..c0821f4a --- /dev/null +++ b/src/javascriptRuntimeAuthorityNodeBuiltinGlobal.test.ts @@ -0,0 +1,57 @@ +import { execFileSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +function scanWithRepositoryAuthority( + source: string, + filename: string, +): unknown { + const scannerUrl = pathToFileURL( + resolve(process.cwd(), 'scripts/javascript-runtime-authority.mjs'), + ).href; + const program = [ + `import { findRuntimeModuleAuthority } from ${JSON.stringify(scannerUrl)};`, + `const source = ${JSON.stringify(source)};`, + `const findings = findRuntimeModuleAuthority(source, ${JSON.stringify(filename)}).map(({ kind, specifier }) => ({ kind, specifier }));`, + 'process.stdout.write(JSON.stringify(findings));', + ].join('\n'); + return JSON.parse( + execFileSync(process.execPath, ['--input-type=module', '--eval', program], { + encoding: 'utf8', + }), + ) as unknown; +} + +describe('Node global.process built-in runtime module authority', () => { + it('reports exact ambient global.process loaders without promoting ordinary objects', () => { + const source = [ + "const fs = global.process.getBuiltinModule('node:fs');", + "const util = global['process']['getBuiltinModule']('node:util');", + 'const ordinaryGlobal = { process: { getBuiltinModule() { return "method-only"; } } };', + 'const benign = ordinaryGlobal.process.getBuiltinModule("node:crypto");', + 'void [fs, util, benign];', + ].join('\n'); + + expect( + scanWithRepositoryAuthority(source, 'node-builtins-global-process.js'), + ).toEqual([ + { kind: 'node-builtin-module', specifier: 'node:fs' }, + { kind: 'node-builtin-module', specifier: 'node:util' }, + ]); + }); + + it('proves the supported Node runtime exposes global.process as the ambient process', () => { + const result = execFileSync( + process.execPath, + [ + '--eval', + "const loaded = global.process.getBuiltinModule('node:path'); process.stdout.write(String(global.process === process && loaded === require('node:path')));", + ], + { encoding: 'utf8' }, + ); + + expect(result).toBe('true'); + }); +}); diff --git a/src/javascriptRuntimeAuthorityReflectConstruct.test.ts b/src/javascriptRuntimeAuthorityReflectConstruct.test.ts new file mode 100644 index 00000000..63fb07f4 --- /dev/null +++ b/src/javascriptRuntimeAuthorityReflectConstruct.test.ts @@ -0,0 +1,55 @@ +import { execFileSync } from 'node:child_process'; +import { resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +function scanWithRepositoryAuthority(source: string): unknown { + const scannerUrl = pathToFileURL( + resolve(process.cwd(), 'scripts/javascript-runtime-authority.mjs'), + ).href; + const program = [ + `import { findRuntimeModuleAuthority } from ${JSON.stringify(scannerUrl)};`, + `const source = ${JSON.stringify(source)};`, + "const findings = findRuntimeModuleAuthority(source, 'reflect-construct-authority.js').map(({ kind, specifier }) => ({ kind, specifier }));", + 'process.stdout.write(JSON.stringify(findings));', + ].join('\n'); + return JSON.parse( + execFileSync(process.execPath, ['--input-type=module', '--eval', program], { + encoding: 'utf8', + }), + ) as unknown; +} + +describe('packed JavaScript Reflect.construct CommonJS authority', () => { + it('reports statically recognizable CommonJS loaders and resolvers used as constructors', () => { + const source = [ + "Reflect.construct(require, ['reflect-package']);", + "Reflect['construct'](require.resolve, ['reflect-resolve-package']);", + 'Reflect.construct((0, require), [runtimePackageName]);', + "Reflect.construct(require.bind(undefined, 'bound-reflect-package'), []);", + 'const object = { construct() { return {}; } };', + "object.construct(require, ['not-reflect']);", + ].join('\n'); + + expect(scanWithRepositoryAuthority(source)).toEqual([ + { kind: 'commonjs-require', specifier: 'reflect-package' }, + { kind: 'commonjs-resolve', specifier: 'reflect-resolve-package' }, + { kind: 'commonjs-require', specifier: undefined }, + { kind: 'commonjs-require', specifier: 'bound-reflect-package' }, + ]); + }); + + it('proves the supported Node CommonJS require function is constructible through Reflect.construct', () => { + const result = execFileSync( + process.execPath, + [ + '--eval', + "const loaded = Reflect.construct(require, ['node:path']); process.stdout.write(String(loaded === require('node:path')));", + ], + { encoding: 'utf8' }, + ); + + expect(result).toBe('true'); + }); +});