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',
+ );
+ });
+});
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);
+ });
+});
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);
+ });
+});
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();
+ });
+});
diff --git a/src/collaboration/CollaborativeCwlEditor.tsx b/src/collaboration/CollaborativeCwlEditor.tsx
index eea89b6c..a0f1166e 100644
--- a/src/collaboration/CollaborativeCwlEditor.tsx
+++ b/src/collaboration/CollaborativeCwlEditor.tsx
@@ -33,6 +33,21 @@ 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.';
+
+/** 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
@@ -97,8 +112,23 @@ 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 (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 +138,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],
@@ -289,17 +318,27 @@ 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));
};
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 =