Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 78 additions & 7 deletions apps/server/src/assets/AssetAccess.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { ASSET_SAME_ORIGIN_RELAY_V1_CAPABILITY, AuthSessionId, ThreadId } from "@t3tools/contracts";
import {
ASSET_SAME_ORIGIN_RELAY_V1_CAPABILITY,
AuthSessionId,
EnvironmentId,
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";
Expand All @@ -11,17 +16,21 @@ import * as PlatformError from "effect/PlatformError";
import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
import * as SessionStore from "../auth/SessionStore.ts";
import * as ServerConfig from "../config.ts";
import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";
import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
import {
ASSET_ROUTE_PREFIX,
ASSET_SURFACE_RELAY_PREFIX,
decodeAssetRelayRoutingClaim,
issueAssetUrl as issueAssetUrlImpl,
resolveAsset as resolveAssetImpl,
resolveLocalAssetRelay,
} from "./AssetAccess.ts";

const TEST_SURFACE_SESSION_ID = AuthSessionId.make("asset-access-test-surface");
const OTHER_SURFACE_SESSION_ID = AuthSessionId.make("asset-access-other-surface");
const TEST_ENVIRONMENT_ID = EnvironmentId.make("asset-access-environment");

const assetRouteSuffix = (relativeUrl: string) => {
const prefix = relativeUrl.startsWith(`${ASSET_SURFACE_RELAY_PREFIX}/`)
Expand Down Expand Up @@ -55,6 +64,10 @@ const testLayer = Layer.mergeAll(
cookieName: "t3_asset_access_test",
isActive: () => Effect.succeed(true),
}),
Layer.mock(ServerEnvironment.ServerEnvironment)({
getEnvironmentId: Effect.succeed(TEST_ENVIRONMENT_ID),
getDescriptor: Effect.die("unused test environment descriptor"),
}),
).pipe(Layer.provideMerge(NodeServices.layer));

describe("AssetAccess", () => {
Expand All @@ -67,11 +80,14 @@ describe("AssetAccess", () => {
});
const htmlPath = path.join(root, "report.html");
const cssPath = path.join(root, "report.css");
const percentCssPath = path.join(root, "theme%20.css");
yield* fileSystem.writeFileString(htmlPath, '<link rel="stylesheet" href="report.css">');
yield* fileSystem.writeFileString(cssPath, "body { color: red; }");
yield* fileSystem.writeFileString(percentCssPath, "body { color: blue; }");
yield* fileSystem.writeFileString(path.join(root, ".env"), "SECRET=value");
const canonicalHtmlPath = yield* fileSystem.realPath(htmlPath);
const canonicalCssPath = yield* fileSystem.realPath(cssPath);
const canonicalPercentCssPath = yield* fileSystem.realPath(percentCssPath);

const result = yield* issueAssetUrl({
resource: {
Expand All @@ -80,11 +96,19 @@ describe("AssetAccess", () => {
path: htmlPath,
},
workspaceRoot: root,
dataAudience: "factory",
issuingAudience: "factory",
});
const suffix = assetRouteSuffix(result.relativeUrl);
const separatorIndex = suffix.indexOf("/");
const token = suffix.slice(0, separatorIndex);

expect(decodeAssetRelayRoutingClaim(token)).toMatchObject({
kind: "workspace-file",
dataAudience: "factory",
issuingAudience: "factory",
});

expect(yield* resolveAsset(token, "report.html")).toEqual({
kind: "file",
path: canonicalHtmlPath,
Expand All @@ -93,9 +117,22 @@ describe("AssetAccess", () => {
kind: "file",
path: canonicalCssPath,
});
expect(yield* resolveAsset(token, "theme%2520.css")).toEqual({
kind: "file",
path: canonicalPercentCssPath,
});
expect(yield* resolveAsset(token, "broken%ZZ.css")).toBeNull();
expect(yield* resolveAsset(token, "../secret.txt")).toBeNull();
expect(yield* resolveAsset(token, ".env")).toBeNull();
expect(yield* resolveAsset(`${token}tampered`, "report.html")).toBeNull();
expect(
yield* resolveLocalAssetRelay({
token,
encodedRelativePath: "report.html",
viewerSessionId: OTHER_SURFACE_SESSION_ID,
viewerAudienceCeiling: "factory",
}),
).toEqual({ kind: "file", path: canonicalHtmlPath });
}).pipe(Effect.provide(testLayer)),
);

Expand Down Expand Up @@ -197,11 +234,19 @@ describe("AssetAccess", () => {
path: imagePath,
},
workspaceRoot: root,
dataAudience: "factory",
issuingAudience: "factory",
});
const suffix = assetRouteSuffix(result.relativeUrl);
const separatorIndex = suffix.indexOf("/");
const token = suffix.slice(0, separatorIndex);

expect(decodeAssetRelayRoutingClaim(token)).toMatchObject({
kind: "workspace-file-exact",
dataAudience: "factory",
issuingAudience: "factory",
});

expect(yield* resolveAsset(token, "icon.png")).toEqual({
kind: "file",
path: canonicalImagePath,
Expand All @@ -223,23 +268,38 @@ describe("AssetAccess", () => {

const result = yield* issueAssetUrl({
resource: { _tag: "attachment", attachmentId },
dataAudience: "factory",
issuingAudience: "factory",
});
const suffix = assetRouteSuffix(result.relativeUrl);
const separatorIndex = suffix.indexOf("/");
const token = suffix.slice(0, separatorIndex);

expect(decodeAssetRelayRoutingClaim(token)).toMatchObject({
dataAudience: "factory",
issuingAudience: "factory",
issuingBackendId: TEST_ENVIRONMENT_ID,
});
expect(yield* resolveAsset(token, "ignored.png")).toEqual({
kind: "file",
path: attachmentPath,
});
expect(yield* resolveAsset(token, "ignored.png", OTHER_SURFACE_SESSION_ID)).toBeNull();
expect(yield* resolveAssetImpl(token, "ignored.png")).toBeNull();
expect(
yield* resolveLocalAssetRelay({
token,
encodedRelativePath: "ignored.png",
viewerSessionId: OTHER_SURFACE_SESSION_ID,
viewerAudienceCeiling: "factory",
}),
).toEqual({ kind: "file", path: attachmentPath });
expect(result.relativeUrl).not.toContain(TEST_SURFACE_SESSION_ID);
expect(result.relativeUrl).not.toContain(result.surfaceCredential);
}).pipe(Effect.provide(testLayer)),
);

it.effect("preserves signed direct URLs for clients without surface credentials", () =>
it.effect("preserves signed direct URLs for web clients without relay capability", () =>
Effect.gen(function* () {
const config = yield* ServerConfig.ServerConfig;
const fileSystem = yield* FileSystem.FileSystem;
Expand All @@ -263,12 +323,12 @@ describe("AssetAccess", () => {
relativeUrl: expect.stringMatching(/^\/api\/assets\/(?!relay\/)/),
expiresAt: expect.any(Number),
});
expect(result).not.toHaveProperty("surfaceCredential");
expect(yield* resolveAssetImpl(token, "ignored.png")).toBeNull();
expect(
yield* resolveAssetImpl(token, "ignored.png", {
allowUnbound: true,
}),
).toEqual({ kind: "file", path: path.join(config.attachmentsDir, `${attachmentId}.png`) });
expect(yield* resolveAssetImpl(token, "ignored.png", { allowUnbound: true })).toEqual({
kind: "file",
path: path.join(config.attachmentsDir, `${attachmentId}.png`),
});
}).pipe(Effect.provide(testLayer)),
);

Expand All @@ -285,9 +345,18 @@ describe("AssetAccess", () => {

const faviconResult = yield* issueAssetUrl({
resource: { _tag: "project-favicon", cwd: root },
dataAudience: "factory",
issuingAudience: "factory",
});
const faviconSuffix = assetRouteSuffix(faviconResult.relativeUrl);
const faviconSeparatorIndex = faviconSuffix.indexOf("/");
expect(
decodeAssetRelayRoutingClaim(faviconSuffix.slice(0, faviconSeparatorIndex)),
).toMatchObject({
kind: "project-favicon",
dataAudience: "factory",
issuingAudience: "factory",
});
expect(
yield* resolveAsset(
faviconSuffix.slice(0, faviconSeparatorIndex),
Expand All @@ -298,6 +367,8 @@ describe("AssetAccess", () => {
yield* fileSystem.remove(faviconPath);
const fallbackResult = yield* issueAssetUrl({
resource: { _tag: "project-favicon", cwd: root },
dataAudience: "factory",
issuingAudience: "factory",
});
expect(fallbackResult.relativeUrl.endsWith(`/${PROJECT_FAVICON_FALLBACK_MARKER}`)).toBe(true);
const fallbackSuffix = assetRouteSuffix(fallbackResult.relativeUrl);
Expand Down
Loading
Loading