[GH-ISSUE #2174] fix(javascript): bundled isolated-world contextBridge exposure is omitted #720

Closed
opened 2026-10-15 18:27:40 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 12, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/2174

Summary

Static application analysis omits an isolated-world API exposure when a literal CommonJS module factory obtains Electron through its loader parameter. The same bundled shape exposes a main-world API successfully, and the direct namespace form exposes either world successfully. The missing graph node also leaves summary.context_bridge_apis at zero. This is a source-build defect in wrapped exposure reconstruction, not evidence that the published package supports that capability.

Runtime or environment

Tested source revision: c22111eca2, using its existing compiled public CLI. Comparison package: published rea-agents 6.4.0, using its installed compiled CLI. Measured runtime: Node.js 24.21.0 on macOS arm64, within the documented supported Node range. The contributor pin is 24.18.0; that exact contributor runtime was not exercised. Targets were disposable directories containing one inert preload.js; no Electron process or native provider was started. Interface: CLI. Area: Evidence, artifacts, or reconstruction. Earliest failure stage: Native analysis or reconstruction.

Steps to reproduce

Use a checkout of the tested source revision with its compiled runtime available. The documented preparation is npm ci && npm run build:cached; this verification used the coordinator's existing build rather than rebuilding. From the repository root, run the following self-contained fixture generator and public CLI comparison. It writes only disposable inputs and deletes them afterward. The JavaScript application fixture is passed to static analysis and never executed.

node --input-type=module <<'JS'
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
const root = await mkdtemp(join(tmpdir(), 'bridge-analysis-'));
const cli = resolve('scripts/rea.mjs');
const calls = {
  isolated: 'electron.contextBridge.exposeInIsolatedWorld(1001, "isolatedApi", { ping: true });',
  main: 'electron.contextBridge.exposeInMainWorld("mainApi", { ping: true });',
};
const bundle = call => `(() => {
  const modules = {
    2(module) { module.exports = require("electron"); },
    5(module, exports, load) {
      const electron = load(2);
      ${call}
    },
  };
  function load(id) {
    const m = { exports: {} };
    modules[id].call(m.exports, m, m.exports, load);
    return m.exports;
  }
  load(5);
})();`;
try {
  for (const mode of ['bundled', 'direct']) {
    for (const world of ['isolated', 'main']) {
      const dir = join(root, `${mode}-${world}`);
      await mkdir(dir);
      const source = mode === 'bundled' ? bundle(calls[world])
        : 'const electron = require("electron");\n' + calls[world];
      await writeFile(join(dir, 'preload.js'), source);
      const run = spawnSync(process.execPath, [cli,
        'analyze-javascript-application', dir, '--format', 'json',
        '--max-heap-mb', '128', '--analysis-timeout-ms', '10000'],
        { encoding: 'utf8', timeout: 30000, maxBuffer: 8 * 1024 * 1024 });
      if (run.error || run.status !== 0) throw new Error(run.stderr || run.error?.message || run.stdout);
      const result = JSON.parse(run.stdout).normalized_result;
      const nodes = result.graph.nodes.filter(n => n.kind === 'context-bridge-api');
      console.log(JSON.stringify({ fixture: `${mode}-${world}`,
        count: result.summary.context_bridge_apis,
        properties: nodes.flatMap(n => n.observations.map(o => o.properties)),
        coverage: result.graph.coverage.status }));
    }
  }
} finally {
  await rm(root, { recursive: true, force: true });
}
JS

For the package comparison, use its installed scripts/rea.mjs as cli while leaving all four generated fixture contents unchanged. Both builds were exercised with those same contents. The generic source-build analyze <directory> --format json route was also checked on both bundled fixtures and produced the same zero/one split.

Expected behavior

The bundled isolated-world fixture should contribute one context-bridge-api node and one summary API, preserving world: "isolated", literal world_id.value: 1001, api_key: "isolatedApi", and members: ["ping"]. The direct isolated-world fixture establishes the output contract for the valid three-argument call, while the bundled main-world fixture establishes applicability of module-factory substitution. Both main-world controls should retain their API key and world: "main".

Actual behavior

All eight dedicated CLI calls exited zero. The source build returned zero bridge APIs and no bridge node for the bundled isolated-world fixture. Its bundled main-world control returned one node with resolution_basis: "wrapper-substitution", and both direct controls returned one node with resolution_basis: "direct-syntax". The direct isolated-world node retained the numeric world ID and the ping member. All four source analyses reported graph coverage complete, no truncation, and no parse failures. The generic source route reproduced the bundled split with both calls exiting zero.

Fixture             Source bridge APIs  Published package bridge APIs
bundled-isolated    0                   0
bundled-main        1                   0
direct-isolated     1                   1
direct-main         1                   1

The published package omits both bundled exposure forms. That comparison establishes an earlier wrapped-exposure limitation, not the source build's method-specific failure. Its direct isolated-world control still records literal world ID 1001. The result includes a general limitation that missing findings do not establish absence; coverage complete is not proof that no exposure exists.

Evidence

  • Expected source: docs/javascript-artifact-reconstruction.md:3-10 and :299-315 describe the public inert-analysis workflow, literal module factories, and API-key/member reconstruction for both contextBridge exposure methods. src/domain/javascript/electronStaticAnalysisBrowser.ts:165-210 records the isolated world's argument positions and output fields. The official Electron contextBridge API requires an integer world ID and recommends 1000 or higher; 1001 and the boolean-valued literal API object satisfy that documented call shape.
  • Failure source: Observed dedicated and generic public CLI output for the inline fixtures. src/domain/javascript/electronWrappedCallPaths.ts:487-491 excludes calls whose first argument is not a string literal, so the valid numeric isolated-world call is skipped. Its :393-409 dispatch recognizes only the main-world exposure, and :289-324 interprets two arguments and records a main world. These are source explanations for the single omitted-exposure result.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

The producer binding check independently recognized one valid literal bootstrap in each bundled fixture and resolved the callee to electron.contextBridge.exposeInIsolatedWorld or its main-world counterpart. Both exports passed intactExport, their arguments resolved to literal keys and the ping object member, and neither had conditions, unresolved conditions, resolver truncation, or unresolved bindings. The bootstrap and binding owners are src/domain/javascript/javascriptBootstrap.ts:22-145, javascriptSymbolicBindings.ts:630-669 and :818-882; javascriptStaticAnalysis.ts:84-111 invokes wrapped collection after direct syntax inspection. The fixture has stable bindings, no property overwrites, no table replacement, no unknown spreads, no conditional dispatch, and no early exits. Its observed failure therefore does not depend on an invalid world ID or an unresolved producer.

Restoration check

Analyze the same bundled isolated-world fixture through the dedicated and generic source CLI routes. Both must return exactly one bridge API whose observation retains the isolated world, literal world ID 1001, isolatedApi key, and ping member. Keep the bundled main-world and both direct controls passing with their existing world/key/member values, and preserve the analyzer's handling of overwritten or unresolved Electron identities. Increasing unrelated counts or declaring complete coverage without the omitted API node does not restore the contract. Implementation and regression-test design remain open.

Additional context

The related closed alias-recovery issue #1656 was filed by another author and describes variable aliases and main-world exposure rather than this valid numeric isolated-world module-factory call. It is reference-only. No matching own report or exact isolated-world report was found in the recorded issue/PR inventory or live read-only searches. An open wrapper-improvement PR was inspected as background; its description does not claim this isolated-world restoration, and its branch was not tested.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A bounded, documented static reconstruction result loses an API key and member for a valid literal module-factory exposure while comparable supported inputs succeed. This causes incomplete application-graph evidence for affected artifacts; no application execution failure or wider impact was observed.
  • Workaround status: unverified
  • Workaround basis: Neither tested public source CLI route recovers the omitted node from the same bundled artifact. Direct namespace analysis works on a different artifact representation, but availability of equivalent original source for an affected caller was not established. No practical same-artifact graph recovery workaround was verified.
Originally created by @coygeek on GitHub (Oct 12, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/2174 ## Summary Static application analysis omits an isolated-world API exposure when a literal CommonJS module factory obtains Electron through its loader parameter. The same bundled shape exposes a main-world API successfully, and the direct namespace form exposes either world successfully. The missing graph node also leaves `summary.context_bridge_apis` at zero. This is a source-build defect in wrapped exposure reconstruction, not evidence that the published package supports that capability. ## Runtime or environment Tested source revision: c22111eca2c4bc84a084f12711f405b3e7422a40, using its existing compiled public CLI. Comparison package: published `rea-agents` 6.4.0, using its installed compiled CLI. Measured runtime: Node.js 24.21.0 on macOS arm64, within the documented supported Node range. The contributor pin is 24.18.0; that exact contributor runtime was not exercised. Targets were disposable directories containing one inert `preload.js`; no Electron process or native provider was started. Interface: CLI. Area: Evidence, artifacts, or reconstruction. Earliest failure stage: Native analysis or reconstruction. ## Steps to reproduce Use a checkout of the tested source revision with its compiled runtime available. The documented preparation is `npm ci && npm run build:cached`; this verification used the coordinator's existing build rather than rebuilding. From the repository root, run the following self-contained fixture generator and public CLI comparison. It writes only disposable inputs and deletes them afterward. The JavaScript application fixture is passed to static analysis and never executed. ```sh node --input-type=module <<'JS' import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { spawnSync } from 'node:child_process'; const root = await mkdtemp(join(tmpdir(), 'bridge-analysis-')); const cli = resolve('scripts/rea.mjs'); const calls = { isolated: 'electron.contextBridge.exposeInIsolatedWorld(1001, "isolatedApi", { ping: true });', main: 'electron.contextBridge.exposeInMainWorld("mainApi", { ping: true });', }; const bundle = call => `(() => { const modules = { 2(module) { module.exports = require("electron"); }, 5(module, exports, load) { const electron = load(2); ${call} }, }; function load(id) { const m = { exports: {} }; modules[id].call(m.exports, m, m.exports, load); return m.exports; } load(5); })();`; try { for (const mode of ['bundled', 'direct']) { for (const world of ['isolated', 'main']) { const dir = join(root, `${mode}-${world}`); await mkdir(dir); const source = mode === 'bundled' ? bundle(calls[world]) : 'const electron = require("electron");\n' + calls[world]; await writeFile(join(dir, 'preload.js'), source); const run = spawnSync(process.execPath, [cli, 'analyze-javascript-application', dir, '--format', 'json', '--max-heap-mb', '128', '--analysis-timeout-ms', '10000'], { encoding: 'utf8', timeout: 30000, maxBuffer: 8 * 1024 * 1024 }); if (run.error || run.status !== 0) throw new Error(run.stderr || run.error?.message || run.stdout); const result = JSON.parse(run.stdout).normalized_result; const nodes = result.graph.nodes.filter(n => n.kind === 'context-bridge-api'); console.log(JSON.stringify({ fixture: `${mode}-${world}`, count: result.summary.context_bridge_apis, properties: nodes.flatMap(n => n.observations.map(o => o.properties)), coverage: result.graph.coverage.status })); } } } finally { await rm(root, { recursive: true, force: true }); } JS ``` For the package comparison, use its installed `scripts/rea.mjs` as `cli` while leaving all four generated fixture contents unchanged. Both builds were exercised with those same contents. The generic source-build `analyze <directory> --format json` route was also checked on both bundled fixtures and produced the same zero/one split. ## Expected behavior The bundled isolated-world fixture should contribute one `context-bridge-api` node and one summary API, preserving `world: "isolated"`, literal `world_id.value: 1001`, `api_key: "isolatedApi"`, and `members: ["ping"]`. The direct isolated-world fixture establishes the output contract for the valid three-argument call, while the bundled main-world fixture establishes applicability of module-factory substitution. Both main-world controls should retain their API key and `world: "main"`. ## Actual behavior All eight dedicated CLI calls exited zero. The source build returned zero bridge APIs and no bridge node for the bundled isolated-world fixture. Its bundled main-world control returned one node with `resolution_basis: "wrapper-substitution"`, and both direct controls returned one node with `resolution_basis: "direct-syntax"`. The direct isolated-world node retained the numeric world ID and the `ping` member. All four source analyses reported graph coverage `complete`, no truncation, and no parse failures. The generic source route reproduced the bundled split with both calls exiting zero. ```text Fixture Source bridge APIs Published package bridge APIs bundled-isolated 0 0 bundled-main 1 0 direct-isolated 1 1 direct-main 1 1 ``` The published package omits both bundled exposure forms. That comparison establishes an earlier wrapped-exposure limitation, not the source build's method-specific failure. Its direct isolated-world control still records literal world ID 1001. The result includes a general limitation that missing findings do not establish absence; coverage `complete` is not proof that no exposure exists. ## Evidence - Expected source: `docs/javascript-artifact-reconstruction.md:3-10` and `:299-315` describe the public inert-analysis workflow, literal module factories, and API-key/member reconstruction for both contextBridge exposure methods. `src/domain/javascript/electronStaticAnalysisBrowser.ts:165-210` records the isolated world's argument positions and output fields. The official [Electron contextBridge API](https://www.electronjs.org/docs/latest/api/context-bridge#contextbridgeexposeinisolatedworldworldid-apikey-api) requires an integer world ID and recommends 1000 or higher; 1001 and the boolean-valued literal API object satisfy that documented call shape. - Failure source: Observed dedicated and generic public CLI output for the inline fixtures. `src/domain/javascript/electronWrappedCallPaths.ts:487-491` excludes calls whose first argument is not a string literal, so the valid numeric isolated-world call is skipped. Its `:393-409` dispatch recognizes only the main-world exposure, and `:289-324` interprets two arguments and records a main world. These are source explanations for the single omitted-exposure result. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete The producer binding check independently recognized one valid literal bootstrap in each bundled fixture and resolved the callee to `electron.contextBridge.exposeInIsolatedWorld` or its main-world counterpart. Both exports passed `intactExport`, their arguments resolved to literal keys and the `ping` object member, and neither had conditions, unresolved conditions, resolver truncation, or unresolved bindings. The bootstrap and binding owners are `src/domain/javascript/javascriptBootstrap.ts:22-145`, `javascriptSymbolicBindings.ts:630-669` and `:818-882`; `javascriptStaticAnalysis.ts:84-111` invokes wrapped collection after direct syntax inspection. The fixture has stable bindings, no property overwrites, no table replacement, no unknown spreads, no conditional dispatch, and no early exits. Its observed failure therefore does not depend on an invalid world ID or an unresolved producer. ## Restoration check Analyze the same bundled isolated-world fixture through the dedicated and generic source CLI routes. Both must return exactly one bridge API whose observation retains the isolated world, literal world ID 1001, `isolatedApi` key, and `ping` member. Keep the bundled main-world and both direct controls passing with their existing world/key/member values, and preserve the analyzer's handling of overwritten or unresolved Electron identities. Increasing unrelated counts or declaring complete coverage without the omitted API node does not restore the contract. Implementation and regression-test design remain open. ## Additional context The related closed [alias-recovery issue #1656](https://github.com/morluto/rea/issues/1656) was filed by another author and describes variable aliases and main-world exposure rather than this valid numeric isolated-world module-factory call. It is reference-only. No matching own report or exact isolated-world report was found in the recorded issue/PR inventory or live read-only searches. An open wrapper-improvement PR was inspected as background; its description does not claim this isolated-world restoration, and its branch was not tested. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A bounded, documented static reconstruction result loses an API key and member for a valid literal module-factory exposure while comparable supported inputs succeed. This causes incomplete application-graph evidence for affected artifacts; no application execution failure or wider impact was observed. - Workaround status: unverified - Workaround basis: Neither tested public source CLI route recovers the omitted node from the same bundled artifact. Direct namespace analysis works on a different artifact representation, but availability of equivalent original source for an affected caller was not established. No practical same-artifact graph recovery workaround was verified.
bsakaguchi 2026-10-15 18:27:40 +00:00
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#720
No description provided.