From 94261deacae25983b76a409c990e2a8a8a0cb692 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 03:15:33 +0900 Subject: [PATCH 1/8] test(reliability): expose unbounded collaboration field metadata --- ...laborativeCwlEditor.fieldBoundary.test.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/collaboration/CollaborativeCwlEditor.fieldBoundary.test.tsx diff --git a/src/collaboration/CollaborativeCwlEditor.fieldBoundary.test.tsx b/src/collaboration/CollaborativeCwlEditor.fieldBoundary.test.tsx new file mode 100644 index 00000000..c25b27de --- /dev/null +++ b/src/collaboration/CollaborativeCwlEditor.fieldBoundary.test.tsx @@ -0,0 +1,58 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as Y from 'yjs'; +import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js'; +import type { CollaborativeCwlEditorProps } from './types.js'; + +const COLLABORATION_FIELD_MAX_CODE_UNITS = 1_024; +const INVALID_COLLABORATION_FIELD_MESSAGE = + 'Collaboration field must be a string within the supported length.'; + +afterEach(cleanup); + +function captureRenderFailure(field: unknown): unknown { + const consoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + let failure: unknown; + try { + render( + , + ); + } catch (error) { + failure = error; + } finally { + consoleError.mockRestore(); + } + return failure; +} + +describe('CollaborativeCwlEditor field resource boundary', () => { + it('rejects non-string runtime field metadata through a stable redacted error', () => { + expect(captureRenderFailure(42)).toEqual( + new RangeError(INVALID_COLLABORATION_FIELD_MESSAGE), + ); + }); + + it('rejects oversized field metadata before normalization without reflecting it', () => { + const privateMarker = 'private-room-marker'; + const field = `${privateMarker}${'x'.repeat(COLLABORATION_FIELD_MAX_CODE_UNITS)}`; + const failure = captureRenderFailure(field); + + expect(failure).toEqual(new RangeError(INVALID_COLLABORATION_FIELD_MESSAGE)); + expect(String(failure)).not.toContain(privateMarker); + }); + + it('accepts an in-bound custom field at the local ceiling', () => { + const field = 'x'.repeat(COLLABORATION_FIELD_MAX_CODE_UNITS); + + render(); + + expect(screen.getByRole('status')).toHaveTextContent( + 'Collaboration ready ยท 0 remote collaborators', + ); + }); +}); From 0a5f8c34807de11635001cf952aa718818da6aef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 03:20:05 +0900 Subject: [PATCH 2/8] fix(reliability): bound collaboration field metadata --- src/collaboration/CollaborativeCwlEditor.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/collaboration/CollaborativeCwlEditor.tsx b/src/collaboration/CollaborativeCwlEditor.tsx index eea89b6c..a233cc69 100644 --- a/src/collaboration/CollaborativeCwlEditor.tsx +++ b/src/collaboration/CollaborativeCwlEditor.tsx @@ -33,6 +33,10 @@ import { } from './awareness.js'; import type { CollaborativeCwlEditorProps } from './types.js'; +const COLLABORATION_FIELD_MAX_CODE_UNITS = 1_024; +const INVALID_COLLABORATION_FIELD_MESSAGE = + 'Collaboration field must be a string within the supported length.'; + /** * Provider-neutral collaborative Inkspan surface backed exclusively by a * host-owned Yjs document. Inkspan owns neither network nor persistence @@ -98,7 +102,14 @@ export const CollaborativeCwlEditor = forwardRef< } = props; assertCollaborationConfiguration(provider, user); - if (field.trim() === '') { + if ( + typeof field !== 'string' || + field.length > COLLABORATION_FIELD_MAX_CODE_UNITS + ) { + throw new RangeError(INVALID_COLLABORATION_FIELD_MESSAGE); + } + const normalizedField = field.trim(); + if (normalizedField === '') { throw new Error('collaboration field must not be empty'); } if ( @@ -108,7 +119,6 @@ export const CollaborativeCwlEditor = forwardRef< throw new Error('collaboration document must be a Y.Doc instance'); } - const normalizedField = field.trim(); const normalizedPlaceholder = useMemo( () => normalizeEditorPlaceholder(placeholder), [placeholder], From cf39f9e5b5c2e1b0c6b9c91d49b5d841ccdff674 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 18:31:28 +0900 Subject: [PATCH 3/8] test(data-integrity): expose collaborative runtime state coercion --- ...llaborativeCwlEditor.runtimeState.test.tsx | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/collaboration/CollaborativeCwlEditor.runtimeState.test.tsx diff --git a/src/collaboration/CollaborativeCwlEditor.runtimeState.test.tsx b/src/collaboration/CollaborativeCwlEditor.runtimeState.test.tsx new file mode 100644 index 00000000..72607f78 --- /dev/null +++ b/src/collaboration/CollaborativeCwlEditor.runtimeState.test.tsx @@ -0,0 +1,60 @@ +// @vitest-environment node + +import { renderToString } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; +import * as Y from 'yjs'; +import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js'; + +describe('collaborative editor runtime state contracts', () => { + it('rejects a non-boolean editable state instead of coercing it into edit authority', () => { + expect(() => + renderToString( + , + ), + ).toThrowError( + new RangeError('editor editable state must be a boolean when provided'), + ); + }); + + it('rejects a non-boolean toolbar visibility state instead of coercing it', () => { + expect(() => + renderToString( + , + ), + ).toThrowError( + new RangeError( + 'editor toolbar visibility state must be a boolean when provided', + ), + ); + }); + + it('preserves omitted and explicit boolean states', () => { + expect(() => + renderToString(), + ).not.toThrow(); + expect(() => + renderToString( + , + ), + ).not.toThrow(); + expect(() => + renderToString( + , + ), + ).not.toThrow(); + }); +}); From 87ee3e6cebe42714912f2350e34bbe5028a0e4b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 18:35:00 +0900 Subject: [PATCH 4/8] fix(data-integrity): validate collaborative runtime state --- src/collaboration/CollaborativeCwlEditor.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/collaboration/CollaborativeCwlEditor.tsx b/src/collaboration/CollaborativeCwlEditor.tsx index a233cc69..b4ac3078 100644 --- a/src/collaboration/CollaborativeCwlEditor.tsx +++ b/src/collaboration/CollaborativeCwlEditor.tsx @@ -101,6 +101,14 @@ export const CollaborativeCwlEditor = forwardRef< ariaRequired, } = props; + if (typeof editable !== 'boolean') { + throw new RangeError('editor editable state must be a boolean when provided'); + } + if (typeof hideToolbar !== 'boolean') { + throw new RangeError( + 'editor toolbar visibility state must be a boolean when provided', + ); + } assertCollaborationConfiguration(provider, user); if ( typeof field !== 'string' || From 0970e5cd70dd15ed8a9fde5e03cdf70ea75d3579 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:34:18 -0700 Subject: [PATCH 5/8] test(collaboration): prove provider awareness read containment --- ...wlEditor.providerAwarenessFailure.test.tsx | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/collaboration/CollaborativeCwlEditor.providerAwarenessFailure.test.tsx diff --git a/src/collaboration/CollaborativeCwlEditor.providerAwarenessFailure.test.tsx b/src/collaboration/CollaborativeCwlEditor.providerAwarenessFailure.test.tsx new file mode 100644 index 00000000..90720322 --- /dev/null +++ b/src/collaboration/CollaborativeCwlEditor.providerAwarenessFailure.test.tsx @@ -0,0 +1,51 @@ +// @vitest-environment node + +import { renderToString } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; +import * as Y from 'yjs'; +import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js'; +import type { + CollaborationAwareness, + CollaborationProviderLike, +} from './types.js'; + +function validAwareness(): CollaborationAwareness { + const states = new Map>(); + return { + clientID: 17, + states, + getLocalState: () => null, + getStates: () => states, + setLocalStateField: () => undefined, + on: () => undefined, + off: () => undefined, + }; +} + +describe('collaborative editor provider awareness access', () => { + it('contains a private awareness getter failure after configuration validation', () => { + const privateFailure = new Error('sensitive-provider-awareness-internal'); + const awareness = validAwareness(); + let reads = 0; + const provider = Object.defineProperty({}, 'awareness', { + enumerable: true, + get() { + reads += 1; + if (reads === 1) return awareness; + throw privateFailure; + }, + }) as CollaborationProviderLike; + + let observed: unknown; + try { + renderToString( + , + ); + } catch (error) { + observed = error; + } + + expect(observed).toBeUndefined(); + expect(reads).toBe(2); + }); +}); From fbe4d3ae34e1c5e0d080ef9076a4f0ebd539faa7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:34:56 -0700 Subject: [PATCH 6/8] fix(collaboration): contain provider awareness getter failures --- src/collaboration/CollaborativeCwlEditor.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/collaboration/CollaborativeCwlEditor.tsx b/src/collaboration/CollaborativeCwlEditor.tsx index b4ac3078..645cad4f 100644 --- a/src/collaboration/CollaborativeCwlEditor.tsx +++ b/src/collaboration/CollaborativeCwlEditor.tsx @@ -37,6 +37,17 @@ const COLLABORATION_FIELD_MAX_CODE_UNITS = 1_024; const INVALID_COLLABORATION_FIELD_MESSAGE = 'Collaboration field must be a string within the supported length.'; +/** Read host-owned awareness for presentation without leaking getter failures. */ +function readProviderAwareness( + provider: CollaborativeCwlEditorProps['provider'], +) { + try { + return provider?.awareness; + } catch { + return undefined; + } +} + /** * Provider-neutral collaborative Inkspan surface backed exclusively by a * host-owned Yjs document. Inkspan owns neither network nor persistence @@ -307,10 +318,10 @@ export const CollaborativeCwlEditor = forwardRef< ]); const [remoteCollaborators, setRemoteCollaborators] = useState(() => - countRemoteCollaborators(provider?.awareness), + countRemoteCollaborators(readProviderAwareness(provider)), ); useEffect(() => { - const awareness = provider?.awareness; + const awareness = readProviderAwareness(provider); const updateCount = () => { setRemoteCollaborators(countRemoteCollaborators(awareness)); }; From 5b7a67e64cd837008d85a6375685c239205c0dca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:35:50 -0700 Subject: [PATCH 7/8] test(collaboration): prove listener failure containment --- ...CwlEditor.providerListenerFailure.test.tsx | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/collaboration/CollaborativeCwlEditor.providerListenerFailure.test.tsx diff --git a/src/collaboration/CollaborativeCwlEditor.providerListenerFailure.test.tsx b/src/collaboration/CollaborativeCwlEditor.providerListenerFailure.test.tsx new file mode 100644 index 00000000..42861b59 --- /dev/null +++ b/src/collaboration/CollaborativeCwlEditor.providerListenerFailure.test.tsx @@ -0,0 +1,87 @@ +import { cleanup, render } from '@testing-library/react'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as Y from 'yjs'; +import { CollaborativeCwlEditor } from './CollaborativeCwlEditor.js'; +import type { + CollaborationAwareness, + CollaborationAwarenessEvent, + CollaborationProviderLike, +} from './types.js'; + +afterEach(cleanup); + +function awarenessWith( + on: CollaborationAwareness['on'], + off: CollaborationAwareness['off'], +): CollaborationAwareness { + const states = new Map>(); + return { + clientID: 23, + states, + getLocalState: () => null, + getStates: () => states, + setLocalStateField: () => undefined, + on, + off, + }; +} + +describe('collaborative editor provider listener failure containment', () => { + it('does not leak a private change-listener registration failure', () => { + const privateFailure = new Error('sensitive-provider-on-internal'); + const awareness = awarenessWith( + (_event: CollaborationAwarenessEvent) => { + throw privateFailure; + }, + () => undefined, + ); + const provider: CollaborationProviderLike = { awareness }; + const consoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + let observed: unknown; + try { + render( + , + ); + } catch (error) { + observed = error; + } finally { + consoleError.mockRestore(); + } + + expect(observed).toBeUndefined(); + }); + + it('does not leak a private change-listener cleanup failure', () => { + const privateFailure = new Error('sensitive-provider-off-internal'); + let offCalls = 0; + const awareness = awarenessWith( + () => undefined, + () => { + offCalls += 1; + throw privateFailure; + }, + ); + const provider: CollaborationProviderLike = { awareness }; + const mounted = render( + , + ); + const consoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + let observed: unknown; + try { + mounted.unmount(); + } catch (error) { + observed = error; + } finally { + consoleError.mockRestore(); + } + + expect(observed).toBeUndefined(); + expect(offCalls).toBeGreaterThan(0); + }); +}); From 8834f15bfc5006c041afeea2fb696498ffdd1106 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:36:23 -0700 Subject: [PATCH 8/8] fix(collaboration): contain provider listener failures --- src/collaboration/CollaborativeCwlEditor.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/collaboration/CollaborativeCwlEditor.tsx b/src/collaboration/CollaborativeCwlEditor.tsx index 645cad4f..a0f1166e 100644 --- a/src/collaboration/CollaborativeCwlEditor.tsx +++ b/src/collaboration/CollaborativeCwlEditor.tsx @@ -327,8 +327,18 @@ export const CollaborativeCwlEditor = forwardRef< }; updateCount(); if (!awareness) return; - awareness.on('change', updateCount); - return () => awareness.off('change', updateCount); + try { + awareness.on('change', updateCount); + } catch { + return; + } + return () => { + try { + awareness.off('change', updateCount); + } catch { + // Host-owned listener cleanup failure is contained at unmount. + } + }; }, [provider]); const collaboratorLabel =