diff --git a/src/browser/modules/NativeModuleResolver.ts b/src/browser/modules/NativeModuleResolver.ts
index 4383a27e0c..3cc45301e4 100644
--- a/src/browser/modules/NativeModuleResolver.ts
+++ b/src/browser/modules/NativeModuleResolver.ts
@@ -33,6 +33,7 @@ import type {
} from "../../domain/webModuleTrace.js";
import { webModuleResolutionSchema } from "../../domain/webModuleTrace.js";
import { ProviderStartupDeadline } from "../../process/ProviderDeadline.js";
+import { importMapForTrustedResolver } from "./importMapForTrustedResolver.js";
const OPERATION = "trace_web_module_imports";
const TIMEOUT_MS = 20000;
@@ -367,7 +368,7 @@ const resolveInBrowser = async (
await route.fulfill({
status: 200,
contentType: "text/html",
- body: `${input.importMap === null ? "" : ``}`,
+ body: `${input.importMap === null ? "" : ``}`,
});
} else if (
!stubSent &&
diff --git a/src/browser/modules/importMapForTrustedResolver.test.ts b/src/browser/modules/importMapForTrustedResolver.test.ts
new file mode 100644
index 0000000000..c1ae0f6200
--- /dev/null
+++ b/src/browser/modules/importMapForTrustedResolver.test.ts
@@ -0,0 +1,98 @@
+import { describe, expect, it } from "vitest";
+import { importMapForTrustedResolver } from "./importMapForTrustedResolver.js";
+
+describe("importMapForTrustedResolver", () => {
+ const baseUrl = "https://app.test/index.html";
+ const importerUrl = "https://app.test/main.js";
+
+ it("omits integrity that covers the selected importer URL", () => {
+ const map = {
+ imports: { dep: "/dep.js" },
+ integrity: {
+ "https://app.test/main.js": "sha256-importer",
+ "https://app.test/dep.js": "sha256-dep",
+ },
+ };
+ expect(importMapForTrustedResolver(map, importerUrl, baseUrl)).toEqual({
+ imports: { dep: "/dep.js" },
+ integrity: { "https://app.test/dep.js": "sha256-dep" },
+ });
+ });
+
+ it("resolves relative integrity keys against the import-map base URL", () => {
+ const map = {
+ imports: { dep: "./dep.js" },
+ integrity: {
+ "./main.js": "sha256-importer",
+ "./dep.js": "sha256-dep",
+ },
+ };
+ expect(
+ importMapForTrustedResolver(map, "https://app.test/main.js", baseUrl),
+ ).toEqual({
+ imports: { dep: "./dep.js" },
+ integrity: { "./dep.js": "sha256-dep" },
+ });
+ });
+
+ it("matches importer integrity across query and fragment differences", () => {
+ const map = {
+ imports: { dep: "/dep.js" },
+ integrity: {
+ "https://app.test/main.js": "sha256-importer",
+ "https://app.test/dep.js": "sha256-dep",
+ },
+ };
+ expect(
+ importMapForTrustedResolver(
+ map,
+ "https://app.test/main.js?build=1#entry",
+ baseUrl,
+ ),
+ ).toEqual({
+ imports: { dep: "/dep.js" },
+ integrity: { "https://app.test/dep.js": "sha256-dep" },
+ });
+ });
+
+ it("drops an empty integrity member after removing the importer entry", () => {
+ const map = {
+ imports: { dep: "/dep.js" },
+ integrity: { "https://app.test/main.js": "sha256-importer" },
+ };
+ expect(importMapForTrustedResolver(map, importerUrl, baseUrl)).toEqual({
+ imports: { dep: "/dep.js" },
+ });
+ });
+
+ it("preserves maps without integrity and dependency-only integrity", () => {
+ const without = { imports: { dep: "/dep.js" } };
+ expect(importMapForTrustedResolver(without, importerUrl, baseUrl)).toBe(
+ without,
+ );
+ const dependencyOnly = {
+ imports: { dep: "/dep.js" },
+ integrity: { "https://app.test/dep.js": "sha256-dep" },
+ };
+ expect(
+ importMapForTrustedResolver(dependencyOnly, importerUrl, baseUrl),
+ ).toBe(dependencyOnly);
+ });
+
+ it("preserves a dependency integrity key named __proto__ without prototype pollution", () => {
+ const integrity = Object.assign(Object.create(null), {
+ "https://app.test/main.js": "sha256-importer",
+ "https://app.test/dep.js": "sha256-dep",
+ });
+ integrity["__proto__"] = "sha256-proto-dep";
+ const map = { imports: { dep: "/dep.js" }, integrity };
+ const next = importMapForTrustedResolver(map, importerUrl, baseUrl);
+ const nextIntegrity = next.integrity as { readonly [key: string]: string };
+ expect(nextIntegrity["https://app.test/dep.js"]).toBe("sha256-dep");
+ expect(
+ Object.getOwnPropertyDescriptor(nextIntegrity, "__proto__")?.value,
+ ).toBe("sha256-proto-dep");
+ expect(Object.getPrototypeOf(nextIntegrity)).toBe(null);
+ expect(Object.prototype.hasOwnProperty.call({}, "__proto__")).toBe(false);
+ });
+});
diff --git a/src/browser/modules/importMapForTrustedResolver.ts b/src/browser/modules/importMapForTrustedResolver.ts
new file mode 100644
index 0000000000..f4e254e2e8
--- /dev/null
+++ b/src/browser/modules/importMapForTrustedResolver.ts
@@ -0,0 +1,75 @@
+import type { JsonValue } from "../../domain/jsonValue.js";
+
+/**
+ * Build the import map installed for REA's trusted resolver stub.
+ *
+ * Chromium applies import-map `integrity` to the module script URL. The
+ * resolver fulfills that URL with REA's replacement bytes, so integrity that
+ * covers the selected importer must not be installed. Dependency integrity and
+ * the caller-selected map identity remain unchanged in Evidence.
+ */
+export const importMapForTrustedResolver = (
+ map: { readonly [key: string]: JsonValue },
+ importerUrl: string,
+ baseUrl: string,
+): { readonly [key: string]: JsonValue } => {
+ const integrity = map.integrity;
+ if (
+ integrity === null ||
+ typeof integrity !== "object" ||
+ Array.isArray(integrity)
+ )
+ return map;
+ const importer = importerModuleIdentity(importerUrl, baseUrl);
+ const nextIntegrity: { [key: string]: JsonValue } = Object.create(null);
+ let removed = false;
+ for (const [key, value] of Object.entries(integrity)) {
+ if (integrityKeyMatchesImporter(key, baseUrl, importer)) {
+ removed = true;
+ continue;
+ }
+ nextIntegrity[key] = value;
+ }
+ if (!removed) return map;
+ if (Object.keys(nextIntegrity).length === 0) {
+ const { integrity: _omit, ...rest } = map;
+ return rest;
+ }
+ return { ...map, integrity: nextIntegrity };
+};
+
+const importerModuleIdentity = (
+ importerUrl: string,
+ baseUrl: string,
+): { readonly hrefs: ReadonlySet; readonly moduleKey: string } => {
+ const absolute = new URL(importerUrl, baseUrl);
+ const withoutHash = new URL(absolute.href);
+ withoutHash.hash = "";
+ return {
+ hrefs: new Set([absolute.href, withoutHash.href]),
+ // Chromium's integrity lookup reports the module URL without search/hash.
+ moduleKey: `${absolute.origin}${absolute.pathname}`,
+ };
+};
+
+const integrityKeyMatchesImporter = (
+ key: string,
+ baseUrl: string,
+ importer: {
+ readonly hrefs: ReadonlySet;
+ readonly moduleKey: string;
+ },
+): boolean => {
+ try {
+ const resolved = new URL(key, baseUrl);
+ const withoutHash = new URL(resolved.href);
+ withoutHash.hash = "";
+ return (
+ importer.hrefs.has(resolved.href) ||
+ importer.hrefs.has(withoutHash.href) ||
+ `${resolved.origin}${resolved.pathname}` === importer.moduleKey
+ );
+ } catch {
+ return false;
+ }
+};