import type { SuaveplanErrorOptions } from "@suaveplan/error"; import { defineCodes, SuaveplanError } from "@suaveplan/error"; /** * Stable error code registry for `@suaveplan/clipboard`. Every error path * the package can take surfaces one of these codes so consumers (loggers, * retry policies, UI surfaces, telemetry pipelines) can classify failures * programmatically without parsing message strings. */ export const CLIPBOARD_CODES = defineCodes("CLIPBOARD", [ "API_UNAVAILABLE", "PERMISSION_DENIED", "FALLBACK_FAILED", "INSECURE_CONTEXT", "NO_CONTENT", "READ_FAILED", "DOCUMENT_UNAVAILABLE", "QUEUE_DRAINED", ] as const); /** Union of every error code emitted by `@suaveplan/clipboard`. */ export type ClipboardErrorCode = (typeof CLIPBOARD_CODES)[keyof typeof CLIPBOARD_CODES]; /** Per-code metadata for HTTP / retry / severity classification. */ export interface ClipboardCodeMetadata { readonly httpStatus: number; readonly severity: "info" | "warning" | "error"; readonly retryable: boolean; readonly description: string; } /** Mapping of every {@link ClipboardErrorCode} to its metadata. */ export const CLIPBOARD_ERROR_METADATA: Readonly< Record > = Object.freeze({ [CLIPBOARD_CODES.API_UNAVAILABLE]: { httpStatus: 501, severity: "warning", retryable: false, description: "navigator.clipboard is undefined; the host environment does not expose the async Clipboard API.", }, [CLIPBOARD_CODES.PERMISSION_DENIED]: { httpStatus: 403, severity: "warning", retryable: false, description: "The Permissions API reported state=denied for clipboard-read or clipboard-write.", }, [CLIPBOARD_CODES.FALLBACK_FAILED]: { httpStatus: 500, severity: "error", retryable: false, description: "document.execCommand('copy') returned false or threw during the textarea fallback.", }, [CLIPBOARD_CODES.INSECURE_CONTEXT]: { httpStatus: 400, severity: "error", retryable: false, description: "window.isSecureContext is false; clipboard operations require HTTPS or localhost when requireSecureContext is enabled.", }, [CLIPBOARD_CODES.NO_CONTENT]: { httpStatus: 400, severity: "warning", retryable: false, description: "copyRichContent was called with empty ClipboardData (no text, html, image, or blobs).", }, [CLIPBOARD_CODES.READ_FAILED]: { httpStatus: 500, severity: "error", retryable: true, description: "Clipboard read failed; the underlying API rejected or the paste-event fallback timed out.", }, [CLIPBOARD_CODES.DOCUMENT_UNAVAILABLE]: { httpStatus: 501, severity: "error", retryable: false, description: "document is undefined; the fallback path cannot run outside a DOM environment.", }, [CLIPBOARD_CODES.QUEUE_DRAINED]: { httpStatus: 503, severity: "warning", retryable: true, description: "ClipboardManager was disposed while a queued operation was still pending.", }, }); /** * Base error class for every failure raised by `@suaveplan/clipboard`. * Extends {@link SuaveplanError} so consumers receive cause-chain traversal, * Zod-validated wire format, redaction hooks, and the OTel span-event * adapter for free. */ export class ClipboardError extends SuaveplanError { constructor( code: ClipboardErrorCode, message: string, options?: SuaveplanErrorOptions ) { super(code, message, options); this.name = "ClipboardError"; } /** Metadata for this error's code. */ get metadata(): ClipboardCodeMetadata { return CLIPBOARD_ERROR_METADATA[this.code]; } } /** * Lift an unknown thrown value into a {@link ClipboardError}. Used at every * `catch` site so call sites can rely on the typed error contract. */ export function asClipboardError( code: ClipboardErrorCode, cause: unknown, fallbackMessage: string ): ClipboardError { const message = cause instanceof Error && cause.message ? cause.message : fallbackMessage; const causeError = cause instanceof Error ? cause : new Error(String(cause)); return new ClipboardError(code, message, { cause: causeError }); }