mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
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>
260 lines
8.5 KiB
TypeScript
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);
|
|
});
|
|
});
|