mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 18:22:42 +00:00
* 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
337 lines
13 KiB
TypeScript
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;
|