import { z } from 'zod'; import { ShadeError } from '@shade/core'; export const FileErrorCodeSchema = z.enum([ 'NOT_FOUND', 'PERMISSION_DENIED', 'CONFLICT', 'QUOTA_EXCEEDED', 'RATE_LIMIT', 'PATH_VALIDATION', 'FINGERPRINT_REQUIRED', 'OPERATION_TIMEOUT', 'IDEMPOTENCY_CONFLICT', 'CANCELLED', 'INTERNAL', 'NOT_IMPLEMENTED', 'CUSTOM_OP_REJECTED', 'INVALID_SIGNATURE', 'INVALID_ARGS', ]); export type FileErrorCode = z.infer; export const FileErrorPayloadSchema = z.object({ code: FileErrorCodeSchema, message: z.string().max(2048), /** Suggested retry delay in ms; only set for retriable errors (rate limit, transient). */ retryAfterMs: z.number().int().nonnegative().optional(), /** Path or arg field the error refers to. */ field: z.string().max(128).optional(), /** Optional cause string (sanitized — no stack traces leak from server). */ cause: z.string().max(2048).optional(), }); export type FileErrorPayload = z.infer; // ─── Class hierarchy ───────────────────────────────────────── /** * Base class for all `@shade/files` errors. Extends `ShadeError` so the * existing `errorToHttpStatus()` mapping in `@shade/core` continues to apply. */ export class FileError extends ShadeError { readonly payload: FileErrorPayload; constructor(payload: FileErrorPayload) { super(`SHADE_FS_${payload.code}`, payload.message); this.name = 'FileError'; this.payload = payload; } override toJSON(): { name: string; code: string; message: string; payload: FileErrorPayload } { return { name: this.name, code: this.code, message: this.message, payload: this.payload, }; } } export class NotFoundError extends FileError { constructor(message = 'Not found', field?: string) { super({ code: 'NOT_FOUND', message, ...(field !== undefined ? { field } : {}) }); this.name = 'NotFoundError'; } } export class PermissionDeniedError extends FileError { constructor(message = 'Permission denied') { super({ code: 'PERMISSION_DENIED', message }); this.name = 'PermissionDeniedError'; } } export class ConflictError extends FileError { constructor(message: string) { super({ code: 'CONFLICT', message }); this.name = 'ConflictError'; } } export class QuotaExceededError extends FileError { constructor(message = 'Quota exceeded', retryAfterMs?: number) { super({ code: 'QUOTA_EXCEEDED', message, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), }); this.name = 'QuotaExceededError'; } } export class FsRateLimitError extends FileError { constructor(message = 'Rate limit exceeded', retryAfterMs?: number) { super({ code: 'RATE_LIMIT', message, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), }); this.name = 'FsRateLimitError'; } } export class PathValidationError extends FileError { constructor(message: string, field = 'path') { super({ code: 'PATH_VALIDATION', message, field }); this.name = 'PathValidationError'; } } export class FingerprintRequiredError extends FileError { constructor(message = 'Peer fingerprint must be verified before this operation') { super({ code: 'FINGERPRINT_REQUIRED', message }); this.name = 'FingerprintRequiredError'; } } export class OperationTimeoutError extends FileError { constructor(message = 'Operation timed out') { super({ code: 'OPERATION_TIMEOUT', message }); this.name = 'OperationTimeoutError'; } } export class IdempotencyConflictError extends FileError { constructor( message = 'Idempotency key reused with different arguments', ) { super({ code: 'IDEMPOTENCY_CONFLICT', message }); this.name = 'IdempotencyConflictError'; } } export class CancelledError extends FileError { constructor(message = 'Cancelled') { super({ code: 'CANCELLED', message }); this.name = 'CancelledError'; } } export class InternalFileError extends FileError { constructor(message = 'Internal server error', cause?: string) { super({ code: 'INTERNAL', message, ...(cause !== undefined ? { cause } : {}), }); this.name = 'InternalFileError'; } } export class NotImplementedError extends FileError { constructor(op: string) { super({ code: 'NOT_IMPLEMENTED', message: `Operation not implemented: ${op}` }); this.name = 'NotImplementedError'; } } export class CustomOpRejectedError extends FileError { constructor(message: string) { super({ code: 'CUSTOM_OP_REJECTED', message }); this.name = 'CustomOpRejectedError'; } } export class InvalidSignatureError extends FileError { constructor(message = 'RPC envelope signature verification failed') { super({ code: 'INVALID_SIGNATURE', message }); this.name = 'InvalidSignatureError'; } } export class InvalidArgsError extends FileError { constructor(message: string, field?: string) { super({ code: 'INVALID_ARGS', message, ...(field !== undefined ? { field } : {}) }); this.name = 'InvalidArgsError'; } } /** * Reconstruct the right `FileError` subclass from a wire payload. Used by * the client to surface typed errors from server responses. */ export function fileErrorFromPayload(payload: FileErrorPayload): FileError { switch (payload.code) { case 'NOT_FOUND': return new NotFoundError(payload.message, payload.field); case 'PERMISSION_DENIED': return new PermissionDeniedError(payload.message); case 'CONFLICT': return new ConflictError(payload.message); case 'QUOTA_EXCEEDED': return new QuotaExceededError(payload.message, payload.retryAfterMs); case 'RATE_LIMIT': return new FsRateLimitError(payload.message, payload.retryAfterMs); case 'PATH_VALIDATION': return new PathValidationError(payload.message, payload.field); case 'FINGERPRINT_REQUIRED': return new FingerprintRequiredError(payload.message); case 'OPERATION_TIMEOUT': return new OperationTimeoutError(payload.message); case 'IDEMPOTENCY_CONFLICT': return new IdempotencyConflictError(payload.message); case 'CANCELLED': return new CancelledError(payload.message); case 'INTERNAL': return new InternalFileError(payload.message, payload.cause); case 'NOT_IMPLEMENTED': return new NotImplementedError(payload.message); case 'CUSTOM_OP_REJECTED': return new CustomOpRejectedError(payload.message); case 'INVALID_SIGNATURE': return new InvalidSignatureError(payload.message); case 'INVALID_ARGS': return new InvalidArgsError(payload.message, payload.field); } } /** Convert any thrown value into a `FileErrorPayload` for wire serialization. */ export function payloadFromError(err: unknown): FileErrorPayload { if (err instanceof FileError) return err.payload; if (err instanceof Error) { return { code: 'INTERNAL', message: err.message.slice(0, 2048), }; } return { code: 'INTERNAL', message: 'unknown error' }; }