SkillAgentSearch skills...

Electroff

A cross browser, electron-less helper, for IoT projects and standalone applications.

Install / Use

/learn @WebReflection/Electroff
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

electroff

Raspberry Pi Oled

A cross browser, electron-less helper, for IoT projects and standalone applications.

With this module, you can run arbitrary Node.js code from the client, from any browser, and without needing electron.

📣 Announcement

electroff can be fully replaced by coincident/server!


Looking for a lighter, faster, much safer, yet slightly more limited alternative? Try proxied-node out instead.

Community

Please ask questions in the dedicated forum to help the community around this project grow ♥


Getting Started

Considering the following app.js file content:

const {PORT = 3000} = process.env;

const express = require('express');
const electroff = require('electroff');

const app = express();
app.use(electroff);
app.use(express.static(`${__dirname}/public`));
app.listen(PORT, () => console.log(`http://localhost:${PORT}`));

The public/index.html folder can contain something like this:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>Electroff Basic Test</title>
  <script type="module">
  // use <script src="electroff"> instead for a global utility
  import CommonJS from '/electroff?module';

  // pass an asynchronous callback to electroff
  // it will be invoked instantly with few helpers
  CommonJS(async ({require}) => {
    const {arch, cpus, freemem} = require('os');
    document.body.innerHTML = `
      <h1>${document.title}</h1>
      <h2> CPUs: ${await cpus().length} </h2>
      <h2> Architecture: ${await arch()} </h2>
      <h2> Free memory: ${await freemem()} </h2>
    `;
  });
  </script>
</head>
</html>

Helpers passed as callback object / API

  • require, usable to require any module or file, same as you would do in CommonJS
  • global, usable to share a mirrored state across multiple electroff(...) calls <sup><sub>(but not shared across multiple clients)</sub></sup>
  • remove, usable to remove instances when these are not available anymore <sup><sub>(a WeakRef implementation is coming soon)</sub></sup>
  • __dirname, which points at the Node.js path that is currently running the module
  • until, usable to await emitters events on the client-side <sup><sub>(read more in F.A.Q.)</sub></sup>

F.A.Q.

<details> <summary><strong>How does it work?</strong></summary> <div>

The JS on the page is exactly like any regular JS, but anything referencing Node.js environment, through any require(...), is executed on a shared sandbox in Node.js, where each user gets its own global namespace a part.

Such sandbox is in charge of executing code from the client, but only when the client await some value.

const {debug} = require('process').features;
console.log('debug is', await debug);

const {join} = require('path');
const {readFile} = require('fs').promises;
const content = await readFile(join(__dirname, 'public', 'index.html'));
console.log(content);

In depth: every time we await something in JS, an implicit lookup for the .then(...) method is performed, and that's when electroff can perform a fancy client/server asynchronous interaction, through all the paths reached through the various references, which are nothing more than Proxies with special abilities.

In few words, the following code:

await require('fs').promises.readFile('file.txt');

would evaluated, within the vm sandbox, the following code:

await require("fs").promises.readFile.apply(
  require("fs").promises,
  ["test.txt"]
)

All operations are inevitably repeated because every single .property access, .method(...) invoke, or even new module.Thing(...), is a branch of the code a part.

The foreign vs local scope

It is important to keep in mind that there is a huge difference between foreign code, and scoped code, where foreign code cannot reach scoped code, and vive-versa.

electroff(async ({require}) => {
  // local scope code
  const num = Math.random();

  // foreign code (needs to be awaited)
  const {EventEmitter} = require('events');
  const ee = await new EventEmitter;
  await ee.on('stuff', async function (value) {
    // nothing in this scope can reach
    // `num`, as example, is not accessible
    // and neither is `ee` ... but `this` works fine
    console.log(this);
    // this log will be on the Node.js site, it won't log
    // anything on the browser
    console.log('stuff', value);
  });

  // DOM listeners should be async if these need to signal
  // or interact with the foreign code because ...
  someButtom.addEventListener('click', async () => {
    // ... foreign code always need to be awaited!
    await ee.emit('stuff', 123);
  });
});
</div> </details> <details> <summary><strong>Is it safe?</strong></summary> <div>

Theoretically, this is either "as safe as", or "as unsafe as", electron can be, but technically, the whole idea behind is based on client side code evaluation through a shared vm and always the same context per each client, although ensuring a "share nothing" global object per each context, so that multiple clients, with multiple instances/invokes, won't interfere with each other, given the same script on the page.

If the ELECTROFF_ONCE=1 environment variable is present, electroff will increase security in the following way:

  • a client can use electroff only via import electroff from '/electroff?module', and any attempt to retrieve the electroff script in a different way will fail
  • previous point ensures that the module can be executed only once, so there's one single room/window in the page to define its behavior, anot nothing else can interfeer with the server side vm
  • using CSP would also work so that only known code on the page can safely run, and there's no eval nor Function call in here, so that nothing else can be injected

Regardless of the ELECTROFF_ONCE=1 security guard though, please bear in mind that even if the whole communication channel is somehow based on very hard to guess unique random IDs per client, this project/module is not suitable for websites, but it can be used in any IoT related project, kiosk, or standalone applications, where we are sure there is no malicious code running arbitrary JS on our machines, which is not always the case for online Web pages.

</div> </details> <details> <summary><strong>Are Node.js instances possible?</strong></summary> <div>

Yes, but there are at least two things to keep in mind:

  • any Node.js instance should be awaited on creation, i.e.: const instance = await new require('events').EventEmitter;, unless we're waiting for a specific listener, in which case it's better to await until(thing).is('ready') (see next F.A.Q.)
  • there is currently no way to automatically free the vm from previously created instances, if not by explicitly using remove(instance)

Last point means the vm memory related to any client would be freed only once the client refreshes the page, or closes the tab, but there's the possibility that the client crashes or has no network all of a sudden, and in such case the vm will trash any reference automatically, in about 5 minutes or more.

</div> </details> <details> <summary><strong>How to react to/until Node.js events?</strong></summary> <div>

The until utility keeps the POST request hanging until the observed event is triggered once. It pollutes the emitter, if not polluted already, with an is(eventName) that returns a promise resolved once the event name happens.

Following an example of how this could work in practice.

CommonJS(async ({require, until}) => {
  const five = require('johnny-five');

  // no need to await here, or ready could
  // be fired before the next request is performed
  const board = new five.Board();

  // simply await everything at once in here
  await until(board).is('ready');

  // now all board dependent instances can be awaited
  const led = await new five.Led(13);
  // so that it's possible to await each method/invoke/property
  await led.blink(500);

  document.body.textContent = `it's blinking!`;
});
</div> </details> <details> <summary><strong>Any best practice?</strong></summary> <div>

At this early stage, I can recommend only few best-practices I've noticed while playing around with this module:

  • don't overdo server side instances/references, try to reach only the utilities you need the most, instead of creating everything on the vm side
  • when a server side reference method is invoked, you must await it, i.e. await emitter.setMaxListeners(20). This grants next time you await emitter.getMaxListeners() you'll receive the answer you expect
  • template literals are passed as plain arrays. If your library optimizes on template literals uniqueness, it will always re-parse/re-do any dance, because the array on the server side will be always a different one. Create a file that queries the DB, and simply require("./db-helper") instead of writing all SQL queries on the client side, and use Node.js regular helpers/files whenever it works
  • try to keep global references to a minimum amount, as the back and forward dance is quite expensive, and most of the time you won't need it
  • if any needed instance has an emit once ready, const instance = new Thing; await until(instance).is('ready') instead of const instance = await new Thing; await instance.once('ready', doThing), so you ensure your instance is ready within th
View on GitHub
GitHub Stars71
CategoryDevelopment
Updated4mo ago
Forks1

Languages

JavaScript

Security Score

97/100

Audited on Nov 18, 2025

No findings