import type { AssetResource } from "@t3tools/contracts";import { AssetAttachmentNotFoundError, AssetPreviewTypeValidationError, AssetProjectFaviconInspectionError, AssetProjectFaviconNotFoundError, AssetProjectFaviconResolutionError, AssetSigningKeyLoadError, AssetWorkspaceAssetInspectionError, AssetWorkspaceAssetNotFoundError, AssetWorkspaceContextNotFoundError, AssetWorkspacePathValidationError, AssetWorkspaceResolutionError, AssetWorkspaceRootNormalizationError, ToolActivityNativeAppReference,} from "@t3tools/contracts";import { hostPreviewMimeTypeFromExtension, isWorkspaceImagePreviewPath, isWorkspacePreviewEntryPath, WORKSPACE_BROWSER_PREVIEW_EXTENSIONS, WORKSPACE_IMAGE_PREVIEW_EXTENSIONS,} from "@t3tools/shared/filePreview";import { IMAGE_DIMENSIONS_HEADER_BYTES, readImageDimensions, type ImageDimensions,} from "@t3tools/shared/imageDimensions";import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon";import * as Clock from "effect/Clock";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as FileSystem from "effect/FileSystem";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as PlatformError from "effect/PlatformError";import * as Schema from "effect/Schema"; import { base64UrlDecodeUtf8, base64UrlEncode, signPayload, timingSafeEqualBase64Url,} from "../auth/utils.ts";import * as ServerSecretStore from "../auth/ServerSecretStore.ts";import { parseAttachmentFileExtension, resolveAttachmentPathById } from "../attachmentStore.ts";import * as ServerConfig from "../config.ts";import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts";import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";import * as NativeAppIconResolver from "./NativeAppIconResolver.ts";import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts"; export const ASSET_ROUTE_PREFIX = "/api/assets"; const SIGNING_SECRET_NAME = "asset-access-signing-key";const ASSET_TOKEN_TTL_MS = 60 * 60 * 1000;const PROJECT_FAVICON_TOKEN_BUCKET_MS = 30 * 60 * 1000;const PROJECT_FAVICON_VERSION_PREFIX = "v";const INLINE_VIDEO_MIME_TYPE_PATTERN = /^video\/[\w!#$&^.+-]+$/i;// Extensions a document viewer may request inline. The extension comes from// the attachment id the server assigned, never from the client's mime type.const INLINE_DOCUMENT_EXTENSIONS = new Set(["pdf", "html", "htm"]);const INLINE_DOCUMENT_MIME_TYPES: Record<string, string> = { pdf: "application/pdf", html: "text/html", htm: "text/html",};const PREVIEW_ASSET_EXTENSIONS = new Set([ ...WORKSPACE_BROWSER_PREVIEW_EXTENSIONS, ...WORKSPACE_IMAGE_PREVIEW_EXTENSIONS, ".css", ".js", ".mjs", ".otf", ".ttf", ".woff", ".woff2",]); const AssetClaimsSchema = Schema.Union([ Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("workspace-file"), workspaceRoot: Schema.String, baseRelativePath: Schema.String, expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("workspace-file-exact"), workspaceRoot: Schema.String, relativePath: Schema.String, expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("media-file-exact"), filePath: Schema.String, device: Schema.String, inode: Schema.String, expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("attachment"), attachmentId: Schema.String, /** Decided at mint time. Absent tokens (from before this field) serve inline, which is only ever the image case. */ download: Schema.optionalKey(Schema.Boolean), /** Display name and mime the caller supplied at mint time; drive the download filename and Content-Type. */ fileName: Schema.optionalKey(Schema.String), mimeType: Schema.optionalKey(Schema.String), expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("project-favicon"), workspaceRoot: Schema.String, relativePath: Schema.NullOr(Schema.String), expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("project-favicon-external"), filePath: Schema.String, expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("native-app-icon"), app: ToolActivityNativeAppReference, expiresAt: Schema.Number, }),]);type AssetClaims = typeof AssetClaimsSchema.Type; const AssetClaimsJson = Schema.fromJsonString(AssetClaimsSchema);const decodeAssetClaims = Schema.decodeUnknownOption(AssetClaimsJson);const encodeAssetClaims = Schema.encodeSync(AssetClaimsJson); export type ResolvedAsset = { readonly kind: "file"; readonly path: string; readonly download?: boolean; readonly fileName?: string; readonly mimeType?: string; readonly file?: OpenMediaFile;}; function decodeClaims(encodedPayload: string): AssetClaims | null { try { return Option.getOrNull(decodeAssetClaims(base64UrlDecodeUtf8(encodedPayload))); } catch { return null; }} function decodeRelativePath(value: string): string | null { try { return decodeURIComponent(value); } catch { return null; }} const optionOnNotFound = <A, R>( effect: Effect.Effect<A, PlatformError.PlatformError, R>,): Effect.Effect<Option.Option<A>, PlatformError.PlatformError, R> => effect.pipe( Effect.map(Option.some), Effect.catchTags({ PlatformError: (error) => error.reason._tag === "NotFound" ? Effect.succeed(Option.none<A>()) : Effect.fail(error), }), ); const resolveCanonicalFile = Effect.fn("AssetAccess.resolveCanonicalFile")(function* ( filePath: string,) { const fileSystem = yield* FileSystem.FileSystem; const canonicalFile = yield* optionOnNotFound(fileSystem.realPath(filePath)); if (Option.isNone(canonicalFile)) return null; const info = yield* optionOnNotFound(fileSystem.stat(canonicalFile.value)); return Option.isSome(info) && info.value.type === "File" ? canonicalFile.value : null;}); const resolveCanonicalWorkspaceFile = Effect.fn("AssetAccess.resolveCanonicalWorkspaceFile")( function* (input: { readonly workspaceRoot: string; readonly relativePath: string }) { const fileSystem = yield* FileSystem.FileSystem; const workspacePaths = yield* WorkspacePaths.WorkspacePaths; const resolved = yield* workspacePaths.resolveRelativePathWithinRoot(input).pipe( Effect.map(Option.some), Effect.catchTags({ WorkspacePathOutsideRootError: () => Effect.succeed(Option.none()), }), ); if (Option.isNone(resolved)) return null; const [canonicalRoot, canonicalFile] = yield* Effect.all([ optionOnNotFound(fileSystem.realPath(input.workspaceRoot)), optionOnNotFound(fileSystem.realPath(resolved.value.absolutePath)), ]); if (Option.isNone(canonicalRoot) || Option.isNone(canonicalFile)) return null; const path = yield* Path.Path; const relative = path.relative(canonicalRoot.value, canonicalFile.value); if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) return null; const info = yield* optionOnNotFound(fileSystem.stat(canonicalFile.value)); return Option.isSome(info) && info.value.type === "File" ? canonicalFile.value : null; },); const resolveCanonicalWorkspaceFileForRequest = (input: { readonly workspaceRoot: string; readonly relativePath: string;}) => resolveCanonicalWorkspaceFile(input).pipe( Effect.tapError((cause) => Effect.logError("Failed to resolve canonical asset path.", { workspaceRoot: input.workspaceRoot, relativePath: input.relativePath, cause, }), ), Effect.orElseSucceed(() => null), ); /** * Reads pixel dimensions from an image's header so clients can reserve the * exact box before the bytes arrive. Best effort: an unreadable or unsupported * file just leaves the field out, and the client measures after decode. Only * formats the parser understands are opened; SVG and the rest are skipped. */const HEADER_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]); /** From the identity-checked, non-blocking handle the caller already holds. */const readImageDimensionsFromOpenFile = (filePath: string, file: OpenMediaFile) => readMediaFileHeader(filePath, file, IMAGE_DIMENSIONS_HEADER_BYTES).pipe( Effect.map(readImageDimensions), Effect.orElseSucceed((): ImageDimensions | null => null), ); /** * Opens through `openMediaFile` so a path swapped for a FIFO cannot block the * request; a regular open would wait for a writer that never comes. */const readImageDimensionsFromHeader = (filePath: string) => openMediaFile(filePath).pipe( Effect.flatMap((file) => file === null ? Effect.succeed(null) : readImageDimensionsFromOpenFile(filePath, file), ), Effect.scoped, Effect.orElseSucceed((): ImageDimensions | null => null), ); export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (input: { readonly resource: AssetResource; readonly workspaceRoot?: string; readonly projectFaviconPath?: string;}) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workspacePaths = yield* WorkspacePaths.WorkspacePaths; let expiresAt = (yield* Clock.currentTimeMillis) + ASSET_TOKEN_TTL_MS; let claims: AssetClaims; let fileName: string; let sourcePath: string | undefined; let imageDimensions: ImageDimensions | null = null; switch (input.resource._tag) { case "media-file": { let requestedPath = input.resource.path; if (!path.isAbsolute(requestedPath)) { if (!input.workspaceRoot) { return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource }); } const workspaceRoot = yield* workspacePaths .normalizeWorkspaceRoot(input.workspaceRoot) .pipe( Effect.mapError( (cause) => new AssetWorkspaceRootNormalizationError({ resource: input.resource, cause }), ), ); requestedPath = path.resolve(workspaceRoot, requestedPath); } const canonicalFile = yield* resolveCanonicalFile(requestedPath).pipe( Effect.mapError( (cause) => new AssetWorkspaceAssetInspectionError({ resource: input.resource, cause }), ), ); if (!canonicalFile) { return yield* new AssetWorkspaceAssetNotFoundError({ resource: input.resource }); } if (hostPreviewMimeTypeFromExtension(path.extname(canonicalFile)) === null) { return yield* new AssetPreviewTypeValidationError({ resource: input.resource }); } const wantsDimensions = HEADER_IMAGE_EXTENSIONS.has( path.extname(canonicalFile).toLowerCase(), ); const opened = yield* openMediaFile(canonicalFile).pipe( Effect.flatMap((file) => file === null ? Effect.succeed(null) : Effect.map( wantsDimensions ? readImageDimensionsFromOpenFile(canonicalFile, file) : Effect.succeed(null), (dimensions) => ({ identity: { device: file.info.dev.toString(), inode: file.info.ino.toString() }, dimensions, }), ), ), Effect.scoped, Effect.mapError( (cause) => new AssetWorkspaceAssetInspectionError({ resource: input.resource, cause }), ), ); if (!opened) { return yield* new AssetWorkspaceAssetNotFoundError({ resource: input.resource }); } const identity = opened.identity; imageDimensions = opened.dimensions; claims = { version: 1, kind: "media-file-exact", filePath: canonicalFile, ...identity, expiresAt, }; fileName = path.basename(canonicalFile); break; } case "workspace-file": { if (!input.workspaceRoot) { return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource, }); } const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.workspaceRoot).pipe( Effect.mapError( (cause) => new AssetWorkspaceRootNormalizationError({ resource: input.resource, cause, }), ), ); const relativePath = path.isAbsolute(input.resource.path) ? path.relative(workspaceRoot, input.resource.path) : input.resource.path; const resolved = yield* workspacePaths .resolveRelativePathWithinRoot({ workspaceRoot, relativePath }) .pipe( Effect.mapError( (cause) => new AssetWorkspacePathValidationError({ resource: input.resource, cause, }), ), ); if (!isWorkspacePreviewEntryPath(resolved.relativePath)) { return yield* new AssetPreviewTypeValidationError({ resource: input.resource, }); } const canonicalFile = yield* resolveCanonicalWorkspaceFile({ workspaceRoot, relativePath: resolved.relativePath, }).pipe( Effect.mapError( (cause) => new AssetWorkspaceAssetInspectionError({ resource: input.resource, cause, }), ), ); if (!canonicalFile) { return yield* new AssetWorkspaceAssetNotFoundError({ resource: input.resource, }); } const canonicalWorkspaceRoot = yield* fileSystem.realPath(workspaceRoot).pipe( Effect.mapError( (cause) => new AssetWorkspaceResolutionError({ resource: input.resource, cause, }), ), ); if (HEADER_IMAGE_EXTENSIONS.has(path.extname(resolved.relativePath).toLowerCase())) { imageDimensions = yield* readImageDimensionsFromHeader(canonicalFile); } claims = isWorkspaceImagePreviewPath(resolved.relativePath) ? { version: 1, kind: "workspace-file-exact", workspaceRoot: canonicalWorkspaceRoot, relativePath: resolved.relativePath, expiresAt, } : { version: 1, kind: "workspace-file", workspaceRoot: canonicalWorkspaceRoot, baseRelativePath: path.dirname(resolved.relativePath), expiresAt, }; fileName = path.basename(resolved.relativePath); break; } case "attachment": { const config = yield* ServerConfig.ServerConfig; const attachmentPath = resolveAttachmentPathById({ attachmentsDir: config.attachmentsDir, attachmentId: input.resource.attachmentId, }); if (!attachmentPath) { return yield* new AssetAttachmentNotFoundError({ resource: input.resource, }); } // Generic files carry their extension inside the attachment id (that // shape resolves the on-disk path); images do not. Videos and images // render inline. Other generic files download, unless a document viewer // asked for inline and the stored extension is one a browser can show. const extension = parseAttachmentFileExtension(input.resource.attachmentId); const isGenericFile = extension !== null; const videoMimeType = input.resource.mimeType?.split(";", 1)[0]?.trim() ?? ""; const isVideo = INLINE_VIDEO_MIME_TYPE_PATTERN.test(videoMimeType); const inlineDocumentMimeType = input.resource.disposition === "inline" && extension !== null && INLINE_DOCUMENT_EXTENSIONS.has(extension) ? INLINE_DOCUMENT_MIME_TYPES[extension] : undefined; if (!isGenericFile) { imageDimensions = yield* readImageDimensionsFromHeader(attachmentPath); } claims = { version: 1, kind: "attachment", attachmentId: input.resource.attachmentId, ...(isGenericFile && !isVideo && inlineDocumentMimeType === undefined ? { download: true } : {}), ...(input.resource.fileName !== undefined ? { fileName: input.resource.fileName } : {}), ...(inlineDocumentMimeType !== undefined ? { mimeType: inlineDocumentMimeType } : input.resource.mimeType !== undefined ? { mimeType: isVideo ? videoMimeType : input.resource.mimeType } : {}), expiresAt, }; fileName = input.resource.fileName ?? path.basename(attachmentPath); break; } case "project-favicon": { const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.resource.cwd).pipe( Effect.mapError( (cause) => new AssetWorkspaceRootNormalizationError({ resource: input.resource, cause, }), ), ); const faviconResolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const faviconPath = yield* faviconResolver .resolvePath(workspaceRoot, input.projectFaviconPath ?? undefined) .pipe( Effect.mapError( (cause) => new AssetProjectFaviconResolutionError({ resource: input.resource, cause, }), ), ); const isExternalOverride = faviconPath !== null && input.projectFaviconPath !== undefined && path.isAbsolute(input.projectFaviconPath) && path.normalize(faviconPath) === path.normalize(input.projectFaviconPath); const relativePath = faviconPath && !isExternalOverride ? path.relative(workspaceRoot, faviconPath) : null; const sourceFaviconPath = isExternalOverride ? faviconPath : relativePath; if (sourceFaviconPath && !isWorkspaceImagePreviewPath(sourceFaviconPath)) { return yield* new AssetPreviewTypeValidationError({ resource: input.resource }); } sourcePath = sourceFaviconPath ?? undefined; const canonicalFaviconPath = sourceFaviconPathLines 1–500 of 739. Load more
import type { AssetResource } from "@t3tools/contracts"; import { AssetAttachmentNotFoundError, AssetPreviewTypeValidationError, AssetProjectFaviconInspectionError, AssetProjectFaviconNotFoundError, AssetProjectFaviconResolutionError, AssetSigningKeyLoadError, AssetWorkspaceAssetInspectionError, AssetWorkspaceAssetNotFoundError, AssetWorkspaceContextNotFoundError, AssetWorkspacePathValidationError, AssetWorkspaceResolutionError, AssetWorkspaceRootNormalizationError, ToolActivityNativeAppReference, } from "@t3tools/contracts"; import { hostPreviewMimeTypeFromExtension, isWorkspaceImagePreviewPath, isWorkspacePreviewEntryPath, WORKSPACE_BROWSER_PREVIEW_EXTENSIONS, WORKSPACE_IMAGE_PREVIEW_EXTENSIONS, } from "@t3tools/shared/filePreview"; import { IMAGE_DIMENSIONS_HEADER_BYTES, readImageDimensions, type ImageDimensions, } from "@t3tools/shared/imageDimensions"; import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon"; import * as Clock from "effect/Clock"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; import * as Path from "effect/Path"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import { base64UrlDecodeUtf8, base64UrlEncode, signPayload, timingSafeEqualBase64Url, } from "../auth/utils.ts"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import { parseAttachmentFileExtension, resolveAttachmentPathById } from "../attachmentStore.ts"; import * as ServerConfig from "../config.ts"; import * as ProjectFaviconResolver from "../project/ProjectFaviconResolver.ts"; import * as WorkspacePaths from "../workspace/WorkspacePaths.ts"; import * as NativeAppIconResolver from "./NativeAppIconResolver.ts"; import { openMediaFile, readMediaFileHeader, type OpenMediaFile } from "./MediaFile.ts"; export const ASSET_ROUTE_PREFIX = "/api/assets"; const SIGNING_SECRET_NAME = "asset-access-signing-key"; const ASSET_TOKEN_TTL_MS = 60 * 60 * 1000; const PROJECT_FAVICON_TOKEN_BUCKET_MS = 30 * 60 * 1000; const PROJECT_FAVICON_VERSION_PREFIX = "v"; const INLINE_VIDEO_MIME_TYPE_PATTERN = /^video\/[\w!#$&^.+-]+$/i; // Extensions a document viewer may request inline. The extension comes from // the attachment id the server assigned, never from the client's mime type. const INLINE_DOCUMENT_EXTENSIONS = new Set(["pdf", "html", "htm"]); const INLINE_DOCUMENT_MIME_TYPES: Record<string, string> = { pdf: "application/pdf", html: "text/html", htm: "text/html", }; const PREVIEW_ASSET_EXTENSIONS = new Set([ ...WORKSPACE_BROWSER_PREVIEW_EXTENSIONS, ...WORKSPACE_IMAGE_PREVIEW_EXTENSIONS, ".css", ".js", ".mjs", ".otf", ".ttf", ".woff", ".woff2", ]); const AssetClaimsSchema = Schema.Union([ Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("workspace-file"), workspaceRoot: Schema.String, baseRelativePath: Schema.String, expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("workspace-file-exact"), workspaceRoot: Schema.String, relativePath: Schema.String, expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("media-file-exact"), filePath: Schema.String, device: Schema.String, inode: Schema.String, expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("attachment"), attachmentId: Schema.String, /** Decided at mint time. Absent tokens (from before this field) serve inline, which is only ever the image case. */ download: Schema.optionalKey(Schema.Boolean), /** Display name and mime the caller supplied at mint time; drive the download filename and Content-Type. */ fileName: Schema.optionalKey(Schema.String), mimeType: Schema.optionalKey(Schema.String), expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("project-favicon"), workspaceRoot: Schema.String, relativePath: Schema.NullOr(Schema.String), expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("project-favicon-external"), filePath: Schema.String, expiresAt: Schema.Number, }), Schema.Struct({ version: Schema.Literal(1), kind: Schema.Literal("native-app-icon"), app: ToolActivityNativeAppReference, expiresAt: Schema.Number, }), ]); type AssetClaims = typeof AssetClaimsSchema.Type; const AssetClaimsJson = Schema.fromJsonString(AssetClaimsSchema); const decodeAssetClaims = Schema.decodeUnknownOption(AssetClaimsJson); const encodeAssetClaims = Schema.encodeSync(AssetClaimsJson); export type ResolvedAsset = { readonly kind: "file"; readonly path: string; readonly download?: boolean; readonly fileName?: string; readonly mimeType?: string; readonly file?: OpenMediaFile; }; function decodeClaims(encodedPayload: string): AssetClaims | null { try { return Option.getOrNull(decodeAssetClaims(base64UrlDecodeUtf8(encodedPayload))); } catch { return null; } } function decodeRelativePath(value: string): string | null { try { return decodeURIComponent(value); } catch { return null; } } const optionOnNotFound = <A, R>( effect: Effect.Effect<A, PlatformError.PlatformError, R>, ): Effect.Effect<Option.Option<A>, PlatformError.PlatformError, R> => effect.pipe( Effect.map(Option.some), Effect.catchTags({ PlatformError: (error) => error.reason._tag === "NotFound" ? Effect.succeed(Option.none<A>()) : Effect.fail(error), }), ); const resolveCanonicalFile = Effect.fn("AssetAccess.resolveCanonicalFile")(function* ( filePath: string, ) { const fileSystem = yield* FileSystem.FileSystem; const canonicalFile = yield* optionOnNotFound(fileSystem.realPath(filePath)); if (Option.isNone(canonicalFile)) return null; const info = yield* optionOnNotFound(fileSystem.stat(canonicalFile.value)); return Option.isSome(info) && info.value.type === "File" ? canonicalFile.value : null; }); const resolveCanonicalWorkspaceFile = Effect.fn("AssetAccess.resolveCanonicalWorkspaceFile")( function* (input: { readonly workspaceRoot: string; readonly relativePath: string }) { const fileSystem = yield* FileSystem.FileSystem; const workspacePaths = yield* WorkspacePaths.WorkspacePaths; const resolved = yield* workspacePaths.resolveRelativePathWithinRoot(input).pipe( Effect.map(Option.some), Effect.catchTags({ WorkspacePathOutsideRootError: () => Effect.succeed(Option.none()), }), ); if (Option.isNone(resolved)) return null; const [canonicalRoot, canonicalFile] = yield* Effect.all([ optionOnNotFound(fileSystem.realPath(input.workspaceRoot)), optionOnNotFound(fileSystem.realPath(resolved.value.absolutePath)), ]); if (Option.isNone(canonicalRoot) || Option.isNone(canonicalFile)) return null; const path = yield* Path.Path; const relative = path.relative(canonicalRoot.value, canonicalFile.value); if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) return null; const info = yield* optionOnNotFound(fileSystem.stat(canonicalFile.value)); return Option.isSome(info) && info.value.type === "File" ? canonicalFile.value : null; }, ); const resolveCanonicalWorkspaceFileForRequest = (input: { readonly workspaceRoot: string; readonly relativePath: string; }) => resolveCanonicalWorkspaceFile(input).pipe( Effect.tapError((cause) => Effect.logError("Failed to resolve canonical asset path.", { workspaceRoot: input.workspaceRoot, relativePath: input.relativePath, cause, }), ), Effect.orElseSucceed(() => null), ); /** * Reads pixel dimensions from an image's header so clients can reserve the * exact box before the bytes arrive. Best effort: an unreadable or unsupported * file just leaves the field out, and the client measures after decode. Only * formats the parser understands are opened; SVG and the rest are skipped. */ const HEADER_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]); /** From the identity-checked, non-blocking handle the caller already holds. */ const readImageDimensionsFromOpenFile = (filePath: string, file: OpenMediaFile) => readMediaFileHeader(filePath, file, IMAGE_DIMENSIONS_HEADER_BYTES).pipe( Effect.map(readImageDimensions), Effect.orElseSucceed((): ImageDimensions | null => null), ); /** * Opens through `openMediaFile` so a path swapped for a FIFO cannot block the * request; a regular open would wait for a writer that never comes. */ const readImageDimensionsFromHeader = (filePath: string) => openMediaFile(filePath).pipe( Effect.flatMap((file) => file === null ? Effect.succeed(null) : readImageDimensionsFromOpenFile(filePath, file), ), Effect.scoped, Effect.orElseSucceed((): ImageDimensions | null => null), ); export const issueAssetUrl = Effect.fn("AssetAccess.issueAssetUrl")(function* (input: { readonly resource: AssetResource; readonly workspaceRoot?: string; readonly projectFaviconPath?: string; }) { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workspacePaths = yield* WorkspacePaths.WorkspacePaths; let expiresAt = (yield* Clock.currentTimeMillis) + ASSET_TOKEN_TTL_MS; let claims: AssetClaims; let fileName: string; let sourcePath: string | undefined; let imageDimensions: ImageDimensions | null = null; switch (input.resource._tag) { case "media-file": { let requestedPath = input.resource.path; if (!path.isAbsolute(requestedPath)) { if (!input.workspaceRoot) { return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource }); } const workspaceRoot = yield* workspacePaths .normalizeWorkspaceRoot(input.workspaceRoot) .pipe( Effect.mapError( (cause) => new AssetWorkspaceRootNormalizationError({ resource: input.resource, cause }), ), ); requestedPath = path.resolve(workspaceRoot, requestedPath); } const canonicalFile = yield* resolveCanonicalFile(requestedPath).pipe( Effect.mapError( (cause) => new AssetWorkspaceAssetInspectionError({ resource: input.resource, cause }), ), ); if (!canonicalFile) { return yield* new AssetWorkspaceAssetNotFoundError({ resource: input.resource }); } if (hostPreviewMimeTypeFromExtension(path.extname(canonicalFile)) === null) { return yield* new AssetPreviewTypeValidationError({ resource: input.resource }); } const wantsDimensions = HEADER_IMAGE_EXTENSIONS.has( path.extname(canonicalFile).toLowerCase(), ); const opened = yield* openMediaFile(canonicalFile).pipe( Effect.flatMap((file) => file === null ? Effect.succeed(null) : Effect.map( wantsDimensions ? readImageDimensionsFromOpenFile(canonicalFile, file) : Effect.succeed(null), (dimensions) => ({ identity: { device: file.info.dev.toString(), inode: file.info.ino.toString() }, dimensions, }), ), ), Effect.scoped, Effect.mapError( (cause) => new AssetWorkspaceAssetInspectionError({ resource: input.resource, cause }), ), ); if (!opened) { return yield* new AssetWorkspaceAssetNotFoundError({ resource: input.resource }); } const identity = opened.identity; imageDimensions = opened.dimensions; claims = { version: 1, kind: "media-file-exact", filePath: canonicalFile, ...identity, expiresAt, }; fileName = path.basename(canonicalFile); break; } case "workspace-file": { if (!input.workspaceRoot) { return yield* new AssetWorkspaceContextNotFoundError({ resource: input.resource, }); } const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.workspaceRoot).pipe( Effect.mapError( (cause) => new AssetWorkspaceRootNormalizationError({ resource: input.resource, cause, }), ), ); const relativePath = path.isAbsolute(input.resource.path) ? path.relative(workspaceRoot, input.resource.path) : input.resource.path; const resolved = yield* workspacePaths .resolveRelativePathWithinRoot({ workspaceRoot, relativePath }) .pipe( Effect.mapError( (cause) => new AssetWorkspacePathValidationError({ resource: input.resource, cause, }), ), ); if (!isWorkspacePreviewEntryPath(resolved.relativePath)) { return yield* new AssetPreviewTypeValidationError({ resource: input.resource, }); } const canonicalFile = yield* resolveCanonicalWorkspaceFile({ workspaceRoot, relativePath: resolved.relativePath, }).pipe( Effect.mapError( (cause) => new AssetWorkspaceAssetInspectionError({ resource: input.resource, cause, }), ), ); if (!canonicalFile) { return yield* new AssetWorkspaceAssetNotFoundError({ resource: input.resource, }); } const canonicalWorkspaceRoot = yield* fileSystem.realPath(workspaceRoot).pipe( Effect.mapError( (cause) => new AssetWorkspaceResolutionError({ resource: input.resource, cause, }), ), ); if (HEADER_IMAGE_EXTENSIONS.has(path.extname(resolved.relativePath).toLowerCase())) { imageDimensions = yield* readImageDimensionsFromHeader(canonicalFile); } claims = isWorkspaceImagePreviewPath(resolved.relativePath) ? { version: 1, kind: "workspace-file-exact", workspaceRoot: canonicalWorkspaceRoot, relativePath: resolved.relativePath, expiresAt, } : { version: 1, kind: "workspace-file", workspaceRoot: canonicalWorkspaceRoot, baseRelativePath: path.dirname(resolved.relativePath), expiresAt, }; fileName = path.basename(resolved.relativePath); break; } case "attachment": { const config = yield* ServerConfig.ServerConfig; const attachmentPath = resolveAttachmentPathById({ attachmentsDir: config.attachmentsDir, attachmentId: input.resource.attachmentId, }); if (!attachmentPath) { return yield* new AssetAttachmentNotFoundError({ resource: input.resource, }); } // Generic files carry their extension inside the attachment id (that // shape resolves the on-disk path); images do not. Videos and images // render inline. Other generic files download, unless a document viewer // asked for inline and the stored extension is one a browser can show. const extension = parseAttachmentFileExtension(input.resource.attachmentId); const isGenericFile = extension !== null; const videoMimeType = input.resource.mimeType?.split(";", 1)[0]?.trim() ?? ""; const isVideo = INLINE_VIDEO_MIME_TYPE_PATTERN.test(videoMimeType); const inlineDocumentMimeType = input.resource.disposition === "inline" && extension !== null && INLINE_DOCUMENT_EXTENSIONS.has(extension) ? INLINE_DOCUMENT_MIME_TYPES[extension] : undefined; if (!isGenericFile) { imageDimensions = yield* readImageDimensionsFromHeader(attachmentPath); } claims = { version: 1, kind: "attachment", attachmentId: input.resource.attachmentId, ...(isGenericFile && !isVideo && inlineDocumentMimeType === undefined ? { download: true } : {}), ...(input.resource.fileName !== undefined ? { fileName: input.resource.fileName } : {}), ...(inlineDocumentMimeType !== undefined ? { mimeType: inlineDocumentMimeType } : input.resource.mimeType !== undefined ? { mimeType: isVideo ? videoMimeType : input.resource.mimeType } : {}), expiresAt, }; fileName = input.resource.fileName ?? path.basename(attachmentPath); break; } case "project-favicon": { const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.resource.cwd).pipe( Effect.mapError( (cause) => new AssetWorkspaceRootNormalizationError({ resource: input.resource, cause, }), ), ); const faviconResolver = yield* ProjectFaviconResolver.ProjectFaviconResolver; const faviconPath = yield* faviconResolver .resolvePath(workspaceRoot, input.projectFaviconPath ?? undefined) .pipe( Effect.mapError( (cause) => new AssetProjectFaviconResolutionError({ resource: input.resource, cause, }), ), ); const isExternalOverride = faviconPath !== null && input.projectFaviconPath !== undefined && path.isAbsolute(input.projectFaviconPath) && path.normalize(faviconPath) === path.normalize(input.projectFaviconPath); const relativePath = faviconPath && !isExternalOverride ? path.relative(workspaceRoot, faviconPath) : null; const sourceFaviconPath = isExternalOverride ? faviconPath : relativePath; if (sourceFaviconPath && !isWorkspaceImagePreviewPath(sourceFaviconPath)) { return yield* new AssetPreviewTypeValidationError({ resource: input.resource }); } sourcePath = sourceFaviconPath ?? undefined; const canonicalFaviconPath = sourceFaviconPath