891 lines · 38.3 KB · TypeScript
RawDownload
// @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"; 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("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("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("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("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("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("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("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;      const path = yield* Path.Path;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-permission-root-",      });      const htmlPath = path.join(root, "report.html");      yield* fileSystem.writeFileString(htmlPath, "<p>report</p>");      const cause = PlatformError.systemError({        _tag: "PermissionDenied",        module: "FileSystem",        method: "realPath",        pathOrDescriptor: htmlPath,      });      const failingFileSystem = FileSystem.FileSystem.of({        ...fileSystem,        realPath: () => Effect.fail(cause),      });       const error = yield* issueAssetUrl({        resource: {          _tag: "workspace-file",          threadId: ThreadId.make("thread-1"),          path: htmlPath,        },        workspaceRoot: root,      }).pipe(Effect.provideService(FileSystem.FileSystem, failingFileSystem), Effect.flip);       expect(error.message).toBe("Failed to inspect the workspace asset.");      expect(error).toMatchObject({        _tag: "AssetWorkspaceAssetInspectionError",        resource: {          _tag: "workspace-file",          threadId: "thread-1",          path: htmlPath,        },      });      expect(error.cause).toBe(cause);    }).pipe(Effect.provide(testLayer)),  );   it.effect("issues exact workspace URLs for image previews", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-image-workspace-",      });      const assetsDirectory = path.join(root, "assets");      const imagePath = path.join(assetsDirectory, "icon.png");      const siblingPath = path.join(assetsDirectory, "other.png");      yield* fileSystem.makeDirectory(assetsDirectory, { recursive: true });

Lines 1–500 of 891.