Files
suaveplan/openspec/archive/2026-05-25-enterprise-clipboard/proposal.md
2026-05-27 23:29:53 +01:00

6.2 KiB

Change: enterprise-clipboard

  • Package: @suaveplan/clipboard
  • Status: PROPOSED
  • Design: ./design.md
  • Spec delta: ./specs/clipboard/spec.md
  • Upstream: ../suaveplan/packages/browser/clipboard (1.0.3)
  • Enterprise audit: ../suaveplan/enterprise/clipboard.md

Why

Genesis is migrating @suaveplan/clipboard from the legacy packages/browser/clipboard/ checkout into the canonical packages/foundation/data/clipboard/ location at enterprise quality. The upstream package shipped at 1.0.3 with a thin ClipboardManager facade over the browser Clipboard API plus a textarea+execCommand fallback, but the enterprise audit flagged seven gaps that block production use:

  1. No image / arbitrary-blob clipboard I/O — ClipboardItem is only used for text/plain and text/html.
  2. Errors are hand-crafted strings ("Clipboard API failed") with no @suaveplan/error integration, no error code registry, no programmatic discrimination.
  3. No secure-context guard — calling code outside HTTPS / localhost falls through to a generic failure with no diagnostic signal.
  4. No paste-event fallback for readText in non-API contexts; the only outcome is "Reading clipboard requires Clipboard API".
  5. No permission-state subscription — UIs cannot react to granted/denied transitions.
  6. No OTel instrumentation — production cannot measure copy/read volume, fallback usage, error rate, or operation latency.
  7. No async serialization — concurrent copy / read calls race and the trailing call wins non-deterministically.

The greenfield rewrite addresses every gap while preserving the upstream public surface (ClipboardManager, ClipboardNavigator, ClipboardData, ClipboardOptions, ClipboardResult, PermissionResult, DEFAULT_CLIPBOARD_OPTIONS) so downstream consumers continue to compile against the same import paths.

What changes

  • Add: @suaveplan/clipboard at version 0.1.0 under packages/foundation/data/clipboard/ with full enterprise surface — typed errors via @suaveplan/error, OTel counters + histogram, async queue serializer, secure-context guard, image / blob copy + read via ClipboardItem, paste-event fallback, permission change subscription, focus-aware retry, MIME-keyed format negotiation, paste sanitizer hook, and a runtime-injectable ClipboardAdapter / ClipboardNavigator seam.
  • Add: error-code namespace CLIPBOARD.* with eight stable codes (API_UNAVAILABLE, PERMISSION_DENIED, FALLBACK_FAILED, INSECURE_CONTEXT, NO_CONTENT, READ_FAILED, DOCUMENT_UNAVAILABLE, QUEUE_DRAINED).
  • Add: OTel instruments — counters clipboard_copy_total, clipboard_read_total, clipboard_fallback_used_total, clipboard_error_total and histogram clipboard_op_latency_ms.
  • Add: subpath exports per feature module (./manager/manager, ./fallback/fallback, ./permissions/permissions, ./queue/queue, ./observability/observability, ./errors/errors, ./security/security, ./sanitizer/sanitizer, ./types).

Impact

Affected dependents

bunx turbo ls --filter=...@suaveplan/clipboard — empty (clipboard is a leaf migration; no current genesis package depends on it).

  • No dependents (verified by bunx turbo ls --filter=...@suaveplan/clipboard).

Future React consumers (react-ui, react-command-palette) will adopt this package via a follow-up change once they migrate; no work is required in this proposal.

Parity Checklist

Capability Upstream Migrated Notes
ClipboardManager facade ✅ ✅ Same constructor signature; options bag + injectable ClipboardNavigator.
copyText(text) ✅ ✅ Now wraps every call in withQueue and emits OTel metrics.
readText() ✅ ⬆️ Paste-event fallback added when no Clipboard API; requireSecureContext gate added.
copyRichContent({text,html}) ✅ ⬆️ Extended with image: Blob, blobs: Record<string, Blob>, TrustedHTML accepted, MIME map.
readRichContent() ✅ ⬆️ Now returns image and blobs fields, not just text/html.
checkPermissions() ✅ ✅ Same shape; resilient to permission-query failures.
isSupported() ✅ ✅ Same semantics.
DEFAULT_CLIPBOARD_OPTIONS ✅ ⬆️ Extended with requireSecureContext, serialize, sanitizer, focusRetry.
execCommand fallback ✅ ✅ Retained; emits clipboard_fallback_used_total{reason} counter.
Permission change subscription 🚫 ⬆️ NEW — onPermissionChange(cb) returning unsubscribe.
Image / blob copy + read 🚫 ⬆️ NEW — ClipboardItem with PNG / arbitrary MIME blobs.
Async queue serializer 🚫 ⬆️ NEW — serialize: true default; concurrent calls execute in arrival order.
Secure-context guard 🚫 ⬆️ NEW — requireSecureContext() throws CLIPBOARD.INSECURE_CONTEXT.
Focus-aware retry 🚫 ⬆️ NEW — on NotAllowedError "Document not focused", awaits next focus once.
Paste-event fallback 🚫 ⬆️ NEW — readTextFallback orchestrates focus + paste capture.
Paste sanitizer hook 🚫 ⬆️ NEW — options.sanitizer(html) => string runs before readRichContent returns.
Typed errors with codes 🚫 ⬆️ NEW — ClipboardError extends SuaveplanError; all paths use codes.
OTel counters + histogram 🚫 ⬆️ NEW — five instruments wired into every public method.
Playwright proof 🚫 🚫 Scoped out: greenfield package starts with happy-dom unit tests at 100% coverage; Playwright spec lands with react-ui adoption follow-up so the host harness exists.

Migration

This is a 0.1.0 first landing — no prior genesis consumers. Downstream waves that adopted the upstream surface from ../suaveplan/packages/browser/clipboard should:

  1. Update imports from @suaveplan/clipboard (path stays the same).
  2. Treat ClipboardResult.error as string | undefined per the upstream contract, but prefer matching on ClipboardResult.code (new field) against CLIPBOARD_CODES.*.
  3. Adopt withQueue-based serialization automatically — no opt-in required (default serialize: true); set serialize: false to restore upstream's race-allowed behavior.