// @effect-diagnostics nodeBuiltinImport:off - tests inject swaps at the native open boundary.import * as NodeServices from "@effect/platform-node/NodeServices";import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform";import * as NodeFSP from "node:fs/promises";import { AssetPreviewTypeValidationError, ThreadId } from "@t3tools/contracts";import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon";import { describe, expect, it } from "@effect/vitest";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path";import * as PlatformError from "effect/PlatformError";import * as TestClock from "effect/testing/TestClock";import { HttpServerResponse } from "effect/unstable/http";import { vi } from "vite-plus/test"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts";import * as ServerConfig from "../config.ts";import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";import * as T3ProjectFileLoader from "../project/T3ProjectFileLoader.ts";import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";import { assetFileResponse } from "../http.ts";import { ASSET_ROUTE_PREFIX, issueAssetUrl, resolveAsset } from "./AssetAccess.ts";import * as NativeAppIconResolver from "./NativeAppIconResolver.ts";import { openMediaFile } from "./MediaFile.ts";import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal<typeof NodeFSP>(); return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) };}); const configLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-asset-access-test-",});const testLayer = Layer.mergeAll( NodeHttpPlatform.layer, configLayer, WorkspacePaths.layer, ProjectFaviconResolver.layer.pipe( Layer.provide(WorkspacePaths.layer), Layer.provide(T3ProjectFileLoader.layer), ), NativeAppIconResolver.layer.pipe(Layer.provide(configLayer)), ServerSecretStore.layer.pipe(Layer.provide(configLayer)),).pipe(Layer.provideMerge(NodeServices.layer)); describe("AssetAccess", () => { it.effect("issues exact URLs for media and browser documents outside the workspace", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-root-" }); const outside = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-outside-" }); for (const [name, mimeType] of [ ["screenshot.png", "image/png"], ["recording.mp4", "video/mp4"], ["recording.webm", "video/webm"], ["report.html", "text/html"], ["report.pdf", "application/pdf"], ] as const) { const filePath = path.join(outside, name); yield* fs.writeFileString(filePath, "media"); const canonicalFile = yield* fs.realPath(filePath); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, workspaceRoot: root, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const token = suffix.slice(0, separator); expect(yield* resolveAsset(token, suffix.slice(separator + 1))).toMatchObject({ kind: "file", path: canonicalFile, mimeType, }); yield* fs.writeFileString(path.join(outside, "sibling.png"), "private sibling"); expect(yield* resolveAsset(token, "sibling.png")).toBeNull(); expect(yield* resolveAsset(token, `../${name}`)).toBeNull(); expect(yield* resolveAsset(`${token}tampered`, name)).toBeNull(); } }).pipe(Effect.provide(testLayer)), ); it.effect("reports pixel dimensions from an image header and nothing for other files", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-dimensions-" }); const png = Uint8Array.from([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 13, 0x49, 0x48, 0x44, 0x52, 0, 0, 0x06, 0x40, 0, 0, 0x03, 0x84, ]); yield* fs.writeFile(path.join(root, "shot.png"), png); yield* fs.writeFileString(path.join(root, "clip.mp4"), "video"); yield* fs.writeFileString(path.join(root, "broken.png"), "not a png"); const issue = (name: string) => issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: name }, workspaceRoot: root, }); expect((yield* issue("shot.png")).imageDimensions).toEqual({ width: 1600, height: 900 }); expect((yield* issue("clip.mp4")).imageDimensions).toBeUndefined(); expect((yield* issue("broken.png")).imageDimensions).toBeUndefined(); }).pipe(Effect.provide(testLayer)), ); it.effect("resolves relative media paths from the thread workspace, including outside it", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-relative-" }); const root = path.join(directory, "workspace"); yield* fs.makeDirectory(root); for (const relativePath of ["screenshot.png", "../recording.mp4"]) { const filePath = path.resolve(root, relativePath); yield* fs.writeFileString(filePath, "media"); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: relativePath }, workspaceRoot: root, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); expect( yield* resolveAsset(suffix.slice(0, separator), suffix.slice(separator + 1)), ).toMatchObject({ kind: "file", path: yield* fs.realPath(filePath), }); } }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "rejects non-previewable files, disguised targets, and directories", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-validation-" }); for (const name of ["report.md", "secret.txt", "secret.%70ng", "secret.png#private.txt"]) { const filePath = path.join(root, name); yield* fs.writeFileString(filePath, "not media"); const error = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, }).pipe(Effect.flip); expect(error).toBeInstanceOf(AssetPreviewTypeValidationError); } const disguisedPath = path.join(root, "disguised.png"); yield* fs.symlink(path.join(root, "secret.txt"), disguisedPath); const disguisedError = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: disguisedPath, }, }).pipe(Effect.flip); expect(disguisedError).toBeInstanceOf(AssetPreviewTypeValidationError); const directoryPath = path.join(root, "directory.png"); yield* fs.makeDirectory(directoryPath); const directoryError = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: directoryPath, }, }).pipe(Effect.flip); expect(directoryError._tag).toBe("AssetWorkspaceAssetNotFoundError"); }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "binds media URLs to the canonical target and rejects symlink substitution", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-symlink-" }); const filePath = path.join(root, "actual.svg"); const aliasPath = path.join(root, "alias.png"); const replacementPath = path.join(root, "other.svg"); yield* fs.writeFileString(filePath, "<svg/>"); yield* fs.writeFileString(replacementPath, "<svg>private</svg>"); yield* fs.symlink(filePath, aliasPath); const canonicalFile = yield* fs.realPath(filePath); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: aliasPath }, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const token = suffix.slice(0, separator); const name = suffix.slice(separator + 1); const expected = { kind: "file", path: canonicalFile, mimeType: "image/svg+xml" }; expect(yield* resolveAsset(token, name)).toMatchObject(expected); yield* fs.remove(aliasPath); yield* fs.symlink(replacementPath, aliasPath); expect(yield* resolveAsset(token, name)).toMatchObject(expected); yield* fs.remove(filePath); yield* fs.symlink(replacementPath, filePath); expect(yield* resolveAsset(token, name)).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "keeps full and partial responses bound to the file opened during resolution", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-open-file-" }); const filePath = path.join(root, "recording.mp4"); const savedPath = path.join(root, "saved.mp4"); const secretPath = path.join(root, "secret.txt"); yield* fs.writeFileString(filePath, "0123456789"); yield* fs.writeFileString(secretPath, "private information"); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); for (const [range, expected, status] of [ [undefined, "0123456789", 200], ["bytes=2-5", "2345", 206], ] as const) { const asset = yield* resolveAsset( suffix.slice(0, separator), suffix.slice(separator + 1), ); if (!asset) throw new Error("Expected a resolved media file"); yield* fs.rename(filePath, savedPath); yield* fs.symlink(secretPath, filePath); const response = HttpServerResponse.toWeb(yield* assetFileResponse(asset, range)); expect(response.status).toBe(status); expect(response.headers.get("content-length")).toBe(String(expected.length)); expect(yield* Effect.promise(() => response.text())).toBe(expected); yield* fs.remove(filePath); yield* fs.rename(savedPath, filePath); } }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "rejects a symlink swapped in after canonical validation but before open", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-open-race-" }); const filePath = path.join(root, "recording.mp4"); const secretPath = path.join(root, "secret.txt"); yield* fs.writeFileString(filePath, "video"); yield* fs.writeFileString(secretPath, "secret"); const canonicalPath = yield* fs.realPath(filePath); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const swappingFileSystem = FileSystem.FileSystem.of({ ...fs, stat: Effect.fn(function* (requestedPath) { const info = yield* fs.stat(requestedPath); if (requestedPath === canonicalPath) { yield* fs.remove(filePath); yield* fs.symlink(secretPath, filePath); } return info; }), }); expect( yield* resolveAsset(suffix.slice(0, separator), suffix.slice(separator + 1)).pipe( Effect.provideService(FileSystem.FileSystem, swappingFileSystem), ), ).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "closes a descriptor rejected when its path changes during open", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-open-rejected-" }); const filePath = path.join(root, "recording.mp4"); const secretPath = path.join(root, "secret.txt"); yield* fs.writeFileString(filePath, "video"); yield* fs.writeFileString(secretPath, "secret"); const canonicalPath = yield* fs.realPath(filePath); const originalOpen = (yield* Effect.promise(() => vi.importActual<typeof NodeFSP>("node:fs/promises"), )).open; let opened: NodeFSP.FileHandle | undefined; const openSpy = vi.mocked(NodeFSP.open).mockImplementation(async (target, flags, mode) => { const handle = await originalOpen(target, flags, mode); if (target === canonicalPath) { opened = handle; await NodeFSP.unlink(filePath); await NodeFSP.symlink(secretPath, filePath); } return handle; }); yield* Effect.addFinalizer(() => Effect.sync(() => openSpy.mockImplementation(originalOpen)), ); expect(yield* openMediaFile(canonicalPath)).toBeNull(); expect(opened).toBeDefined(); expect(opened?.fd).toBe(-1); }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "rejects an ancestor symlink race even when canonical path rechecks would pass", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-parent-race-" }); const publicDirectory = path.join(root, "public"); const privateDirectory = path.join(root, "private"); yield* fs.makeDirectory(publicDirectory); yield* fs.makeDirectory(privateDirectory); const filePath = path.join(publicDirectory, "recording.mp4"); yield* fs.writeFileString(filePath, "public video"); yield* fs.writeFileString(path.join(privateDirectory, "recording.mp4"), "private video"); const canonicalPath = yield* fs.realPath(filePath); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const native = yield* Effect.promise(() => vi.importActual<typeof NodeFSP>("node:fs/promises"), ); const savedDirectory = path.join(root, "saved"); const realpathSpy = vi.mocked(NodeFSP.realpath).mockImplementationOnce(async () => { // A pathname-only guard can see the original parents during realpath, // but the private file during both lstat calls and open. await native.unlink(publicDirectory); await native.rename(savedDirectory, publicDirectory); const canonical = await native.realpath(canonicalPath); await native.rename(publicDirectory, savedDirectory); await native.symlink(privateDirectory, publicDirectory, "junction"); return canonical; }); yield* Effect.addFinalizer(() => Effect.sync(() => realpathSpy.mockReset().mockImplementation(native.realpath)), ); const swappingFileSystem = FileSystem.FileSystem.of({ ...fs, realPath: Effect.fn(function* (requestedPath) { const canonical = yield* fs.realPath(requestedPath); if (requestedPath === canonicalPath) { yield* fs.rename(publicDirectory, savedDirectory); yield* Effect.promise(() => NodeFSP.symlink(privateDirectory, publicDirectory, "junction"), ); } return canonical; }), }); expect( yield* resolveAsset(suffix.slice(0, separator), suffix.slice(separator + 1)).pipe( Effect.provideService(FileSystem.FileSystem, swappingFileSystem), ), ).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect("keeps in-place edits readable but requires a new URL after atomic replacement", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-replacement-" }); const filePath = path.join(root, "recording.mp4"); yield* fs.writeFileString(filePath, "original"); const input = { resource: { _tag: "media-file" as const, threadId: ThreadId.make("thread-1"), path: filePath, }, }; const original = yield* issueAssetUrl(input); const suffix = original.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const token = suffix.slice(0, separator); const name = suffix.slice(separator + 1); yield* fs.writeFileString(filePath, "in-place edit"); const edited = yield* resolveAsset(token, name); if (!edited) throw new Error("Expected the edited media file"); const editedResponse = HttpServerResponse.toWeb(yield* assetFileResponse(edited)); expect(yield* Effect.promise(() => editedResponse.text())).toBe("in-place edit"); const replacement = path.join(root, "replacement.mp4"); yield* fs.writeFileString(replacement, "replacement"); yield* fs.rename(replacement, filePath); expect(yield* resolveAsset(token, name)).toBeNull(); const renewed = yield* issueAssetUrl(input); const renewedSuffix = renewed.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const renewedSeparator = renewedSuffix.indexOf("/"); const renewedAsset = yield* resolveAsset( renewedSuffix.slice(0, renewedSeparator), renewedSuffix.slice(renewedSeparator + 1), ); if (!renewedAsset) throw new Error("Expected the replacement media file"); const renewedResponse = HttpServerResponse.toWeb(yield* assetFileResponse(renewedAsset)); expect(yield* Effect.promise(() => renewedResponse.text())).toBe("replacement"); yield* fs.remove(filePath); expect( yield* resolveAsset( renewedSuffix.slice(0, renewedSeparator), renewedSuffix.slice(renewedSeparator + 1), ), ).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect("issues workspace URLs that resolve the entry file and sibling assets", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-asset-workspace-", }); const htmlPath = path.join(root, "report.html"); const cssPath = path.join(root, "report.css"); yield* fileSystem.writeFileString(htmlPath, '<link rel="stylesheet" href="report.css">'); yield* fileSystem.writeFileString(cssPath, "body { color: red; }"); yield* fileSystem.writeFileString(path.join(root, ".env"), "SECRET=value"); const canonicalHtmlPath = yield* fileSystem.realPath(htmlPath); const canonicalCssPath = yield* fileSystem.realPath(cssPath); const result = yield* issueAssetUrl({ resource: { _tag: "workspace-file", threadId: ThreadId.make("thread-1"), path: htmlPath, }, workspaceRoot: root, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separatorIndex = suffix.indexOf("/"); const token = suffix.slice(0, separatorIndex); expect(yield* resolveAsset(token, "report.html")).toEqual({ kind: "file", path: canonicalHtmlPath, }); expect(yield* resolveAsset(token, "report.css")).toEqual({ kind: "file", path: canonicalCssPath, }); expect(yield* resolveAsset(token, "../secret.txt")).toBeNull(); expect(yield* resolveAsset(token, ".env")).toBeNull(); expect(yield* resolveAsset(`${token}tampered`, "report.html")).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect("rejects workspace files outside the authorized root", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-asset-root-", }); const outside = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-asset-outside-", }); const htmlPath = path.join(outside, "report.html"); yield* fileSystem.writeFileString(htmlPath, "<p>outside</p>"); const error = yield* issueAssetUrl({ resource: { _tag: "workspace-file", threadId: ThreadId.make("thread-1"), path: htmlPath, }, workspaceRoot: root, }).pipe(Effect.flip); expect(error.message).toBe("Workspace file path must be relative to the project root."); expect(error).toMatchObject({ _tag: "AssetWorkspacePathValidationError", resource: { _tag: "workspace-file", threadId: "thread-1", path: htmlPath, }, }); expect(error.cause).toBeInstanceOf(WorkspacePaths.WorkspacePathOutsideRootError); }).pipe(Effect.provide(testLayer)), ); it.effect("preserves non-missing canonical path failures when issuing asset URLs", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem;Lines 1–500 of 941. Load more
// @effect-diagnostics nodeBuiltinImport:off - tests inject swaps at the native open boundary. import * as NodeServices from "@effect/platform-node/NodeServices"; import * as NodeHttpPlatform from "@effect/platform-node/NodeHttpPlatform"; import * as NodeFSP from "node:fs/promises"; import { AssetPreviewTypeValidationError, ThreadId } from "@t3tools/contracts"; import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon"; import { describe, expect, it } from "@effect/vitest"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import * as TestClock from "effect/testing/TestClock"; import { HttpServerResponse } from "effect/unstable/http"; import { vi } from "vite-plus/test"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as ServerConfig from "../config.ts"; import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts"; import * as T3ProjectFileLoader from "../project/T3ProjectFileLoader.ts"; import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; import { assetFileResponse } from "../http.ts"; import { ASSET_ROUTE_PREFIX, issueAssetUrl, resolveAsset } from "./AssetAccess.ts"; import * as NativeAppIconResolver from "./NativeAppIconResolver.ts"; import { openMediaFile } from "./MediaFile.ts"; import { symlinksSupported } from "@t3tools/shared/testing/symlinks"; vi.mock("node:fs/promises", async (importOriginal) => { const actual = await importOriginal<typeof NodeFSP>(); return { ...actual, open: vi.fn(actual.open), realpath: vi.fn(actual.realpath) }; }); const configLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-asset-access-test-", }); const testLayer = Layer.mergeAll( NodeHttpPlatform.layer, configLayer, WorkspacePaths.layer, ProjectFaviconResolver.layer.pipe( Layer.provide(WorkspacePaths.layer), Layer.provide(T3ProjectFileLoader.layer), ), NativeAppIconResolver.layer.pipe(Layer.provide(configLayer)), ServerSecretStore.layer.pipe(Layer.provide(configLayer)), ).pipe(Layer.provideMerge(NodeServices.layer)); describe("AssetAccess", () => { it.effect("issues exact URLs for media and browser documents outside the workspace", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-root-" }); const outside = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-outside-" }); for (const [name, mimeType] of [ ["screenshot.png", "image/png"], ["recording.mp4", "video/mp4"], ["recording.webm", "video/webm"], ["report.html", "text/html"], ["report.pdf", "application/pdf"], ] as const) { const filePath = path.join(outside, name); yield* fs.writeFileString(filePath, "media"); const canonicalFile = yield* fs.realPath(filePath); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, workspaceRoot: root, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const token = suffix.slice(0, separator); expect(yield* resolveAsset(token, suffix.slice(separator + 1))).toMatchObject({ kind: "file", path: canonicalFile, mimeType, }); yield* fs.writeFileString(path.join(outside, "sibling.png"), "private sibling"); expect(yield* resolveAsset(token, "sibling.png")).toBeNull(); expect(yield* resolveAsset(token, `../${name}`)).toBeNull(); expect(yield* resolveAsset(`${token}tampered`, name)).toBeNull(); } }).pipe(Effect.provide(testLayer)), ); it.effect("reports pixel dimensions from an image header and nothing for other files", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-dimensions-" }); const png = Uint8Array.from([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 13, 0x49, 0x48, 0x44, 0x52, 0, 0, 0x06, 0x40, 0, 0, 0x03, 0x84, ]); yield* fs.writeFile(path.join(root, "shot.png"), png); yield* fs.writeFileString(path.join(root, "clip.mp4"), "video"); yield* fs.writeFileString(path.join(root, "broken.png"), "not a png"); const issue = (name: string) => issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: name }, workspaceRoot: root, }); expect((yield* issue("shot.png")).imageDimensions).toEqual({ width: 1600, height: 900 }); expect((yield* issue("clip.mp4")).imageDimensions).toBeUndefined(); expect((yield* issue("broken.png")).imageDimensions).toBeUndefined(); }).pipe(Effect.provide(testLayer)), ); it.effect("resolves relative media paths from the thread workspace, including outside it", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-relative-" }); const root = path.join(directory, "workspace"); yield* fs.makeDirectory(root); for (const relativePath of ["screenshot.png", "../recording.mp4"]) { const filePath = path.resolve(root, relativePath); yield* fs.writeFileString(filePath, "media"); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: relativePath }, workspaceRoot: root, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); expect( yield* resolveAsset(suffix.slice(0, separator), suffix.slice(separator + 1)), ).toMatchObject({ kind: "file", path: yield* fs.realPath(filePath), }); } }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "rejects non-previewable files, disguised targets, and directories", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-validation-" }); for (const name of ["report.md", "secret.txt", "secret.%70ng", "secret.png#private.txt"]) { const filePath = path.join(root, name); yield* fs.writeFileString(filePath, "not media"); const error = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, }).pipe(Effect.flip); expect(error).toBeInstanceOf(AssetPreviewTypeValidationError); } const disguisedPath = path.join(root, "disguised.png"); yield* fs.symlink(path.join(root, "secret.txt"), disguisedPath); const disguisedError = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: disguisedPath, }, }).pipe(Effect.flip); expect(disguisedError).toBeInstanceOf(AssetPreviewTypeValidationError); const directoryPath = path.join(root, "directory.png"); yield* fs.makeDirectory(directoryPath); const directoryError = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: directoryPath, }, }).pipe(Effect.flip); expect(directoryError._tag).toBe("AssetWorkspaceAssetNotFoundError"); }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "binds media URLs to the canonical target and rejects symlink substitution", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-symlink-" }); const filePath = path.join(root, "actual.svg"); const aliasPath = path.join(root, "alias.png"); const replacementPath = path.join(root, "other.svg"); yield* fs.writeFileString(filePath, "<svg/>"); yield* fs.writeFileString(replacementPath, "<svg>private</svg>"); yield* fs.symlink(filePath, aliasPath); const canonicalFile = yield* fs.realPath(filePath); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: aliasPath }, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const token = suffix.slice(0, separator); const name = suffix.slice(separator + 1); const expected = { kind: "file", path: canonicalFile, mimeType: "image/svg+xml" }; expect(yield* resolveAsset(token, name)).toMatchObject(expected); yield* fs.remove(aliasPath); yield* fs.symlink(replacementPath, aliasPath); expect(yield* resolveAsset(token, name)).toMatchObject(expected); yield* fs.remove(filePath); yield* fs.symlink(replacementPath, filePath); expect(yield* resolveAsset(token, name)).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "keeps full and partial responses bound to the file opened during resolution", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-open-file-" }); const filePath = path.join(root, "recording.mp4"); const savedPath = path.join(root, "saved.mp4"); const secretPath = path.join(root, "secret.txt"); yield* fs.writeFileString(filePath, "0123456789"); yield* fs.writeFileString(secretPath, "private information"); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); for (const [range, expected, status] of [ [undefined, "0123456789", 200], ["bytes=2-5", "2345", 206], ] as const) { const asset = yield* resolveAsset( suffix.slice(0, separator), suffix.slice(separator + 1), ); if (!asset) throw new Error("Expected a resolved media file"); yield* fs.rename(filePath, savedPath); yield* fs.symlink(secretPath, filePath); const response = HttpServerResponse.toWeb(yield* assetFileResponse(asset, range)); expect(response.status).toBe(status); expect(response.headers.get("content-length")).toBe(String(expected.length)); expect(yield* Effect.promise(() => response.text())).toBe(expected); yield* fs.remove(filePath); yield* fs.rename(savedPath, filePath); } }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "rejects a symlink swapped in after canonical validation but before open", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-open-race-" }); const filePath = path.join(root, "recording.mp4"); const secretPath = path.join(root, "secret.txt"); yield* fs.writeFileString(filePath, "video"); yield* fs.writeFileString(secretPath, "secret"); const canonicalPath = yield* fs.realPath(filePath); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const swappingFileSystem = FileSystem.FileSystem.of({ ...fs, stat: Effect.fn(function* (requestedPath) { const info = yield* fs.stat(requestedPath); if (requestedPath === canonicalPath) { yield* fs.remove(filePath); yield* fs.symlink(secretPath, filePath); } return info; }), }); expect( yield* resolveAsset(suffix.slice(0, separator), suffix.slice(separator + 1)).pipe( Effect.provideService(FileSystem.FileSystem, swappingFileSystem), ), ).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "closes a descriptor rejected when its path changes during open", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-open-rejected-" }); const filePath = path.join(root, "recording.mp4"); const secretPath = path.join(root, "secret.txt"); yield* fs.writeFileString(filePath, "video"); yield* fs.writeFileString(secretPath, "secret"); const canonicalPath = yield* fs.realPath(filePath); const originalOpen = (yield* Effect.promise(() => vi.importActual<typeof NodeFSP>("node:fs/promises"), )).open; let opened: NodeFSP.FileHandle | undefined; const openSpy = vi.mocked(NodeFSP.open).mockImplementation(async (target, flags, mode) => { const handle = await originalOpen(target, flags, mode); if (target === canonicalPath) { opened = handle; await NodeFSP.unlink(filePath); await NodeFSP.symlink(secretPath, filePath); } return handle; }); yield* Effect.addFinalizer(() => Effect.sync(() => openSpy.mockImplementation(originalOpen)), ); expect(yield* openMediaFile(canonicalPath)).toBeNull(); expect(opened).toBeDefined(); expect(opened?.fd).toBe(-1); }).pipe(Effect.provide(testLayer)), ); it.effect.skipIf(!symlinksSupported)( "rejects an ancestor symlink race even when canonical path rechecks would pass", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-parent-race-" }); const publicDirectory = path.join(root, "public"); const privateDirectory = path.join(root, "private"); yield* fs.makeDirectory(publicDirectory); yield* fs.makeDirectory(privateDirectory); const filePath = path.join(publicDirectory, "recording.mp4"); yield* fs.writeFileString(filePath, "public video"); yield* fs.writeFileString(path.join(privateDirectory, "recording.mp4"), "private video"); const canonicalPath = yield* fs.realPath(filePath); const result = yield* issueAssetUrl({ resource: { _tag: "media-file", threadId: ThreadId.make("thread-1"), path: filePath }, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const native = yield* Effect.promise(() => vi.importActual<typeof NodeFSP>("node:fs/promises"), ); const savedDirectory = path.join(root, "saved"); const realpathSpy = vi.mocked(NodeFSP.realpath).mockImplementationOnce(async () => { // A pathname-only guard can see the original parents during realpath, // but the private file during both lstat calls and open. await native.unlink(publicDirectory); await native.rename(savedDirectory, publicDirectory); const canonical = await native.realpath(canonicalPath); await native.rename(publicDirectory, savedDirectory); await native.symlink(privateDirectory, publicDirectory, "junction"); return canonical; }); yield* Effect.addFinalizer(() => Effect.sync(() => realpathSpy.mockReset().mockImplementation(native.realpath)), ); const swappingFileSystem = FileSystem.FileSystem.of({ ...fs, realPath: Effect.fn(function* (requestedPath) { const canonical = yield* fs.realPath(requestedPath); if (requestedPath === canonicalPath) { yield* fs.rename(publicDirectory, savedDirectory); yield* Effect.promise(() => NodeFSP.symlink(privateDirectory, publicDirectory, "junction"), ); } return canonical; }), }); expect( yield* resolveAsset(suffix.slice(0, separator), suffix.slice(separator + 1)).pipe( Effect.provideService(FileSystem.FileSystem, swappingFileSystem), ), ).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect("keeps in-place edits readable but requires a new URL after atomic replacement", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-media-replacement-" }); const filePath = path.join(root, "recording.mp4"); yield* fs.writeFileString(filePath, "original"); const input = { resource: { _tag: "media-file" as const, threadId: ThreadId.make("thread-1"), path: filePath, }, }; const original = yield* issueAssetUrl(input); const suffix = original.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separator = suffix.indexOf("/"); const token = suffix.slice(0, separator); const name = suffix.slice(separator + 1); yield* fs.writeFileString(filePath, "in-place edit"); const edited = yield* resolveAsset(token, name); if (!edited) throw new Error("Expected the edited media file"); const editedResponse = HttpServerResponse.toWeb(yield* assetFileResponse(edited)); expect(yield* Effect.promise(() => editedResponse.text())).toBe("in-place edit"); const replacement = path.join(root, "replacement.mp4"); yield* fs.writeFileString(replacement, "replacement"); yield* fs.rename(replacement, filePath); expect(yield* resolveAsset(token, name)).toBeNull(); const renewed = yield* issueAssetUrl(input); const renewedSuffix = renewed.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const renewedSeparator = renewedSuffix.indexOf("/"); const renewedAsset = yield* resolveAsset( renewedSuffix.slice(0, renewedSeparator), renewedSuffix.slice(renewedSeparator + 1), ); if (!renewedAsset) throw new Error("Expected the replacement media file"); const renewedResponse = HttpServerResponse.toWeb(yield* assetFileResponse(renewedAsset)); expect(yield* Effect.promise(() => renewedResponse.text())).toBe("replacement"); yield* fs.remove(filePath); expect( yield* resolveAsset( renewedSuffix.slice(0, renewedSeparator), renewedSuffix.slice(renewedSeparator + 1), ), ).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect("issues workspace URLs that resolve the entry file and sibling assets", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-asset-workspace-", }); const htmlPath = path.join(root, "report.html"); const cssPath = path.join(root, "report.css"); yield* fileSystem.writeFileString(htmlPath, '<link rel="stylesheet" href="report.css">'); yield* fileSystem.writeFileString(cssPath, "body { color: red; }"); yield* fileSystem.writeFileString(path.join(root, ".env"), "SECRET=value"); const canonicalHtmlPath = yield* fileSystem.realPath(htmlPath); const canonicalCssPath = yield* fileSystem.realPath(cssPath); const result = yield* issueAssetUrl({ resource: { _tag: "workspace-file", threadId: ThreadId.make("thread-1"), path: htmlPath, }, workspaceRoot: root, }); const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length); const separatorIndex = suffix.indexOf("/"); const token = suffix.slice(0, separatorIndex); expect(yield* resolveAsset(token, "report.html")).toEqual({ kind: "file", path: canonicalHtmlPath, }); expect(yield* resolveAsset(token, "report.css")).toEqual({ kind: "file", path: canonicalCssPath, }); expect(yield* resolveAsset(token, "../secret.txt")).toBeNull(); expect(yield* resolveAsset(token, ".env")).toBeNull(); expect(yield* resolveAsset(`${token}tampered`, "report.html")).toBeNull(); }).pipe(Effect.provide(testLayer)), ); it.effect("rejects workspace files outside the authorized root", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const root = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-asset-root-", }); const outside = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3-asset-outside-", }); const htmlPath = path.join(outside, "report.html"); yield* fileSystem.writeFileString(htmlPath, "<p>outside</p>"); const error = yield* issueAssetUrl({ resource: { _tag: "workspace-file", threadId: ThreadId.make("thread-1"), path: htmlPath, }, workspaceRoot: root, }).pipe(Effect.flip); expect(error.message).toBe("Workspace file path must be relative to the project root."); expect(error).toMatchObject({ _tag: "AssetWorkspacePathValidationError", resource: { _tag: "workspace-file", threadId: "thread-1", path: htmlPath, }, }); expect(error.cause).toBeInstanceOf(WorkspacePaths.WorkspacePathOutsideRootError); }).pipe(Effect.provide(testLayer)), ); it.effect("preserves non-missing canonical path failures when issuing asset URLs", () => Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem;