authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-04 13:33:23-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-04 20:33:23+00:00
log5f4c7161fa7782ea93b63fe934d43229742e7ec8
tree6b389bdb1e94c2ed890c4594f12b7d7bb474f809
parent912276bcd9ca258ad9309727df8e279e5ab698f8
signature Signed by PGP key B5690EEEBB952194

fix(server): canonicalise media paths the same way their callers do (#9575)

Co-authored-by: Claude Code <noreply@anthropic.com>

3 files changed, 22 insertions(+), 11 deletions(-)

apps/server/src/assets/AssetAccess.test.ts+2-2
......@@ -754,7 +754,7 @@ describe("AssetAccess", () => {
754754 projectFaviconPath: "brand/custom.svg",
755755 });
756756
757 expect(result.sourcePath).toBe("brand/custom.svg");
757 expect(result.sourcePath).toBe(path.join("brand", "custom.svg"));
758758 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-custom\.svg$/);
759759 }).pipe(Effect.provide(testLayer)),
760760 );
......@@ -813,7 +813,7 @@ describe("AssetAccess", () => {
813813 projectFaviconPath: "brand/saved.svg",
814814 });
815815
816 expect(result.sourcePath).toBe("brand/saved.svg");
816 expect(result.sourcePath).toBe(path.join("brand", "saved.svg"));
817817 expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-saved\.svg$/);
818818 }).pipe(Effect.provide(testLayer)),
819819 );
apps/server/src/assets/MediaFile.ts+10-1
......@@ -37,6 +37,11 @@ export interface OpenMediaFile {
3737 readonly info: NodeFS.BigIntStats;
3838}
3939
40const realpathLikeFileSystem = (filePath: string) =>
41 new Promise<string>((resolve, reject) => {
42 NodeFS.realpath(filePath, (error, resolved) => (error ? reject(error) : resolve(resolved)));
43 });
44
4045/** Opens a canonical media path once. Replacements cannot change the response's source. */
4146export const openMediaFile = Effect.fn("openMediaFile")(function* (
4247 filePath: string,
......@@ -71,7 +76,11 @@ export const openMediaFile = Effect.fn("openMediaFile")(function* (
7176 ) {
7277 return null;
7378 }
74 if ((await NodeFSP.realpath(filePath)) !== filePath) return null;
79 // Callers canonicalise with Effect's FileSystem.realPath, which is
80 // Node's JS realpath. fs/promises.realpath is the native binding and
81 // on Windows also expands 8.3 short names, so a path that is already
82 // canonical by the caller's rules would still look swapped here.
83 if ((await realpathLikeFileSystem(filePath)) !== filePath) return null;
7584 const after = await NodeFSP.lstat(filePath, { bigint: true });
7685 if (!after.isFile() || info.dev !== after.dev || info.ino !== after.ino) return null;
7786 accepted = true;
apps/server/src/project/ProjectFaviconResolver.test.ts+10-8
......@@ -126,6 +126,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
126126 it.effect("prefers a t3.json iconPath over well-known files", () =>
127127 Effect.gen(function* () {
128128 const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
129 const path = yield* Path.Path;
129130 const cwd = yield* makeTempDir;
130131 yield* writeTextFile(cwd, "t3.json", '{ "iconPath": "brand/mark.svg" }');
131132 yield* writeTextFile(cwd, "brand/mark.svg", "<svg>mark</svg>");
......@@ -134,13 +135,14 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
134135 const resolved = yield* resolver.resolvePath(cwd);
135136
136137 expect(resolved).not.toBeNull();
137 expect(resolved).toContain("brand/mark.svg");
138 expect(resolved).toBe(path.join(cwd, "brand", "mark.svg"));
138139 }),
139140 );
140141
141142 it.effect("uses a saved project favicon override", () =>
142143 Effect.gen(function* () {
143144 const resolver = yield* ProjectFaviconResolver.ProjectFaviconResolver;
145 const path = yield* Path.Path;
144146 const cwd = yield* makeTempDir;
145147 yield* writeTextFile(cwd, "brand/custom.svg", "<svg>custom</svg>");
146148 yield* writeTextFile(cwd, "favicon.svg", "<svg>automatic</svg>");
......@@ -148,7 +150,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
148150 const resolved = yield* resolver.resolvePath(cwd, "brand/custom.svg");
149151
150152 expect(resolved).not.toBeNull();
151 expect(resolved).toContain("brand/custom.svg");
153 expect(resolved).toBe(path.join(cwd, "brand", "custom.svg"));
152154 }),
153155 );
154156
......@@ -232,7 +234,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
232234 const resolved = yield* resolver.resolvePath(cwd);
233235
234236 expect(resolved).not.toBeNull();
235 expect(resolved).toContain("public/brand/logo.svg");
237 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
236238 }),
237239 );
238240
......@@ -257,7 +259,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
257259 const resolved = yield* resolver.resolvePath(cwd);
258260
259261 expect(resolved).not.toBeNull();
260 expect(resolved).toContain("public/brand/logo.svg");
262 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
261263 }),
262264 );
263265
......@@ -275,7 +277,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
275277 const resolved = yield* resolver.resolvePath(cwd);
276278
277279 expect(resolved).not.toBeNull();
278 expect(resolved).toContain("public/brand/logo.svg");
280 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
279281 }),
280282 );
281283
......@@ -293,7 +295,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
293295 const resolved = yield* resolver.resolvePath(cwd);
294296
295297 expect(resolved).not.toBeNull();
296 expect(resolved).toContain("public/brand/logo.svg");
298 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
297299 }),
298300 );
299301
......@@ -311,7 +313,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
311313 const resolved = yield* resolver.resolvePath(cwd);
312314
313315 expect(resolved).not.toBeNull();
314 expect(resolved).toContain("public/brand/logo.svg");
316 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
315317 }),
316318 );
317319
......@@ -459,7 +461,7 @@ it.layer(TestLayer)("ProjectFaviconResolverLive", (it) => {
459461 const resolved = yield* resolver.resolvePath(cwd);
460462
461463 expect(resolved).not.toBeNull();
462 expect(resolved).toContain("public/brand/logo.svg");
464 expect(resolved).toBe((yield* Path.Path).join(cwd, "public", "brand", "logo.svg"));
463465 }),
464466 );
465467 });