feat(browser): trace captured website source-map locations (#897)

* refactor(browser): share captured-script acquisition boundaries

Reuse stable capture/source acquisition and identity validation for module
and source-map analysis. Preserve host I/O, malformed input, unavailable
source and digest mismatch diagnostics.

Refs #851

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* feat(browser): trace captured website source-map locations

Refs #851. Preserve selected capture/map byte identity, duplicate mappings and embedded source evidence through the pinned upstream codec.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* test(browser): verify source-map artifacts and synchronize generated contracts

Cover byte-preserving map reads and actionable filesystem constraints. Refresh generated public schemas and metadata on current main.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): preserve inert source-map URL context

The selected URL is local codec context and grants no transport authority. Preserve its reported text and verify normal/userinfo contexts through real public interfaces, alongside independent manifest byte identity.

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
This commit is contained in:
ysopwnable
2026-10-07 16:21:18 +08:00
committed by GitHub
parent 4801a24089
commit f430c416c7
58 changed files with 3420 additions and 282 deletions
@@ -0,0 +1,59 @@
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
import { traceSourceMap } from "./TraceSourceMap.js";
import { SourceMapFormatFailure } from "./SourceMapFormat.js";
import {
sourceMapCodecInputSchema,
sourceMapCodecReplySchema,
} from "./SourceMapCodecProtocol.js";
import { z } from "zod";
/** Dedicated process entrypoint invoked only for an explicitly selected map. */
const execute = async (): Promise<string> => {
try {
const path = process.argv[2];
if (path === undefined)
throw new SourceMapFormatFailure(
"format",
"Codec request path is missing.",
);
const data = await readStableArtifact(
path,
WEB_SOURCE_MAP_LIMITS.outputBytes,
);
const text = new TextDecoder("utf-8", { fatal: true }).decode(data.bytes);
const value: unknown = JSON.parse(text);
const input = sourceMapCodecInputSchema.parse(value);
if (Buffer.byteLength(input.text) > WEB_SOURCE_MAP_LIMITS.mapBytes)
throw new SourceMapFormatFailure(
"limit",
"Source map exceeds the 4 MiB codec input budget.",
);
const report = traceSourceMap(input.text, input.url, input.position);
const reply = JSON.stringify(
sourceMapCodecReplySchema.parse({ state: "success", report }),
);
if (Buffer.byteLength(reply) > WEB_SOURCE_MAP_LIMITS.outputBytes)
throw new SourceMapFormatFailure(
"limit",
"Complete source-map point evidence exceeds the 32 MiB output budget; no partial evidence was returned.",
);
return reply;
} catch (cause: unknown) {
if (
!(
cause instanceof SourceMapFormatFailure ||
cause instanceof z.ZodError ||
cause instanceof SyntaxError
)
)
throw cause;
return JSON.stringify({
state: "failure",
reason: cause instanceof SourceMapFormatFailure ? cause.reason : "format",
message: cause.message,
});
}
};
process.stdout.write(await execute());
@@ -0,0 +1,25 @@
import { z } from "zod";
import {
webSourceMapReportSchema,
webSourcePositionSchema,
} from "../../domain/webSourceLocation.js";
/** Trusted codec process input; captured application code is never evaluated. */
export const sourceMapCodecInputSchema = z.strictObject({
text: z.string(),
url: z.string().min(1),
position: webSourcePositionSchema,
});
/** Codec success or a specific producer/resource constraint. */
export const sourceMapCodecReplySchema = z.discriminatedUnion("state", [
z.strictObject({
state: z.literal("success"),
report: webSourceMapReportSchema,
}),
z.strictObject({
state: z.literal("failure"),
reason: z.enum(["format", "unsupported", "limit"]),
message: z.string(),
}),
]);
@@ -0,0 +1,242 @@
import { randomUUID } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
AnalysisTimeoutError,
} from "../../domain/analysisErrorCore.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { err, ok, type Result } from "../../domain/result.js";
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
import {
PrivateRuntimeRoot,
PrivateRuntimeRootUnavailableError,
} from "../../process/PrivateRuntimeRoot.js";
import { ProviderStartupDeadline } from "../../process/ProviderDeadline.js";
import { cleanupOwnedProcessGroup } from "../../process/ProcessOwnership.js";
import {
ProviderProcessSupervisor,
spawnOwnedProviderProcess,
type OwnedProviderProcessSpawnOptions,
type SpawnedOwnedProviderProcess,
} from "../../process/ProviderProcess.js";
const OPERATION = "trace_web_source_location";
const PROVIDER = "source-map-decoder";
/** Launcher seam retains the actual codec protocol and ownership supervisor. */
export type SourceMapCodecLauncher = (
options: OwnedProviderProcessSpawnOptions,
) => Promise<SpawnedOwnedProviderProcess>;
/** Runtime implementations establish privacy before returning an owned directory. */
export interface SourceMapCodecRuntime {
readonly path: string;
close(): Promise<void>;
}
/** Replaceable acquisition boundaries for an owned codec operation. */
export interface SourceMapDecoderDependencies {
readonly launcher?: SourceMapCodecLauncher;
readonly createRuntime?: () => Promise<SourceMapCodecRuntime>;
readonly environment?: Readonly<NodeJS.ProcessEnv>;
}
/** Run a trusted codec command and release its process/root before returning output. */
export const runSourceMapCommand = async (
request: string,
path: string,
options: ExecutionOptions | undefined,
dependencies: SourceMapDecoderDependencies,
): Promise<Result<string, AnalysisError>> => {
const deadline = new ProviderStartupDeadline(
WEB_SOURCE_MAP_LIMITS.decodeTimeoutMs,
options?.signal,
);
const command = new OwnedCodec(path, dependencies, deadline);
let result: Result<string, AnalysisError>;
try {
await command.start(request);
result = ok(await command.collect());
} catch (cause: unknown) {
result = err(command.failure(cause));
} finally {
deadline.dispose();
}
const cleanup = await command.close(result.ok ? null : result.error.message);
if (cleanup !== undefined) return err(cleanup);
return result.ok && options?.signal?.aborted
? err(new AnalysisCancelledError(OPERATION))
: result;
};
class OwnedCodec {
readonly #runId = `rea-source-map-${randomUUID()}`;
#runtime: SourceMapCodecRuntime | undefined;
#supervisor: ProviderProcessSupervisor | undefined;
#oversized = false;
#processError: string | undefined;
constructor(
readonly path: string,
readonly dependencies: SourceMapDecoderDependencies,
readonly deadline: ProviderStartupDeadline,
) {}
async start(request: string): Promise<void> {
this.check();
this.#runtime = await (
this.dependencies.createRuntime ??
(() => PrivateRuntimeRoot.create({ prefix: "rea-source-map-" }))
)();
this.check();
const requestPath = join(this.#runtime.path, "request.json");
await writeFile(requestPath, request, {
flag: "wx",
mode: 0o600,
signal: this.deadline.signal,
});
this.check();
const environment: NodeJS.ProcessEnv = {
...(this.dependencies.environment ?? process.env),
UV_THREADPOOL_SIZE: "1",
};
delete environment.NODE_OPTIONS;
const spawned = await (
this.dependencies.launcher ?? spawnOwnedProviderProcess
)({
command: process.execPath,
arguments: [
"--max-old-space-size=192",
"--max-semi-space-size=8",
"--v8-pool-size=1",
"--single-threaded",
"--stack_size=2048",
fileURLToPath(new URL("./SourceMapCodecProcess.js", import.meta.url)),
requestPath,
],
runId: this.#runId,
cwd: this.#runtime.path,
hostEnvironment: environment,
});
this.#supervisor = new ProviderProcessSupervisor(
{
...spawned,
ownsProcessLifetime: true,
cleanup:
spawned.cleanup ??
(() => cleanupOwnedProcessGroup(spawned.ownership)),
},
{
onDiagnostic: (event) => {
if (event.type === "error") this.#processError = event.message;
if (
event.type === "output" &&
event.totalBytes >
(event.stream === "stdout"
? WEB_SOURCE_MAP_LIMITS.outputBytes
: 1024 * 1024)
)
this.#oversized = true;
},
},
);
}
async collect(): Promise<string> {
const supervisor = this.#supervisor;
if (supervisor === undefined)
throw new Error("Codec process was not acquired");
while (!(await supervisor.waitForOutputClose(50))) this.check();
this.check();
const snapshot = supervisor.snapshot();
if (snapshot.exitCode !== 0 || snapshot.signal !== null)
throw new ProviderAdapterError(PROVIDER, OPERATION, {
diagnostics: {
map_path: this.path,
exit_code: snapshot.exitCode ?? null,
signal: snapshot.signal ?? null,
stderr: snapshot.stderr.text,
},
});
return snapshot.stdout.text;
}
check(): void {
if (this.deadline.interruption === "cancelled")
throw new AnalysisCancelledError(OPERATION);
if (this.deadline.interruption === "timeout")
throw new AnalysisTimeoutError(
OPERATION,
WEB_SOURCE_MAP_LIMITS.decodeTimeoutMs,
);
if (this.#oversized)
throw new AnalysisOutputError(
OPERATION,
"Codec output exceeds its complete-evidence or diagnostic budget.",
);
if (this.#processError !== undefined)
throw new ProviderAdapterError(PROVIDER, OPERATION, {
diagnostics: { map_path: this.path, error_message: this.#processError },
});
}
failure(cause: unknown): AnalysisError {
if (this.deadline.interruption === "cancelled")
return new AnalysisCancelledError(OPERATION);
if (this.deadline.interruption === "timeout")
return new AnalysisTimeoutError(
OPERATION,
WEB_SOURCE_MAP_LIMITS.decodeTimeoutMs,
);
if (cause instanceof AnalysisError) return cause;
return cause instanceof PrivateRuntimeRootUnavailableError
? new AnalysisCapabilityUnavailableError(
PROVIDER,
OPERATION,
cause.message,
{ cause },
)
: new ProviderAdapterError(PROVIDER, OPERATION, {
cause,
diagnostics: { map_path: this.path, error_message: message(cause) },
});
}
async close(
previousError: string | null,
): Promise<ProviderCleanupError | undefined> {
const failures: string[] = [];
if (this.#supervisor !== undefined) {
const stopped = await this.#supervisor.stop();
if (stopped.status === "incomplete") failures.push(stopped.reason);
}
if (this.#runtime !== undefined) {
try {
await this.#runtime.close();
} catch (cause: unknown) {
failures.push(message(cause));
}
}
return failures.length === 0
? undefined
: new ProviderCleanupError(
PROVIDER,
[
this.#runId,
...(this.#runtime === undefined ? [] : [this.#runtime.path]),
],
{
map_path: this.path,
cleanup_failures: failures,
previous_error: previousError,
codec_pid: this.#supervisor?.launch.process.pid ?? null,
},
{ operation: OPERATION },
);
}
}
const message = (cause: unknown): string =>
cause instanceof Error ? cause.message : String(cause);
@@ -0,0 +1,124 @@
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
import type { WebSourceMapPort } from "../../application/WebSourceLocationPorts.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisOutputError,
} from "../../domain/analysisErrorCore.js";
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
import { err, ok, type Result } from "../../domain/result.js";
import {
WEB_SOURCE_MAP_LIMITS,
type WebSourceMapReport,
} from "../../domain/webSourceLocation.js";
import {
sourceMapCodecInputSchema,
sourceMapCodecReplySchema,
} from "./SourceMapCodecProtocol.js";
import {
runSourceMapCommand,
type SourceMapDecoderDependencies,
} from "./SourceMapCommand.js";
export type { SourceMapDecoderDependencies } from "./SourceMapCommand.js";
const OPERATION = "trace_web_source_location";
/** Integrate the pinned upstream codec through an independently bounded owned process. */
export class SourceMapDecoder implements WebSourceMapPort {
constructor(readonly dependencies: SourceMapDecoderDependencies = {}) {}
/** Return complete point evidence after process and private-root cleanup. */
async trace(
input: Parameters<WebSourceMapPort["trace"]>[0],
options?: ExecutionOptions,
): Promise<Result<WebSourceMapReport, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
const request = sourceMapCodecInputSchema.safeParse({
text: input.text,
url: input.url,
position: input.position,
});
if (!request.success)
return err(new AnalysisInputError(OPERATION, { cause: request.error }));
const serialized = JSON.stringify(request.data);
if (
Buffer.byteLength(input.text) > WEB_SOURCE_MAP_LIMITS.mapBytes ||
Buffer.byteLength(serialized) > WEB_SOURCE_MAP_LIMITS.outputBytes
)
return err(
new ArtifactOperationError(
OPERATION,
"limit",
undefined,
`${input.path}: Source-map input exceeds the 4 MiB map or 32 MiB encoded request budget.`,
),
);
const response = await runSourceMapCommand(
serialized,
input.path,
options,
this.dependencies,
);
if (!response.ok) return response;
let value: unknown;
try {
value = JSON.parse(response.value);
} catch (cause: unknown) {
return err(
new AnalysisOutputError(
OPERATION,
`Malformed codec JSON reply: ${cause instanceof Error ? cause.message : String(cause)}`,
),
);
}
const reply = sourceMapCodecReplySchema.safeParse(value);
if (!reply.success)
return err(
new AnalysisOutputError(
OPERATION,
`Malformed codec reply: ${reply.error.message}`,
),
);
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
if (reply.data.state === "failure")
return codecFailure(input.path, reply.data.reason, reply.data.message);
if (reply.data.report.runtime.v8_heap_limit_bytes > 256 * 1024 * 1024)
return err(
new AnalysisOutputError(
OPERATION,
"Codec reported a V8 heap limit exceeding its independent process budget.",
),
);
return ok(reply.data.report);
}
}
const codecFailure = (
path: string,
reason: "format" | "unsupported" | "limit",
detail: string,
): Result<never, AnalysisError> =>
err(
reason === "limit"
? new ArtifactOperationError(
OPERATION,
"limit",
undefined,
`${path}: ${detail}`,
)
: reason === "unsupported"
? new AnalysisCapabilityUnavailableError(
"source-map-decoder",
OPERATION,
"unsupported_source_map_profile",
{ userMessage: `${path}: ${detail}` },
)
: new AnalysisInputError(OPERATION, undefined, [
{
path: ["source_map", "path"],
reason: "invalid_format",
message: `${path}: ${detail}`,
},
]),
);
@@ -0,0 +1,269 @@
import { z } from "zod";
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
const leafSchema = z.object({
version: z.literal(3),
file: z.string().nullable().optional(),
sourceRoot: z.string().nullable().optional(),
sources: z.array(z.string().nullable()),
sourcesContent: z.array(z.string().nullable()).nullable().optional(),
names: z.array(z.string()).default([]),
mappings: z.string().regex(/^[A-Za-z0-9+/,;]*$/u),
ignoreList: z.array(z.number().int().min(0)).optional(),
x_google_ignoreList: z.array(z.number().int().min(0)).optional(),
});
const mapHeaderSchema = z.object({
version: z.literal(3),
file: z.string().nullable().optional(),
sections: z.unknown().optional(),
});
const sectionSchema = z.object({
offset: z.object({
line: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
column: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
}),
map: z.unknown().optional(),
url: z.string().optional(),
});
const indexedSchema = z.object({
version: z.literal(3),
sections: z.array(sectionSchema),
});
type Offset = { readonly line: number; readonly column: number };
interface PendingMap {
readonly value: unknown;
readonly path: readonly number[];
readonly offset: Offset;
readonly stop: Offset | null;
}
/** A decoder-profile failure with an actionable producer constraint. */
export class SourceMapFormatFailure extends Error {
constructor(
readonly reason: "format" | "unsupported" | "limit",
message: string,
) {
super(message);
}
}
/** Raw declaration identity, separate from flattened engine URL identity. */
export interface SourceMapSourceDeclaration {
readonly sectionPath: readonly number[];
readonly sourceIndex: number;
readonly source: string | null;
readonly root: string | null;
readonly content: string | null;
readonly ignored: boolean;
}
/** Validated leaf with absolute first-line offsets and its enclosing section boundary. */
export interface SourceMapLeaf {
readonly map: z.output<typeof leafSchema>;
readonly offset: Offset;
readonly stop: Offset | null;
}
const compare = (a: Offset, b: Offset): number =>
a.line - b.line || a.column - b.column;
const addOffset = (base: Offset, offset: Offset): Offset => {
const result = {
line: base.line + offset.line,
column: offset.column + (offset.line === 0 ? base.column : 0),
};
if (
!Number.isSafeInteger(result.line) ||
!Number.isSafeInteger(result.column) ||
result.line >= WEB_SOURCE_MAP_LIMITS.decodedRows
)
throw new SourceMapFormatFailure(
"limit",
"Indexed offset exceeds the 262144 generated-row decoder budget.",
);
return result;
};
const addSections = (node: PendingMap, pending: PendingMap[]): void => {
const sections = indexedSchema.parse(node.value).sections;
for (const [index, section] of sections.entries()) {
const previous = sections[index - 1];
if (previous !== undefined && compare(section.offset, previous.offset) <= 0)
throw new SourceMapFormatFailure(
"format",
"Indexed section offsets must be strictly increasing.",
);
if (section.url !== undefined)
throw new SourceMapFormatFailure(
"unsupported",
`External source-map section URL is not fetched: ${section.url}`,
);
if (section.map === undefined)
throw new SourceMapFormatFailure(
"format",
"Indexed section requires an embedded map.",
);
}
for (let index = sections.length - 1; index >= 0; index -= 1) {
const section = sections[index];
if (section === undefined) continue;
const next = sections[index + 1];
const nextOffset =
next === undefined ? null : addOffset(node.offset, next.offset);
const stop =
nextOffset === null
? node.stop
: node.stop === null || compare(nextOffset, node.stop) < 0
? nextOffset
: node.stop;
const offset = addOffset(node.offset, section.offset);
if (node.stop !== null && compare(offset, node.stop) >= 0)
throw new SourceMapFormatFailure(
"format",
"Nested source-map section overlaps its enclosing boundary.",
);
pending.push({
value: section.map,
path: [...node.path, index],
offset,
stop,
});
}
};
const inspectMappingLayout = (
mappings: string,
): { lines: number; segments: number } => {
let lines = 1;
let segments = 0;
let components = 0;
let inSegment = false;
let continued = false;
let digits = 0;
const finish = (): void => {
if (continued)
throw new SourceMapFormatFailure(
"format",
"Source-map VLQ ends with an unfinished continuation.",
);
if (inSegment && ![1, 4, 5].includes(components))
throw new SourceMapFormatFailure(
"format",
"Source-map segment requires one, four or five VLQ components.",
);
};
for (const character of mappings) {
if (character === "," || character === ";") {
finish();
inSegment = false;
components = 0;
if (character === ";") lines += 1;
} else {
digits += 1;
if (
(digits === 7 && !/[A-Dg-j]/u.test(character)) ||
(digits > 7 && character !== "A" && character !== "g")
)
throw new SourceMapFormatFailure(
"format",
"Source-map VLQ exceeds the format's 32-bit quantity representation.",
);
if (!inSegment) segments += 1;
inSegment = true;
continued = /[g-z0-9+/]/u.test(character);
if (!continued) {
components += 1;
digits = 0;
}
}
if (
segments > WEB_SOURCE_MAP_LIMITS.decodedSegments ||
lines > WEB_SOURCE_MAP_LIMITS.decodedRows
)
throw new SourceMapFormatFailure(
"limit",
"Source-map layout exceeds the 262144 segment/generated-row decoder budget.",
);
}
finish();
return { lines, segments };
};
/** Validate structure before allocation, retaining raw declarations and section boundaries. */
export const inspectSourceMap = (
text: string,
): {
readonly jsonText: string;
readonly format: "regular" | "indexed";
readonly reportedFile: string | null;
readonly declarations: readonly SourceMapSourceDeclaration[];
readonly leaves: readonly SourceMapLeaf[];
} => {
const withoutBom = text.replace(/^\uFEFF/u, "");
const jsonText = withoutBom.startsWith(")]}'")
? withoutBom.slice(withoutBom.indexOf("\n") + 1)
: withoutBom;
const value: unknown = JSON.parse(jsonText);
const root = mapHeaderSchema.parse(value);
const declarations: SourceMapSourceDeclaration[] = [];
const leaves: SourceMapLeaf[] = [];
const pending: PendingMap[] = [
{ value, path: [], offset: { line: 0, column: 0 }, stop: null },
];
let segmentCount = 0;
while (pending.length > 0) {
const node = pending.pop();
if (node === undefined) break;
if (node.path.length > WEB_SOURCE_MAP_LIMITS.sectionDepth)
throw new SourceMapFormatFailure(
"limit",
"Indexed source-map nesting exceeds the 64-level decoder stack budget.",
);
if (mapHeaderSchema.parse(node.value).sections !== undefined) {
addSections(node, pending);
continue;
}
const leaf = leafSchema.parse(node.value);
const layout = inspectMappingLayout(leaf.mappings);
segmentCount += layout.segments;
if (
segmentCount > WEB_SOURCE_MAP_LIMITS.decodedSegments ||
node.offset.line + layout.lines > WEB_SOURCE_MAP_LIMITS.decodedRows
)
throw new SourceMapFormatFailure(
"limit",
"Source-map layout exceeds the 262144 segment/generated-row decoder budget.",
);
leaves.push({ map: leaf, offset: node.offset, stop: node.stop });
for (const [sourceIndex, source] of leaf.sources.entries())
declarations.push({
sectionPath: node.path,
sourceIndex,
source,
root: leaf.sourceRoot ?? null,
content: leaf.sourcesContent?.[sourceIndex] ?? null,
ignored: (leaf.ignoreList ?? leaf.x_google_ignoreList ?? []).includes(
sourceIndex,
),
});
}
// Absolute leaves adapt nested first-line column offsets before upstream AnyMap.
// Raw map bytes and the original section/source identities are retained separately.
const normalized =
root.sections === undefined
? leaves[0]?.map
: {
version: 3,
sections: leaves.map((leaf) => ({
offset: leaf.offset,
map: leaf.map,
})),
};
return {
jsonText: JSON.stringify(normalized),
format: root.sections === undefined ? "regular" : "indexed",
reportedFile: root.file ?? null,
declarations,
leaves,
};
};
@@ -0,0 +1,227 @@
import { describe, expect, it } from "vitest";
import { traceSourceMap } from "./TraceSourceMap.js";
const url = "https://app.test/maps/app.js.map?v=2#map";
const point = { line: 1, column: 0 };
const leaf = {
version: 3,
names: [],
mappings: "AAAA",
sources: ["app.ts"],
sourcesContent: ["original"],
};
const trace = (map: unknown, position = point) =>
traceSourceMap(JSON.stringify(map), url, position);
describe("upstream source-map point evidence", () => {
it("preserves every duplicate-column/source-URL candidate and its own embedded bytes", () => {
const report = trace({
...leaf,
sources: ["same.ts", "same.ts"],
sourcesContent: ["first", "second"],
mappings: "AAAA,ACAA",
});
expect(report.matches).toHaveLength(2);
expect(report.matches).toMatchObject([
{
flattened_source_index: 0,
source_index: 0,
resolved_url: "https://app.test/maps/same.ts",
content: { text: "first" },
},
{
flattened_source_index: 1,
source_index: 1,
resolved_url: "https://app.test/maps/same.ts",
content: { text: "second" },
},
]);
});
it("keeps sourceRoot, query/fragment identity and decoded-text UTF-8 digest", () => {
const report = trace({
...leaf,
sourceRoot: "../src",
sources: ["a.ts?v=2#source"],
sourcesContent: ["🎋a\r\nb"],
names: ["answer"],
mappings: "AAAEA",
});
expect(report.matches[0]).toMatchObject({
reported_source: "a.ts?v=2#source",
reported_source_root: "../src",
resolved_url: "https://app.test/src/a.ts?v=2#source",
name: "answer",
original_position: {
line: 1,
column: 2,
offset: 2,
content_position: "verified-in-embedded-text",
},
content: { utf8_bytes: 8 },
});
});
it("retains generated-only mappings and no-match positions explicitly", () => {
expect(
trace({ ...leaf, mappings: "AAAA,K" }, { line: 1, column: 5 }).matches,
).toEqual([{ state: "generated-only" }]);
expect(trace({ ...leaf, mappings: "KAAA" }).matches).toEqual([]);
expect(
trace({ ...leaf, mappings: "" }).lookup.matched_generated_position,
).toBeNull();
});
it("uses cross-line greatest-position lookup without inventing a mapping", () => {
expect(
trace({ ...leaf, mappings: "AAAA;;" }, { line: 3, column: 2 }).lookup
.matched_generated_position,
).toEqual({ line: 1, column: 0 });
});
it("preserves unknown source identity while retaining embedded content", () => {
expect(trace({ ...leaf, sources: [null] }).matches[0]).toMatchObject({
reported_source: null,
resolved_url: null,
content: { text: "original" },
});
});
it("keeps indexed original content aligned when a preceding section has partial contents", () => {
const report = trace(
{
version: 3,
sections: [
{
offset: { line: 0, column: 0 },
map: { ...leaf, sources: ["a", "b"], sourcesContent: ["A"] },
},
{
offset: { line: 1, column: 0 },
map: { ...leaf, sources: ["c"], sourcesContent: ["C"] },
},
],
},
{ line: 2, column: 0 },
);
expect(report.matches[0]).toMatchObject({
section_path: [1],
source_index: 0,
flattened_source_index: 2,
reported_source: "c",
content: { text: "C" },
});
});
it("supports nested indexed line offsets with first-line-only column shifts", () => {
const report = trace(
{
version: 3,
sections: [
{
offset: { line: 1, column: 2 },
map: {
version: 3,
sections: [{ offset: { line: 1, column: 3 }, map: leaf }],
},
},
],
},
{ line: 3, column: 3 },
);
expect(report.lookup.matched_generated_position).toEqual({
line: 3,
column: 3,
});
expect(report.matches[0]).toMatchObject({
section_path: [0, 0],
original_position: { line: 1, column: 0 },
});
});
it("reports original positions outside embedded contents without fabricating an offset", () => {
expect(trace({ ...leaf, mappings: "AAUA" }).matches[0]).toMatchObject({
original_position: {
line: 11,
column: 0,
offset: null,
content_position: "outside-embedded-text",
},
});
});
});
describe("source-map layout admission", () => {
it.each(["ACAA", "AAAAC", "g", "!", "AA", "D"])(
"rejects malformed VLQ/components/indexes: %s",
(mappings) => {
expect(() => trace({ ...leaf, mappings })).toThrow();
},
);
it("rejects huge indexed offsets before upstream row allocation", () => {
expect(() =>
trace({
version: 3,
sections: [{ offset: { line: 1000000000, column: 0 }, map: leaf }],
}),
).toThrow(/decoder budget/u);
});
it("reports the actual malformed section offset constraint", () => {
expect(() =>
trace({
version: 3,
sections: [{ offset: { line: -1, column: 0 }, map: leaf }],
}),
).toThrow(/offset/u);
});
it("rejects overlapping section mappings rather than silently clipping evidence", () => {
expect(() =>
trace({
version: 3,
sections: [
{
offset: { line: 0, column: 0 },
map: { ...leaf, mappings: "AAAA,UAAA" },
},
{ offset: { line: 0, column: 5 }, map: leaf },
],
}),
).toThrow(/overlap/u);
});
it("accepts absent optional names and null contents without changing reported identity", () => {
expect(
trace({
version: 3,
sources: ["app.ts"],
sourcesContent: null,
mappings: "AAAA",
}).matches[0],
).toMatchObject({
reported_source: "app.ts",
content: { state: "unavailable" },
});
});
});
it.each(["ACAA", "AAAAC"])(
"rejects invalid section-local source/name indexes before flattening: %s",
(mappings) => {
expect(() =>
traceSourceMap(
JSON.stringify({
version: 3,
sections: [
{ offset: { line: 0, column: 0 }, map: { ...leaf, mappings } },
{
offset: { line: 1, column: 0 },
map: { ...leaf, names: ["other"] },
},
],
}),
url,
point,
),
).toThrow(/index/u);
},
);
it.each(["ggggggEAAA", "gggggggBAAA"])(
"rejects overflowing VLQ quantities before upstream bitwise decoding: %s",
(mappings) => {
expect(() =>
traceSourceMap(JSON.stringify({ ...leaf, mappings }), url, point),
).toThrow(/32-bit/u);
},
);
+203
View File
@@ -0,0 +1,203 @@
import { createHash } from "node:crypto";
import { createRequire } from "node:module";
import { getHeapStatistics } from "node:v8";
import {
AnyMap,
TraceMap,
decodedMappings,
type SourceMapSegment,
} from "@jridgewell/trace-mapping";
import { z } from "zod";
import { webSourceOffset } from "../../domain/webSourcePosition.js";
import {
webSourceMapReportSchema,
type WebSourceMapReport,
WEB_SOURCE_MAP_LIMITS,
} from "../../domain/webSourceLocation.js";
import type { WebSourcePosition } from "../../domain/webSourcePosition.js";
import {
inspectSourceMap,
SourceMapFormatFailure,
type SourceMapSourceDeclaration,
} from "./SourceMapFormat.js";
/** Decode with the pinned upstream codec; preserve every equal-position candidate. */
export const traceSourceMap = (
text: string,
url: string,
position: WebSourcePosition,
): WebSourceMapReport => {
const inspected = inspectSourceMap(text);
const { map, rows } = decodeValidatedMap(inspected, url);
const matched = greatestPosition(rows, position);
const segments =
matched === null
? []
: (rows[matched.line - 1] ?? []).filter(
(segment) => segment[0] === matched.column,
);
let bytes = 0;
const matches: WebSourceMapReport["matches"] = [];
for (const segment of segments) {
const match = originalMatch(segment, map, inspected.declarations);
bytes += Buffer.byteLength(JSON.stringify(match)) + 1;
if (bytes > WEB_SOURCE_MAP_LIMITS.outputBytes)
throw new SourceMapFormatFailure(
"limit",
"Complete source-map point evidence exceeds the 32 MiB output budget; no partial evidence was returned.",
);
matches.push(match);
}
const metadata: unknown = createRequire(import.meta.url)(
"@jridgewell/trace-mapping/package.json",
);
const version = z
.object({ version: z.string().min(1) })
.parse(metadata).version;
return webSourceMapReportSchema.parse({
source_map_sha256: createHash("sha256").update(text).digest("hex"),
url_context: url,
engine: { id: "jridgewell-trace-mapping", version },
runtime: {
id: "node-v8",
version: process.version,
v8_heap_limit_bytes: getHeapStatistics().heap_size_limit,
},
format: inspected.format,
reported_file: inspected.reportedFile,
lookup: {
requested: position,
matched_generated_position: matched,
semantics:
"greatest generated position <= requested, across lines; all equal-position segments",
location_units: "one-based lines; zero-based UTF-16 columns",
},
matches,
diagnostics: [],
});
};
const decodeValidatedMap = (
inspected: ReturnType<typeof inspectSourceMap>,
url: string,
) => {
for (const leaf of inspected.leaves) {
const decoded = decodedMappings(
new TraceMap(JSON.stringify(leaf.map), url),
);
for (const [line, row] of decoded.entries())
for (const segment of row) {
if (
segment.some((value) => !Number.isSafeInteger(value) || value < 0) ||
(segment.length !== 1 && segment[1] >= leaf.map.sources.length) ||
(segment.length === 5 && segment[4] >= leaf.map.names.length)
)
throw new SourceMapFormatFailure(
"format",
"Decoded section-local position, source index or name index is invalid.",
);
const generatedLine = leaf.offset.line + line;
const generatedColumn =
segment[0] + (line === 0 ? leaf.offset.column : 0);
if (
leaf.stop !== null &&
(generatedLine > leaf.stop.line ||
(generatedLine === leaf.stop.line &&
generatedColumn >= leaf.stop.column))
)
throw new SourceMapFormatFailure(
"format",
"Embedded source-map mappings overlap the following section boundary.",
);
}
}
const map = new AnyMap(inspected.jsonText, url);
const rows = decodedMappings(map);
for (const row of rows)
for (const segment of row) {
if (
![1, 4, 5].includes(segment.length) ||
segment.some((v) => !Number.isSafeInteger(v) || v < 0) ||
(segment.length !== 1 && segment[1] >= inspected.declarations.length) ||
(segment.length === 5 && segment[4] >= map.names.length)
)
throw new SourceMapFormatFailure(
"format",
"Decoded mapping has invalid component count, position, source index or name index.",
);
}
return { map, rows };
};
const greatestPosition = (
rows: ReturnType<typeof decodedMappings>,
position: WebSourcePosition,
): WebSourcePosition | null => {
for (
let line = Math.min(position.line - 1, rows.length - 1);
line >= 0;
line -= 1
) {
let column: number | undefined;
for (const segment of rows[line] ?? [])
if (line < position.line - 1 || segment[0] <= position.column)
column = segment[0];
if (column !== undefined) return { line: line + 1, column };
}
return null;
};
const originalMatch = (
segment: Readonly<SourceMapSegment>,
map: TraceMap,
declarations: readonly SourceMapSourceDeclaration[],
): WebSourceMapReport["matches"][number] => {
if (segment.length === 1) return { state: "generated-only" };
const declaration = declarations[segment[1]];
if (declaration === undefined)
throw new SourceMapFormatFailure(
"format",
"Mapping references an absent source declaration.",
);
const original = { line: segment[2] + 1, column: segment[3] };
const offset =
declaration.content === null
? undefined
: webSourceOffset(declaration.content, original);
return {
state: "mapped",
flattened_source_index: segment[1],
section_path: [...declaration.sectionPath],
source_index: declaration.sourceIndex,
reported_source: declaration.source,
reported_source_root: declaration.root,
resolved_url:
declaration.source === null
? null
: (map.resolvedSources[segment[1]] ?? null),
ignored: declaration.ignored,
content:
declaration.content === null
? { state: "unavailable", reason: "sourcesContent-not-retained" }
: {
state: "embedded",
text: declaration.content,
utf8_sha256: createHash("sha256")
.update(declaration.content)
.digest("hex"),
utf8_bytes: Buffer.byteLength(declaration.content),
representation: "UTF-8 encoding of decoded sourcesContent text",
},
original_position: {
...original,
offset: offset ?? null,
content_position:
declaration.content === null
? "content-unavailable"
: offset === undefined
? "outside-embedded-text"
: "verified-in-embedded-text",
},
name: segment.length === 5 ? (map.names[segment[4]] ?? null) : null,
};
};