Skip to content
Merged
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim

## [Unreleased]

### Fixed
- Added the selected standalone Markdown or HTML value to an explicitly configured SSR native form field, preserving controlled-value precedence, external form association, React attribute escaping, and the synchronous post-hydration TipTap transaction mirror

### Security
- Added a fail-closed draft release asset inventory gate that requires exactly one npm tarball, one Office wheel, and `SHA256SUMS`, rejects stale or unexpected draft assets before immutable publication, and verifies every GitHub-reported `sha256:` asset digest against the transferred local file
- Kept SSR document disclosure opt-in through `formFieldName`; hidden-field values remain client-controlled submission data and do not replace host authentication, authorization, tenant isolation, CSRF defenses, server validation, durable concurrency, or persistence controls
- Kept collaborative Yjs document content out of server markup until the host-owned client collaboration lifecycle is bound

### Added
- Added one optional construction-time `onSnapshotChange` callback to the framework-free autosave queue and durable autosave session so hosts can observe saving, pending, blocked, recovery, idle, and shutdown state without polling or introducing a subscriber collection
Expand All @@ -23,6 +28,10 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim
- Preserved buyer-facing README guidance within the same validated safe-link command boundary and moved the shortcut-specific behavior contract to the authoritative accessibility and doctoring records so it is not misattributed to Tiptap
- Added deterministic regression and documentation contracts plus APA 7th doctoring for exact `Control`/`Meta` shortcut alternatives, the descriptive-only accessibility boundary, repository-level shortcut verification, and omission of unsupported shortcut claims

### Tests
- Added test-first Node `renderToString` evidence for the missing SSR native value, controlled-over-default selection, escaping, external form ownership, no ProseMirror server construction, and opt-out non-disclosure
- Added browser-DOM handoff tests proving the field retains and updates the selected value before TipTap exists while reset-only unnamed fields remain empty

### Documentation
- Added a repository-native security disclosure and vulnerability-handling policy with supported pre-1.0 release lines, private GitHub Security Advisory routing and safe public fallback, minimized evidence guidance, explicit Inkspan-versus-host ownership boundaries, no-SLA and non-conformance claim limits, deterministic documentation tests, and APA 7th doctoring grounded in current ISO/IEC 29147:2018, ISO/IEC 30111:2019, final NIST SP 800-218 SSDF 1.1, the draft-status boundary for SSDF 1.2, and GitHub primary documentation
- Added an authoritative standalone and modular MSA architecture contract with reviewable deployment, optimistic-concurrency, data-ownership, security, and acquisition-evidence diagrams and tables
Expand All @@ -32,6 +41,7 @@ Historical release entries from **0.1.0 through 0.5.27** are preserved verbatim
- Added stale-generation conflict recovery and operational save failure recovery through one reason-aware single-flight host workflow, so newer local edits cannot hide or duplicate recovery while retained work remains blocked; rejected or malformed resume attempts retain the same recovery surface until a valid resume succeeds or the editing context is disposed
- Added exact-head read-only CI with fixed Ubuntu 24.04 runners, immutable action pins, explicit contributor-head checkout, disabled persisted Git credentials, and a documented merge-result compatibility boundary
- Added deterministic documentation contract tests and APA 7th doctoring grounded in RFC 9110, WCAG 2.2, NIST SP 800-204, NIST SP 800-204D, OWASP ASVS 5.0.0, React, current Next.js App Router guidance, and GitHub Actions primary documentation
- Added APA 7th doctoring for the SSR native form field, including the WHATWG hidden-input/form-entry contract, React server/hydration continuity, client-controlled-data boundary, host-owned CSRF and acceptance controls, collaboration exclusion, and rollback
- Added lifecycle-observation doctoring covering bounded callback retention, local-versus-shareable evidence, durable-validator coherence, WCAG 2.2 status-message responsibilities, rollback, and APA 7 references to RFC 9110, WCAG 2.2, and optimistic concurrency research

## [0.5.29] — 2026-08-05
Expand Down
151 changes: 151 additions & 0 deletions docs/doctoring/ssr-native-form-serialization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# SSR native form serialization

## Decision

When a standalone `CwlEditor` host explicitly supplies `formFieldName`, Inkspan
renders the selected Markdown or HTML prop value into the native hidden form
field in the server-generated shell. A controlled `value` takes precedence over
`defaultValue`, matching the document source selected for initial TipTap
construction. React owns attribute escaping, so source HTML is serialized as an
ordinary form-control value rather than injected as markup.

The field is rendered as a `readOnly` controlled React input. Before TipTap is
authoritative, its controlled value is the selected server/client prop value.
After TipTap becomes authoritative, the existing transaction subscriber stores
the current serialization in the shared value ref and writes it directly to
`HTMLInputElement.value` before a transaction returns. Subsequent React renders
read that same serialization rather than restoring an older server/default
value. This preserves synchronous `FormData` and native-submission behavior
without waiting for a React state update.

If `formFieldName` is absent, no document-bearing hidden input is rendered. A
reset-only unnamed field remains empty. `CollaborativeCwlEditor` does not place
Yjs content into server markup because its authoritative document becomes
available only after the host-owned client collaboration lifecycle is bound.

## Root cause and test-first evidence

The pre-existing SSR shell rendered a named hidden input but gave it no server
value because the TipTap editor is intentionally `null` until client hydration.
The browser later received a synchronized value, but an enterprise host reading
or submitting server HTML before JavaScript ran could not receive the configured
document value.

Commit `fb37cd9a4344a3b369553c49eb4bf557c082c7da` introduced a Node
`renderToString` regression first. CI run `31177509823` failed because the
server output contained the expected hidden input, `name`, and external `form`
association but omitted the escaped `value` attribute. The implementation then
added the explicit pre-hydration handoff and focused browser-DOM tests for
initial-value retention, pre-editor prop updates, and reset-only non-disclosure.

A later review found that this doctoring record described the field as an
uncontrolled `defaultValue` input even though the implementation deliberately
uses a `readOnly` controlled `value` backed by the same serialization ref that
serves synchronous DOM writes. A deterministic documentation contract was added
first to reject that mismatch, then this record was corrected without changing
runtime behavior.

## HTML and hydration contract

The WHATWG HTML Standard defines a hidden input as a submittable
form-associated element whose data is an arbitrary string. Its `name` supplies
the form-entry name, its `form` attribute can associate it with an external form,
and its value supplies the submitted value unless the control is disabled or
otherwise excluded by the form-data construction algorithm.

React server APIs generate the initial HTML representation. `hydrateRoot`
expects the first client render to match that server output, so Inkspan renders
the same controlled value on the server and during hydration. The field is
`readOnly`: user editing is not an input path. Before TipTap initialization, prop
updates remain authoritative. Once the editor is initialized, synchronous
imperative writes and the shared serialization ref keep the native field aligned
with editor transactions; a later React render therefore consumes the latest
serialized value rather than a stale initial prop.

This contract applies to `renderToString` and to streaming React server APIs
because the component tree emits the same input element. It does not claim that
a server renderer has created or validated a TipTap document.

## Security and privacy boundary

A hidden field is hidden from ordinary layout, not secret. Its value is present
in page source, the DOM, browser developer tools, form submission, extensions,
and any intermediary that can observe the response or request. Hosts must not
use `formFieldName` when server markup must omit the document body. They must
apply appropriate response caching, content classification, transport security,
and tenant authorization before rendering private content.

Every hidden-field value is client-controlled submission data. The server must
revalidate document syntax and resource limits, authenticate the request,
authorize the actor and tenant, enforce CSRF protections, and perform durable
concurrency and persistence checks. The field is not an authorization grant,
signature, CSRF token, tenant identifier, integrity proof, strong entity tag, or
proof that the browser submitted an unchanged server value.

OWASP documents hidden fields as one possible transport for synchronized CSRF
tokens, while also requiring server validation and protecting those tokens from
logs and URLs. Inkspan does not generate, store, or validate CSRF tokens; that
responsibility remains host-owned and separate from the document field.

Inkspan owns deterministic value selection, safe React serialization, hydration
continuity, and synchronous post-hydration mirroring. Hosts retain form action
and method, authentication, authorization, tenant isolation, CSRF defenses,
request-size limits, persistence, credentials, migration, retention, audit,
conflict policy, and model-use policy.

## Standalone and modular MSA behavior

The feature requires no transport, database, environment variable, provider,
model, or collaboration dependency. A standalone application can submit the
native field directly. A naruon `compose` or `ui.panel` host can associate the
field with its own form through `formId`, including when the form is outside the
editor subtree.

Server-selected data and authorization remain host concerns. Inkspan does not
create a database object. A host persistence object introduced for submitted
editor data should use at least two descriptive words and `snake_case` by
default, or a valid ecosystem-required CamelCase/PascalCase form.

## Verification and rollback

Exact-head tests require:

- controlled `value` precedence over `defaultValue` in server markup;
- `readOnly` controlled-field semantics across SSR, hydration, and rerenders;
- safe HTML-attribute escaping by React;
- preservation of native `name` and external `form` association;
- no TipTap/ProseMirror server construction;
- no document-bearing field when form serialization is not configured;
- retention and updates before the editor exists;
- empty reset-only unnamed fields;
- existing synchronous transaction-to-`FormData`, disabled-field, mode-change,
reset, external-form, and collaborative client contracts; and
- repository-wide 100% production statement, branch, function, and line
coverage plus packaging and SSR-import evidence.

Rollback removes the initial-value prop path and this evidence. No stored-data,
database, tenant, credential, migration, or release rollback is required.
Before rollback, hosts relying on no-JavaScript form submission must provide an
equivalent server-owned field or accept that the initial form entry is empty.

No formal WHATWG, React, OWASP, accessibility, security, or privacy conformance
is claimed.

## APA 7 references

Meta Platforms, Inc. (n.d.). *hydrateRoot*. React. Retrieved August 7, 2026,
from https://react.dev/reference/react-dom/client/hydrateRoot

Meta Platforms, Inc. (n.d.). *renderToString*. React. Retrieved August 7, 2026,
from https://react.dev/reference/react-dom/server/renderToString

OWASP Foundation. (n.d.). *Cross-site request forgery prevention cheat sheet*.
OWASP Cheat Sheet Series. Retrieved August 7, 2026, from
https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html

WHATWG. (2026). *HTML Standard: Form control infrastructure*. Retrieved August
7, 2026, from
https://html.spec.whatwg.org/multipage/form-control-infrastructure.html

WHATWG. (2026). *HTML Standard: The input element*. Retrieved August 7, 2026,
from https://html.spec.whatwg.org/multipage/input.html
58 changes: 51 additions & 7 deletions docs/server-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,52 @@ mismatches caused by constructing an editor view during server rendering.
collaborative surface, the non-interactive connection-status region.
- Server output does not contain a ProseMirror editable view or serialize the
initial document into presentation markup.
- The TipTap editor, event handlers, native-form mirror, toolbar, and Yjs binding
become available after client hydration.
- When a standalone host explicitly supplies `formFieldName`, the selected
Markdown or HTML prop value is present in one server-rendered hidden field so
native form submission does not depend on TipTap having hydrated.
- Without `formFieldName`, server output contains no document-bearing native
field. Reset-only unnamed fields remain empty.
- The TipTap editor, event handlers, toolbar, and Yjs binding become available
after client hydration. A configured native field then follows editor
transactions synchronously.
- `onReady` and the imperative `CwlEditorHandle` remain client lifecycle
surfaces. Hosts must not expect either during server rendering.
- The server-rendered shell does not perform network requests, open a
collaboration provider, persist content, or destroy a host-owned `Y.Doc`.

## Native form serialization before hydration

`CwlEditor` selects a controlled `value` before `defaultValue`, exactly as it
does for the initial editor document. If `formFieldName` is configured, that
selected value is supplied to a `readOnly` controlled hidden input. React escapes
HTML attribute syntax; HTML-mode content remains an ordinary string value rather
than becoming page markup.

The same controlled value is rendered on the server and during the first client
render, satisfying React's requirement that initial hydration output match.
Until TipTap exists, prop updates remain authoritative for the native field. Once
the editor is initialized, Inkspan's transaction listener records the current
serialization in the shared value ref and writes it directly into
`HTMLInputElement.value` before each document-changing transaction returns.
Subsequent React renders consume that same serialized value, preserving immediate
`FormData` construction and browser submission without restoring stale initial
content.

A hidden field is not a secrecy control. Its content is visible in the response,
page source, DOM, browser tools, and submitted request. Treat it as
client-controlled submission data. Hosts must authenticate and authorize the
request, validate the submitted document and limits, apply CSRF protection,
enforce tenant isolation, and perform durable concurrency and persistence checks
server-side. Do not enable `formFieldName` when the document body must be absent
from server markup or intermediate caches.

`CollaborativeCwlEditor` does not serialize Yjs content into the server shell.
The host-owned `Y.Doc` and provider become authoritative only in the client
collaboration lifecycle; its native form mirror is populated after that binding
exists. See
[`docs/doctoring/ssr-native-form-serialization.md`](doctoring/ssr-native-form-serialization.md)
for the test-first rationale, security limits, rollback, and APA 7 references.

## Next.js App Router

Inkspan uses React hooks and remains an interactive client component. In a
Expand Down Expand Up @@ -61,8 +100,9 @@ and document workspaces have different layout contracts.
through server boundaries.
- Instantiate browser transports, credentials, collaboration providers, and
`Y.Doc` ownership within the authorized client/service integration layer.
- Treat initial editor content as client presentation state; authorize and
validate all persisted mutations at the service boundary.
- Treat initial editor content and native form values as client presentation and
submission state; authorize and validate all persisted mutations at the
service boundary.
- Do not embed secrets in editor props, server-rendered markup, collaboration
awareness, hidden form fields, or inline diagnostics.
- Preserve descriptive nonnumeric identifiers across document, user, session,
Expand All @@ -72,13 +112,17 @@ and document workspaces have different layout contracts.

Repository tests run standalone and collaborative components through
`react-dom/server` in a Node environment and assert that the stable shell is
emitted without a ProseMirror view or document-body leakage. The normal browser
suite then verifies client initialization, editing, accessibility, forms,
collaboration, and the repository-wide 100% coverage gate.
emitted without a ProseMirror view. They additionally prove opt-in controlled-
value precedence, React attribute escaping, external form association, and
opt-out document non-disclosure. The normal browser suite verifies the
SSR-to-editor field handoff, synchronous transaction mirroring, editing,
accessibility, forms, collaboration, and the repository-wide 100% coverage gate.

## References

- TipTap React SSR guidance: <https://tiptap.dev/docs/editor/getting-started/install/react>
- TipTap performance and `immediatelyRender`: <https://tiptap.dev/docs/guides/performance>
- React `renderToString`: <https://react.dev/reference/react-dom/server/renderToString>
- React `hydrateRoot`: <https://react.dev/reference/react-dom/client/hydrateRoot>
- Next.js client components: <https://nextjs.org/docs/app/getting-started/server-and-client-components>
- WHATWG HTML form controls: <https://html.spec.whatwg.org/multipage/form-control-infrastructure.html>
4 changes: 3 additions & 1 deletion src/components/CwlEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const CwlEditor = forwardRef<CwlEditorHandle, CwlEditorProps>(
ref,
) {
const isControlled = value !== undefined;
const selectedDocumentValue = value ?? defaultValue ?? '';
const emittingRef = useRef(false);
const editorInstanceRef = useRef<Editor | null>(null);
const modeRef = useLatestRef(mode);
Expand Down Expand Up @@ -111,7 +112,7 @@ export const CwlEditor = forwardRef<CwlEditorHandle, CwlEditorProps>(
image,
onImageError: reportImageError,
}),
content: editorValueToHtml(value ?? defaultValue ?? '', mode),
content: editorValueToHtml(selectedDocumentValue, mode),
editorProps: {
attributes: editorAttributes,
},
Expand Down Expand Up @@ -223,6 +224,7 @@ export const CwlEditor = forwardRef<CwlEditorHandle, CwlEditorProps>(
formFieldName={formFieldName}
formId={formId}
formFieldDisabled={formFieldDisabled}
formFieldInitialValue={selectedDocumentValue}
onFormReset={editor && observesFormReset ? handleFormReset : undefined}
/>
);
Expand Down
Loading
Loading