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

103 lines
6.2 KiB
Markdown

# 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.