fix(browser): isolate script URLs with empty query or fragment (#2074)

This commit is contained in:
xin zhuang authored and GitHub committed 2026-10-12 14:56:51 +08:00
1 parent 98d8470d36
commit 9b780c47bc
3 files changed
+46 -2

No files matched your search

+3
View File
@@ -73,6 +73,9 @@ describe("captured script path projection", () => {
it.each([
"",
"https://a.test/main.js?v=2",
"https://a.test/main.js?",
"https://a.test/main.js#",
"https://a.test/main.js?#",
"https://a.test/main.js#variant",
"data:text/javascript,export{}",
"https://a.test/%2fescape.js",
+3 -2
View File
@@ -94,8 +94,9 @@ const candidatePath = (script: CapturedWebScript): CandidatePath => {
return {
path: `origins/${hash(url.origin)}/${parts.join("/")}`,
reason:
url.search !== "" ||
url.hash !== "" ||
// URL.search/hash collapse absent and explicitly empty components.
url.href.includes("?") ||
url.href.includes("#") ||
url.username !== "" ||
url.password !== ""
? "URL query, fragment, or credentials cannot select a canonical local module."
@@ -74,6 +74,46 @@ const setup = async (capture: unknown = scriptCaptureEvidenceFixture()) => {
return { root, input, resources };
};
describe("empty URL component publication", () => {
it("isolates empty query and fragment identities while retaining durable script bytes", async () => {
const scripts = [
{
url: "https://fixture.test/ordinary.js",
bytes: Buffer.from("export const ordinary = 1;"),
},
{
url: "https://fixture.test/query.js?",
bytes: Buffer.from("export const query = 2;"),
},
{
url: "https://fixture.test/fragment.js#",
bytes: Buffer.from("export const fragment = 3;"),
},
];
const { input, resources } = await setup(scriptScenarioFixture(scripts));
const exported = await exportWebScripts(input, resources);
if (!exported.ok) throw exported.error;
const result = webScriptExportResultSchema.parse(
exported.value.normalized_result,
);
if (result.analysis_input === null)
throw new Error("Missing analysis input");
for (const [index, script] of result.scripts.entries()) {
if (script.content.state !== "exported")
throw new Error("Missing source bytes");
expect(script.url).toBe(scripts[index]?.url);
expect(script.content.layout).toBe(index === 0 ? "url-path" : "isolated");
expect(
await readFile(
join(result.analysis_input.input_path, script.content.relative_path),
),
).toEqual(scripts[index]?.bytes);
}
const manifest = JSON.parse(await readFile(result.manifest.path, "utf8"));
expect(manifest.scripts).toEqual(result.scripts);
});
});
describe("captured script publication boundary", () => {
it("verifies durable bytes, manifest, capture identity, and existing module analysis", async () => {
const capture = scriptScenarioFixture([