From b33cde38f7df4a0eedacd63162a5122b67e73710 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:07:20 +0900 Subject: [PATCH 01/86] docs: design safe rich clipboard ingestion --- .../2026-08-05-safe-rich-clipboard-design.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-05-safe-rich-clipboard-design.md diff --git a/docs/superpowers/specs/2026-08-05-safe-rich-clipboard-design.md b/docs/superpowers/specs/2026-08-05-safe-rich-clipboard-design.md new file mode 100644 index 0000000..34ec537 --- /dev/null +++ b/docs/superpowers/specs/2026-08-05-safe-rich-clipboard-design.md @@ -0,0 +1,235 @@ +# Safe rich clipboard ingestion design + +**Date:** 2026-08-05 +**Status:** Approved by the standing autonomous-commercialization mandate +**Target:** Inkspan after 0.5.29 + +## Buyer-visible problem + +Users routinely paste content from Microsoft Word, Google Docs, email clients, +and other web applications. Inkspan currently lets ProseMirror parse the +browser-provided `text/html` representation directly. The existing schema, +SafeLink transaction filter, and inline-image policy provide important downstream +boundaries, but they do not define a complete clipboard contract for hidden +content, active or embedded elements, proprietary metadata, remote-resource +references, event handlers, oversized markup, or style-only semantic formatting. + +The product must preserve useful document structure while making clipboard HTML +safe, bounded, deterministic, explainable to integrators, and identical across +standalone and provider-neutral collaborative editors. + +## Standards and framework evidence + +The W3C Clipboard API and Events Working Draft dated 24 June 2026 identifies +`text/plain`, `text/html`, and `image/png` as mandatory clipboard +representations. A paste event occurs before insertion and can be intercepted; +the specification explicitly warns about hidden data, malicious JavaScript, +referenced online resources, and excessive clipboard content. It also notes that +user agents may expose unsanitized HTML, so an editor cannot assume the browser +has already established the product's trust boundary. + +TipTap exposes ProseMirror `editorProps` and extension-level +`transformPastedHTML` hooks that run before pasted HTML is parsed and inserted. +This is the narrowest integration point that preserves ordinary browser paste +semantics without introducing asynchronous clipboard permissions. + +OWASP's Cross Site Scripting Prevention guidance recommends HTML sanitization +for untrusted HTML rather than context-insensitive encoding when rich markup +must be retained. Inkspan applies that principle through a strict positive +allowlist and reconstructs a new fragment instead of mutating or serializing the +untrusted tree. + +## Considered approaches + +### A. Trust ProseMirror schema parsing + +This is the smallest implementation, but it leaves hidden content, proprietary +metadata, remote-resource references, resource exhaustion, and style-derived +semantics without an explicit product contract. Rejected. + +### B. Add a general-purpose sanitization dependency + +A mature sanitizer reduces parser-security maintenance, but it adds another +runtime dependency and broad HTML/SVG/MathML behavior that Inkspan would still +need to narrow. It also complicates framework-free packaging and supply-chain +review. Deferred as a future interchangeable backend, not selected for the +bounded first slice. + +### C. Reconstruct an allowlisted fragment in a detached template + +Selected. Parse at most a bounded input into an inert template, iteratively +traverse it, and create a new output tree containing only Inkspan-supported +semantic elements and attributes. Unsupported ordinary containers are unwrapped; +dangerous or hidden subtrees are discarded. No source node or attribute is +reused. + +## Public contract + +### Configuration + +```ts +export interface ClipboardConfig { + maxHtmlBytes?: number; + maxNodes?: number; + maxDepth?: number; +} +``` + +Defaults: + +- `maxHtmlBytes`: 1,048,576 bytes; +- `maxNodes`: 10,000 nodes; +- `maxDepth`: 64 levels. + +All values must be finite safe integers in documented ranges. Invalid +configuration fails closed at paste time through a redacted error. + +### Errors and host callback + +```ts +export type ClipboardSanitizationErrorCode = + | 'dom_unavailable' + | 'input_too_large' + | 'node_limit_exceeded' + | 'depth_limit_exceeded' + | 'invalid_configuration'; + +export class ClipboardSanitizationError extends Error { + readonly code: ClipboardSanitizationErrorCode; +} +``` + +`CwlEditorProps` adds `clipboard?: ClipboardConfig` and +`onClipboardError?: (error: ClipboardSanitizationError) => void`. The callback +never receives source HTML, text, URLs, document content, or private parser +exceptions. + +### Sanitizer + +```ts +sanitizeRichClipboardHtml( + sourceHtml: string, + config?: ClipboardConfig, + documentOverride?: Document, +): string +``` + +The third parameter is for deterministic testing and controlled browser-like +hosts. Ordinary consumers omit it. Calling the function without a DOM-capable +document fails closed. + +## Allowed content + +The reconstructed fragment may contain: + +- paragraphs and generic divisions; +- headings 1–6; +- blockquotes, preformatted text, code, line breaks, and horizontal rules; +- strong/bold, emphasis/italic, underline, strike, superscript, and subscript; +- ordered and unordered lists with list items; +- tables, sections, rows, header cells, and data cells; +- safe hyperlinks accepted by the existing `isSafeLinkHref()` policy. + +Only the following attributes survive: + +- safe `href` on links, with fixed `rel="noopener noreferrer nofollow"`; +- bounded integer `start` on ordered lists; +- bounded integer `colspan` and `rowspan` on table cells. + +All IDs, classes, inline styles, event handlers, `data-*`, arbitrary ARIA, +`target`, `contenteditable`, and proprietary Office/Google attributes are +removed from the output. + +## Semantic style conversion + +Many office applications express formatting only through inline style. Before +styles are discarded, four narrowly defined declarations are converted to +semantic wrappers: + +- bold `font-weight` to ``; +- italic or oblique `font-style` to ``; +- underline `text-decoration` to ``; +- line-through `text-decoration` to ``. + +No color, font family, font size, background, positioning, visibility, generated +content, direction override, or layout style is preserved. + +## Dropped content + +The entire subtree is discarded for active, embedded, executable, form, +metadata, resource-fetching, or non-editor namespaces, including script, style, +iframe, object, embed, applet, form controls, template, SVG, MathML, canvas, +media, source, picture, metadata, and stylesheet/base elements. + +Elements are also discarded with their descendants when they carry `hidden`, +a case-insensitive `aria-hidden="true"`, or inline declarations equivalent to +`display:none`, `visibility:hidden`, or Office `mso-hide:all`. HTML comments, +including Office conditional comments, are omitted. + +All `` elements in `text/html` are dropped. Binary clipboard image items +remain governed by the existing Base64Image size, decoding, and dimension +pipeline. This prevents HTML paste from causing remote image fetches, local-file +references, tracking pixels, or unvalidated inline image ingestion. + +## Integration + +A `SafeClipboard` TipTap extension implements `transformPastedHTML`. It is part +of `buildExtensions()` by default and receives validated config and the latest +error callback. Both `CwlEditor` and `CollaborativeCwlEditor` pass the same +options, so local and Yjs-backed editing have the same clipboard policy. + +On sanitization error, the extension reports one redacted error and returns an +empty fragment. It never falls back to unsanitized HTML. Plain-text clipboard +handling and binary image handling remain the browser/ProseMirror and +Base64Image paths respectively. + +## Security, privacy, and performance invariants + +1. No active or embedded element survives. +2. No remote or local resource reference survives except a SafeLink hyperlink. +3. No source attribute other than the explicitly validated allowlist survives. +4. Source nodes are never inserted into the output tree. +5. Input bytes, node count, and depth are bounded before or during traversal. +6. Traversal is iterative to avoid hostile recursion depth. +7. Errors expose only stable codes and bounded static messages. +8. The sanitizer never performs network, filesystem, clipboard permission, + model, provider, storage, credential, or database operations. +9. The feature introduces no database objects. +10. Standalone and collaborative surfaces use the same extension and policy. + +## Realistic verification + +Tests use Word-like and Google-Docs-like HTML fixtures, hidden tracking data, +remote images, JavaScript and credential-bearing links, Office conditional +comments, tables, lists, semantic styles, malformed nesting, control text, +oversized input, excessive breadth, excessive depth, hostile configuration, +and DOM-unavailable execution. + +Integration tests instantiate the shared TipTap kit and prove that the +extension transforms pasted HTML before parsing. React and collaborative tests +prove the latest host error callback is used without recreating the editor. +Repository acceptance remains 100% production statement, branch, function, and +line coverage, complete public docstrings, package consumers, deterministic +builds, security scans, exact-head review, and branch protection. + +## Release policy + +This changes default rich-HTML paste behavior and therefore targets the next +minor release, 0.6.0. The feature remains under `Unreleased` until the integrated +exact head passes every release gate. Tagging and registry publication are +separate post-merge operations and must not be claimed early. + +## References — APA 7th edition + +Open Worldwide Application Security Project. (n.d.). *Cross site scripting +prevention cheat sheet*. OWASP Cheat Sheet Series. +https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html + +Tiptap GmbH. (2026). *Editor API and custom extension paste transforms*. +https://tiptap.dev/docs/editor/api/editor + +World Wide Web Consortium. (2026, June 24). *Clipboard API and events* (W3C +Working Draft). https://www.w3.org/TR/2026/WD-clipboard-apis-20260624/ + +WHATWG. (2026). *HTML living standard: The template element and parsing HTML +fragments*. https://html.spec.whatwg.org/ From c33d6426816e876d3c582f4f42a80c1ce80752cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:09:02 +0900 Subject: [PATCH 02/86] docs: plan safe rich clipboard implementation --- .../plans/2026-08-05-safe-rich-clipboard.md | 282 ++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-05-safe-rich-clipboard.md diff --git a/docs/superpowers/plans/2026-08-05-safe-rich-clipboard.md b/docs/superpowers/plans/2026-08-05-safe-rich-clipboard.md new file mode 100644 index 0000000..c306086 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-safe-rich-clipboard.md @@ -0,0 +1,282 @@ +# Safe Rich Clipboard Ingestion Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a bounded, fail-closed, semantic rich-HTML clipboard sanitizer that is enabled consistently in standalone and collaborative Inkspan editors. + +**Architecture:** A framework-independent `SafeClipboard` TipTap extension transforms `text/html` before ProseMirror parsing. The sanitizer parses into an inert template, iteratively reconstructs an allowlisted fragment, reuses the existing SafeLink URI policy, drops all resource-bearing images and active/hidden subtrees, and reports only redacted stable errors through the latest host callback. + +**Tech Stack:** TypeScript 5.7, TipTap 2.27/ProseMirror, React 18/19, Vitest 3 with jsdom, Vite 6, existing Inkspan SafeLink and Base64Image policies. + +## Global Constraints + +- Default maximum source HTML: 1,048,576 UTF-8 bytes. +- Default maximum traversed nodes: 10,000. +- Default maximum source depth: 64. +- No new runtime dependency. +- No network, filesystem, model, provider, credential, storage, or database operation. +- Drop every HTML ``; binary clipboard images continue through Base64Image. +- Preserve only the elements and attributes listed in the approved design. +- Convert only bold, italic/oblique, underline, and line-through inline styles to semantic elements. +- Use the existing `isSafeLinkHref()` policy; never repair or trim a link. +- Errors contain only stable codes and static bounded messages. +- Production statement, branch, function, and line coverage: 100%. +- Public module, type, class, method, function, and property documentation: 100%. +- The behavior targets release 0.6.0 but remains under `Unreleased` until exact-head acceptance. + +--- + +### Task 1: Public sanitizer contract and red tests + +**Files:** +- Create: `src/extensions/SafeClipboard.ts` +- Create: `src/extensions/SafeClipboard.test.ts` + +**Interfaces:** +- Consumes: `isSafeLinkHref(href: unknown): href is string` from `src/extensions/SafeLink.ts`. +- Produces: `ClipboardConfig`, `ClipboardSanitizationErrorCode`, `ClipboardSanitizationError`, `sanitizeRichClipboardHtml()`, and `SafeClipboard`. + +- [ ] **Step 1: Write failing validation and resource-limit tests** + +Create tests that require: + +```ts +expect(() => sanitizeRichClipboardHtml('

x

', { maxHtmlBytes: 0 }, document)) + .toThrowError(expect.objectContaining({ code: 'invalid_configuration' })); +expect(() => sanitizeRichClipboardHtml('x'.repeat(1_048_577), {}, document)) + .toThrowError(expect.objectContaining({ code: 'input_too_large' })); +``` + +Add breadth, depth, and missing-DOM cases with exact stable error codes. + +- [ ] **Step 2: Run the focused tests and confirm red** + +Run: + +```bash +pnpm vitest run src/extensions/SafeClipboard.test.ts +``` + +Expected: failure because the module and exported contracts do not exist. + +- [ ] **Step 3: Implement validated configuration and error types** + +Use safe-integer validation and static messages: + +```ts +export type ClipboardSanitizationErrorCode = + | 'dom_unavailable' + | 'input_too_large' + | 'node_limit_exceeded' + | 'depth_limit_exceeded' + | 'invalid_configuration'; +``` + +Never include source HTML, URLs, attribute values, or private exceptions. + +- [ ] **Step 4: Implement iterative allowlist reconstruction** + +Parse into `documentOverride.implementation.createHTMLDocument('').createElement('template')`, traverse `template.content` using an explicit stack, and append only newly created output nodes. Drop comments and dangerous/hidden subtrees. Unwrap unsupported ordinary elements. + +- [ ] **Step 5: Implement semantic style conversion and safe attributes** + +Convert the four documented style categories to semantic wrappers. Preserve only SafeLink `href` plus fixed `rel`, bounded ordered-list `start`, and bounded table-cell span attributes. + +- [ ] **Step 6: Add realistic Word/Google Docs/security tests** + +Fixtures must include: + +```html +

Word text

+
Docs
+ + +unsafe +``` + +Assert semantic text remains while script, image, unsafe link, class, style, event handler, and hidden data do not. + +- [ ] **Step 7: Run focused sanitizer tests** + +```bash +pnpm vitest run src/extensions/SafeClipboard.test.ts +``` + +Expected: all focused tests pass. + +- [ ] **Step 8: Commit the sanitizer** + +```bash +git add src/extensions/SafeClipboard.ts src/extensions/SafeClipboard.test.ts +git commit -m "feat(clipboard): sanitize rich HTML paste" +``` + +### Task 2: Shared editor integration and host callback + +**Files:** +- Modify: `src/extensions/kit.ts` +- Modify: `src/extensions/kit.test.ts` +- Modify: `src/types.ts` +- Modify: `src/components/CwlEditor.tsx` +- Modify: `src/collaboration/CollaborativeCwlEditor.tsx` +- Test: `src/components/CwlEditor.test.tsx` or the repository's focused editor integration suite +- Test: `src/collaboration/CollaborativeCwlEditor.test.tsx` + +**Interfaces:** +- Consumes: Task 1 `ClipboardConfig`, `ClipboardSanitizationError`, and `SafeClipboard`. +- Produces: `clipboard?: ClipboardConfig` and `onClipboardError?: (error: ClipboardSanitizationError) => void` on both editor surfaces. + +- [ ] **Step 1: Write failing kit tests** + +Require `buildExtensions()` to include exactly one `safeClipboard` extension by default and to forward explicit config/error callback options. + +- [ ] **Step 2: Write failing React callback-liveness tests** + +Render an editor, replace `onClipboardError` without recreating the editor, trigger the extension transform with oversized HTML, and assert only the latest callback receives one redacted `input_too_large` error. + +Repeat for the collaborative surface with a host-owned Yjs document. + +- [ ] **Step 3: Run the focused tests and confirm red** + +```bash +pnpm vitest run src/extensions/kit.test.ts src/components/CwlEditor.test.tsx src/collaboration/CollaborativeCwlEditor.test.tsx +``` + +Expected: failures because the props and extension forwarding do not exist. + +- [ ] **Step 4: Add public prop and config documentation** + +Import the Task 1 types into `src/types.ts`, document the security defaults, and add `clipboard` and `onClipboardError` to `CwlEditorProps`. `CollaborativeCwlEditorProps` inherits the same contract. + +- [ ] **Step 5: Add the extension to the shared kit** + +Append `SafeClipboard.configure({ ... })` in `buildExtensions()` before host-provided `additionalExtensions`, forwarding validated config and the error callback. + +- [ ] **Step 6: Wire latest callbacks without editor recreation** + +Use `useLatestRef` and a stable `useCallback` in both React editors, matching the existing image-error pattern. + +- [ ] **Step 7: Run focused integration tests** + +```bash +pnpm vitest run src/extensions/kit.test.ts src/components/CwlEditor.test.tsx src/collaboration/CollaborativeCwlEditor.test.tsx +``` + +Expected: all focused tests pass. + +- [ ] **Step 8: Commit integration** + +```bash +git add src/extensions/kit.ts src/extensions/kit.test.ts src/types.ts src/components/CwlEditor.tsx src/collaboration/CollaborativeCwlEditor.tsx src/components/CwlEditor.test.tsx src/collaboration/CollaborativeCwlEditor.test.tsx +git commit -m "feat(clipboard): enforce shared paste policy" +``` + +### Task 3: Public exports, documentation, and release evidence + +**Files:** +- Modify: `src/index.ts` +- Modify: `README.md` +- Modify: `CHANGELOG.md` +- Create: `docs/clipboard-security.md` +- Create: `docs/doctoring/safe-rich-clipboard.md` +- Modify: `ARCHITECTURE.md` if present; otherwise create it with the editor trust-boundary section +- Test: `src/exports.test.ts` or the repository's export-contract suite +- Test: `scripts/release-metadata.test.mjs` only when preparing 0.6.0, not in the feature PR + +**Interfaces:** +- Consumes: Tasks 1 and 2 public contracts. +- Produces: buyer-facing API discovery, architecture ownership, APA 7 evidence, and `Unreleased` changelog scope. + +- [ ] **Step 1: Write failing export and documentation contract tests** + +Require root exports for sanitizer types/functions and README text that identifies safe Word/Google Docs paste, dropped HTML images, and the host error callback. + +- [ ] **Step 2: Run export/documentation tests and confirm red** + +```bash +pnpm vitest run src/exports.test.ts +pnpm run test:package-config +``` + +Expected: failures for missing public exports or documentation markers. + +- [ ] **Step 3: Add public exports** + +Export the sanitizer, extension, error class, error-code type, and config type from `src/index.ts` without adding a new package subpath. + +- [ ] **Step 4: Write operator and architecture documentation** + +Document allowed structure, dropped content, HTML-image behavior, limits, errors, standalone/collaboration equivalence, SSR behavior, host responsibilities, and rollback. Add the W3C Clipboard, OWASP, TipTap, and WHATWG sources in APA 7 form. + +- [ ] **Step 5: Update `CHANGELOG.md` under `Unreleased`** + +Record the buyer-visible feature, security boundary, realistic tests, and that the next minor release is 0.6.0 only after integrated acceptance. + +- [ ] **Step 6: Run focused export/documentation tests** + +```bash +pnpm vitest run src/exports.test.ts +pnpm run test:package-config +``` + +Expected: pass. + +- [ ] **Step 7: Commit docs and exports** + +```bash +git add src/index.ts README.md CHANGELOG.md ARCHITECTURE.md docs/clipboard-security.md docs/doctoring/safe-rich-clipboard.md src/exports.test.ts +git commit -m "docs(clipboard): publish safe paste contract" +``` + +### Task 4: Exact-head repository verification and pull request + +**Files:** +- No new production files unless a test exposes a real defect. + +**Interfaces:** +- Consumes: all previous tasks. +- Produces: one reviewable feature PR with exact-head evidence. + +- [ ] **Step 1: Run the complete TypeScript gate** + +```bash +pnpm install --frozen-lockfile +pnpm typecheck +pnpm coverage +pnpm build +pnpm verify:package +pnpm build:demo +``` + +Expected: every command succeeds and production statement, branch, function, and line coverage are 100%. + +- [ ] **Step 2: Run Office gates** + +```bash +cd office +python -m pip install --require-hashes --only-binary=:all: -r requirements-ci.txt +python -m pip check +python scripts/check_docstrings.py +pytest +python -m pip wheel . --no-deps --no-build-isolation --wheel-dir dist +``` + +Expected: Python 3.11 and 3.14 CI lanes later repeat the same 100% branch/docstring and package checks. + +- [ ] **Step 3: Inspect package contents and dependency graph** + +Confirm no new runtime dependency, no unexpected package artifact, and no React/TipTap duplication outside existing surfaces. + +- [ ] **Step 4: Open one PR** + +Title: + +```text +feat: sanitize rich clipboard HTML +``` + +The body must list exact head, TDD evidence, realistic fixtures, 100% gates, security ownership, standalone/collaboration equivalence, and the 0.6.0 release boundary. + +- [ ] **Step 5: Review, fix, and merge loop** + +Inspect every current-head review thread and required check, fix only valid findings, rerun failed checks, resolve addressed threads, request exact-head independent review, enable auto-merge, and merge only with expected-head protection after all repository policy is satisfied. From c9ab5f87b7ee7a43a5dc05bdbae4ddcc7b8ed6e2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 5 Aug 2026 22:12:00 +0900 Subject: [PATCH 03/86] test(clipboard): define safe rich paste contract --- src/extensions/SafeClipboard.test.ts | 275 +++++++++++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 src/extensions/SafeClipboard.test.ts diff --git a/src/extensions/SafeClipboard.test.ts b/src/extensions/SafeClipboard.test.ts new file mode 100644 index 0000000..e056b9b --- /dev/null +++ b/src/extensions/SafeClipboard.test.ts @@ -0,0 +1,275 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + ClipboardSanitizationError, + DEFAULT_CLIPBOARD_HTML_BYTES, + DEFAULT_CLIPBOARD_MAX_DEPTH, + DEFAULT_CLIPBOARD_MAX_NODES, + SafeClipboard, + sanitizeRichClipboardHtml, +} from './SafeClipboard.js'; + +/** Create nested markup with an exact element depth. */ +function nestedHtml(depth: number): string { + return `${'
'.repeat(depth)}x${'
'.repeat(depth)}`; +} + +describe('sanitizeRichClipboardHtml', () => { + it('preserves Word and Google Docs semantics while stripping proprietary styling', () => { + const source = ` + +

+ Word italic +

+
+ Docs +
+ Office namespace text + `; + + const sanitized = sanitizeRichClipboardHtml(source, undefined, document); + const container = document.createElement('div'); + container.innerHTML = sanitized; + + expect(container.querySelector('strong')).toHaveTextContent('Word italic'); + expect(container.querySelector('em')).toHaveTextContent('italic'); + expect(container.querySelector('u s, s u')).toHaveTextContent('Docs'); + expect(container).toHaveTextContent('Office namespace text'); + expect(sanitized).not.toMatch(/MsoNormal|data-docs-id|style=|onclick|