Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
fc2ed31
Keep project env fetch credentials authoritative
kojiwakayama Aug 3, 2026
c5da86b
Keep same-origin SSR module imports on pinned paths
kojiwakayama Aug 3, 2026
aecfdcf
Keep prebundled RSC scripts aligned with pinned module paths
kojiwakayama Aug 3, 2026
20283a9
Separate SSR module pinning transports
kojiwakayama Aug 3, 2026
fa31d3b
fix(ssr): canonicalize HTTP module specifiers
kojiwakayama Aug 3, 2026
a1227db
fix(mdx): preserve trusted local project identity
kojiwakayama Aug 3, 2026
53e7d2b
fix(ssr): preserve nested protocol-relative URLs
kojiwakayama Aug 3, 2026
64237b2
fix(mdx): validate pinned module path transport
kojiwakayama Aug 3, 2026
5872f71
test(ssr): name local-project loader argument
kojiwakayama Aug 3, 2026
33f6e70
test(modules): remove vacuous fetch assertions
kojiwakayama Aug 3, 2026
26350e5
fix(ssr): harden protocol-relative resolution and address review find…
kwakayama Aug 3, 2026
190f488
test(mdx): migrate remaining loadModuleESM call site to options object
kwakayama Aug 3, 2026
d488ddb
Fail closed on malformed pinned module paths
kojiwakayama Aug 3, 2026
32035c2
Preserve MDX loader compatibility while rejecting bad pins
kojiwakayama Aug 3, 2026
0ac8d33
Classify reserved dependency pin paths
kojiwakayama Aug 3, 2026
3296f83
Handle undefined MDX load options
kojiwakayama Aug 3, 2026
02d0801
Keep MDX review coverage colocated
kojiwakayama Aug 3, 2026
cff5921
Close remaining MDX review gaps
kojiwakayama Aug 3, 2026
8d21877
Reject nested dependency pin path markers
kojiwakayama Aug 3, 2026
244a57b
Remove obsolete pin encoding helper
kojiwakayama Aug 3, 2026
b09a711
Preserve nested project pin paths and refresh RSC bundle
kojiwakayama Aug 3, 2026
5594eee
Preserve ordinary _pins source paths under snapshot wrappers
kojiwakayama Aug 3, 2026
ae08709
Reject nested dependency pin path markers
kojiwakayama Aug 3, 2026
80351a6
Keep CI checks deterministic for PR 3332
kojiwakayama Aug 3, 2026
6d3f8b3
Merge main into PR 3332 after request-init helper changes
kojiwakayama Aug 3, 2026
958de92
Fail closed on malformed pinned MDX transport errors
kojiwakayama Aug 3, 2026
c2562b3
Integrate pinned SSR follow-up with current main
kojiwakayama Aug 3, 2026
ed89012
Preserve poisoned-prefix resolver safety
kojiwakayama Aug 3, 2026
1110818
Honor guarded fetch test boundaries
kojiwakayama Aug 3, 2026
7e451d6
Keep pinned SSR transport current with main
kojiwakayama Aug 3, 2026
e1a444c
Stabilize import-map capacity retry coverage
kojiwakayama Aug 3, 2026
ae24ffc
Merge origin/main into codex/pr3290-post-merge-fixes
kojiwakayama Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/modules/server/module-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2486,12 +2486,11 @@ describe({ name: "serveModule", sanitizeResources: false, sanitizeOps: false },
ssrParentUrl.searchParams.set("ssr", "true");
const ssrParentResponse = await serve(new Request(ssrParentUrl), projectDir);
assertEquals(ssrParentResponse.status, 200);
for (const childName of ["Absolute.js", "Protocol.js"]) {
const childFetch = nestedFetches.find((href) =>
new URL(href).pathname.endsWith(`/shared/${childName}`)
);
assertEquals(childFetch !== undefined, true);
const childUrl = new URL(childFetch!);
const ssrParentCode = await ssrParentResponse.text();
assertStringIncludes(ssrParentCode, absolutePath);
assertStringIncludes(ssrParentCode, protocolPath);
for (const childFetch of nestedFetches) {
const childUrl = new URL(childFetch);
assertEquals(childUrl.searchParams.get("ssr"), "true");
assertEquals(childUrl.searchParams.get("pins"), snapshot.cacheKey);
}
Expand Down
35 changes: 34 additions & 1 deletion src/server/project-env/fetcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import "#veryfront/schemas/_test-setup.ts";
import { assertEquals, assertInstanceOf, assertRejects } from "#veryfront/testing/assert";
import { describe, it } from "#veryfront/testing/bdd";
import { createMockServer } from "../../../tests/_helpers/utils.ts";
import { fetchProjectEnvVars, PROJECT_ENV_RESPONSE_MAX_BYTES } from "./fetcher.ts";
import {
fetchProjectEnvVars,
PROJECT_ENV_RESPONSE_MAX_BYTES,
projectEnvFetcherInternals,
} from "./fetcher.ts";

const INTERNAL_USER_ENV = "VERYFRONT_API_INTERNAL_USER";
const INTERNAL_PASS_ENV = "VERYFRONT_API_INTERNAL_PASS";
Expand Down Expand Up @@ -317,6 +321,35 @@ describe("project-env/fetcher", () => {
}
});

it("keeps required env fetch headers authoritative over optional Headers input", async () => {
const originalFetch = globalThis.fetch;
let capturedHeaders: Headers | undefined;
globalThis.fetch = ((_input: string | URL | Request, init?: RequestInit) => {
capturedHeaders = new Headers(init?.headers);
return Promise.resolve(Response.json({ data: [] }));
}) as typeof fetch;

try {
await projectEnvFetcherInternals.fetchEnvironmentVariables(
"https://api.veryfront.test/internal/project-environment-variables",
"Basic runtime-secret",
"my-project",
"env-123",
undefined,
new Headers({
accept: "text/plain",
authorization: "Bearer attacker",
"x-project-slug": "my-project",
}),
);
assertEquals(capturedHeaders?.get("authorization"), "Basic runtime-secret");
assertEquals(capturedHeaders?.get("accept"), "application/json");
assertEquals(capturedHeaders?.get("x-project-slug"), "my-project");
} finally {
globalThis.fetch = originalFetch;
}
});

it("fails closed when the configured internal endpoint is absent", async () => {
const paths: string[] = [];
const { server, port } = createMockServer((req: Request) => {
Expand Down
13 changes: 8 additions & 5 deletions src/server/project-env/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,11 @@ async function fetchEnvironmentVariables(
headers: HeadersInit = {},
): Promise<Response> {
try {
const requestHeaders = new Headers(headers);
requestHeaders.set("Authorization", authorization);
requestHeaders.set("Accept", "application/json");
return await fetch(url, {
headers: {
Authorization: authorization,
Accept: "application/json",
...headers,
},
headers: requestHeaders,
redirect: "error",
signal,
});
Expand Down Expand Up @@ -270,3 +269,7 @@ export async function fetchProjectEnvVars(
throw error;
}
}

export const projectEnvFetcherInternals = {
fetchEnvironmentVariables,
} as const;
4 changes: 2 additions & 2 deletions src/server/services/rsc/endpoints/rsc-bundles.generated.ts

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion src/transforms/esm/specifier-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import {
/** Function signature for caching an HTTP module and returning its local path. */
export type CacheHttpModuleFn = (url: string, options: CacheOptions) => Promise<string | null>;

function toCacheableHttpSpecifier(specifier: string, moduleServerOrigin?: string): string {
if (!specifier.startsWith("/") || !moduleServerOrigin) return specifier;
return new URL(specifier, moduleServerOrigin).toString();
}

function isLocalMappedSpecifier(specifier: string): boolean {
return specifier.startsWith("/_vf_modules/") ||
specifier.startsWith("_vf_modules/") ||
Expand Down Expand Up @@ -95,7 +100,10 @@ async function resolveSpecifier(
options.dependencyPinningCacheKey,
options.moduleServerOrigin,
);
const cached = await cacheHttpModule(effectiveSpecifier, options);
const cached = await cacheHttpModule(
toCacheableHttpSpecifier(effectiveSpecifier, options.moduleServerOrigin),
options,
);
if (!cached) {
throw new Error(`Failed to cache absolute HTTP module ${effectiveSpecifier}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,10 +253,10 @@ describe("Hydration Parity", () => {
/\n(function (__veryfrontPinDynamicImport_*)[\s\S]+)\n$/,
);
expect(result).toContain(
`from "https://app.example/_vf_modules/shared/StaticAbsolute.js?ssr=true&pins=on%3Asnapshot-a"`,
`from "/_vf_modules/_pins/on%3Asnapshot-a/shared/StaticAbsolute.js?ssr=true"`,
);
expect(result).toContain(
`from "https://app.example/_vf_modules/shared/StaticProtocol.js?ssr=true&pins=on%3Asnapshot-a"`,
`from "/_vf_modules/_pins/on%3Asnapshot-a/shared/StaticProtocol.js?ssr=true"`,
);
expect(result).toContain(
`from "https://cdn.example/_vf_modules/shared/StaticForeign.js"`,
Expand Down
4 changes: 2 additions & 2 deletions src/transforms/import-rewriter/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { relativeToProjectDir } from "./project-paths.ts";
import {
appendDependencyPinningPathKey,
appendSameOriginDependencyPinningPathKey,
appendSameOriginSSRDependencyPinningKey,
appendSameOriginSSRDependencyPinningPathKey,
normalizeExtension,
} from "./url-builder.ts";
import type {
Expand Down Expand Up @@ -39,7 +39,7 @@ function pinSameOriginModuleUrl(
ctx.dependencyPinningCacheKey,
ctx.moduleServerOrigin,
)
: appendSameOriginSSRDependencyPinningKey(
: appendSameOriginSSRDependencyPinningPathKey(
specifier,
ctx.dependencyPinningCacheKey,
ctx.moduleServerOrigin,
Expand Down
40 changes: 40 additions & 0 deletions src/transforms/import-rewriter/url-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
appendDependencyPinningKey,
appendDependencyPinningPathKey,
appendSameOriginDependencyPinningPathKey,
appendSameOriginSSRDependencyPinningKey,
appendSameOriginSSRDependencyPinningPathKey,
buildCrossProjectUrl,
buildEsmShUrl,
buildModuleServerUrl,
Expand Down Expand Up @@ -117,6 +119,44 @@ describe("transforms/import-rewriter/url-builder", () => {
);
});

it("adds query pinning to same-origin SSR module URLs for fetchable imports", () => {
assertEquals(
appendSameOriginSSRDependencyPinningKey(
"https://app.example/_vf_modules/components/Button.js?pins=on%3Astale#default",
"on:snapshot-a",
"https://app.example",
),
"https://app.example/_vf_modules/components/Button.js?pins=on%3Asnapshot-a&ssr=true#default",
);
assertEquals(
appendSameOriginSSRDependencyPinningKey(
"//app.example/_vf_modules/components/Protocol.js?debug=1",
"on:snapshot-a",
"https://app.example",
),
"https://app.example/_vf_modules/components/Protocol.js?debug=1&pins=on%3Asnapshot-a&ssr=true",
);
assertEquals(
appendSameOriginSSRDependencyPinningKey(
"https://cdn.example/_vf_modules/components/Button.js",
"on:snapshot-a",
"https://app.example",
),
"https://cdn.example/_vf_modules/components/Button.js",
);
});

it("canonicalizes same-origin SSR module-server URLs to snapshot paths", () => {
assertEquals(
appendSameOriginSSRDependencyPinningPathKey(
"https://app.example/_vf_modules/components/Button.js?pins=on%3Astale#default",
"on:snapshot-a",
"https://app.example",
),
"/_vf_modules/_pins/on%3Asnapshot-a/components/Button.js?ssr=true#default",
);
});

it("preserves flag-off and non-module targets", () => {
assertEquals(
appendDependencyPinningPathKey("/_vf_modules/components/", "off"),
Expand Down
54 changes: 52 additions & 2 deletions src/transforms/import-rewriter/url-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,58 @@ export function appendSameOriginSSRDependencyPinningKey(
return url;
}

target.searchParams.set("ssr", "true");
return appendDependencyPinningKey(target.href, dependencyPinningCacheKey);
const params = target.searchParams;
params.set("pins", dependencyPinningCacheKey);
params.set("ssr", "true");
return target.toString();
} catch {
return url;
}
}

/**
* Bind a same-origin absolute module URL to an SSR dependency snapshot using
* the module-server path transport. Use this only when the consumer resolves
* `/_vf_modules/` paths through the module server instead of generic fetch.
*/
export function appendSameOriginSSRDependencyPinningPathKey(
url: string,
dependencyPinningCacheKey?: string,
moduleServerOrigin?: string,
): string {
if (
!dependencyPinningCacheKey?.startsWith("on:") ||
!moduleServerOrigin ||
(!/^https?:\/\//i.test(url) && !url.startsWith("//"))
) {
return url;
}

try {
const requestOrigin = new URL(moduleServerOrigin);
const target = new URL(url, requestOrigin);
if (
target.origin !== requestOrigin.origin ||
!target.pathname.startsWith("/_vf_modules/")
) {
return url;
}

const moduleUrl = appendDependencyPinningPathKey(
`${target.pathname}${target.search}${target.hash}`,
dependencyPinningCacheKey,
);
const hashIndex = moduleUrl.indexOf("#");
const hash = hashIndex === -1 ? "" : moduleUrl.slice(hashIndex);
const urlWithoutHash = hashIndex === -1 ? moduleUrl : moduleUrl.slice(0, hashIndex);
const queryIndex = urlWithoutHash.indexOf("?");
const path = queryIndex === -1 ? urlWithoutHash : urlWithoutHash.slice(0, queryIndex);
const params = new URLSearchParams(
queryIndex === -1 ? "" : urlWithoutHash.slice(queryIndex + 1),
);

params.set("ssr", "true");
return `${path}?${params.toString()}${hash}`;
} catch {
return url;
}
Expand Down
4 changes: 2 additions & 2 deletions src/transforms/mdx/esm-module-loader/import-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { buildMdxJsxCacheFileName } from "./cache-format.ts";
import { rewriteDntImports } from "./module-fetcher/index.ts";
import { ensureCachedJsxModulePatched } from "./jsx-cache.ts";
import type { ESMLoaderContext } from "./types.ts";
import { appendSameOriginSSRDependencyPinningKey } from "#veryfront/transforms/import-rewriter/url-builder.ts";
import { appendSameOriginSSRDependencyPinningPathKey } from "#veryfront/transforms/import-rewriter/url-builder.ts";
import {
bareStrategy,
UnifiedImportRewriter,
Expand Down Expand Up @@ -165,7 +165,7 @@ export async function pinSameOriginSSRModuleImports(
if (!dependencyPinningCacheKey?.startsWith("on:") || !moduleServerOrigin) return code;

return await replaceSpecifiers(code, (specifier) => {
const pinned = appendSameOriginSSRDependencyPinningKey(
const pinned = appendSameOriginSSRDependencyPinningPathKey(
specifier,
dependencyPinningCacheKey,
moduleServerOrigin,
Expand Down
1 change: 1 addition & 0 deletions src/transforms/mdx/esm-module-loader/loader-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export async function processVfModuleImports(
context.projectId,
{
contentSourceId: context.contentSourceId,
isLocalProject: context.isLocalProject,
reactVersion: context.reactVersion,
moduleServerOrigin: context.moduleServerOrigin,
dependencyPinningCacheKey: context.dependencyPinningCacheKey,
Expand Down
9 changes: 7 additions & 2 deletions src/transforms/mdx/esm-module-loader/module-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { mdxRenderer } from "../index.ts";
import { denoAdapter } from "#veryfront/platform/adapters/deno.ts";
import { hashString } from "#veryfront/cache/hash.ts";
import { withMockFetch } from "#veryfront/testing/mock-fetch.ts";
import { extractDependencyPinningPathKey } from "#veryfront/transforms/import-rewriter/url-builder.ts";

function cacheKeyForDependencies(
dependencies: Readonly<Record<string, string>>,
Expand Down Expand Up @@ -103,9 +104,12 @@ describe("MDX root module cache identity", () => {
async (input, init) => {
const request = new Request(input, init);
const url = new URL(request.url);
if (url.pathname !== modulePath) return new Response("not found", { status: 404 });
const pinnedPath = extractDependencyPinningPathKey(url.pathname);
if (pinnedPath.pathname !== modulePath) {
return new Response("not found", { status: 404 });
}
if (
url.searchParams.get("pins") !== snapshotPinKey ||
pinnedPath.cacheKey !== snapshotPinKey ||
url.searchParams.get("ssr") !== "true"
) {
rawRequests++;
Expand All @@ -130,6 +134,7 @@ describe("MDX root module cache identity", () => {
dependencies,
projectDir,
origin,
true,
),
);

Expand Down
1 change: 1 addition & 0 deletions src/transforms/mdx/esm-module-loader/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface ESMLoaderContext {
projectDir?: string;
projectSlug?: string;
contentSourceId?: string;
isLocalProject?: boolean;
/** React version for transforms (from project config) */
reactVersion?: string;
/** Absolute request origin used to identify same-origin module URLs. */
Expand Down
2 changes: 2 additions & 0 deletions src/transforms/mdx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class MDXRenderer {
dependencyPinningDependencies?: Readonly<Record<string, string>>,
dependencyPinningSource?: DependencyPinningSourceInput,
moduleServerOrigin?: string,
isLocalProject?: boolean,
Comment thread
kojiwakayama marked this conversation as resolved.
Resolved
): Promise<MDXModule> {
Comment thread
kojiwakayama marked this conversation as resolved.
Resolved
const resolvedDependencyPinningSource = dependencyPinningSource ?? projectDir;
const dependencySnapshot = await resolveDependencyPinningSnapshot(
Expand All @@ -67,6 +68,7 @@ export class MDXRenderer {
moduleServerOrigin: dependencySnapshot.cacheKey.startsWith("on:")
? moduleServerOrigin
: undefined,
isLocalProject,
};

return await loadModuleESM(compiledProgramCode, context);
Expand Down