fix(browser): retain bundle default origin scope (#4413)

* fix(browser): retain bundle default origin scope

* test(browser): cover bundle URLs without origins

* fix(browser): retain validated bundle target origin

* fix(browser): validate recorded origins canonically

---------

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-23 02:29:29 +08:00
1 parent 7e8ec0e456
commit b5c47f5255
7 files changed
+173 -10

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
import { describe, expect, it } from "vitest";
import { createBrowserEvidence } from "./BrowserEvidence.js";
import {
analyzeWebBundleInputSchema,
webBundleAnalysisSchema,
} from "../domain/webBundleAnalysis.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
const result = webBundleAnalysisSchema.parse({
capture: {
target_url: "https://app.example.test/dashboard?token=redacted",
target_origin: "https://app.example.test",
scripts_observed: 0,
scripts_analyzed: 0,
source_artifacts: [],
},
observations: {
chunks: { nodes: [], edges: [] },
routes: [],
endpoints: [],
webmcp_declarations: [],
source_maps: {
status: "not_requested",
requested: 0,
processed: 0,
items: [],
},
},
inferences: [],
unknowns: [],
completeness: {
status: "complete",
parsed_scripts: 0,
parse_failures: 0,
visited_ast_nodes: 0,
},
limitations: [],
});
const input = (allowedOrigins: string[]) =>
analyzeWebBundleInputSchema.parse({
cdp_endpoint: "http://127.0.0.1:9222",
target_id: "page-1",
observation_ms: 0,
allowed_origins: allowedOrigins,
});
describe("web bundle Evidence origin scope", () => {
it("records the captured target origin when the request uses the default scope", () => {
const evidence = createBrowserEvidence(
"analyze_web_bundle",
input([]),
result,
provider,
);
expect(evidence.parameters.allowed_origins).toEqual([
"https://app.example.test",
]);
});
it("preserves an explicit caller origin scope", () => {
const evidence = createBrowserEvidence(
"analyze_web_bundle",
input(["https://explicit.example.test"]),
result,
provider,
);
expect(evidence.parameters.allowed_origins).toEqual([
"https://explicit.example.test",
]);
});
it("does not infer an origin from a URL without one", () => {
const evidence = createBrowserEvidence(
"analyze_web_bundle",
input([]),
webBundleAnalysisSchema.parse({
...result,
capture: {
...result.capture,
target_url: "file:///tmp/app.html",
target_origin: null,
},
}),
provider,
);
expect(evidence.parameters.allowed_origins).toEqual([]);
});
it("does not infer a non-HTTP origin from a bundle URL", () => {
const evidence = createBrowserEvidence(
"analyze_web_bundle",
input([]),
webBundleAnalysisSchema.parse({
...result,
capture: {
...result.capture,
target_url: "ftp://host/app",
target_origin: null,
},
}),
provider,
);
expect(evidence.parameters.allowed_origins).toEqual([]);
});
});
+2
View File
@@ -198,6 +198,8 @@ const browserResultTargetOrigin = (
typeof result.inspection.target.origin === "string"
)
return result.inspection.target.origin;
if ("capture" in result && "target_origin" in result.capture)
return result.capture.target_origin ?? undefined;
if (!("target" in result)) return undefined;
if ("origin" in result.target) return result.target.origin;
if ("initial_url" in result.target)
+15
View File
@@ -4,6 +4,7 @@ import {
browserEndpointSchema,
browserObservationEndpointSchema,
browserOriginSchema,
recordedBrowserOriginSchema,
recordedBrowserObservationEndpointSchema,
sanitizeEndpointCandidate,
sanitizeBrowserUrl,
@@ -29,6 +30,20 @@ describe("browser observation contracts", () => {
expect(browserOriginSchema.safeParse(value).success, value).toBe(false);
});
it("accepts only canonical recorded HTTP origins", () => {
expect(recordedBrowserOriginSchema.parse("https://app.example.test")).toBe(
"https://app.example.test",
);
for (const value of [
"https://%2A.example.com",
"https://*.example.com",
"https://Example.COM:443",
])
expect(recordedBrowserOriginSchema.safeParse(value).success, value).toBe(
false,
);
});
it("accepts only literal loopback HTTP CDP endpoints", () => {
expect(browserEndpointSchema.parse("http://127.0.0.1:9222")).toBe(
"http://127.0.0.1:9222",
+12
View File
@@ -44,6 +44,18 @@ export const browserOriginSchema = z
return origin;
});
/** Already-normalized HTTP(S) origin retained in provider output. */
export const recordedBrowserOriginSchema = z
.string()
.superRefine((value, context) => {
const parsed = browserOriginSchema.safeParse(value);
if (parsed.success && parsed.data === value) return;
context.addIssue({
code: "custom",
message: "Expected one canonical HTTP(S) origin",
});
});
/** Recognize the URL API's bracketed IPv6 form and normalized bare literals. */
export const isLiteralLoopbackHostname = (hostname: string): boolean =>
hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
+7 -1
View File
@@ -9,7 +9,10 @@ import {
optionalBrowserDeadlineMs,
} from "./browserDeadline.js";
import { emptyArraySchema } from "./emptyArraySchema.js";
import { inspectWebPageWithSourceInputSchema } from "./browserObservation.js";
import {
inspectWebPageWithSourceInputSchema,
recordedBrowserOriginSchema,
} from "./browserObservation.js";
import {
webTextArtifactMetadataSchema,
webTextArtifactSchema,
@@ -366,6 +369,9 @@ export const webBundleAnalysisSchema = z.object({
route_inventory: javaScriptRouteInventorySchema.exactOptional(),
capture: z.object({
target_url: z.string(),
// Older saved observations omit this field; current analyses retain the
// validated HTTP(S) target origin or null when no browser origin exists.
target_origin: recordedBrowserOriginSchema.nullable().optional(),
scripts_observed: z.int().min(0),
scripts_analyzed: z.int().min(0),
source_artifacts: z.array(webTextArtifactSummarySchema),
+11
View File
@@ -1100,6 +1100,17 @@ describe("web bundle configuration and navigation parity", () => {
});
describe("web bundle artifact metadata", () => {
it("retains only a validated HTTP target origin", () => {
expect(
analyzeCapturedWebBundle(inspection("export {};")).capture.target_origin,
).toBe(origin);
const nonHttp = inspection("export {};");
nonHttp.target.url = "ftp://host/app";
nonHttp.target.origin = "ftp://host";
expect(analyzeCapturedWebBundle(nonHttp).capture.target_origin).toBeNull();
});
it("preserves long provider media types in artifact summaries", () => {
const result = analyzeCapturedWebBundle(inspection("export {};"));
const mediaType = `application/${"x".repeat(300)}`;
+14 -9
View File
@@ -2,6 +2,7 @@ import { buildJavaScriptRouteInventorySteps } from "./javascript/javascriptRoute
import { completeSemanticSteps } from "./javascript/javascriptSemanticTraversal.js";
import { RETURNED_REFERENCE_WIDENING_LIMITATION } from "./javascript/javascriptSemanticCoverage.js";
import type { WebPageInspection } from "./browserObservationSchemas.js";
import { browserOriginSchema } from "./browserObservation.js";
import {
webBundleAnalysisSchema,
type WebBundleAnalysis,
@@ -231,15 +232,19 @@ const buildCaptureObservation = (
inspection: WebPageInspection,
sourceScripts: IncludedScript[],
attemptedScripts: number,
) => ({
target_url: inspection.target.url,
scripts_observed: inspection.scripts.total,
// This preserves the historical meaning: source scripts submitted for
// analysis, including worker/deadline failures; explicitly unstarted suffixes
// are excluded. Successfully parsed scripts are counted separately below.
scripts_analyzed: attemptedScripts,
source_artifacts: sourceScripts.map(sourceArtifact),
});
) => {
const targetOrigin = browserOriginSchema.safeParse(inspection.target.origin);
return {
target_url: inspection.target.url,
target_origin: targetOrigin.success ? targetOrigin.data : null,
scripts_observed: inspection.scripts.total,
// This preserves the historical meaning: source scripts submitted for
// analysis, including worker/deadline failures; explicitly unstarted suffixes
// are excluded. Successfully parsed scripts are counted separately below.
scripts_analyzed: attemptedScripts,
source_artifacts: sourceScripts.map(sourceArtifact),
};
};
const sourceArtifact = (script: IncludedScript) => {
if (!script.source.included) throw new TypeError("Filtered source changed");