[GH-ISSUE #1656] [Bug] Electron APIs are not recovered through aliasing variables (e.g. const ipc = ipcMain) #559

Closed
opened 2026-10-15 18:26:10 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @thelmoadv-blip on GitHub (Oct 11, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1656

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

analyze-javascript-application does not recover an Electron API usage when the API object reaches the call site through a local variable initialized from another binding, instead of through the binding created directly by require('electron') or import:

Aliasing variable (everything else identical to the control) Counters that drop to 0
const Win = BrowserWindow; new Win({ webPreferences: { preload } }) browser_windows, preload_entrypoints
const bridge = contextBridge; bridge.exposeInMainWorld(...) context_bridge_apis, exposed_api_members
const ipc = ipcMain; ipc.handle('ch:ping', ...) ipc.main_handlers, ipc.paired_renderer_transmissions
const ipc = ipcRenderer; ipc.invoke('ch:ping') ipc.renderer_transmissions, ipc.paired_renderer_transmissions
const electron = require('electron'); const ipc = electron.ipcMain; (and the same for ipcRenderer) all four ipc.* counters above

The command exits 0 and the affected counters are reported as 0. None of the unknowns or limitations in these outputs refers to the missed usages: the call-flow/object-flow unknowns are the same kinds that also appear in the control output (where everything is recovered), and the IPC limitation present in the control output ("Literal channel equality does not prove that the handler is registered or reachable at runtime.") disappears from the IPC-alias outputs. A consumer cannot tell "this app has no IPC" from "IPC was not recovered".

Reproduced on 6.3.0 (npm) and on main @ 2603891.

Reproduction

mkdir -p alias-repro && cd alias-repro
cat > package.json <<'EOF'
{ "name": "alias-repro", "version": "1.0.0", "main": "main.js" }
EOF
cat > main.js <<'EOF'
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const Win = BrowserWindow;
const ipc = ipcMain;
app.whenReady().then(() => {
  new Win({ webPreferences: { preload: path.join(__dirname, 'preload.js') } });
});
ipc.handle('ch:ping', async () => 'pong');
EOF
cat > preload.js <<'EOF'
const { contextBridge, ipcRenderer } = require('electron');
const bridge = contextBridge;
const ipc = ipcRenderer;
bridge.exposeInMainWorld('api', { ping: () => ipc.invoke('ch:ping') });
EOF
npx -y rea-agents@6.3.0 analyze-javascript-application . \
  --filter-output normalized_result.summary --format json

Control: the same three files without the four aliasing lines, calling new BrowserWindow(...), ipcMain.handle(...), contextBridge.exposeInMainWorld(...) and ipcRenderer.invoke(...) directly.

Expected behavior

const Win = BrowserWindow (or const ipc = electron.ipcMain) binds the same object as the control, so the summary should match the control output:

{"browser_windows":1,"explicit_web_preferences":1,"preload_entrypoints":1,"context_bridge_apis":1,"exposed_api_members":1,"ipc":{"operations":2,"literal_channels":1,"dynamic_channel_operations":0,"renderer_transmissions":1,"renderer_listeners":0,"main_handlers":1,"paired_renderer_transmissions":1,"ambiguous_renderer_transmissions":0,"unpaired_literal_renderer_transmissions":0}}

If an alias cannot be followed, I would expect an explicit Electron-specific unknown or limitation for the unresolved usage rather than a silent 0.

Impact and scope

Variant matrix, same app each time. Columns: browser_windows / preload_entrypoints / context_bridge_apis / exposed_api_members / ipc.literal_channels / ipc.main_handlers / ipc.renderer_transmissions / ipc.paired_renderer_transmissions. Expected: all 1.

How the Electron API is reached 6.3.0 main @ 2603891
const { BrowserWindow, ipcMain } = require('electron') (control) 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
const Win = BrowserWindow 0 0 1 1 1 1 1 1 0 0 1 1 1 1 1 1
const bridge = contextBridge 1 1 0 0 1 1 1 1 1 1 0 0 1 1 1 1
const ipc = ipcMain (main process only) 1 1 1 1 1 0 1 0 1 1 1 1 1 0 1 0
const ipc = ipcRenderer (preload only) 1 1 1 1 1 1 0 0 1 1 1 1 1 1 0 0
const ipc = electron.ipcMain / electron.ipcRenderer 1 1 1 1 0 0 0 0 1 1 1 1 0 0 0 0
every API through const x = electron.<Api> 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
reproduction above (assignment aliases everywhere) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
const ipcMain = require('electron').ipcMain 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
electron.ipcMain.handle(...) (namespace member call) 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
  • Affected: static recovery of BrowserWindow construction (and its preload), contextBridge exposure, and IPC handlers/transmissions and their pairing, whenever the Electron API object is reached through an aliasing variable.
  • Not affected on main @ 2603891: direct destructuring (renamed or not), import { x as y }, direct require('electron').<Api> reads, and namespace member calls (see Additional context for what 6.3.0 still misses).
  • Evidence integrity: silent under-reporting; the relationship is absent and no specific unknown records it.

REA version

6.3.0 (npm) and main @ 26038912f6 (built from source: npm ci --ignore-scripts && npx tsc -p tsconfig.build.json, run as node scripts/rea.mjs)

Runtime and platform

Node.js 22.22.1; Ubuntu 26.04 x86_64

Target and artifact context

Synthetic three-file Electron app directory (package.json, main.js, preload.js), plain CommonJS. No real application involved.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Reproduction output (6.3.0 via npx; main @ 2603891 prints the same values):

{
  "normalized_result": {
    "summary": {
      "browser_windows": 0,
      "explicit_web_preferences": 0,
      "preload_entrypoints": 0,
      "context_bridge_apis": 0,
      "exposed_api_members": 0,
      "ipc": {
        "operations": 0,
        "literal_channels": 0,
        "dynamic_channel_operations": 0,
        "renderer_transmissions": 0,
        "renderer_listeners": 0,
        "main_handlers": 0,
        "paired_renderer_transmissions": 0,
        "ambiguous_renderer_transmissions": 0,
        "unpaired_literal_renderer_transmissions": 0
      },
      "sender_validation_observations": 0,
      "utility_processes": 0,
      "resolved_utility_entrypoints": 0,
      "native_addon_bindings": 0,
      "resolved_native_addon_bindings": 0
    }
  }
}

Analysis or suspected cause

Hypothesis from black-box testing only; I have not read the analyzer source. The Electron recognizers seem to accept the binding created directly by require('electron') / import (destructured, renamed, or require('electron').<Api>) and namespace member calls, but do not carry the API identity through a second variable initialized from such a binding (const Win = BrowserWindow) or from a namespace member read (const ipc = electron.ipcMain).

Additional context

  • Already fixed on main: on 6.3.0, renamed destructuring (const { ipcMain: m } = require('electron')), import { ipcMain as m } from 'electron' and const ipc = require('electron').ipcMain also reported 0, so esbuild --minify output of the control app reported no windows, bridges or IPC at all. All of these are correct on main @ 2603891.
  • Separate observation, not part of this report (happy to file it separately): with an ESM main process, preload: path.join(import.meta.dirname, 'preload.mjs') gives preload_entrypoints: 0, while path.join(path.dirname(fileURLToPath(import.meta.url)), 'preload.mjs') and fileURLToPath(new URL('./preload.mjs', import.meta.url)) resolve. Same on 6.3.0 and main.
  • Possible regression fixture: the variants in the matrix above, where every counter should be 1.
  • This report was drafted with AI assistance. Every number above comes from the commands shown, run against both versions.
Originally created by @thelmoadv-blip on GitHub (Oct 11, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1656 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `analyze-javascript-application` does not recover an Electron API usage when the API object reaches the call site through a local variable initialized from another binding, instead of through the binding created directly by `require('electron')` or `import`: | Aliasing variable (everything else identical to the control) | Counters that drop to 0 | |---|---| | `const Win = BrowserWindow; new Win({ webPreferences: { preload } })` | `browser_windows`, `preload_entrypoints` | | `const bridge = contextBridge; bridge.exposeInMainWorld(...)` | `context_bridge_apis`, `exposed_api_members` | | `const ipc = ipcMain; ipc.handle('ch:ping', ...)` | `ipc.main_handlers`, `ipc.paired_renderer_transmissions` | | `const ipc = ipcRenderer; ipc.invoke('ch:ping')` | `ipc.renderer_transmissions`, `ipc.paired_renderer_transmissions` | | `const electron = require('electron'); const ipc = electron.ipcMain;` (and the same for `ipcRenderer`) | all four `ipc.*` counters above | The command exits 0 and the affected counters are reported as `0`. None of the unknowns or limitations in these outputs refers to the missed usages: the call-flow/object-flow unknowns are the same kinds that also appear in the control output (where everything is recovered), and the IPC limitation present in the control output ("Literal channel equality does not prove that the handler is registered or reachable at runtime.") disappears from the IPC-alias outputs. A consumer cannot tell "this app has no IPC" from "IPC was not recovered". Reproduced on 6.3.0 (npm) and on `main` @ `2603891`. ### Reproduction ```sh mkdir -p alias-repro && cd alias-repro cat > package.json <<'EOF' { "name": "alias-repro", "version": "1.0.0", "main": "main.js" } EOF cat > main.js <<'EOF' const { app, BrowserWindow, ipcMain } = require('electron'); const path = require('path'); const Win = BrowserWindow; const ipc = ipcMain; app.whenReady().then(() => { new Win({ webPreferences: { preload: path.join(__dirname, 'preload.js') } }); }); ipc.handle('ch:ping', async () => 'pong'); EOF cat > preload.js <<'EOF' const { contextBridge, ipcRenderer } = require('electron'); const bridge = contextBridge; const ipc = ipcRenderer; bridge.exposeInMainWorld('api', { ping: () => ipc.invoke('ch:ping') }); EOF npx -y rea-agents@6.3.0 analyze-javascript-application . \ --filter-output normalized_result.summary --format json ``` Control: the same three files without the four aliasing lines, calling `new BrowserWindow(...)`, `ipcMain.handle(...)`, `contextBridge.exposeInMainWorld(...)` and `ipcRenderer.invoke(...)` directly. ### Expected behavior `const Win = BrowserWindow` (or `const ipc = electron.ipcMain`) binds the same object as the control, so the summary should match the control output: ```json {"browser_windows":1,"explicit_web_preferences":1,"preload_entrypoints":1,"context_bridge_apis":1,"exposed_api_members":1,"ipc":{"operations":2,"literal_channels":1,"dynamic_channel_operations":0,"renderer_transmissions":1,"renderer_listeners":0,"main_handlers":1,"paired_renderer_transmissions":1,"ambiguous_renderer_transmissions":0,"unpaired_literal_renderer_transmissions":0}} ``` If an alias cannot be followed, I would expect an explicit Electron-specific unknown or limitation for the unresolved usage rather than a silent `0`. ### Impact and scope Variant matrix, same app each time. Columns: `browser_windows` / `preload_entrypoints` / `context_bridge_apis` / `exposed_api_members` / `ipc.literal_channels` / `ipc.main_handlers` / `ipc.renderer_transmissions` / `ipc.paired_renderer_transmissions`. Expected: all 1. | How the Electron API is reached | 6.3.0 | main @ 2603891 | |---|---|---| | `const { BrowserWindow, ipcMain } = require('electron')` (control) | 1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1 | | `const Win = BrowserWindow` | 0 0 1 1 1 1 1 1 | 0 0 1 1 1 1 1 1 | | `const bridge = contextBridge` | 1 1 0 0 1 1 1 1 | 1 1 0 0 1 1 1 1 | | `const ipc = ipcMain` (main process only) | 1 1 1 1 1 0 1 0 | 1 1 1 1 1 0 1 0 | | `const ipc = ipcRenderer` (preload only) | 1 1 1 1 1 1 0 0 | 1 1 1 1 1 1 0 0 | | `const ipc = electron.ipcMain` / `electron.ipcRenderer` | 1 1 1 1 0 0 0 0 | 1 1 1 1 0 0 0 0 | | every API through `const x = electron.<Api>` | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | | reproduction above (assignment aliases everywhere) | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | | `const ipcMain = require('electron').ipcMain` | 1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1 | | `electron.ipcMain.handle(...)` (namespace member call) | 1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1 | - Affected: static recovery of `BrowserWindow` construction (and its preload), `contextBridge` exposure, and IPC handlers/transmissions and their pairing, whenever the Electron API object is reached through an aliasing variable. - Not affected on main @ 2603891: direct destructuring (renamed or not), `import { x as y }`, direct `require('electron').<Api>` reads, and namespace member calls (see Additional context for what 6.3.0 still misses). - Evidence integrity: silent under-reporting; the relationship is absent and no specific unknown records it. ### REA version 6.3.0 (npm) and main @ 26038912f6ce55cf3ce887b6f522969febec6721 (built from source: `npm ci --ignore-scripts && npx tsc -p tsconfig.build.json`, run as `node scripts/rea.mjs`) ### Runtime and platform Node.js 22.22.1; Ubuntu 26.04 x86_64 ### Target and artifact context Synthetic three-file Electron app directory (package.json, main.js, preload.js), plain CommonJS. No real application involved. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Reproduction output (6.3.0 via npx; main @ 2603891 prints the same values): ```text { "normalized_result": { "summary": { "browser_windows": 0, "explicit_web_preferences": 0, "preload_entrypoints": 0, "context_bridge_apis": 0, "exposed_api_members": 0, "ipc": { "operations": 0, "literal_channels": 0, "dynamic_channel_operations": 0, "renderer_transmissions": 0, "renderer_listeners": 0, "main_handlers": 0, "paired_renderer_transmissions": 0, "ambiguous_renderer_transmissions": 0, "unpaired_literal_renderer_transmissions": 0 }, "sender_validation_observations": 0, "utility_processes": 0, "resolved_utility_entrypoints": 0, "native_addon_bindings": 0, "resolved_native_addon_bindings": 0 } } } ``` ### Analysis or suspected cause Hypothesis from black-box testing only; I have not read the analyzer source. The Electron recognizers seem to accept the binding created directly by `require('electron')` / `import` (destructured, renamed, or `require('electron').<Api>`) and namespace member calls, but do not carry the API identity through a second variable initialized from such a binding (`const Win = BrowserWindow`) or from a namespace member read (`const ipc = electron.ipcMain`). ### Additional context - Already fixed on main: on 6.3.0, renamed destructuring (`const { ipcMain: m } = require('electron')`), `import { ipcMain as m } from 'electron'` and `const ipc = require('electron').ipcMain` also reported 0, so `esbuild --minify` output of the control app reported no windows, bridges or IPC at all. All of these are correct on main @ 2603891. - Separate observation, not part of this report (happy to file it separately): with an ESM main process, `preload: path.join(import.meta.dirname, 'preload.mjs')` gives `preload_entrypoints: 0`, while `path.join(path.dirname(fileURLToPath(import.meta.url)), 'preload.mjs')` and `fileURLToPath(new URL('./preload.mjs', import.meta.url))` resolve. Same on 6.3.0 and main. - Possible regression fixture: the variants in the matrix above, where every counter should be 1. - This report was drafted with AI assistance. Every number above comes from the commands shown, run against both versions.
bsakaguchi 2026-10-15 18:26:10 +00:00
Author
Owner

@N0zoM1z0 commented on GitHub (Oct 11, 2026):

Taking a bounded investigation of this report. I will reproduce it on current main and trace API identity through lexical aliases, including shadowing/reassignment controls, before changing recovery. Public CLI and stdio MCP evidence will be checked together; no Electron runtime execution claim.

<!-- gh-comment-id:6103564790 --> @N0zoM1z0 commented on GitHub (Oct 11, 2026): Taking a bounded investigation of this report. I will reproduce it on current main and trace API identity through lexical aliases, including shadowing/reassignment controls, before changing recovery. Public CLI and stdio MCP evidence will be checked together; no Electron runtime execution claim.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#559
No description provided.