Skip to content

Commit

Permalink
feat: allow cascading calls
Browse files Browse the repository at this point in the history
  • Loading branch information
gabidobo authored and andreimarinescu committed Sep 1, 2022
1 parent e73309a commit c8a91cf
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 52 deletions.
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import track, {setSkipTracking, setTrackingServer} from './track';
import {
addSourceMap,
addTrustedModules,
getCurrentModule,
getCurrentModuleInfo,
setAllowsAll,
setIgnoreExtensions,
setPermissions,
Expand Down Expand Up @@ -56,7 +56,7 @@ const init = ({
return Promise.resolve();
}

const {name: callerModule} = getCurrentModule({
const {name: callerModule} = getCurrentModuleInfo({
allowURLs: false,
});
if (
Expand Down
60 changes: 51 additions & 9 deletions src/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,17 @@ export const mapStackItemToSource = (item) => {

export const getModuleNameFromLocation = (location, allowURLs) => {
// Infer the module name
if (location && location.includes('node_modules')) {
if (!location || typeof location !== 'string') {
return undefined;
}

if (location.startsWith('node:')) {
// locations like node:internal/modules/cjs/loader should map to node:internal
// node:fs should map to node:fs
return location.split('/')[0];
}

if (location.includes('node_modules')) {
const components = location.split('/');
const nodeModulesIndex = components.findIndex((v) => v === 'node_modules');
let moduleName = components[nodeModulesIndex + 1];
Expand All @@ -89,35 +99,49 @@ export const getModuleNameFromLocation = (location, allowURLs) => {
url = new URL(location);
// eslint-disable-next-line no-empty
} catch (error) {}
if (url && url.protocol !== 'node:') {
if (url) {
return location;
}
}

return undefined;
return 'root';
};

export const getCurrentModule = ({stack: stackInput, allowURLs = false} = {}) => {
export const getCurrentModuleInfo = ({stack: stackInput, allowURLs = false} = {}) => {
try {
const stack = (stackInput || currentStack())
.reverse()
.map((item) => mapStackItemToSource(item, sourcemaps))
.map(({item, mapping, mappingLine, mappingColumn}) => {
const module = getModuleNameFromLocation(mapping, allowURLs);

return {
caller: item.caller,
called: item.called,
name: item.name,
alias: item.alias,
file: item.file,
fileLine: item.line,
fileColumn: item.column,
mapping,
mappingLine,
mappingColumn,
module: trustedModules.includes(module) ? undefined : module,
module,
};
});

const modules = stack.map(({module}) => module).filter((v) => v !== undefined);
const directCaller = stack.find(({module}) => module !== 'sandworm');
const lastModuleCaller = stack.find(
({module}) => module !== 'sandworm' && module !== undefined && !module.startsWith('node:'),
);

const modules = stack
.reverse()
.map(({module}) =>
module === 'root' || trustedModules.includes(module) || module?.startsWith('node:')
? undefined
: module,
)
.filter((v) => v !== undefined);
let name = 'root';

if (modules.length) {
Expand All @@ -133,7 +157,7 @@ export const getCurrentModule = ({stack: stackInput, allowURLs = false} = {}) =>
}
}

return {name, stack};
return {name, stack, directCaller, lastModuleCaller};
} catch (error) {
logger.error(error);
return {name: 'root', error: error.message};
Expand Down Expand Up @@ -163,7 +187,25 @@ export const getModulePermissions = (module) => {
return false;
};

export const isModuleAllowedToExecute = ({module, family, method}) => {
export const isModuleAllowedToExecute = ({
module,
family,
method,
directCaller,
lastModuleCaller,
}) => {
if (directCaller?.module?.startsWith?.('node:')) {
logger.debug(
'-> call has been allowed',
lastModuleCaller
? `as a consequence of \`${lastModuleCaller.module}\` calling \`${
lastModuleCaller.alias || lastModuleCaller.name
}.${lastModuleCaller.called}\``
: '',
);
return true;
}

const modulePermissions = getModulePermissions(module, permissions);
if (typeof modulePermissions === 'boolean' && !method.needsExplicitPermission) {
return modulePermissions;
Expand Down
8 changes: 6 additions & 2 deletions src/patch.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logger from './logger';
import {getCurrentModule, isModuleAllowedToExecute} from './module';
import {getCurrentModuleInfo, isModuleAllowedToExecute} from './module';

export class SandwormError extends Error {
constructor(message) {
Expand All @@ -25,15 +25,19 @@ export default ({family, track = () => {}}) => {
const {
name: module,
stack,
directCaller,
lastModuleCaller,
error,
} = getCurrentModule({
} = getCurrentModuleInfo({
allowURLs: true,
});
logger.debug(`${module} called ${family.name}.${method.name}`);
const allowed = isModuleAllowedToExecute({
module,
family,
method,
directCaller,
lastModuleCaller,
});
track({
module,
Expand Down
24 changes: 22 additions & 2 deletions src/stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ export const parseStackLine = (l) => {
}

let fileLineColumn = [];
let name;
let alias;
let match;

if (
(match = line.match(/at (.+) \(eval at .+ \((.+)\), .+\)/)) || // eval calls
(match = line.match(/at (.+) \((.+)\)/)) ||
(line.slice(0, 3) !== 'at ' && (match = line.match(/(.*)@(.*)/)))
) {
[, name] = match;
fileLineColumn = (
match[2].match(/(.*):(\d+):(\d+)/) ||
match[2].match(/(.*):(\d+)/) ||
Expand All @@ -29,11 +32,17 @@ export const parseStackLine = (l) => {
return undefined;
}

if (name?.includes?.(' [as ')) {
[name, alias] = name.slice(0, -1).split(' [as ');
}

lineParseCache[line] = {
beforeParse: line,
file: nixSlashes(fileLineColumn[0] || ''),
line: parseInt(fileLineColumn[1] || '', 10) || undefined,
column: parseInt(fileLineColumn[2] || '', 10) || undefined,
name,
alias,
};
return lineParseCache[line];
};
Expand All @@ -43,6 +52,17 @@ export const currentStack = () => {
Error.stackTraceLimit = Infinity;
const lines = (new Error().stack || '').split('\n');
Error.stackTraceLimit = currentStackLimit;
const entries = lines.map((line) => line.trim()).map(parseStackLine);
return entries.filter((x) => x !== undefined);
const entries = lines
.map(parseStackLine)
.filter((x) => x !== undefined)
.map((value, index, original) => {
const pre = original[index + 1];
const post = original[index - 1];
return {
...value,
caller: pre ? pre.alias || pre.name : undefined,
called: post ? post.alias || post.name : undefined,
};
});
return entries;
};
74 changes: 58 additions & 16 deletions tests/unit/module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {SourceMapConsumer} from 'source-map-js';
import {
addSourceMap,
addTrustedModules,
getCurrentModule,
getCurrentModuleInfo,
getModuleNameFromLocation,
getModulePermissions,
isModuleAllowedToExecute,
Expand Down Expand Up @@ -111,6 +111,15 @@ describe('module', () => {
}),
).toBeFalsy();

expect(
isModuleAllowedToExecute({
module: 'imate>one',
family: {name: 'imate'},
method: {name: 'method', needsExplicitPermission: true},
directCaller: {module: 'node:internal'},
}),
).toBeTruthy();

expect(
isModuleAllowedToExecute({
module: 'imate>two',
Expand Down Expand Up @@ -195,9 +204,11 @@ describe('module', () => {
});

test('getModuleNameFromLocation', () => {
expect(getModuleNameFromLocation()).toBeUndefined();

expect(
getModuleNameFromLocation('/Users/jason/code/sandworm/tests/node/prod/stack.test.js'),
).toBeUndefined();
).toBe('root');

expect(
getModuleNameFromLocation('/Users/jason/code/sandworm/node_modules/lodash/lodash.js'),
Expand All @@ -207,7 +218,7 @@ describe('module', () => {
getModuleNameFromLocation('/Users/jason/code/sandworm/node_modules/@apollo/client/index.js'),
).toBe('@apollo/client');

expect(getModuleNameFromLocation('http://localhost:3000/static/js/bundle.js')).toBeUndefined();
expect(getModuleNameFromLocation('http://localhost:3000/static/js/bundle.js')).toBe('root');

expect(getModuleNameFromLocation('http://localhost:3000/static/js/bundle.js', true)).toBe(
'http://localhost:3000/static/js/bundle.js',
Expand All @@ -220,20 +231,21 @@ describe('module', () => {
),
).toBe('chrome-extension://fmkadmapgofadopljbjfkapdkoienihi/build/react_devtools_backend.js');

expect(getModuleNameFromLocation('node:https')).toBeUndefined();
expect(getModuleNameFromLocation('node:https')).toBe('node:https');
expect(getModuleNameFromLocation('node:internal/modules/cjs/loader')).toBe('node:internal');
});

test('getCurrentModule', () => {
test('getCurrentModuleInfo', () => {
// Basic uses
expect(getCurrentModule({stack: []}).name).toBe('root');
expect(getCurrentModule({stack: [{file: 'app.js', line: 1, column: 1}]}).name).toBe('root');
expect(getCurrentModuleInfo({stack: []}).name).toBe('root');
expect(getCurrentModuleInfo({stack: [{file: 'app.js', line: 1, column: 1}]}).name).toBe('root');
expect(
getCurrentModule({
getCurrentModuleInfo({
stack: [{file: 'project/node_modules/module-name/dist/index.js', line: 1, column: 1}],
}).name,
).toBe('module-name');
expect(
getCurrentModule({
getCurrentModuleInfo({
stack: [
{file: 'app.js', line: 1, column: 1},
{file: 'project/node_modules/module-name/dist/index.js', line: 1, column: 1},
Expand All @@ -243,7 +255,7 @@ describe('module', () => {

// Composite chains
expect(
getCurrentModule({
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/other/root.js', line: 1, column: 1},
{file: 'app.js', line: 1, column: 1},
Expand All @@ -254,7 +266,7 @@ describe('module', () => {

// Ignore URLs by default
expect(
getCurrentModule({
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/other/root.js', line: 1, column: 1},
{file: 'app.js', line: 1, column: 1},
Expand All @@ -266,7 +278,7 @@ describe('module', () => {

// Ignore extensions by default
expect(
getCurrentModule({
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/other/root.js', line: 1, column: 1},
{file: 'app.js', line: 1, column: 1},
Expand All @@ -280,7 +292,7 @@ describe('module', () => {
// Allow extensions
setIgnoreExtensions(false);
expect(
getCurrentModule({
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/other/root.js', line: 1, column: 1},
{file: 'app.js', line: 1, column: 1},
Expand All @@ -294,7 +306,7 @@ describe('module', () => {

// Ignore URLs by default
expect(
getCurrentModule({
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/other/root.js', line: 1, column: 1},
{file: 'app.js', line: 1, column: 1},
Expand All @@ -305,7 +317,7 @@ describe('module', () => {

// Allow URLs
expect(
getCurrentModule({
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/other/root.js', line: 1, column: 1},
{file: 'app.js', line: 1, column: 1},
Expand All @@ -318,13 +330,43 @@ describe('module', () => {
// Ignore trusted modules
addTrustedModules(['other']);
expect(
getCurrentModule({
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/other/root.js', line: 1, column: 1},
{file: 'app.js', line: 1, column: 1},
{file: 'project/node_modules/module-name/dist/index.js', line: 1, column: 1},
],
}).name,
).toBe('module-name');

expect(
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/sandworm/root.js', line: 1, column: 1},
{file: 'app.js', line: 1, column: 1},
{file: 'project/node_modules/module-name/dist/index.js', line: 1, column: 1},
],
}).directCaller.module,
).toBe('root');

expect(
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/sandworm/root.js', line: 1, column: 1},
{file: 'node:internal/modules/cjs/loader', line: 1, column: 1},
{file: 'project/node_modules/module-name/dist/index.js', line: 1, column: 1},
],
}).directCaller.module,
).toBe('node:internal');

expect(
getCurrentModuleInfo({
stack: [
{file: 'project/node_modules/sandworm/root.js', line: 1, column: 1},
{file: 'node:internal/modules/cjs/loader', line: 1, column: 1},
{file: 'project/node_modules/module-name/dist/index.js', line: 1, column: 1},
],
}).lastModuleCaller.module,
).toBe('module-name');
});
});
Loading

0 comments on commit c8a91cf

Please sign in to comment.