fix(javascript): preserve ESM path intent and artifact confinement (#2700)
* fix(javascript): preserve legacy ESM directory intent * fix(javascript): normalize legacy ESM URL directory requests * fix(javascript): retain invalid package URL evidence * test(apple): bound cold archive fixture compilation * test(release): scope preflight schema to selected jobs * fix(javascript): preserve package URL admission and target facts * fix(javascript): validate package target structure before URL lookup * fix(javascript): preserve surrogate identity in structural package URLs * fix(javascript): isolate structural package URL identities * fix(javascript): resolve paths after opaque ancestors normalize away * fix(javascript): confine legacy URLs and retain target provenance --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
1 parent
92a9591a41
commit
6714fb1d02
3 files changed
+89
-4
No files matched your search
@@ -1096,10 +1096,19 @@ const resolveLegacyEsmSubpath = (
|
||||
directory: string,
|
||||
subpath: string,
|
||||
): CandidateResolution => {
|
||||
const declarationRejection = rejectDeclaration(
|
||||
stripQueryAndFragment(htmlUrlText(subpath)),
|
||||
);
|
||||
const subpathPath = stripQueryAndFragment(htmlUrlText(subpath));
|
||||
const declarationRejection = rejectDeclaration(subpathPath);
|
||||
if (declarationRejection !== null) return declarationRejection;
|
||||
let depth = directory === "." ? 0 : directory.split("/").length;
|
||||
for (const segment of subpathPath.split("/")) {
|
||||
if (segment === "..") {
|
||||
if (depth === 0)
|
||||
return unresolvedCandidate("rejected", [
|
||||
"The resolved candidate escapes the canonical artifact root.",
|
||||
]);
|
||||
depth--;
|
||||
} else if (segment !== ".") depth++;
|
||||
}
|
||||
const metadataUrl = packageMetadataUrl(
|
||||
posix.join(directory, "package.json"),
|
||||
subpath,
|
||||
@@ -1208,7 +1217,9 @@ const resolveExportTarget = (
|
||||
const candidate = confineCandidate(decoded);
|
||||
if (typeof candidate !== "string") return withTargetSource(candidate);
|
||||
if (target.lookupLimitation !== undefined)
|
||||
return unresolvedCandidate("unavailable", [target.lookupLimitation]);
|
||||
return withTargetSource(
|
||||
unresolvedCandidate("unavailable", [target.lookupLimitation]),
|
||||
);
|
||||
return (
|
||||
resolveFileCandidates(input, [candidate]) ??
|
||||
withTargetSource(exactPackageTargetNotFound(candidate, mapKind))
|
||||
|
||||
@@ -9,6 +9,7 @@ import { expect } from "vitest";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
||||
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
type PackageSpecifierCase = {
|
||||
readonly specifier: string;
|
||||
@@ -382,3 +383,62 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"confines legacy package traversal before URL normalization through %s",
|
||||
async (surface, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-package-root-traversal-");
|
||||
const cases = [
|
||||
{ specifier: "legacy/../../outside.js", status: "resolved" },
|
||||
{
|
||||
specifier: "legacy/../../outside.js?cache=/../../rea-artifact/#/..",
|
||||
status: "resolved",
|
||||
},
|
||||
{ specifier: "legacy//../../../outside.js", status: "resolved" },
|
||||
{
|
||||
specifier: "legacy/../../../rea-artifact/outside.js",
|
||||
status: "rejected",
|
||||
},
|
||||
{
|
||||
specifier: "legacy/../../../../rea-artifact/outside.js",
|
||||
status: "rejected",
|
||||
},
|
||||
{
|
||||
specifier: "legacy/../.\t./.\n./rea-artifact/outside.js?cache=1#entry",
|
||||
status: "rejected",
|
||||
},
|
||||
] as const;
|
||||
const source = cases
|
||||
.map(({ specifier }) => `import ${JSON.stringify(specifier)};`)
|
||||
.join("\n");
|
||||
await writeFixtureFiles(root, {
|
||||
"main.mjs": source,
|
||||
"outside.js": "export default 1;",
|
||||
"node_modules/legacy/package.json": "{}",
|
||||
});
|
||||
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
|
||||
for (const { specifier, status } of cases)
|
||||
expect(graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "imports",
|
||||
properties: expect.objectContaining({
|
||||
specifier,
|
||||
resolution_status: status,
|
||||
resolved_path: status === "resolved" ? "outside.js" : null,
|
||||
}),
|
||||
evidence: expect.objectContaining({
|
||||
...(status === "rejected"
|
||||
? {
|
||||
limitations: expect.arrayContaining([
|
||||
"The resolved candidate escapes the canonical artifact root.",
|
||||
]),
|
||||
}
|
||||
: {}),
|
||||
artifact: expect.objectContaining({
|
||||
sha256: createHash("sha256").update(source).digest("hex"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -232,6 +232,13 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
request.status === "root-legacy"
|
||||
? fileStatus
|
||||
: request.status;
|
||||
const mapKind = request.specifier.startsWith("#")
|
||||
? "imports"
|
||||
: "exports";
|
||||
const selectedTarget =
|
||||
request.specifier === "mapped/structuralReentry"
|
||||
? targets.structuralReentry.value
|
||||
: targets.feature.value;
|
||||
expect(graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "imports",
|
||||
@@ -249,6 +256,13 @@ cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
status === "unavailable" && request.status !== "unavailable"
|
||||
? expect.arrayContaining([
|
||||
expect.stringContaining("unpaired UTF-16 surrogate"),
|
||||
...(request.status === "relative"
|
||||
? [
|
||||
expect.stringContaining(
|
||||
`${mapKind} target ${JSON.stringify(selectedTarget)}:`,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
])
|
||||
: expect.not.arrayContaining([
|
||||
expect.stringContaining("unpaired UTF-16 surrogate"),
|
||||
|
||||
Reference in new issue
Block a user