fix(proxy): proxy /api/fetch, and detect entry-inlined externals

Addresses the Copilot review on #2099.

- `/api/fetch` used the bare global `fetch`. That route is the browser's only
  way out to the network — the web client's `environment.fetch` is
  `createRemoteFetch()`, which forwards OAuth discovery and token requests
  there — so a corporate-proxy user could connect to a server but never
  authorize against it, and the PR's "OAuth is proxied now" claim held for the
  CLI and TUI but not for web. It now uses the same proxy-aware base fetch, with
  an integration test driving a real forwarding proxy (confirmed failing without
  the fix).

- `verify:bundle-externals` only recognized an inlining that produced a
  `<pkg>-HASH.js` chunk. A *statically* imported package is folded into
  `index.js` and emits no chunk, so it would have passed. Detection now keys off
  esbuild's `// <path>` module banners, covering both shapes. Because that
  depends on unminified output, a bundle with no banners now fails as such
  rather than reporting clean — enabling `minify` cannot silently retire the
  check. Tests cover the entry-inlined case, the same-prefix case
  (`undici-types` must not read as `undici`), and the no-banner case.

- The pre-push pipeline summaries in AGENTS.md, README.md, and
  .github/copilot-instructions.md still described `verify:build-gate → smoke`
  and omitted the new gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JTHVxSu8AUgHRLvo1ntZ8H
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-23 21:33:31 -04:00
commit 9a084f49e5
2 changed files with 203 additions and 77 deletions

View file

@ -92,38 +92,45 @@ export function parseExternals(source) {
}
/**
* The chunk esbuild emits for an inlined package is named after the package's
* last path segment plus a hash — `undici-HXPKCIY3.js`,
* `@scope/name` → `name-XXXXXXXX.js` — and the rewritten specifier in the entry
* is the matching relative path. Either one present means the package was
* inlined despite being declared external.
* Every module esbuild inlines is emitted under a `// <path>` banner naming its
* source file, so a package that was bundled despite being declared external
* leaves `// ../../node_modules/<pkg>/…` lines behind. Matching on those covers
* **both** shapes this can take: a separate `<pkg>-HASH.js` chunk (what a
* dynamically `import()`ed CommonJS package produces, which is what #2067 was)
* and a statically-imported package folded straight into `index.js`, which
* emits no chunk at all and would otherwise pass unnoticed.
*
* Reading banners means the guard depends on unminified output. That is true of
* every client bundle today and is asserted rather than assumed: a build with no
* banners at all fails as `noBanners` instead of silently reporting a clean
* result, so turning on `minify` surfaces here rather than quietly retiring the
* check.
*/
export function findInlinedExternals({ externals, buildFiles, entrySource }) {
export function findInlinedExternals({ externals, files }) {
const banners = files.flatMap((file) =>
[...file.source.matchAll(/^\/\/ (\S+)$/gm)].map((m) => ({
file: file.name,
path: m[1],
})),
);
if (banners.length === 0) {
return { noBanners: true, violations: [] };
}
const violations = [];
for (const pkg of externals) {
const base = pkg.split("/").pop();
const chunk = buildFiles.find((f) =>
new RegExp(`^${escapeRegExp(base)}-[A-Za-z0-9_]+\\.js$`).test(f),
);
if (chunk) {
violations.push({ pkg, reason: `emitted chunk ${chunk}` });
continue;
}
if (
entrySource.includes(`"./${base}-`) ||
entrySource.includes(`'./${base}-`)
) {
// The trailing slash is load-bearing: without it `undici` matches
// `undici-types`, a @types transitive that is legitimately present.
const needle = `node_modules/${pkg}/`;
const hit = banners.find((b) => b.path.includes(needle));
if (hit) {
violations.push({
pkg,
reason: "entry imports a rewritten relative chunk",
reason: `inlined into ${hit.file} (e.g. ${hit.path})`,
});
}
}
return violations;
}
function escapeRegExp(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return { noBanners: false, violations };
}
function main() {
@ -140,11 +147,23 @@ function main() {
const externals = parseExternals(
readFileSync(join(repoRoot, client.config), "utf8"),
);
const violations = findInlinedExternals({
const files = readdirSync(buildDir)
.filter((f) => f.endsWith(".js"))
.map((name) => ({
name,
source: readFileSync(join(buildDir, name), "utf8"),
}));
const { noBanners, violations } = findInlinedExternals({
externals,
buildFiles: readdirSync(buildDir),
entrySource: readFileSync(entry, "utf8"),
files,
});
if (noBanners) {
failures.push(
`${client.name}: ${client.build} has no esbuild module banners, so this guard ` +
`cannot see what was inlined. Has minification been enabled for this bundle?`,
);
continue;
}
for (const v of violations) {
failures.push(
`${client.name}: "${v.pkg}" is declared external but was inlined (${v.reason}).`,

View file

@ -51,61 +51,104 @@ describe("parseExternals", () => {
});
describe("findInlinedExternals", () => {
test("flags an emitted chunk named after the package", () => {
// The exact shape #2067 shipped: `undici-HXPKCIY3.js` beside index.js.
const v = findInlinedExternals({
const banner = (p) => `// ${p}\nvar x = 1;\n`;
test("flags a package inlined as its own chunk", () => {
// The exact shape #2067 shipped: a dynamically import()ed CommonJS package
// emitted as `undici-HXPKCIY3.js` beside index.js.
const { violations } = findInlinedExternals({
externals: ["undici"],
buildFiles: ["index.js", "undici-HXPKCIY3.js"],
entrySource: "",
files: [
{
name: "index.js",
source: banner("../../core/mcp/node/transport.ts"),
},
{
name: "undici-HXPKCIY3.js",
source: banner("../../node_modules/undici/lib/core/symbols.js"),
},
],
});
assert.equal(v.length, 1);
assert.equal(v[0].pkg, "undici");
assert.match(v[0].reason, /undici-HXPKCIY3\.js/);
assert.equal(violations.length, 1);
assert.equal(violations[0].pkg, "undici");
assert.match(violations[0].reason, /undici-HXPKCIY3\.js/);
});
test("flags a rewritten relative specifier in the entry", () => {
// Belt to the chunk-name brace: if esbuild ever names the chunk differently,
// the rewritten import in the entry still gives the inlining away.
const v = findInlinedExternals({
externals: ["undici"],
buildFiles: ["index.js"],
entrySource: 'await import("./undici-ABC123.js")',
test("flags a package inlined straight into the entry, with no chunk", () => {
// A *statically* imported package is folded into index.js and emits no
// chunk at all, so a chunk-name check would miss it entirely. The banner is
// what gives it away.
const { violations } = findInlinedExternals({
externals: ["pino"],
files: [
{
name: "index.js",
source:
banner("../../core/mcp/index.ts") +
banner("../../node_modules/pino/pino.js"),
},
],
});
assert.equal(v.length, 1);
assert.match(v[0].reason, /rewritten relative chunk/);
});
test("matches a scoped package on its last segment", () => {
const v = findInlinedExternals({
externals: ["@napi-rs/keyring"],
buildFiles: ["index.js", "keyring-QQ11.js"],
entrySource: "",
});
assert.equal(v.length, 1);
assert.equal(v[0].pkg, "@napi-rs/keyring");
assert.equal(violations.length, 1);
assert.equal(violations[0].pkg, "pino");
assert.match(violations[0].reason, /index\.js/);
});
test("passes a genuinely external package", () => {
assert.deepEqual(
findInlinedExternals({
externals: ["undici", "pino"],
buildFiles: ["index.js"],
entrySource: 'await import("undici"); import "pino";',
}),
[],
);
const { noBanners, violations } = findInlinedExternals({
externals: ["undici", "pino"],
files: [
{
name: "index.js",
source:
banner("../../core/mcp/node/proxyFetch.ts") +
'await import("undici");\nimport "pino";\n',
},
],
});
assert.equal(noBanners, false);
assert.deepEqual(violations, []);
});
test("does not mistake an unrelated chunk for the package", () => {
// `undici-types` is a @types transitive; a substring match would flag it.
assert.deepEqual(
findInlinedExternals({
externals: ["undici"],
buildFiles: ["index.js", "undici-types-AA22.js"],
entrySource: "",
}),
[],
);
test("does not mistake a same-prefix package for the external", () => {
// `undici-types` is a @types transitive and is legitimately inlined; the
// trailing slash in the needle is what keeps it from reading as `undici`.
const { violations } = findInlinedExternals({
externals: ["undici"],
files: [
{
name: "index.js",
source: banner("../../node_modules/undici-types/index.js"),
},
],
});
assert.deepEqual(violations, []);
});
test("matches a scoped package on its full name", () => {
const { violations } = findInlinedExternals({
externals: ["@napi-rs/keyring"],
files: [
{
name: "index.js",
source: banner("../../node_modules/@napi-rs/keyring/index.js"),
},
],
});
assert.equal(violations.length, 1);
assert.equal(violations[0].pkg, "@napi-rs/keyring");
});
test("reports noBanners rather than a clean pass when banners are absent", () => {
// The guard reads esbuild's `// <path>` module banners, so minified output
// would make every bundle look clean. That must fail loudly instead —
// otherwise enabling `minify` silently retires the check.
const { noBanners, violations } = findInlinedExternals({
externals: ["undici"],
files: [{ name: "index.js", source: "var a=1;var b=2;" }],
});
assert.equal(noBanners, true);
assert.deepEqual(violations, []);
});
});