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 <noreply@anthropic.com>
This commit is contained in:
parent
6cc45e8dc9
commit
35e5508fd0
|
|
@ -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([""]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue