Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
51f9edf
test(reliability): prove clipboard preflight allocation gaps
cursoragent Aug 16, 2026
71654a8
fix(reliability): preflight clipboard size and traversal budget
cursoragent Aug 16, 2026
58f004e
docs(clipboard): bind preflight doctoring to GREEN head
cursoragent Aug 16, 2026
1721f10
chore: synchronize clipboard preflight with protected main
seonghobae Aug 17, 2026
37105d7
test(clipboard): require actionable customer guidance
seonghobae Aug 28, 2026
d0255ec
fix(ux): make clipboard rejection guidance actionable
seonghobae Aug 28, 2026
4cfa7ef
test(clipboard): align coverage contract with actionable guidance
seonghobae Aug 28, 2026
8185d86
test(clipboard): align sanitizer expectations with actionable guidance
seonghobae Aug 28, 2026
47032a5
test(clipboard): align adapter expectation with actionable guidance
seonghobae Aug 28, 2026
b6108e2
test(docs): require truthful clipboard guidance contract
seonghobae Aug 28, 2026
b35acb4
docs(clipboard): keep preflight record truthful about guidance
seonghobae Aug 28, 2026
9ab6980
docs(clipboard): describe stable codes and actionable messages
seonghobae Aug 28, 2026
8f090ca
Merge remote-tracking branch 'origin/fix/clipboard-hostile-throw-349'…
seonghobae Sep 4, 2026
d7a8a32
test(clipboard): align stacked guidance contract
seonghobae Sep 4, 2026
bb3187c
chore(stack): synchronize clipboard containment parent
seonghobae Sep 4, 2026
9061add
test(clipboard): expose repeated child-list reads
seonghobae Sep 7, 2026
ea65e3b
perf(clipboard): reuse the bounded source child list
seonghobae Sep 7, 2026
1ca6ed3
docs(clipboard): record bounded child-list reuse evidence
seonghobae Sep 7, 2026
3603701
test(clipboard): clarify the source child-list binding
seonghobae Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim
- Raised workspace-wide transitive development-tool overrides for `fast-uri`, `nanoid`, and `postcss` to patched minimums, keeping the lockfile audit clean without changing runtime package authority.
- Normalized isolated package-verifier temporary roots before containment checks on macOS.

### Reliability
- Reject obviously oversized rich clipboard HTML from UTF-16 code-unit length before allocating a UTF-8 copy, and reject broad source trees before materializing children beyond `maxNodes`.

## [0.6.0] — 2026-08-10

### Release
Expand Down
9 changes: 9 additions & 0 deletions docs/TRACEABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This record maps durable Inkspan product decisions to authoritative standards, p
| Provenance semantics | Local transition/release evidence keeps content lineage separate from actor/authorization/durable claims | W3C PROV family | transition evidence, release evidence, canonical data model | Inkspan does not claim complete PROV conformance or host audit provenance |
| Accessibility | Native controls, keyboard semantics, shortcut metadata, semantic placeholder guidance, and host-facing status state support accessible embedding | W3C WCAG 2.2; WAI-ARIA 1.2 where used | protected toolbar/accessibility tests, SSR tests, autosave lifecycle data, protected #131 placeholder tests/packed consumer and `docs/doctoring/editor-placeholder-accessibility.md` | Component evidence alone is not a full host WCAG conformance claim; `aria-placeholder` supplements but never replaces the accessible name |
| Browser clipboard behavior | Security-relevant rich HTML handling requires actual paste-pipeline integration and bounded semantic reconstruction before editor state | WHATWG HTML parsing; W3C Clipboard API | protected-main rich-clipboard unit/integration corpus and SafeClipboard ADR | Protected jsdom/TipTap integration success is not universal browser-engine conformance |
| Clipboard resource preflight | Reject UTF-16 length above `maxHtmlBytes` before UTF-8 allocation, and reject `visited + queued + enqueueable > maxNodes` before child materialization | Unicode Standard 16.0 §3.9; ECMA-262 string length; WHATWG DOM `NodeList`; CWE-770; W3C Clipboard API | Active-PR doctoring `docs/doctoring/clipboard-resource-preflight.md`, operator guide, and SafeClipboard preflight/traversal regressions | Proposed until protected `main`; jsdom proof is not cross-engine conformance or a claim that remaining HTML is trusted |
| Cross-engine release assurance | The same committed synthetic adversarial corpus runs under required Chromium, Firefox, and WebKit projects; exact package-lock and packed npm artifact SHA-256 digests are required, and only focused standards-grounded safe differences may be admitted | WHATWG HTML Living Standard; W3C Clipboard API and events; Playwright 1.62 release notes and browser/project documentation | ADR 0016, protected-main browser evidence source/workflows, TEST_STRATEGY, OPERABILITY and UML | Protected-main implementation is the release-policy authority; every release candidate must regenerate fresh exact-source/lock/run/browser evidence bound to the exact packed npm artifact SHA-256 and does not claim byte-identical browser serialization or branded enterprise-policy coverage |
| CSS paged-media output | Shipped editor CSS has a declarative print boundary that removes interactive chrome and screen clipping while preserving authored document flow and bounded fragmentation behavior | W3C Media Queries Level 3; CSS Fragmentation Level 3; CSS Paged Media Level 3 as tracked draft input | protected-main #116 packaged stylesheet, real-browser print-media evidence, ADR 0021, print doctoring and tests | `implemented_on_protected_main`; browser print styling does not create a durable PDF service, page-number/header authority, persistence, signing, or PDF-conformance claim |
| Editor chrome design tokens | Repeating toolbar/editor surfaces share named `--cwl-*` custom properties; hosts override those properties on `.cwl-editor` and preview them in Storybook | Design Tokens Format Module 2025.10; WCAG 2.2 contrast including inventoried `--cwl-accent` on `--cwl-accent-soft`; Storybook React/Vite | Active-PR doctoring `docs/doctoring/editor-design-tokens.md`, operator guide, ADR 0031, token catalog tests, and Storybook inventory | Proposed until protected `main`; the interchange snapshot is not complete DTCG conformance, Figma Variables sync, or a host WCAG certification |
Expand All @@ -43,6 +44,8 @@ Design Tokens Community Group. (2025, October 28). *Design Tokens Format Module

Ecma International. (2021). *ECMA-376: Office Open XML file formats* (5th ed.). https://ecma-international.org/publications-and-standards/standards/ecma-376/

Ecma International. (2025). *ECMAScript® 2025 language specification* (ECMA-262, 16th ed.). https://tc39.es/ecma262/2025/

Ecma International. (2026). *ECMA-402: ECMAScript 2026 internationalization API specification* (13th ed.). https://402.ecma-international.org/

Fielding, R., Nottingham, M., & Reschke, J. (Eds.). (2022). *HTTP Semantics* (RFC 9110; STD 97). RFC Editor. https://doi.org/10.17487/RFC9110
Expand All @@ -61,6 +64,8 @@ Microsoft. (n.d.-e). *Release notes: Version 1.62*. Playwright. Retrieved August

Microsoft. (n.d.-f). *Working with paragraphs*. Microsoft Learn. Retrieved August 10, 2026, from https://learn.microsoft.com/en-us/office/open-xml/word/working-with-paragraphs

MITRE. (2024). *CWE-770: Allocation of resources without limits or throttling*. https://cwe.mitre.org/data/definitions/770.html

Node.js contributors. (2026). *Modules: Packages*. Node.js documentation. https://nodejs.org/api/packages.html

ProseMirror. (n.d.). *ProseMirror reference manual*. Retrieved August 10, 2026, from https://prosemirror.net/docs/ref/
Expand All @@ -73,6 +78,10 @@ Rundgren, A., Jordan, B., & Erdtman, S. (2020). *JSON Canonicalization Scheme (J

Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software Development Framework (SSDF) Version 1.1: Recommendations for Mitigating the Risk of Software Vulnerabilities* (NIST SP 800-218). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218

Unicode Consortium. (2024). *The Unicode Standard, Version 16.0.0*. https://www.unicode.org/versions/Unicode16.0.0/

Web Hypertext Application Technology Working Group. (2026). *DOM Standard*. Retrieved August 16, 2026, from https://dom.spec.whatwg.org/

Storybook. (n.d.). *Storybook for React with Vite*. Retrieved August 16, 2026, from https://storybook.js.org/docs/get-started/frameworks/react-vite

Web Hypertext Application Technology Working Group. (2026). *HTML Standard: Parsing HTML documents* (Living Standard). Retrieved August 10, 2026, from https://html.spec.whatwg.org/multipage/parsing.html
Expand Down
23 changes: 23 additions & 0 deletions docs/clipboard-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@ Error observers are live: replacing `onClipboardError` does not recreate the
editor or the Yjs binding. A host callback failure is contained and cannot make
rejected HTML enter the document.

## Resource preflight

If a paste is rejected with `input_too_large` or `node_limit_exceeded`, raise
the matching ceiling only after measuring a trusted source. Do not disable the
limits to “make paste work.”

Inkspan rejects an obviously oversized string when its UTF-16 code-unit length
already exceeds `maxHtmlBytes`, before allocating a UTF-8 copy. Every UTF-16
code unit encodes to at least one UTF-8 byte, so that length check is a safe
lower bound. Strings whose code-unit length is within the ceiling still receive
the exact UTF-8 byte-length check, because non-ASCII text can expand.

Inkspan also rejects a broad source tree when already visited nodes, already
queued frames, and newly enqueueable children would exceed `maxNodes`, before
materializing those children. Dropped or hidden subtrees are never traversed
and therefore do not consume descendant budget. The closed-`details` summary
path uses the same queue invariant.

The machine-readable error codes remain stable, while customer-facing rejection
messages give a bounded next action. The preflight checks do not authorize the
remaining HTML. See `docs/doctoring/clipboard-resource-preflight.md` for the
standards basis, test-first evidence, residual risk, and rollback.

## Preserved structure

Inkspan reconstructs a new fragment containing only:
Expand Down
135 changes: 135 additions & 0 deletions docs/doctoring/clipboard-resource-preflight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Doctoring record: rich clipboard resource preflight

**Date:** 2026-08-16
**Status:** Active PR / Proposed
**Decision owner:** ContextualWisdomLab
**Scope:** Allocation bounds inside `sanitizeRichClipboardHtml()` before UTF-8
encoding and before DOM child materialization.

## Buyer-visible gap

Hosts paste untrusted HTML from Word, browsers, mail, and support tools. A
caller-controlled string can already be larger than `maxHtmlBytes` in UTF-16
code units, yet a complete `TextEncoder` copy was still allocated before
rejection. A broad surviving source node could also enqueue every child before
`maxNodes` was enforced. Buyers therefore saw the configured ceilings as
weaker than their names: Inkspan could still amplify memory on rejected paste.

If a paste fails with `input_too_large` or `node_limit_exceeded`, measure the
trusted source and raise only that ceiling. Do not remove the limits.

## Decision

1. Reject `sourceHtml.length > maxHtmlBytes` with the existing redacted
`input_too_large` error before constructing `TextEncoder`. Retain the exact
UTF-8 `byteLength` check when code-unit length alone cannot reject.
2. Reject when `visited + queued + newly enqueueable` source nodes would exceed
`maxNodes`, before `NodeList.item()` materializes those children. The
closed-`details` first-summary path uses the same invariant.
3. Keep dropped and hidden subtrees unvisited so their descendants do not
consume budget. Preserve source order, allowlist, SafeLink, depth, and
preserve the stable error codes while allowing the customer-facing messages to remain actionable.

No network, persistence, credential, model, tenant, collaboration-provider, or
durable-audit authority is added.

## Standards rationale

The Unicode Standard defines UTF-8 as a variable-width encoding in which every
scalar value uses one or more 8-bit code units, and never zero
(Unicode Consortium, 2024, §3.9). ECMA-262 exposes `String` length as UTF-16
code units (Ecma International, 2025, §6.1.4). A UTF-16 code unit therefore
contributes at least one UTF-8 byte, so `sourceHtml.length > maxHtmlBytes` is a
conservative lower-bound rejection. Non-ASCII text can still expand, so the
exact encoder check remains required inside the ceiling.

WHATWG DOM defines `Node.childNodes` as a live `NodeList` whose members are
retrieved by index (Web Hypertext Application Technology Working Group, 2026).
Counting enqueueable children against the remaining node budget before index
access prevents the sanitizer from allocating a traversal stack larger than the
configured ceiling.

CWE-770 records allocation without a matching limit as a reliability and
availability defect (MITRE, 2024). The W3C Clipboard API Working Draft treats
HTML clipboard payloads as untrusted input (World Wide Web Consortium, 2026).
The cited Working Draft is work in progress and is not a conformance claim.

## Test-first evidence

- RED `51f9edfdff7de9072cafa8cebaf068dc39f92208` on protected
`main@e8109ec2a17de8bd6594487aa12c8c8a93cb2c03` proved an ASCII
nine-code-unit string under `maxHtmlBytes: 8` still called
`TextEncoder.prototype.encode`, and a three-child fragment under
`maxNodes: 2` performed three `NodeList.item()` reads before
`node_limit_exceeded`.
- GREEN `71654a8e59eecd72f2a23ebec173e4e537c927d9` rejects both cases at the
preflight boundary without changing the stable error codes.
- Customer-guidance RED `37105d712c774b7ef638315d7d691311fd52c1ad`
required actionable, privacy-safe messages while the source still emitted
implementation-oriented text.
- Customer-guidance GREEN `d0255ecb05e77b3c98f365c51fc3a15c630c736a`
keeps those codes stable while making size, complexity, nesting, and invalid
HTML rejections tell the customer what to do next. Exact-message regression
contracts remain redacted and contain no pasted source content.

Predecessor Draft #164 remains historical. It is not current-main
implementation authority.

## Accepted-child list reuse finding — 2026-09-07

Status remains Active PR / Proposed (#359), not protected-main behavior.
The shared child-enqueue loop read `sourceNode.childNodes` once to count the
children and again for every accepted child. RED `9061addb` records four getter
reads for a three-child source fragment while preserving its exact output.
Candidate `ea65e3b` retains one local reference and uses it for both the existing
capacity check and reverse-order item reads. The DOM Standard marks this
attribute `[SameObject]`; the reference remains a live `NodeList`, not a copied
snapshot or a cache (Web Hypertext Application Technology Working Group, 2026).

The original over-budget rejection still happens before child item reads.
Source order, hidden-subtree handling, error containment, limits, and public
contracts are unchanged. No new traversal implementation or dependency is added.
Expanded clipboard coverage retained 50 passing tests and one existing Word
capacity timeout; it is not full acceptance. Independent TypeScript and the
four-test getter/preflight/full-3,000-paragraph capacity diagnostic passed.
This establishes fewer property reads, not a measured buyer speedup or the
cause of the earlier capacity timeout. Whole-suite, packed-consumer, browser,
and protected-integration proof remain separate requirements. Reverting only
the local-reference change restores the prior lookup pattern without removing
the resource guards.

## Residual risk

The UTF-16 lower bound does not replace the exact UTF-8 check. Queue preflight
counts enqueueable children of a surviving parent; it does not invent a second
hidden-content policy. Actionable customer-facing messages do not authorize
rejected HTML and do not weaken the fail-closed empty-fragment behavior. jsdom
success is not Chromium, Firefox, or WebKit conformance. Cross-engine corpus
evidence remains a 0.6.0 release-acceptance gate.

## Rollback

Rollback must remove the length preflight, the traversal-capacity guard, this
record, the operator guidance, the documentation contract, and the changelog
entry together. It must restore the previous encode-then-compare and
visit-then-reject behavior only as one change. Any rollback of customer-facing
message text must keep the stable codes and privacy-redaction contract intact.
Rollback requires the same exact-head review, coverage, security, packaging,
and independent-approval gates.

## References (APA 7th edition)

Ecma International. (2025). *ECMAScript® 2025 language specification*
(ECMA-262, 16th ed.). https://tc39.es/ecma262/2025/

MITRE. (2024). *CWE-770: Allocation of resources without limits or throttling*.
https://cwe.mitre.org/data/definitions/770.html

Unicode Consortium. (2024). *The Unicode Standard, Version 16.0.0*.
https://www.unicode.org/versions/Unicode16.0.0/

Web Hypertext Application Technology Working Group. (2026). *DOM Standard*.
Retrieved September 7, 2026, from https://dom.spec.whatwg.org/#dom-node-childnodes

World Wide Web Consortium. (2026, June 24). *Clipboard API and events* (W3C
Working Draft). https://www.w3.org/TR/2026/WD-clipboard-apis-20260624/
49 changes: 49 additions & 0 deletions src/clipboardResourcePreflightDocumentation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';

import { describe, expect, it } from 'vitest';

/** Read one repository document and normalize layout whitespace. */
function normalizedDocument(path: string): string {
return readFileSync(resolve(process.cwd(), path), 'utf8')
.replace(/\s+/gu, ' ')
.trim();
}

describe('clipboard resource-preflight documentation contract', () => {
it('tells hosts how to respond to size and node-limit rejections', () => {
const operatorGuide = normalizedDocument('docs/clipboard-security.md');
const doctoring = normalizedDocument(
'docs/doctoring/clipboard-resource-preflight.md',
);
const changelog = normalizedDocument('CHANGELOG.md');

expect(operatorGuide).toContain(
'raise the matching ceiling only after measuring a trusted source',
);
expect(operatorGuide).toContain(
'UTF-16 code-unit length already exceeds `maxHtmlBytes`',
);
expect(operatorGuide).toContain(
'before materializing those children',
);
expect(operatorGuide).toContain(
'The machine-readable error codes remain stable, while customer-facing rejection messages give a bounded next action',
);
expect(doctoring).toContain(
'preserve the stable error codes while allowing the customer-facing messages to remain actionable',
);
expect(doctoring).toContain('Unicode Standard');
expect(doctoring).toContain('CWE-770');
expect(doctoring).toContain('https://www.unicode.org/versions/Unicode16.0.0/');
expect(doctoring).toContain(
'https://www.w3.org/TR/2026/WD-clipboard-apis-20260624/',
);
expect(changelog).toContain(
'Reject obviously oversized rich clipboard HTML from UTF-16 code-unit length',
);
expect(changelog).toContain(
'before materializing children beyond `maxNodes`',
);
});
});
4 changes: 2 additions & 2 deletions src/extensions/SafeClipboard.coverageContract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe('SafeClipboard fail-closed coverage contract', () => {
).toThrowError(
expect.objectContaining({
code: 'invalid_html',
message: 'Rich clipboard HTML could not be sanitized.',
message: "This content can't be inserted here. Try pasting as plain text instead.",
}),
);
});
Expand All @@ -59,7 +59,7 @@ describe('SafeClipboard fail-closed coverage contract', () => {
it('keeps the redacted sanitizer error class stable', () => {
expect(new ClipboardSanitizationError('invalid_html')).toMatchObject({
code: 'invalid_html',
message: 'Rich clipboard HTML could not be sanitized.',
message: "This content can't be inserted here. Try pasting as plain text instead.",
name: 'ClipboardSanitizationError',
});
});
Expand Down
9 changes: 6 additions & 3 deletions src/extensions/SafeClipboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ describe('sanitizeRichClipboardHtml', () => {
).toThrowError(
expect.objectContaining({
code: 'input_too_large',
message: 'Rich clipboard HTML exceeds the configured byte limit.',
message:
'The pasted content is too large to insert. Try pasting less content at once.',
}),
);
});
Expand All @@ -176,7 +177,8 @@ describe('sanitizeRichClipboardHtml', () => {
).toThrowError(
expect.objectContaining({
code: 'invalid_html',
message: 'Rich clipboard HTML could not be sanitized.',
message:
"This content can't be inserted here. Try pasting as plain text instead.",
}),
);
});
Expand Down Expand Up @@ -373,7 +375,8 @@ describe('SafeClipboard extension', () => {
expect(onError).toHaveBeenCalledWith(
expect.objectContaining({
code: 'invalid_html',
message: 'Rich clipboard HTML could not be sanitized.',
message:
"This content can't be inserted here. Try pasting as plain text instead.",
}),
);

Expand Down
Loading
Loading