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:
7 files changed
+173
-10
No files matched your search
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)}`;
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in new issue
Block a user