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:
- No image / arbitrary-blob clipboard I/O —
ClipboardItemis only used fortext/plainandtext/html. - Errors are hand-crafted strings (
"Clipboard API failed") with no@suaveplan/errorintegration, no error code registry, no programmatic discrimination. - No secure-context guard — calling code outside HTTPS /
localhostfalls through to a generic failure with no diagnostic signal. - No paste-event fallback for
readTextin non-API contexts; the only outcome is"Reading clipboard requires Clipboard API". - No permission-state subscription — UIs cannot react to
granted/deniedtransitions. - No OTel instrumentation — production cannot measure copy/read volume, fallback usage, error rate, or operation latency.
- No async serialization — concurrent
copy/readcalls 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/clipboardat version0.1.0underpackages/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 viaClipboardItem, paste-event fallback, permission change subscription, focus-aware retry, MIME-keyed format negotiation, paste sanitizer hook, and a runtime-injectableClipboardAdapter/ClipboardNavigatorseam. - 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_totaland histogramclipboard_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:
- Update imports from
@suaveplan/clipboard(path stays the same). - Treat
ClipboardResult.errorasstring | undefinedper the upstream contract, but prefer matching onClipboardResult.code(new field) againstCLIPBOARD_CODES.*. - Adopt
withQueue-based serialization automatically — no opt-in required (defaultserialize: true); setserialize: falseto restore upstream's race-allowed behavior.