739 lines · 26.6 KB · TypeScript
RawDownload
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

Lines 1–500 of 739.