import * as NodeServices from "@effect/platform-node/NodeServices"; import { ThreadId } from "@t3tools/contracts"; import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon"; import { describe, expect, it } from "@effect/vitest"; 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 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, ''); 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, "

outside

"); 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, "

report

"); 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("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"); yield* fileSystem.writeFileString(faviconPath, ""); const canonicalFaviconPath = yield* fileSystem.realPath(faviconPath); const faviconResult = yield* issueAssetUrl({ resource: { _tag: "project-favicon", cwd: root }, }); 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.remove(faviconPath); const fallbackResult = yield* issueAssetUrl({ resource: { _tag: "project-favicon", cwd: root }, }); expect(fallbackResult.relativeUrl.endsWith(`/${PROJECT_FAVICON_FALLBACK_MARKER}`)).toBe(true); 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("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)), ); });