From 35e5508fd095a062d7477bd6bb7d2d8e28201cd8 Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 Date: Tue, 6 Oct 2026 22:00:14 -0700 Subject: [PATCH] fix(javascript): advertise the exact empty seed rule and match empty API keys Express the empty-seed rule as a JSON-Schema-representable union, so the advertised trace_application_feature schema rejects what the server rejects, and match context-bridge seeds on the exact api_key. Co-Authored-By: Claude Opus 5.5 --- ...JavaScriptApplicationEmptyLiterals.test.ts | 54 +++++++++++++------ src/domain/javascriptFeatureSeed.ts | 2 +- src/domain/javascriptFeatureTraceSchemas.ts | 50 +++++++++-------- 3 files changed, 64 insertions(+), 42 deletions(-) diff --git a/src/application/JavaScriptApplicationEmptyLiterals.test.ts b/src/application/JavaScriptApplicationEmptyLiterals.test.ts index 3390ed085..efb942d60 100644 --- a/src/application/JavaScriptApplicationEmptyLiterals.test.ts +++ b/src/application/JavaScriptApplicationEmptyLiterals.test.ts @@ -1,7 +1,9 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; +import { Ajv2020 } from "ajv/dist/2020.js"; import { expect, it } from "vitest"; +import { z } from "zod"; import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js"; import { javascriptApplicationAnalysisResultSchema } from "../domain/javascriptApplicationAnalysis.js"; @@ -181,23 +183,27 @@ it("selects an empty IPC channel with an exact empty seed", async () => { ]); }); -it.each([ - { kind: "channel", value: "" }, - { kind: "string", value: "", match: "exact" }, -])("accepts an exact empty seed: %j", (seed) => { - expect( - traceApplicationFeatureInputSchema.shape.seed.safeParse(seed).success, - ).toBe(true); -}); +const seedSchema = traceApplicationFeatureInputSchema.shape.seed; +const validateAdvertisedSeed = new Ajv2020({ + strict: false, + validateFormats: false, +}).compile(z.toJSONSchema(seedSchema, { io: "input" })); it.each([ - { kind: "string", value: "" }, - { kind: "channel", value: "", match: "contains" }, - { kind: "node-id", value: "", match: "exact" }, -])("rejects an empty seed that is not an exact literal: %j", (seed) => { - const parsed = traceApplicationFeatureInputSchema.shape.seed.safeParse(seed); - expect(parsed.success).toBe(false); -}); + [{ kind: "channel", value: "", match: "exact" }, true], + [{ kind: "string", value: "", match: "exact" }, true], + [{ kind: "channel", value: "/x" }, true], + [{ kind: "string", value: "" }, false], + [{ kind: "channel", value: "" }, false], + [{ kind: "channel", value: "", match: "contains" }, false], + [{ kind: "node-id", value: "", match: "exact" }, false], +] as const)( + "validates empty seed %j at runtime and in the advertised schema", + (seed, accepted) => { + expect(seedSchema.safeParse(seed).success).toBe(accepted); + expect(validateAdvertisedSeed(seed)).toBe(accepted); + }, +); it("pairs the same empty IPC channel across application versions", async () => { const left = await analyzeSource(IPC_SOURCE); @@ -225,3 +231,21 @@ it("pairs the same empty IPC channel across application versions", async () => { "ipc-channel", ); }); + +it("selects an empty context-bridge API with an exact empty seed", async () => { + const output = await analyzeSource( + "const { contextBridge } = require('electron'); contextBridge.exposeInMainWorld('', {}); contextBridge.exposeInMainWorld('\"\"', {});", + ); + const apis = new Map( + output.graph.nodes + .filter(({ kind }) => kind === "context-bridge-api") + .map((node) => [node.node_id, node.observations[0]?.properties.api_key]), + ); + const matches = findApplicationFeatureSeeds(output.graph.nodes, { + kind: "api", + value: "", + match: "exact", + case_sensitive: true, + }); + expect(matches.map(({ node_id: nodeId }) => apis.get(nodeId))).toEqual([""]); +}); diff --git a/src/domain/javascriptFeatureSeed.ts b/src/domain/javascriptFeatureSeed.ts index a860c2a96..819bb7577 100644 --- a/src/domain/javascriptFeatureSeed.ts +++ b/src/domain/javascriptFeatureSeed.ts @@ -132,7 +132,7 @@ const memberFieldSuffixes = new Set([ const relevantFields = { route: [".label", ".value", ".path"], - api: [".label", ".api_name", ".key", ".methods", ".members"], + api: [".label", ".api_name", ".api_key", ".key", ".methods", ".members"], channel: [".label", ".channel", ".key"], module: [ ".label", diff --git a/src/domain/javascriptFeatureTraceSchemas.ts b/src/domain/javascriptFeatureTraceSchemas.ts index 384e468d1..5ee879a7c 100644 --- a/src/domain/javascriptFeatureTraceSchemas.ts +++ b/src/domain/javascriptFeatureTraceSchemas.ts @@ -17,36 +17,34 @@ export const featureSeedMatchMode = (seed: { }): "exact" | "contains" => seed.match ?? (seed.kind === "string" ? "contains" : "exact"); +const literalSeedKinds = [ + "route", + "string", + "api", + "channel", + "module", + "native-export", +] as const; + /** - * Literal starting point for one application feature trace. An exact literal - * seed may be empty, such as the channel in `ipcMain.handle("")`; an empty - * containment seed would select every node, so it is rejected. + * Literal starting point for one application feature trace. An empty value, + * such as the channel in `ipcMain.handle("")`, must be an explicit exact + * literal seed: empty containment would select every node. */ -const applicationFeatureSeedSchema = z - .strictObject({ - kind: z.enum([ - "node-id", - "route", - "string", - "api", - "channel", - "module", - "native-export", - ]), - value: z.string(), +const applicationFeatureSeedSchema = z.union([ + z.strictObject({ + kind: z.enum(["node-id", ...literalSeedKinds]), + value: boundedTextSchema, match: z.enum(["exact", "contains"]).nullable().default(null), case_sensitive: z.boolean().default(false), - }) - .superRefine((seed, context) => { - if (seed.value !== "") return; - if (seed.kind === "node-id" || featureSeedMatchMode(seed) === "contains") - context.addIssue({ - code: "custom", - path: ["value"], - message: - 'An empty seed value requires an exact literal match: use match "exact" with a non-node-id kind', - }); - }); + }), + z.strictObject({ + kind: z.enum(literalSeedKinds), + value: z.literal(""), + match: z.literal("exact"), + case_sensitive: z.boolean().default(false), + }), +]); /** Evidence-backed application graph and feature seed. */ export const traceApplicationFeatureInputSchema = z