Files
reverse-engineering-anything/tests/boundary/filesystem/javascript-applications/electronProtocolBoundaries.test.ts
T
Abdelrahman Teimaandgensokyo-dev 47fa6ab379 feat(electron): report custom URL protocol registrations
Expose static Electron protocol call facts through the shared application graph and existing CLI/MCP Evidence views. Preserve canonical case-insensitive scheme identity, original spellings, exact source provenance and explicit unknowns; keep registration success and runtime routing unproven.

Integration fixes also enforce module-only privileged API ownership, reuse the shared finding-key owner, and align the owning type documentation with the Evidence semantics. Validation and bounded real Obsidian CLI/MCP measurements are recorded in the PR.

Co-authored-by: UnknownHacker1 <35694178+UnknownHacker1@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-31 02:23:47 +08:00

260 lines
8.5 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
import {
analyzeJavaScriptApplication,
reconstructJavaScriptArtifact,
} from "../../../support/javascriptApplicationScope.js";
import { parseEvidence } from "../../../../src/domain/evidence.js";
import { parseJavaScriptApplicationGraph } from "../../../../src/domain/javascript/javascriptApplicationGraph.js";
import { traceApplicationFeature } from "../../../../src/domain/javascript/javascriptFeatureTrace.js";
import { applicationObligationPolicy } from "../../../../src/application/ReconstructionObligationPolicies.js";
const MAIN = String.raw`
const { app, protocol } = require("electron");
protocol.registerSchemesAsPrivileged([
{ scheme: "app", privileges: { standard: true, secure: true } },
]);
app.setAsDefaultProtocolClient("notes");
app.whenReady().then(() => {
protocol.handle("app", (request) => fetch(request.url));
protocol.handle(config.scheme, (request) => fetch(request.url));
});
`;
const protocolFixture = async (): Promise<string> => {
const root = await createTestTempDirectory("rea-electron-protocols-");
await Promise.all([
writeFile(
join(root, "package.json"),
JSON.stringify({ name: "rea-electron-protocols", main: "main.cjs" }),
),
writeFile(join(root, "main.cjs"), MAIN),
]);
return root;
};
describe("Electron URL scheme boundaries", () => {
it("joins case-insensitive scheme identities while preserving declared spellings", async () => {
const root = await createTestTempDirectory("rea-electron-protocol-case-");
await Promise.all([
writeFile(
join(root, "package.json"),
JSON.stringify({ name: "protocol-case", main: "main.cjs" }),
),
writeFile(
join(root, "main.cjs"),
[
'const { protocol } = require("electron");',
'protocol.registerSchemesAsPrivileged([{ scheme: "App", privileges: { secure: true } }]);',
'protocol.handle("app", () => null);',
].join("\n"),
),
]);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const schemes = parseJavaScriptApplicationGraph(result.graph).nodes.filter(
({ kind }) => kind === "url-scheme",
);
expect(schemes).toHaveLength(1);
expect(
schemes[0]?.observations.map(({ properties }) => properties),
).toEqual(
expect.arrayContaining([
expect.objectContaining({
scheme: "app",
scheme_literal: "App",
role: "privileged-scheme",
}),
expect.objectContaining({
scheme: "app",
scheme_literal: "app",
role: "handler",
}),
]),
);
});
it("projects each registered scheme into the application graph", async () => {
const result = await reconstructJavaScriptArtifact({
input_path: await protocolFixture(),
});
const graph = parseJavaScriptApplicationGraph(result.graph);
const schemes = graph.nodes.filter(({ kind }) => kind === "url-scheme");
const byLabel = new Map(
schemes.map((node) => [node.observations[0]?.label, node]),
);
expect([...byLabel.keys()].toSorted()).toEqual([
"app",
"config.scheme",
"notes",
]);
// The privileged registration and the handler describe one entry point.
expect(
byLabel
.get("app")
?.observations.map(({ properties }) => properties)
.toSorted((left, right) =>
String(left.role).localeCompare(String(right.role)),
),
).toMatchObject([
{ api: "handle", role: "handler", scheme: "app", resolution: "literal" },
{
api: "registerSchemesAsPrivileged",
role: "privileged-scheme",
enabled_privileges: ["secure", "standard"],
},
]);
expect(byLabel.get("notes")?.observations[0]?.properties).toMatchObject({
api: "setAsDefaultProtocolClient",
role: "os-default-client",
});
expect(
byLabel.get("config.scheme")?.observations[0]?.properties,
).toMatchObject({ scheme: null, resolution: "dynamic" });
const relations = graph.edges
.filter(({ target_node_id: target }) =>
schemes.some(({ node_id: id }) => id === target),
)
.map(({ relation, properties }) => [relation, properties.role])
.toSorted((left, right) =>
String(left[1]).localeCompare(String(right[1])),
);
expect(relations).toEqual([
["handles", "handler"],
["handles", "handler"],
["handles", "os-default-client"],
["exposes", "privileged-scheme"],
]);
});
it("counts every registration in the application summary", async () => {
const result = await analyzeJavaScriptApplication({
input_path: await protocolFixture(),
});
if (!result.ok) throw result.error;
expect(parseEvidence(result.value)).toMatchObject({
normalized_result: { summary: { protocol_registrations: 4 } },
});
});
it("keeps non-string and malformed literal schemes as observed, not dynamic", async () => {
const root = await createTestTempDirectory(
"rea-electron-protocol-literal-",
);
await Promise.all([
writeFile(
join(root, "package.json"),
JSON.stringify({
name: "rea-electron-protocol-literal",
main: "main.cjs",
}),
),
writeFile(
join(root, "main.cjs"),
[
'const { protocol } = require("electron");',
"protocol.handle(42, () => null);",
'protocol.handle("not a scheme", () => null);',
].join("\n"),
),
]);
const result = await reconstructJavaScriptArtifact({ input_path: root });
const observations = parseJavaScriptApplicationGraph(result.graph)
.nodes.filter(({ kind }) => kind === "url-scheme")
.map(({ observations: [first] }) => first)
.toSorted((left, right) =>
String(left?.label).localeCompare(String(right?.label)),
);
expect(observations).toMatchObject([
{
label: "42",
properties: {
scheme: null,
scheme_literal: 42,
resolution: "invalid-literal",
},
},
{
label: "not a scheme",
properties: {
scheme: null,
scheme_literal: "not a scheme",
resolution: "invalid-literal",
},
},
]);
});
});
describe("Electron URL scheme graph consumers", () => {
it("ends feature traces at schemes and gives them a reconstruction policy", async () => {
const result = await reconstructJavaScriptArtifact({
input_path: await protocolFixture(),
});
const graph = parseJavaScriptApplicationGraph(result.graph);
const schemeIds = new Set(
graph.nodes
.filter(({ kind }) => kind === "url-scheme")
.map(({ node_id: id }) => id),
);
const registering = graph.edges.find(({ target_node_id: target }) =>
schemeIds.has(target),
)?.source_node_id;
const trace = traceApplicationFeature({
sourceEvidenceId: `ev_${"0".repeat(64)}`,
graph,
nativeEvidence: [],
direction: "outgoing",
seed: {
kind: "node-id",
value: registering ?? "",
match: "exact",
case_sensitive: true,
},
});
expect(
new Set(
trace.paths
.filter(({ end_kind: kind }) => kind === "url-scheme")
.map(({ end_node_id: id }) => id),
),
).toEqual(schemeIds);
expect(applicationObligationPolicy("url-scheme")).toMatchObject({
applicationLayer: "electron",
family: "url-scheme",
fixtureAuthority: "packaged-process",
});
});
it("keeps dynamic schemes in identical files as separate occurrences", async () => {
const root = await createTestTempDirectory("rea-electron-protocol-copies-");
const source = [
'const { protocol } = require("electron");',
"protocol.handle(config.scheme, () => null);",
].join("\n");
// Byte-identical files share a digest but are separate module occurrences.
await Promise.all([
writeFile(
join(root, "package.json"),
JSON.stringify({
name: "rea-electron-protocol-copies",
main: "main.cjs",
}),
),
writeFile(join(root, "main.cjs"), source),
writeFile(join(root, "copy.cjs"), source),
]);
const result = await reconstructJavaScriptArtifact({ input_path: root });
expect(
parseJavaScriptApplicationGraph(result.graph).nodes.filter(
({ kind }) => kind === "url-scheme",
),
).toHaveLength(2);
});
});