fix(javascript): resolve module-relative Worker URL references (#2104)
* fix(javascript): resolve module-relative Worker URL references * fix(javascript): validate Worker URL paths independently of suffixes * fix(javascript): preserve directory-form Worker URL paths * fix(javascript): unwrap type assertions in Worker URL references * fix(javascript): validate Worker bindings before URL resolution * fix(javascript): leave drive-letter Worker URLs unresolved Use WHATWG file URL parsing before pathname decoding to detect drive-letter forms whose resource identity depends on the unmodeled source URL scheme. Preserve encoded and ordinary relative pipe paths, and cover false matches through the compiled CLI and resolver boundary tests. * fix(javascript): preserve Worker URL identity boundaries Recognize known Worker bindings before filtering callee spellings so ESM aliases and transparent constructor wrappers reach URL analysis. Keep root-relative module URLs unknown without a modeled runtime base root. Index direct source-local global writes with the shared mutation collector before promoting URL and browser Worker constructors. Preserve lexical shadowing and unrelated property identities, and exercise all three review regressions through the compiled CLI. * fix(javascript): invalidate mutated import.meta URL bases Track source-local import.meta writes alongside ambient global writes so Worker URL expressions keep an unknown base after a possible URL mutation. Tag the identity roots independently and cover direct mutation and unrelated property cases through the compiled CLI. * fix(javascript): close Worker URL identity and pathname gaps Resolve ambient object identities before appending written properties, and retain possible source-local aliases across rebinding. Reuse intrinsic invocation handling for aliased and bound mutators, array spreads, and transparent TypeScript wrappers while preserving Electron write semantics. Keep malformed-percent nested Worker URL pathnames unknown because the base scheme is unavailable and Node rejects these file URLs. Preserve tolerant HTML URL handling, valid escapes, and query/fragment evidence. Cover the new cases and unrelated writes through the compiled CLI with existing-file decoys. All 591 targeted tests and npm run check pass. * fix(javascript): bound alias expansion and classify Worker paths Join ambient identity projections instead of enumerating alias histories. Memoize repeated value and provenance queries while preserving capture, depth, and cycle-dependent uncertainty. Keep Node Worker string arguments unresolved without filesystem or cwd context, and admit qualified global URL constructors through identity checks. Validate three 40-level alias branch shapes through the compiled CLI, 925 source tests, 25 CLI/boundary tests, and the full npm run check gate. * fix(server): preserve partial Evidence target-content provenance Pass the exact named contract returnsTargetContent flag through the shared partial-Evidence error delivery path introduced by the shortlist integration. Exercise successful and failed recording through in-memory MCP for marked and unmarked contracts, preserving source payload and original failures. Observe both shared-handler emission and wire delivery so the SDK error-marker fallback cannot hide omissions. Suppressing the flag fails exactly the two marked cases. Validation: check:fast, check, 736 semantic tests, 29 MCP transport/error tests and six compiled static Worker CLI journeys pass. No live target/provider execution or hosted publication. * fix(javascript): structurally share semantic dependency proofs --------- Co-authored-by: baron <richstyles@gmail.com>
This commit is contained in:
1 parent
3b0ea590fc
commit
653067d799
19 files changed
+1946
-171
No files matched your search
@@ -376,10 +376,14 @@ const resolveReference = (
|
||||
): ResolvedReference | null => {
|
||||
const specifier = reference.specifier;
|
||||
if (specifier === null) return null;
|
||||
const chunk = resolveBundlerChunkReference(context, sourcePath, reference);
|
||||
const isWorker =
|
||||
reference.kind === "worker" || reference.kind === "service-worker";
|
||||
const chunk = isWorker
|
||||
? null
|
||||
: resolveBundlerChunkReference(context, sourcePath, reference);
|
||||
if (chunk !== null) return chunk;
|
||||
const module =
|
||||
reference.module_runtime === undefined
|
||||
isWorker || reference.module_runtime === undefined
|
||||
? undefined
|
||||
: context.moduleNodes.get(
|
||||
moduleLookupKey(sourcePath, reference.module_runtime, specifier),
|
||||
@@ -391,24 +395,23 @@ const resolveReference = (
|
||||
: { path: `${sourcePath}#module:${specifier}`, file, node: module };
|
||||
}
|
||||
// Worker scripts are URLs relative to the script, not module specifiers.
|
||||
const path =
|
||||
reference.kind === "worker" || reference.kind === "service-worker"
|
||||
? resolveArtifactPathByContext({
|
||||
declaredPath: specifier,
|
||||
sourcePath,
|
||||
context: "url-reference",
|
||||
files: context.filesByPath,
|
||||
}).resolved_path
|
||||
: resolveArtifactPath(
|
||||
specifier,
|
||||
sourcePath,
|
||||
context.filesByPath,
|
||||
reference.kind === "require" ||
|
||||
reference.kind === "static-import" ||
|
||||
reference.kind === "dynamic-import"
|
||||
? moduleKindForJavaScriptReference(sourcePath, reference.kind)
|
||||
: undefined,
|
||||
);
|
||||
const path = isWorker
|
||||
? resolveArtifactPathByContext({
|
||||
declaredPath: specifier,
|
||||
sourcePath,
|
||||
context: "url-reference",
|
||||
files: context.filesByPath,
|
||||
}).resolved_path
|
||||
: resolveArtifactPath(
|
||||
specifier,
|
||||
sourcePath,
|
||||
context.filesByPath,
|
||||
reference.kind === "require" ||
|
||||
reference.kind === "static-import" ||
|
||||
reference.kind === "dynamic-import"
|
||||
? moduleKindForJavaScriptReference(sourcePath, reference.kind)
|
||||
: undefined,
|
||||
);
|
||||
if (path === null) return null;
|
||||
const file = context.filesByPath.get(path);
|
||||
const node = context.fileNodes.get(path);
|
||||
|
||||
@@ -1979,15 +1979,143 @@ describe("TypeScript NodeNext emitted-extension resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("url-reference directory and empty-segment semantics", () => {
|
||||
it.each([
|
||||
"./worker.js/.",
|
||||
"./worker.js/child/..",
|
||||
"./worker.js/",
|
||||
"/dist/worker.js/.",
|
||||
"./worker.js/.?cache=1#entry",
|
||||
])("keeps directory-form URLs distinct from files: %s", (declaredPath) => {
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath,
|
||||
sourcePath: "dist/app.mjs",
|
||||
context: "url-reference",
|
||||
files: fileMap([file("dist/worker.js", "root")]),
|
||||
}),
|
||||
).toMatchObject({
|
||||
declared_path: declaredPath,
|
||||
resolution_status: "not-found",
|
||||
resolved_path: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["./nested//worker.js", "./nested//../worker.js"])(
|
||||
"does not collapse empty URL path segments: %s",
|
||||
(declaredPath) => {
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath,
|
||||
sourcePath: "dist/app.mjs",
|
||||
context: "url-reference",
|
||||
files: fileMap([
|
||||
file("dist/worker.js", "root"),
|
||||
file("dist/nested/worker.js", "root"),
|
||||
]),
|
||||
}),
|
||||
).toMatchObject({ resolved_path: null });
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["./nested/.././worker.js?cache=1#entry", "dist/worker.js"],
|
||||
["./.worker.js", "dist/.worker.js"],
|
||||
])("resolves exact file URLs with dots: %s", (declaredPath, resolvedPath) => {
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath,
|
||||
sourcePath: "dist/app.mjs",
|
||||
context: "url-reference",
|
||||
files: fileMap([file(resolvedPath, "root")]),
|
||||
}),
|
||||
).toMatchObject({
|
||||
resolution_status: "resolved",
|
||||
resolved_path: resolvedPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("url-reference JavaScript artifact path resolution", () => {
|
||||
it.each([
|
||||
["C|?cache=1#entry", "dist/C|"],
|
||||
["/c:/worker.js", "c:/worker.js"],
|
||||
["/./c|/worker.js", "c|/worker.js"],
|
||||
["../c|/worker.js", "c|/worker.js"],
|
||||
["/nested/../c|/worker.js", "c|/worker.js"],
|
||||
["c|/../worker.js", "dist/worker.js"],
|
||||
])("leaves drive-letter URL paths unresolved: %s", (declaredPath, decoy) => {
|
||||
const resolution = resolve({
|
||||
declaredPath,
|
||||
sourcePath: "dist/app.mjs",
|
||||
context: "url-reference",
|
||||
files: fileMap([file(decoy, "root")]),
|
||||
});
|
||||
expect(resolution).toMatchObject({
|
||||
declared_path: declaredPath,
|
||||
resolution_status: "unavailable",
|
||||
resolved_path: null,
|
||||
});
|
||||
expect(resolution.limitations.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["./c|/worker.js", "dist/c|/worker.js"],
|
||||
["c|name/worker.js", "dist/c|name/worker.js"],
|
||||
["/%63|/worker.js", "c|/worker.js"],
|
||||
["/c%3A/worker.js", "c:/worker.js"],
|
||||
])(
|
||||
"retains non-drive URL path identities: %s",
|
||||
(declaredPath, resolvedPath) => {
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath,
|
||||
sourcePath: "dist/app.mjs",
|
||||
context: "url-reference",
|
||||
files: fileMap([file(resolvedPath, "root")]),
|
||||
}),
|
||||
).toMatchObject({
|
||||
declared_path: declaredPath,
|
||||
resolution_status: "resolved",
|
||||
resolved_path: resolvedPath,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["?value=", "#value="])(
|
||||
"admits the pathname independently of its %s suffix",
|
||||
(suffix) => {
|
||||
for (const value of ["%2F", "%2E", "%5C", "%00", "%FF", "\\", "\0"]) {
|
||||
const declaredPath = `./worker.js${suffix}${value}`;
|
||||
expect(
|
||||
resolve({
|
||||
declaredPath,
|
||||
sourcePath: "dist/app.js",
|
||||
context: "url-reference",
|
||||
files: fileMap([file("dist/worker.js", "root")]),
|
||||
}),
|
||||
declaredPath,
|
||||
).toMatchObject({
|
||||
declared_path: declaredPath,
|
||||
resolution_status: "resolved",
|
||||
resolved_path: "dist/worker.js",
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"app\0.js.map?valid=1",
|
||||
"nested\\worker.js#valid",
|
||||
"app%00.js.map",
|
||||
"app%FF.js.map",
|
||||
"app%C0%AF.js.map",
|
||||
"app%2ejs.map",
|
||||
"nested%2fworker.js",
|
||||
"nested%5cworker.js",
|
||||
"h\nttp://[",
|
||||
"../../outside.js.map",
|
||||
"../../outside/.",
|
||||
])("rejects an inadmissible URL path: %s", (declaredPath) => {
|
||||
const resolution = resolve({
|
||||
declaredPath,
|
||||
|
||||
@@ -153,17 +153,15 @@ const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
|
||||
input.declaredPath.startsWith("#")
|
||||
)
|
||||
return resolvePackageImportAlias(input, input.declaredPath);
|
||||
const rejected = rejectDeclaration(input.declaredPath);
|
||||
if (rejected !== null) return rejected;
|
||||
const candidate = contextualCandidate(input);
|
||||
if (typeof candidate !== "string") return candidate;
|
||||
const confined = confineCandidate(candidate);
|
||||
if (typeof confined !== "string") return confined;
|
||||
if (input.context === "html-reference")
|
||||
if (input.context === "html-reference" || input.context === "url-reference")
|
||||
return (
|
||||
resolveFileCandidates(input, [confined]) ??
|
||||
unresolvedCandidate("not-found", [
|
||||
`The exact HTML resource ${confined} was not found among the selected application files.`,
|
||||
`The exact ${input.context === "html-reference" ? "HTML" : "URL"} resource ${confined} was not found among the selected application files.`,
|
||||
])
|
||||
);
|
||||
const isModuleUrlPath =
|
||||
@@ -201,10 +199,14 @@ const contextualCandidate = (
|
||||
input: ResolveArtifactPathInput,
|
||||
): string | CandidateResolution => {
|
||||
const { context } = input;
|
||||
// URL suffixes are evidence, not pathname bytes. Admit only the path while
|
||||
// retaining the complete declaration on the resolution result.
|
||||
let declared =
|
||||
context === "html-reference" || context === "url-reference"
|
||||
context === "url-reference"
|
||||
? stripQueryAndFragment(input.declaredPath)
|
||||
: input.declaredPath;
|
||||
const rejected = rejectDeclaration(declared);
|
||||
if (rejected !== null) return rejected;
|
||||
if (context === "html-reference") return htmlCandidate(input);
|
||||
if (context === "module-specifier") {
|
||||
const fileUrl = fileUrlPath(declared);
|
||||
@@ -230,12 +232,39 @@ const contextualCandidate = (
|
||||
"URL schemes and protocol-relative URLs are outside this local artifact path context.",
|
||||
]);
|
||||
if (context === "url-reference") {
|
||||
// A file base gives raw drive-letter segments special meaning, including
|
||||
// after dot segments reach the root. Parse before percent decoding so an
|
||||
// encoded drive-like filename stays literal. The synthetic base establishes
|
||||
// only this ambiguity; it does not establish the source's runtime scheme.
|
||||
const sourceUrlPath = input.sourcePath
|
||||
.split("/")
|
||||
.map(encodeURIComponent)
|
||||
.join("/");
|
||||
const fileReference = URL.parse(declared, `file:///${sourceUrlPath}`);
|
||||
if (fileReference === null)
|
||||
return unresolvedCandidate("rejected", [
|
||||
"The URL reference cannot be parsed as a URL.",
|
||||
]);
|
||||
if (/^\/[A-Za-z]:(?:\/|$)/u.test(fileReference.pathname))
|
||||
return unresolvedCandidate("unavailable", [
|
||||
"Drive-letter URL paths cannot be mapped without the source URL scheme and filesystem root.",
|
||||
]);
|
||||
const decoded = percentDecodeUrlPath(declared);
|
||||
if (decoded === null)
|
||||
return unresolvedCandidate("rejected", [
|
||||
"The URL reference path percent-decodes to NUL or to bytes that are not UTF-8.",
|
||||
]);
|
||||
declared = decoded;
|
||||
// Empty URL segments are significant; canonical filesystem paths cannot
|
||||
// preserve them, including their interaction with following dot segments.
|
||||
if (decoded.includes("//"))
|
||||
return unresolvedCandidate("unavailable", [
|
||||
"Repeated URL path separators cannot be mapped to an exact canonical artifact file.",
|
||||
]);
|
||||
return exactUrlPathCandidate(
|
||||
decoded,
|
||||
relativeCandidate(input.sourcePath, decoded),
|
||||
"URL",
|
||||
);
|
||||
}
|
||||
return relativeCandidate(input.sourcePath, declared);
|
||||
};
|
||||
@@ -689,37 +718,34 @@ const htmlCandidate = (
|
||||
const basePath = decodedBase.startsWith("/")
|
||||
? decodedBase.slice(1)
|
||||
: posix.join(posix.dirname(input.sourcePath), decodedBase);
|
||||
if (declaredPath === "") return emptyHtmlReference(decodedBase, basePath);
|
||||
if (declaredPath === "")
|
||||
return exactUrlPathCandidate(decodedBase, basePath, "HTML");
|
||||
return posix.join(htmlBaseDirectory(decodedBase, basePath), declaredPath);
|
||||
};
|
||||
|
||||
const emptyHtmlReference = (
|
||||
base: string,
|
||||
basePath: string,
|
||||
const exactUrlPathCandidate = (
|
||||
declared: string,
|
||||
candidate: string,
|
||||
kind: "HTML" | "URL",
|
||||
): string | CandidateResolution => {
|
||||
if (!htmlBaseIsDirectory(base)) return basePath;
|
||||
const confined = confineCandidate(basePath);
|
||||
if (!urlPathIsDirectory(declared)) return candidate;
|
||||
const confined = confineCandidate(candidate);
|
||||
if (typeof confined !== "string") return confined;
|
||||
return unresolvedCandidate("not-found", [
|
||||
`The HTML reference resolves to directory ${confined || "."}, not an exact selected application file.`,
|
||||
`The ${kind} reference resolves to directory ${confined || "."}, not an exact selected application file.`,
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* The directory a relative HTML reference resolves against. A base path
|
||||
* ending in "/" or in a "." or ".." segment is already a directory, because a
|
||||
* relative reference resolves against the base URL's directory and those
|
||||
* segments are dropped rather than stepped through.
|
||||
*/
|
||||
const htmlBaseIsDirectory = (base: string): boolean =>
|
||||
base.endsWith("/") ||
|
||||
base.endsWith("/.") ||
|
||||
base.endsWith("/..") ||
|
||||
base === "." ||
|
||||
base === "..";
|
||||
/** Whether a URL path retains directory form after dot-segment normalization. */
|
||||
const urlPathIsDirectory = (path: string): boolean =>
|
||||
path.endsWith("/") ||
|
||||
path.endsWith("/.") ||
|
||||
path.endsWith("/..") ||
|
||||
path === "." ||
|
||||
path === "..";
|
||||
|
||||
const htmlBaseDirectory = (base: string, basePath: string): string =>
|
||||
htmlBaseIsDirectory(base) ? basePath : posix.dirname(basePath);
|
||||
urlPathIsDirectory(base) ? basePath : posix.dirname(basePath);
|
||||
|
||||
const confineCandidate = (candidate: string): string | CandidateResolution => {
|
||||
const normalized = normalizeJoinedLogicalPath(candidate);
|
||||
@@ -862,8 +888,6 @@ const resolveDirectory = (
|
||||
files: input.files,
|
||||
...(input.moduleKind === undefined ? {} : { moduleKind: input.moduleKind }),
|
||||
};
|
||||
const rejected = rejectDeclaration(entryInput.declaredPath);
|
||||
if (rejected !== null) return rejected;
|
||||
const candidate = contextualCandidate(entryInput);
|
||||
if (typeof candidate !== "string") return candidate;
|
||||
const confined = confineCandidate(candidate);
|
||||
|
||||
Reference in new issue
Block a user