Isolation
Why should i use it ? How often do you see libraries which mutates global variables Or how often
do you check libraries actions ? Library provides script isolation in custom contexts to solve this
issues. Also, isolation prevents global scope and prototypes pollution.
[!TIP]
Possible use case
May be useful as routing loader, if some loaded route makes an error while runtime, you may
recreate it - to prevent memory leaks. Another worlds, with this library you can create
multi-tenant applications;
Installation
npm i isolation --save
Basic Usage
-
Prevent intentionally damage
It will stop tricky users code, while you don't allow it.
const Isolation = require('isolation');
const options = { access: { realm: module => module !== 'fs' } };
const routes = Isolation.read('./routes', options);
const dangerousLibrary = require('unchecked-dangerous-library');
const fs = require('fs');
fs.rm(process.cwd(), { recursive: true });
-
Prevent unintentionally damage
This solves problem where libraries used to mutate global variables.
const Isolation = require('isolation');
Isolation.read('./routes');
console.log('All works fine');
console('Here it not works');
String.prototype;
const dangerousLibrary = require('unchecked-dangerous-library');
var console = msg => process.stdout.write(msg);
global.console = console;
console('Here it works fine');
String.prototype = {};
Module types / Script syntax
[!CAUTION]
You can run any script from string, just like eval, but in custom VM container. But you
shouldn't use it for unknown script evaluation, it may create security issues.
Commonjs
By default Isolation will use Standard Nodejs syntax. With this type of syntax it will provide to
your realms global variables such as:
- require function, which is almost same as nodejs require, but with extra cup of isolation;
- module & exports variables provided to manipulate with exports between modules;
- __filename & __dirname are same as with default nodejs realm;
[!IMPORTANT]
You always should use module.exports to export, otherwise you will see undefined as the result of
realm execution.
const Isolation = require('isolation');
console.log(new Isolation(`module.exports = { field: 'value' };`).execute());
console.log(Isolation.execute(`module.exports = (a, b) => a + b;`)(2 + 2));
Isolation.execute(`module.exports = async (a, b) => a + b;`)(2 + 2).then(console.log);
ISO
This type of syntax will stand your script alone without any of extra global variables. That means
that you would not see module
or exports
in your environment. But your
context variables will still work well.
[!IMPORTANT]
In this mode, your realms will export result of the last expression, that means that you should
put a reference to your variable or expression at the end of the file / row;
const Isolation = require('isolation');
const options = { type: 'iso' };
console.log(new Isolation(`{ field: 'value' };`, options).execute());
console.log(Isolation.execute(`(a, b) => a + b;`, options)(2 + 2));
Isolation.execute(`async (a, b) => a + b;`, options)(2 + 2).then(console.log);
ESM
Isolation does'nt support esm syntax yet. That's because currently node.vm
ESM modules
are experimental.
Context API
You can create custom context or use default presets with context api. This will allow you to
provide your custom variables to the context without requiring any module.
[!TIP]
Remember to reuse your contexts. This will increase performance of your application. To help you
with this we have default contexts:
Context example
const { contextify, execute } = require('isolation');
const custom = contextify({ console });
execute(`console.log(123);`, { ctx: custom });
execute(`console.log(123);`);
Also its allow you to change program behavior with something like:
const ctx = Isolation.contextify({ a: 1000, b: 10 });
const realm = new Isolation(`module.exports = a - b`, { ctx });
realm.execute();
realm.execute({ ...ctx, a: 0 });
realm.execute({ ...ctx, b: 7 });
Default contexts
Default contexts are accessible from Isolation.contextify, there you can find:
- Isolation.contextify.EMPTY, that just empty context
- Isolation.contextify.COMMON, timers, buffer, fetch etc...
- Isolation.contextify.NODE, global, console, process & COMMON context You should'nt use
NODE context, it may create security issues, otherwise it may road to possible context
escaping.
Reader API
Reader allow you to run scripts from files and extends possible provided options with:
-
Option prepare:boolean
reader will return non-executed scripts, default false
-
Option depth:number|boolean
nested directories restrictions, default true
-
Option flat:boolean
reader will flat nested scripts, default false
-
read
Allow you to read source codes from files and directories
const Isolation = require('isolation');
Isolation.read('./path/to/script.js').then(console.log);
Isolation.read('./path/to').then(console.log);
Isolation.read('./path/to', { prepare: true }).then(console.log);
By default reader works with nested directories, to disable this behavior you can do:
const Isolation = require('isolation');
Isolation.read('./path/to', { depth: false });
Isolation.read('./path/to', { depth: 3 });
-
read.file
Allow you to execute script from single file
const Isolation = require('isolation');
Isolation.read.file('./path/to/script.js').then(console.log);
Isolation.read.file('./path/to/script.js', { prepare: true }).then(console.log);
-
read.dir
Allow you to execute multiple scripts from directory
const Isolation = require('isolation');
Isolation.read.dir('./path/to').then(console.log);
Isolation.read.dir('./path/to', { prepare: true }).then(console.log); Output: { script: Script {} }
Isolation.read.dir('./path/to', { depth: false }).then(console.log);
Access control
You may control access over realm submodules and reader api;
[!NOTE] If access doesn't provided realm submodules would'nt be accessible and reader will read
all files in directed repository.
const options = { access: pathOrModule => pathOrModule === 'fs' || pathOrModule.endsWith('.js') };
Isolation.execute('module.exports = require("fs")', options);
Isolation.read('./path/to/script.js', options);
const options2 = {
access: {
reader: path => true,
realm: module => {},
},
};
Library substitution
You can replace result of require for specific libraries with anything what you want;
const Isolation = require('isolation');
const src = `
const fs = require('fs');
module.exports = fs.readFile('Isolation.js');
`;
const sub = name => {
if (name !== 'fs') return true;
return {
readFile: filename => filename + ' Works !',
};
};
const result = Isolation.execute(src, { access: { realm: sub } });
console.log(result);
Possible script options
Option | Possible | Default | Description |
---|
type | iso | cjs | cjs | Type of script handling, see syntax types |
ctx | object | - | Realm context, see Context API |
filename | string | ISO | Name of the module , also it is global variable __filename for root realm |
dir | string | process.cwd() | Module directory , also it is global variable __dirname and realm require start point |
npmIsolation | boolean | false | Controls npm modules isolation |
access | Access | - | Isolation restrictions, see Access API |
prepare | boolean | false | Reader would'nt execute script for you |
flat | boolean | false | Reader will flat nested scripts |
depth | boolean | number | true | Restricts dir reading depth |
script | ScriptOptions | - | Configuration for VM.Script initialization |
run | RunningCodeOptions | { timeout: 1000 } | Configuration for VM.Script execution |
Copyright & contributors
Copyright © 2023 Astrohelm contributors.
This library is MIT licensed license.
And it is part of Astrohelm solutions.