import * as Schema from "effect/Schema"; import { NonNegativeInt, ThreadId, TrimmedNonEmptyString } from "./baseSchemas.ts"; import { PROVIDER_SEND_TURN_MAX_FILE_BYTES, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES, ProjectFaviconPath, } from "./orchestration.ts"; const ASSET_PATH_MAX_LENGTH = 1024; export const AssetResource = Schema.Union([ Schema.TaggedStruct("workspace-file", { threadId: ThreadId, path: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)), }), Schema.TaggedStruct("attachment", { attachmentId: TrimmedNonEmptyString.check(Schema.isMaxLength(256)), /** Display name and mime from the `ChatAttachment` the caller holds. The server bakes both into the signed URL so downloads carry the real filename and Content-Type. Absent on older clients, which fall back to an octet-stream download without a filename. */ fileName: Schema.optionalKey(TrimmedNonEmptyString.check(Schema.isMaxLength(255))), mimeType: Schema.optionalKey(TrimmedNonEmptyString.check(Schema.isMaxLength(100))), }), Schema.TaggedStruct("project-favicon", { cwd: TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)), // A cache-key hint only. The server reads the authoritative path from the // project projection before it issues the signed URL. path: Schema.optional(ProjectFaviconPath), }), ]); export type AssetResource = typeof AssetResource.Type; export const AssetCreateUrlInput = Schema.Struct({ resource: AssetResource, }); export type AssetCreateUrlInput = typeof AssetCreateUrlInput.Type; export const AssetCreateUrlResult = Schema.Struct({ relativeUrl: TrimmedNonEmptyString.check(Schema.isMaxLength(4096)), expiresAt: Schema.Number, sourcePath: Schema.optional( TrimmedNonEmptyString.check(Schema.isMaxLength(ASSET_PATH_MAX_LENGTH)), ), }); export type AssetCreateUrlResult = typeof AssetCreateUrlResult.Type; export const ATTACHMENT_UPLOAD_URL_TTL_MS = 10 * 60_000; const ImageAttachmentCreateUploadUrlInput = Schema.Struct({ type: Schema.optionalKey(Schema.Literal("image")), name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), mimeType: Schema.Literals(PROVIDER_SEND_TURN_SUPPORTED_IMAGE_MIME_TYPES), sizeBytes: NonNegativeInt.check( Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES), ), }); const FileAttachmentCreateUploadUrlInput = Schema.Struct({ type: Schema.Literal("file"), name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100)), sizeBytes: NonNegativeInt.check( Schema.isGreaterThanOrEqualTo(1), Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_FILE_BYTES), ), }); export const AttachmentCreateUploadUrlInput = Schema.Union([ ImageAttachmentCreateUploadUrlInput, FileAttachmentCreateUploadUrlInput, ]); export type AttachmentCreateUploadUrlInput = typeof AttachmentCreateUploadUrlInput.Type; export const AttachmentCreateUploadUrlResult = Schema.Struct({ attachmentId: TrimmedNonEmptyString.check(Schema.isMaxLength(256)), relativeUrl: TrimmedNonEmptyString.check(Schema.isMaxLength(4096)), expiresAt: Schema.Number, }); export type AttachmentCreateUploadUrlResult = typeof AttachmentCreateUploadUrlResult.Type; export const AttachmentDeleteInput = Schema.Struct({ attachmentId: TrimmedNonEmptyString.check(Schema.isMaxLength(256)), }); export type AttachmentDeleteInput = typeof AttachmentDeleteInput.Type; export class AttachmentUploadSigningKeyError extends Schema.TaggedErrorClass()( "AttachmentUploadSigningKeyError", { cause: Schema.Defect(), }, ) { override get message(): string { return "Failed to load the attachment upload signing key."; } } export class AssetWorkspaceContextNotFoundError extends Schema.TaggedErrorClass()( "AssetWorkspaceContextNotFoundError", { resource: AssetResource, }, ) { override get message(): string { return "Workspace context was not found."; } } export class AssetWorkspaceContextResolutionError extends Schema.TaggedErrorClass()( "AssetWorkspaceContextResolutionError", { resource: AssetResource, cause: Schema.Defect(), }, ) { override get message(): string { return "Failed to resolve workspace context."; } } export class AssetWorkspaceRootNormalizationError extends Schema.TaggedErrorClass()( "AssetWorkspaceRootNormalizationError", { resource: AssetResource, cause: Schema.Defect(), }, ) { override get message(): string { return "Failed to normalize the workspace root."; } } export class AssetWorkspacePathValidationError extends Schema.TaggedErrorClass()( "AssetWorkspacePathValidationError", { resource: AssetResource, cause: Schema.Defect(), }, ) { override get message(): string { return "Workspace file path must be relative to the project root."; } } export class AssetPreviewTypeValidationError extends Schema.TaggedErrorClass()( "AssetPreviewTypeValidationError", { resource: AssetResource, }, ) { override get message(): string { return "Only browser documents and images can be previewed."; } } export class AssetWorkspaceAssetInspectionError extends Schema.TaggedErrorClass()( "AssetWorkspaceAssetInspectionError", { resource: AssetResource, cause: Schema.Defect(), }, ) { override get message(): string { return "Failed to inspect the workspace asset."; } } export class AssetWorkspaceAssetNotFoundError extends Schema.TaggedErrorClass()( "AssetWorkspaceAssetNotFoundError", { resource: AssetResource, }, ) { override get message(): string { return "Workspace asset was not found."; } } export class AssetWorkspaceResolutionError extends Schema.TaggedErrorClass()( "AssetWorkspaceResolutionError", { resource: AssetResource, cause: Schema.Defect(), }, ) { override get message(): string { return "Failed to resolve workspace."; } } export class AssetAttachmentNotFoundError extends Schema.TaggedErrorClass()( "AssetAttachmentNotFoundError", { resource: AssetResource, }, ) { override get message(): string { return "Attachment was not found."; } } export class AssetProjectFaviconResolutionError extends Schema.TaggedErrorClass()( "AssetProjectFaviconResolutionError", { resource: AssetResource, cause: Schema.Defect(), }, ) { override get message(): string { return "Failed to resolve project favicon."; } } export class AssetProjectFaviconInspectionError extends Schema.TaggedErrorClass()( "AssetProjectFaviconInspectionError", { resource: AssetResource, cause: Schema.Defect(), }, ) { override get message(): string { return "Failed to inspect the project favicon."; } } export class AssetProjectFaviconNotFoundError extends Schema.TaggedErrorClass()( "AssetProjectFaviconNotFoundError", { resource: AssetResource, }, ) { override get message(): string { return "Project favicon was not found."; } } export class AssetSigningKeyLoadError extends Schema.TaggedErrorClass()( "AssetSigningKeyLoadError", { resource: AssetResource, cause: Schema.Defect(), }, ) { override get message(): string { return "Failed to load the asset signing key."; } } export const AssetAccessError = Schema.Union([ AssetWorkspaceContextNotFoundError, AssetWorkspaceContextResolutionError, AssetWorkspaceRootNormalizationError, AssetWorkspacePathValidationError, AssetPreviewTypeValidationError, AssetWorkspaceAssetInspectionError, AssetWorkspaceAssetNotFoundError, AssetWorkspaceResolutionError, AssetAttachmentNotFoundError, AssetProjectFaviconResolutionError, AssetProjectFaviconInspectionError, AssetProjectFaviconNotFoundError, AssetSigningKeyLoadError, ]); export type AssetAccessError = typeof AssetAccessError.Type;