import { z } from "zod"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { evidenceSchema } from "../evidence.js"; import { javascriptRuntimeUnresolvedLocationSchema } from "./javascriptRuntimeObservation.js"; import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js"; import { digestSchema } from "../digests.js"; import { prefixedDigestSchema } from "../digests.js"; import { urlDeclaresQueryOrFragment } from "../urlComponents.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const nodeIdSchema = prefixedDigestSchema("jag_node"); const boundedTextSchema = z.string().min(1); const safePrefixSchema = z .string() .refine( (value) => value === "" || (!value.startsWith("/") && !value.includes("\\") && value.split("/").every((part) => !["", ".", ".."].includes(part))), "Artifact prefix must be empty or a safe relative POSIX path", ); const runtimeFileMappingSchema = z.strictObject({ kind: z.literal("file-root"), root: z.string().min(1), artifact_prefix: safePrefixSchema.default(""), }); const runtimeUrlMappingSchema = z.strictObject({ kind: z.literal("url-prefix"), prefix: z .string() .min(1) .refine( (value) => normalizeRuntimeUrlPrefix(value) !== null, "Runtime URL prefix must be HTTP(S) without credentials, query, or fragment", ) .overwrite((value) => normalizeRuntimeUrlPrefix(value) ?? value), artifact_prefix: safePrefixSchema.default(""), }); const normalizeRuntimeUrlPrefix = (value: string): string | null => { let url: URL; try { url = new URL(value); } catch (cause: unknown) { // Invalid input is represented by the null return. void cause; return null; } if ( !["http:", "https:"].includes(url.protocol) || url.username !== "" || url.password !== "" || urlDeclaresQueryOrFragment(url) ) return null; if (!url.pathname.endsWith("/")) url.pathname += "/"; return url.href; }; /** Caller-declared translation from an authorized runtime location to a layer. */ const runtimeLocationMappingSchema = z.discriminatedUnion("kind", [ runtimeFileMappingSchema, runtimeUrlMappingSchema, ]); type RuntimeLocationMapping = z.infer; const staticLayerSchema = z.strictObject({ role: z.enum(["application", "cache", "assets"]).default("application"), analysis: evidenceSchema, runtime_mappings: z .array(runtimeLocationMappingSchema) .overwrite(normalizeRuntimeMappings) .default([]), }); function normalizeRuntimeMappings( mappings: RuntimeLocationMapping[], ): RuntimeLocationMapping[] { return [ ...new Map( mappings.map((mapping) => [runtimeMappingKey(mapping), mapping]), ).values(), ].sort((left, right) => compareUnicodeCodePoints(runtimeMappingKey(left), runtimeMappingKey(right)), ); } function runtimeMappingKey(mapping: RuntimeLocationMapping): string { return mapping.kind === "file-root" ? `file-root\0${mapping.root}\0${mapping.artifact_prefix}` : `url-prefix\0${mapping.prefix}\0${mapping.artifact_prefix}`; } /** Evidence-backed static layers and passive captures to reconcile. */ export const reconcileJavaScriptRuntimeInputSchema = z .strictObject({ static_layers: z.array(staticLayerSchema).min(1), runtime_observations: z.array(evidenceSchema).min(1), }) .superRefine((input, context) => { if ( input.static_layers.filter(({ role }) => role === "application") .length !== 1 ) context.addIssue({ code: "custom", path: ["static_layers"], message: "Exactly one application static layer is required", }); const staticIds = input.static_layers.map( ({ analysis }) => analysis.evidence_id, ); if (new Set(staticIds).size !== staticIds.length) context.addIssue({ code: "custom", path: ["static_layers"], message: "Static layer Evidence must be unique", }); const runtimeIds = input.runtime_observations.map( ({ evidence_id: id }) => id, ); if (new Set(runtimeIds).size !== runtimeIds.length) context.addIssue({ code: "custom", path: ["runtime_observations"], message: "Runtime observation Evidence must be unique", }); }); const layerSummarySchema = z.strictObject({ layer_id: prefixedDigestSchema("jrl"), role: z.enum(["application", "cache", "assets"]), evidence_id: evidenceIdSchema, graph_id: prefixedDigestSchema("jag"), root_artifact_sha256: digestSchema, input_path: z.string().min(1), format: z.enum(["asar", "directory"]), runtime_mappings: z.array(runtimeLocationMappingSchema), }); const captureSummarySchema = z.strictObject({ evidence_id: evidenceIdSchema, capture_sha256: digestSchema, kind: z.enum(["browser", "electron", "v8-inspector", "electron-active"]), target_node_id: nodeIdSchema, target_key: boundedTextSchema, target_location: z .string() .min(1) .or(javascriptRuntimeUnresolvedLocationSchema), frames: z.number().int().min(0), scripts: z.number().int().min(0), workers: z.number().int().min(0), scripts_complete_within_scope: z.boolean(), }); const reconciliationReasonSchema = z.enum([ "content-and-location-match", "unique-content-match", "unique-location-match", "ambiguous-static-candidates", "captured-content-disagrees-with-static-location", "no-static-content-match", "no-authorized-location-mapping", "runtime-location-unresolved", "runtime-frame-unattributed", "static-or-runtime-coverage-incomplete", "reconciliation-limit-reached", ]); const reconciliationItemShape = { reconciliation_id: prefixedDigestSchema("jrr_item"), entity_kind: z.enum(["target", "frame", "script", "worker"]), runtime_evidence_id: evidenceIdSchema, runtime_node_id: nodeIdSchema, basis: z.enum([ "content-and-location", "content-sha256", "module-source-sha256", "artifact-path", "operator-file-mapping", "operator-url-mapping", "none", ]), confidence: z.enum(["exact", "high", "medium", "low", "unknown"]), reason: reconciliationReasonSchema, candidate_static_count: z.number().int().min(0), candidate_static_nodes: z.array( z.strictObject({ static_layer_id: prefixedDigestSchema("jrl"), static_node_id: nodeIdSchema, }), ), }; const reconciliationItemSchema = z.union([ z.strictObject({ ...reconciliationItemShape, static_layer_id: prefixedDigestSchema("jrl"), static_node_id: nodeIdSchema, status: z.literal("matched"), }), z.strictObject({ ...reconciliationItemShape, static_layer_id: z.null(), static_node_id: z.null(), status: z.enum(["ambiguous", "unmatched", "unknown"]), }), ]); const staticLoadStateSchema = z.strictObject({ static_layer_id: prefixedDigestSchema("jrl"), static_node_id: nodeIdSchema, kind: z.enum(["javascript-asset", "javascript-chunk", "javascript-module"]), status: z.enum([ "loaded", "resident-in-loaded-asset", "not-observed-in-capture", "unknown", ]), runtime_node_ids: z.array(nodeIdSchema), reason: z.enum([ "runtime-script-correspondence", "containing-asset-was-loaded", "not-observed-in-bounded-capture", "layer-outside-runtime-scope", "static-or-runtime-coverage-incomplete", ]), }); /** Deterministic static/passive-runtime reconciliation with a combined JAG. */ export const javascriptRuntimeReconciliationResultSchema = z.strictObject({ reconciliation_id: prefixedDigestSchema("jrr"), static_layers: z.array(layerSummarySchema).min(1), runtime_captures: z.array(captureSummarySchema).min(1), graph: javascriptApplicationGraphSchema, summary: z.strictObject({ runtime_targets: z.number().int().min(0), runtime_frames: z.number().int().min(0), runtime_scripts: z.number().int().min(0), runtime_workers: z.number().int().min(0), matched: z.number().int().min(0), ambiguous: z.number().int().min(0), unmatched: z.number().int().min(0), unknown: z.number().int().min(0), static_loaded: z.number().int().min(0), static_resident: z.number().int().min(0), static_not_observed: z.number().int().min(0), static_unknown: z.number().int().min(0), }), reconciliations: z.array(reconciliationItemSchema), static_load_states: z.array(staticLoadStateSchema), source_map_authority: z.strictObject({ used_for_primary_matching: z.literal(false), static_layer_count: z.number().int().min(0), runtime_script_declarations: z.number().int().min(0), limitation: boundedTextSchema, }), coverage: z.strictObject({ status: z.enum(["complete-within-inputs", "partial", "truncated"]), truncated: z.boolean(), omitted_runtime_entities: z.number().int().min(0), omitted_reconciliation_items: z.number().int().min(0), omitted_static_load_states: z.number().int().min(0), omitted_graph_items: z.number().int().min(0), }), evidence_links: z.array(evidenceIdSchema).min(2), limitations: z.array(boundedTextSchema), }); export type ReconcileJavaScriptRuntimeInput = z.infer< typeof reconcileJavaScriptRuntimeInputSchema >; export type JavaScriptRuntimeReconciliationResult = z.infer< typeof javascriptRuntimeReconciliationResultSchema >; export type JavaScriptRuntimeReconciliationItem = z.infer< typeof reconciliationItemSchema >; export type JavaScriptStaticLoadState = z.infer;