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, '<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("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, "<svg />"); 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)), );});
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, '<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("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, "<svg />");
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)),
);
});