476 lines · 19.3 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";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 * 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 { ASSET_ROUTE_PREFIX, issueAssetUrl, resolveAsset } from "./AssetAccess.ts"; const configLayer = ServerConfig.ServerConfig.layerTest(process.cwd(), {  prefix: "t3-asset-access-test-",});const testLayer = Layer.mergeAll(  configLayer,  WorkspacePaths.layer,  ProjectFaviconResolver.layer.pipe(    Layer.provide(WorkspacePaths.layer),    Layer.provide(T3ProjectFileLoader.layer),  ),  ServerSecretStore.layer.pipe(Layer.provide(configLayer)),).pipe(Layer.provideMerge(NodeServices.layer)); describe("AssetAccess", () => {  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 });      yield* fileSystem.writeFile(imagePath, new Uint8Array([137, 80, 78, 71]));      yield* fileSystem.writeFile(siblingPath, new Uint8Array([137, 80, 78, 71]));      const canonicalImagePath = yield* fileSystem.realPath(imagePath);       const result = yield* issueAssetUrl({        resource: {          _tag: "workspace-file",          threadId: ThreadId.make("thread-1"),          path: imagePath,        },        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, "icon.png")).toEqual({        kind: "file",        path: canonicalImagePath,      });      expect(yield* resolveAsset(token, "other.png")).toBeNull();      expect(yield* resolveAsset(token, "../icon.png")).toBeNull();    }).pipe(Effect.provide(testLayer)),  );   it.effect("issues exact attachment capabilities by attachment id", () =>    Effect.gen(function* () {      const config = yield* ServerConfig.ServerConfig;      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const attachmentId = "thread-1-00000000-0000-4000-8000-000000000001";      const attachmentPath = path.join(config.attachmentsDir, `${attachmentId}.png`);      yield* fileSystem.makeDirectory(config.attachmentsDir, { recursive: true });      yield* fileSystem.writeFile(attachmentPath, new Uint8Array([1, 2, 3]));       const result = yield* issueAssetUrl({        resource: { _tag: "attachment", attachmentId },      });      const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length);      const separatorIndex = suffix.indexOf("/");      const token = suffix.slice(0, separatorIndex);       expect(yield* resolveAsset(token, "ignored.png")).toEqual({        kind: "file",        path: attachmentPath,      });    }).pipe(Effect.provide(testLayer)),  );   it.effect("serves video attachments inline", () =>    Effect.gen(function* () {      const config = yield* ServerConfig.ServerConfig;      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const attachmentId = "thread-1-00000000-0000-4000-8000-000000000001-mp4";      const attachmentPath = path.join(config.attachmentsDir, `${attachmentId}.mp4`);      yield* fileSystem.makeDirectory(config.attachmentsDir, { recursive: true });      yield* fileSystem.writeFile(attachmentPath, new Uint8Array([1, 2, 3]));       const result = yield* issueAssetUrl({        resource: {          _tag: "attachment",          attachmentId,          fileName: "demo.mp4",          mimeType: 'video/mp4; codecs="avc1.42E01E"',        },      });      const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length);      const separatorIndex = suffix.indexOf("/");       expect(        yield* resolveAsset(suffix.slice(0, separatorIndex), suffix.slice(separatorIndex + 1)),      ).toEqual({        kind: "file",        path: attachmentPath,        fileName: "demo.mp4",        mimeType: "video/mp4",      });    }).pipe(Effect.provide(testLayer)),  );  it.effect("issues project favicon capabilities with a signed fallback", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-favicon-",      });      const faviconPath = path.join(root, "favicon.svg");      const initialFavicon = "<svg>a</svg>";      const updatedFavicon = "<svg>b</svg>";      expect(updatedFavicon).toHaveLength(initialFavicon.length);      yield* fileSystem.writeFileString(faviconPath, initialFavicon);      const canonicalFaviconPath = yield* fileSystem.realPath(faviconPath);       const faviconResult = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root },      });      expect(faviconResult.sourcePath).toBe("favicon.svg");      expect(faviconResult.relativeUrl).toMatch(/\/v[0-9a-f]{64}-favicon\.svg$/);      expect(        yield* issueAssetUrl({          resource: { _tag: "project-favicon", cwd: root },        }),      ).toEqual(faviconResult);      const faviconSuffix = faviconResult.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length);      const faviconSeparatorIndex = faviconSuffix.indexOf("/");      expect(        yield* resolveAsset(          faviconSuffix.slice(0, faviconSeparatorIndex),          faviconSuffix.slice(faviconSeparatorIndex + 1),        ),      ).toEqual({ kind: "file", path: canonicalFaviconPath });       yield* fileSystem.writeFileString(faviconPath, updatedFavicon);      const updatedFaviconResult = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root },      });      expect(        updatedFaviconResult.relativeUrl.slice(updatedFaviconResult.relativeUrl.lastIndexOf("/")),      ).not.toBe(faviconResult.relativeUrl.slice(faviconResult.relativeUrl.lastIndexOf("/")));       yield* fileSystem.remove(faviconPath);      const fallbackResult = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root },      });      expect(fallbackResult.relativeUrl.endsWith(`/${PROJECT_FAVICON_FALLBACK_MARKER}`)).toBe(true);      expect(fallbackResult.sourcePath).toBeUndefined();      const fallbackSuffix = fallbackResult.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length);      const fallbackSeparatorIndex = fallbackSuffix.indexOf("/");      expect(        yield* resolveAsset(          fallbackSuffix.slice(0, fallbackSeparatorIndex),          fallbackSuffix.slice(fallbackSeparatorIndex + 1),        ),      ).toBeNull();    }).pipe(Effect.provide(testLayer)),  );   it.effect("issues project favicon capabilities for a saved override", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-favicon-override-",      });      yield* fileSystem.makeDirectory(path.join(root, "brand"));      yield* fileSystem.writeFileString(path.join(root, "brand", "custom.svg"), "<svg />");      yield* fileSystem.writeFileString(path.join(root, "favicon.svg"), "<svg>auto</svg>");       const result = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root },        projectFaviconPath: "brand/custom.svg",      });       expect(result.sourcePath).toBe("brand/custom.svg");      expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-custom\.svg$/);    }).pipe(Effect.provide(testLayer)),  );   it.effect("issues an exact capability for a saved favicon outside the workspace", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-favicon-workspace-",      });      const pictures = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-favicon-pictures-",      });      const externalPath = path.join(pictures, "custom.png");      const siblingPath = path.join(pictures, "sibling.png");      yield* fileSystem.writeFile(externalPath, new Uint8Array([1, 2, 3]));      yield* fileSystem.writeFile(siblingPath, new Uint8Array([4, 5, 6]));      const canonicalPath = yield* fileSystem.realPath(externalPath);      const canonicalSiblingPath = yield* fileSystem.realPath(siblingPath);       const result = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root },        projectFaviconPath: externalPath,      });      const suffix = result.relativeUrl.slice(`${ASSET_ROUTE_PREFIX}/`.length);      const separatorIndex = suffix.indexOf("/");       expect(result.sourcePath).toBe(externalPath);      expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-custom\.png$/);      expect(        yield* resolveAsset(suffix.slice(0, separatorIndex), suffix.slice(separatorIndex + 1)),      ).toEqual({ kind: "file", path: canonicalPath });      const tamperedSuffixResult = yield* resolveAsset(        suffix.slice(0, separatorIndex),        "sibling.png",      );      expect(tamperedSuffixResult).toEqual({ kind: "file", path: canonicalPath });      expect(tamperedSuffixResult).not.toEqual({ kind: "file", path: canonicalSiblingPath });    }).pipe(Effect.provide(testLayer)),  );   it.effect("ignores a client favicon path hint", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-favicon-hint-",      });      yield* fileSystem.makeDirectory(path.join(root, "brand"));      yield* fileSystem.writeFileString(path.join(root, "brand", "hint.svg"), "<svg>hint</svg>");      yield* fileSystem.writeFileString(path.join(root, "brand", "saved.svg"), "<svg>saved</svg>");       const result = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root, path: "brand/hint.svg" },        projectFaviconPath: "brand/saved.svg",      });       expect(result.sourcePath).toBe("brand/saved.svg");      expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-saved\.svg$/);    }).pipe(Effect.provide(testLayer)),  );   it.effect("keeps automatic favicon resolution separate from a saved override", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-favicon-automatic-",      });      yield* fileSystem.makeDirectory(path.join(root, "brand"));      yield* fileSystem.writeFileString(path.join(root, "brand", "saved.svg"), "<svg>saved</svg>");      yield* fileSystem.writeFileString(path.join(root, "favicon.svg"), "<svg>automatic</svg>");       const result = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root },      });       expect(result.sourcePath).toBe("favicon.svg");      expect(result.relativeUrl).toMatch(/\/v[0-9a-f]{64}-favicon\.svg$/);    }).pipe(Effect.provide(testLayer)),  );   it.effect("rejects a resolved project favicon with a non-image extension", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-favicon-type-",      });      yield* fileSystem.writeFileString(path.join(root, "secret.txt"), "not an image");       const error = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root },        projectFaviconPath: "secret.txt",      }).pipe(Effect.flip);       expect(error).toBeInstanceOf(AssetPreviewTypeValidationError);    }).pipe(Effect.provide(testLayer)),  );   it.effect("buckets project favicon expiry after content hashing", () =>    Effect.gen(function* () {      const crypto = yield* Crypto.Crypto;      const fileSystem = yield* FileSystem.FileSystem;      const path = yield* Path.Path;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-favicon-expiry-",      });      yield* fileSystem.writeFileString(path.join(root, "favicon.svg"), "<svg />");       const bucketMs = 30 * 60 * 1000;      yield* TestClock.setTime(bucketMs - 1);      const crossingCrypto = Crypto.make({        randomBytes: (size) => new Uint8Array(size),        digest: (algorithm, data) =>          TestClock.adjust("2 millis").pipe(Effect.andThen(crypto.digest(algorithm, data))),      });      const result = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root },      }).pipe(Effect.provideService(Crypto.Crypto, crossingCrypto));       expect(result.expiresAt).toBe(3 * bucketMs);    }).pipe(Effect.provide(testLayer)),  );   it.effect("preserves structured project favicon resolution causes", () =>    Effect.gen(function* () {      const fileSystem = yield* FileSystem.FileSystem;      const root = yield* fileSystem.makeTempDirectoryScoped({        prefix: "t3-asset-favicon-error-",      });      const platformCause = PlatformError.systemError({        _tag: "PermissionDenied",        module: "FileSystem",        method: "stat",      });      const resolutionCause = new ProjectFaviconResolver.ProjectFaviconResolutionError({        operation: "stat-candidate",        workspaceRoot: root,        relativePath: "favicon.svg",        cause: platformCause,      });      const resolver = ProjectFaviconResolver.ProjectFaviconResolver.of({        resolvePath: () => Effect.fail(resolutionCause),      });       const error = yield* issueAssetUrl({        resource: { _tag: "project-favicon", cwd: root },      }).pipe(        Effect.provideService(ProjectFaviconResolver.ProjectFaviconResolver, resolver),        Effect.flip,      );       expect(error.message).toBe("Failed to resolve project favicon.");      expect(error._tag).toBe("AssetProjectFaviconResolutionError");      expect(error.cause).toBe(resolutionCause);    }).pipe(Effect.provide(testLayer)),  );});