Files
reverse-engineering-anything/src/application/WebSourceLocationService.ts
T
morluto cb4e5cfbff feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)
* refactor(analysis): centralize resource budgets and recovery facts

* feat(analysis): expose ASAR and Ghidra runtime admission controls

* feat(artifacts): expose reader admission and preserve limited recognition

* feat(javascript): expose recoverable semantic precision budgets

* feat(web): add recoverable source-map budgets and compact retained views

* feat(web): make historical capture decoding budgets recoverable

* fix(desktop): enforce selected view policies across capture and export

* feat(runtime): make Frida and LLDB retention budgets selectable

* feat(native): expose diagnostic budgets and preserve exhaustion causes

* feat(managed): expose recoverable snapshot and representation budgets

* feat(metadata): make Go and Rust inspection budgets recoverable

* feat(wasm): expose representation and producer admissions

* feat(evm): expose per-call worker resource budgets

* feat(android): make resource and string-pool omissions recoverable

* feat(producers): expose command and recorded-capture budgets

* docs(analysis): record recovery contracts and integrated validation

* fix(docs): link calibration oracle from published verification pages

* fix(docs): restore desktop task anchors and receipt links

* test(analysis): align fixtures with selectable budget contracts

* fix(analysis): remove source-location projector dependency cycle

* test(analysis): refresh budget recovery and verifier regressions

* test(analysis): preserve format and publication recovery expectations

* test(windows): align conformance with owned runtime admission

* fix(desktop): preserve selected alias kinds in exported state

* test(analysis): align provider fixtures with recoverable budgets

* test(desktop): validate emitted alias metadata structurally

* fix(ghidra): classify bounded Windows endpoint refusals

* test(cli): assert selectable artifact admission recovery

* fix(cli): preserve capture provenance in environment builds
2026-11-10 05:11:20 -05:00

337 lines
13 KiB
TypeScript

import {
selectedSourceMapLimits,
type SourceMapLimits,
} from "../domain/sourceMapLimits.js";
import { WEB_SOURCE_LOCATION_PROVIDER } from "../domain/webTraceProviders.js";
import { closeOwnedWebResources } from "./closeOwnedWebResources.js";
import { createHash } from "node:crypto";
import { z } from "zod";
import {
primaryAnalysisError,
type AnalysisError,
} from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { err, ok, type Result } from "../domain/result.js";
import {
selectedWebScriptArtifactsSchema,
webArtifactFileSchema,
} from "../domain/webScriptArtifacts.js";
import {
DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
webSourceLocationInputSchema,
webSourceLocationResultSchema,
webSourceMapReportSchema,
type WebSourceLocationInput,
type WebSourceMapReport,
} from "../domain/webSourceLocation.js";
import type { WebTraceObservation } from "../domain/webTraceObservation.js";
import { resolveWebSourcePosition } from "../domain/webSourcePosition.js";
import type { ExecutionOptions } from "./AnalysisProvider.js";
import {
capturedWebManifestIdentity,
validateSelectedWebScript,
} from "./ValidateSelectedWebScript.js";
import type {
WebSourceLocationArtifactPort,
WebSourceMapPort,
} from "./WebSourceLocationPorts.js";
import { completeProviderEvidence } from "./ProviderOperationExecution.js";
import { parseAnalysisInput } from "./AnalysisInput.js";
const OPERATION = "trace_web_source_location";
const artifactsSchema = selectedWebScriptArtifactsSchema.extend({
sourceMap: z.strictObject({
file: webArtifactFileSchema,
url: z.string().min(1),
text: z.string(),
}),
});
const LIMITATIONS = [
"The source/map pairing and source-map URL context are caller-selected. Deployment authenticity and execution remain unknown.",
"Capture metadata and its Evidence reference are historical context; this trace independently verifies only the selected manifest, script and map bytes.",
"Source names and sourceRoot are reported declarations. REA derives resolved URLs using WHATWG URL resolution; the pinned upstream codec decodes mappings. No original source is fetched or executed.",
"Embedded source hashes identify the UTF-8 encoding of decoded sourcesContent text, not an independently retained original file. Missing or out-of-range original content positions remain explicit.",
"Lookup returns all segments at the greatest generated position not greater than the selected point, including earlier lines and generated-only segments. A mapped point does not establish an executed range or UI causality.",
"Regular maps and inline indexed sections are supported. External section URLs are unsupported; unknown extensions are retained in raw map text and are not interpreted.",
];
const codecBudget = (timeoutMs: number, limits: SourceMapLimits): string =>
`Complete evidence has a ${String(limits.mapBytes)}-byte map, ${String(limits.outputBytes)}-byte reply and a ${String(limits.decodedRecords)}-record shared decoder budget for generated rows, mapping segments, leaf maps, source and name declarations, sourcesContent entries, and indexed sections. Generated rows and mapping segments have separate ${String(limits.decodedRows)} and ${String(limits.decodedSegments)} limits, and indexed maps have a ${String(limits.sectionDepth)}-level section budget. The owned codec has a ${String(timeoutMs)} ms decode_timeout_ms deadline`;
/** Bun accepts but ignores the codec's V8 heap flags; Windows enforces a commit limit instead. */
const codecBudgetLimitation = (
runtime: WebSourceMapReport["runtime"],
timeoutMs: number,
limits: SourceMapLimits,
): string => {
const budget = codecBudget(timeoutMs, limits);
if (runtime.id === "node-v8")
return `${budget}, ${String(limits.codecHeapMiB)} MiB old-generation heap and one V8 worker; cleanup may extend the deadline. V8 heap limits do not establish aggregate RSS limits.`;
return runtime.process_memory_limit_bytes === null
? `${budget}; cleanup may extend the deadline. The codec ran on Bun, which does not enforce REA's V8 heap flags, so no codec heap limit is established.`
: `${budget}; cleanup may extend the deadline. The codec ran on Bun, which does not enforce REA's V8 heap flags; a Windows Job Object limited its committed memory to ${String(runtime.process_memory_limit_bytes)} bytes, which is not a heap or RSS limit.`;
};
/** Join verified captured bytes with an explicitly selected source-map point. */
export class WebSourceLocationService {
constructor(
readonly artifacts: WebSourceLocationArtifactPort,
readonly decoder: WebSourceMapPort,
) {}
close(): Promise<void> {
return closeOwnedWebResources(
[
this.artifacts.close?.bind(this.artifacts),
this.decoder.close?.bind(this.decoder),
],
"Web source-location cleanup failed",
);
}
/** Shared CLI/MCP workflow preserves raw declarations and every equal-position match. */
async trace(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
const parsed = parseAnalysisInput(
OPERATION,
rawInput,
webSourceLocationInputSchema,
options?.signal,
);
if (!parsed.ok) return parsed;
const input = parsed.value;
const admitted = admitContext(input);
if (!admitted.ok) return admitted;
const loaded = await this.artifacts.load(input, options);
if (!loaded.ok) return loaded;
const verified = artifactsSchema.safeParse(loaded.value);
if (!verified.success)
return invalidOutput(
"Artifact port returned malformed source/map evidence.",
);
const data = verified.data;
const bound = validateSelectedWebScript(input, data, OPERATION);
if (!bound.ok) return bound;
const map = data.sourceMap;
const observation: WebTraceObservation = {
kind: "web-trace-observation",
analysis: "incomplete",
manifest: data.manifestFile,
source: data.sourceFile,
};
const incomplete = (error: AnalysisError): Result<never, AnalysisError> => {
const retained = webSourceMapReportSchema.safeParse(
error.partialObservation,
);
error.partialObservation = {
...observation,
...(retained.success
? {
source_map_observation: {
association:
observation.source_map !== undefined &&
matchesSelectedReport(input, map.file.sha256, retained.data)
? ("selected" as const)
: ("independent" as const),
report: retained.data,
},
}
: {}),
};
primaryAnalysisError(error).partialObservation = error.partialObservation;
return err(error);
};
const pairing = verifyPairing(input, data);
if (!pairing.ok) return incomplete(pairing.error);
observation.source_map = map.file;
const generated = admitGeneratedOffset(input, data.source);
if (!generated.ok) return incomplete(generated.error);
const generatedOffset = generated.value;
if (options?.signal?.aborted)
return incomplete(
new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }),
);
const traced = await this.decoder.trace(
{
text: map.text,
path: map.file.path,
url: map.url,
position: input.generated_position,
...(input.source_map_limits === undefined
? {}
: { limits: input.source_map_limits }),
...(input.decode_timeout_ms === undefined
? {}
: { decodeTimeoutMs: input.decode_timeout_ms }),
},
options,
);
if (!traced.ok) return incomplete(traced.error);
const report = verifyReport(input, map.file.sha256, traced.value);
if (!report.ok) return incomplete(report.error);
const decodeTimeoutMs =
input.decode_timeout_ms ?? DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS;
const limitations = [
...LIMITATIONS,
codecBudgetLimitation(
report.value.runtime,
decodeTimeoutMs,
selectedSourceMapLimits(input.source_map_limits),
),
];
const normalized = webSourceLocationResultSchema.safeParse({
...report.value,
manifest: capturedWebManifestIdentity(data),
source: {
...data.sourceFile,
script_index: input.script_index,
script: bound.value,
},
source_map: { ...map.file, url: map.url, association: "caller-selected" },
generated_offset: generatedOffset,
source_authenticity: "unknown",
execution: "unknown",
limitations,
});
if (!normalized.success)
return incomplete(
new AnalysisOutputError(OPERATION, normalized.error.message),
);
return completeProviderEvidence(
createEvidence(
{
path: data.sourceFile.path,
sha256: data.sourceFile.sha256,
format: "file",
},
WEB_SOURCE_LOCATION_PROVIDER,
{
operation: OPERATION,
parameters: input,
result: normalized.data,
rawResult: { source_map_text: map.text },
confidence: "derived",
authority: "historical-reference",
limitations,
locations: [
data.sourceFile.path,
data.manifestFile.path,
map.file.path,
].map((path) => ({ kind: "artifact-path", path })),
evidenceLinks:
data.manifest.source_evidence_id === null
? []
: [data.manifest.source_evidence_id],
},
),
options?.signal,
);
}
}
const invalidOutput = (message: string): Result<never, AnalysisError> =>
err(new AnalysisOutputError(OPERATION, message));
const invalid = (
path: readonly string[],
message: string,
): Result<never, AnalysisError> =>
err(
new AnalysisInputError(OPERATION, undefined, [
{ path, reason: "invalid_format", message },
]),
);
const admitGeneratedOffset = (
input: WebSourceLocationInput,
source: string,
): Result<number, AnalysisError> => {
const resolved = resolveWebSourcePosition(source, input.generated_position);
if (resolved === undefined)
return invalid(
["generated_position"],
"Selected generated position is outside the retained script text (one-based lines; zero-based UTF-16 columns).",
);
if ("offset" in resolved) return ok(resolved.offset);
// Valid coordinates past the retained script are a caller selection fault.
const { line, column } = input.generated_position;
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["generated_position", resolved.field],
reason: "out_of_range",
minimum: resolved.field === "line" ? 1 : 0,
maximum: resolved.maximum,
message:
resolved.field === "line"
? `No line ${String(line)}; the retained script ends at line ${String(resolved.maximum)} (one-based lines).`
: `No column ${String(column)} on line ${String(line)}; it ends at column ${String(resolved.maximum)} (zero-based UTF-16 columns).`,
},
]),
);
};
const admitContext = (
input: WebSourceLocationInput,
): Result<null, AnalysisError> => {
if (!URL.canParse(input.source_map.url))
return invalid(
["source_map", "url"],
"Expected an absolute source-map URL context. Select its deployment context explicitly; no URL is fetched.",
);
return ok(null);
};
const verifyPairing = (
input: WebSourceLocationInput,
data: z.output<typeof artifactsSchema>,
): Result<null, AnalysisError> => {
const map = data.sourceMap;
if (
map.file.path !== input.source_map.path ||
map.url !== input.source_map.url ||
Buffer.byteLength(map.text) !== map.file.bytes ||
createHash("sha256").update(map.text).digest("hex") !== map.file.sha256 ||
map.file.bytes > selectedSourceMapLimits(input.source_map_limits).mapBytes
)
return invalidOutput(
"Artifact port changed the selected source-map byte identity or URL context.",
);
return ok(null);
};
const verifyReport = (
input: WebSourceLocationInput,
sha256: string,
value: unknown,
): Result<WebSourceMapReport, AnalysisError> => {
const report = webSourceMapReportSchema.safeParse(value);
if (!report.success)
return invalidOutput(
"Codec port returned malformed source-map point evidence.",
);
if (!matchesSelectedReport(input, sha256, report.data))
return err(
new AnalysisOutputError(
OPERATION,
"Codec port changed the selected map, URL context or generated point.",
{ partialObservation: report.data },
),
);
return ok(report.data);
};
const matchesSelectedReport = (
input: WebSourceLocationInput,
sha256: string,
report: WebSourceMapReport,
): boolean =>
report.source_map_sha256 === sha256 &&
report.url_context === input.source_map.url &&
report.lookup.requested.line === input.generated_position.line &&
report.lookup.requested.column === input.generated_position.column;