Skip to content

Commit 27652d4

Browse files
committed
fix(ui): keep paste limits as safety hatches
1 parent 0f80278 commit 27652d4

9 files changed

Lines changed: 38 additions & 90 deletions

File tree

apps/realtime/src/config/socket.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ const logger = createLogger('SocketIOConfig')
1111
const PING_TIMEOUT_MS = 60000
1212
/** Socket.IO ping interval - how often to send ping packets */
1313
const PING_INTERVAL_MS = 25000
14-
/** Maximum HTTP buffer size for Socket.IO messages */
15-
const MAX_HTTP_BUFFER_SIZE = 1e6
14+
/**
15+
* Accommodates the existing 5 MiB collaborative-document boundary plus Yjs and Socket.IO framing.
16+
* This remains a transport safety hatch, not a product-sized text limit.
17+
*/
18+
const MAX_HTTP_BUFFER_SIZE = 8 * 1024 * 1024
1619

1720
let adapterPubClient: RedisClientType | null = null
1821
let adapterSubClient: RedisClientType | null = null

apps/sim/app/_shell/paste-admission-guard.test.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,14 @@ describe('PasteAdmissionGuard', () => {
5353
expect(warning).toHaveBeenCalledOnce()
5454
})
5555

56-
it('uses projected native value size and accounts for the selection', () => {
56+
it('does not reject a small payload because the existing native value is large', () => {
5757
const input = document.createElement('textarea')
5858
input.dataset.pasteMaxBytes = '6'
5959
input.value = '123456'
6060
host.appendChild(input)
6161

62-
input.setSelectionRange(1, 5)
63-
expect(dispatchPaste(input, 'abcd').defaultPrevented).toBe(false)
64-
6562
input.setSelectionRange(6, 6)
66-
expect(dispatchPaste(input, 'a').defaultPrevented).toBe(true)
63+
expect(dispatchPaste(input, 'a').defaultPrevented).toBe(false)
6764
})
6865

6966
it('honors a surface-specific character contract', () => {
@@ -75,19 +72,13 @@ describe('PasteAdmissionGuard', () => {
7572
expect(dispatchPaste(input, '💡💡').defaultPrevented).toBe(true)
7673
})
7774

78-
it('uses a contenteditable selection when projecting the result', () => {
75+
it('does not reject a small payload because contenteditable text is already large', () => {
7976
const editable = document.createElement('div')
8077
editable.contentEditable = 'true'
8178
editable.dataset.pasteMaxBytes = '6'
8279
editable.textContent = '123456'
8380
host.appendChild(editable)
84-
const range = document.createRange()
85-
range.setStart(editable.firstChild as Text, 1)
86-
range.setEnd(editable.firstChild as Text, 5)
87-
const selection = document.getSelection()
88-
selection?.removeAllRanges()
89-
selection?.addRange(range)
90-
91-
expect(dispatchPaste(editable, 'abcd').defaultPrevented).toBe(false)
81+
82+
expect(dispatchPaste(editable, 'a').defaultPrevented).toBe(false)
9283
})
9384
})

apps/sim/app/_shell/paste-admission-guard.tsx

Lines changed: 5 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,12 @@ function finitePositiveAttribute(element: Element | null, name: string): number
1414
return Number.isFinite(value) && value > 0 ? value : undefined
1515
}
1616

17-
function contentEditableSelection(element: HTMLElement): {
18-
currentText: string
19-
selectionStart: number
20-
selectionEnd: number
21-
} {
22-
const currentText = element.textContent ?? ''
23-
const selection = document.getSelection()
24-
if (!selection || selection.rangeCount === 0) {
25-
return { currentText, selectionStart: currentText.length, selectionEnd: currentText.length }
26-
}
27-
28-
const range = selection.getRangeAt(0)
29-
if (!element.contains(range.startContainer) || !element.contains(range.endContainer)) {
30-
return { currentText, selectionStart: currentText.length, selectionEnd: currentText.length }
31-
}
32-
33-
const before = document.createRange()
34-
before.selectNodeContents(element)
35-
before.setEnd(range.startContainer, range.startOffset)
36-
const selectionStart = before.toString().length
37-
return {
38-
currentText,
39-
selectionStart,
40-
selectionEnd: selectionStart + range.toString().length,
41-
}
42-
}
43-
4417
/**
45-
* Last-resort admission for every editable workspace surface. Specialized editors publish a larger
46-
* or smaller payload ceiling on an ancestor with `data-paste-max-bytes`; controls without one inherit
47-
* the Socket.IO-sized default. The capture listener runs before React, ProseMirror, Monaco, and xterm
48-
* can parse or render the clipboard value.
18+
* Last-resort admission for every editable workspace surface. Specialized editors publish their
19+
* downstream ceiling on an ancestor with `data-paste-max-bytes`; controls without one inherit a
20+
* crash-only fallback. This layer bounds only the clipboard payload, so a small paste into an already
21+
* large field keeps native behavior. Editors with a real result-size contract enforce it themselves.
22+
* The capture listener runs before React, ProseMirror, Monaco, and xterm parse the clipboard value.
4923
*/
5024
export function PasteAdmissionGuard() {
5125
const { toast: notify } = useToast()
@@ -68,31 +42,10 @@ export function PasteAdmissionGuard() {
6842
policyElement,
6943
'data-paste-max-characters'
7044
)
71-
const nativeControl =
72-
event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement
73-
const editableElement = event.target.closest<HTMLElement>(
74-
'[contenteditable]:not([contenteditable="false"])'
75-
)
76-
const projectedValue = nativeControl
77-
? {
78-
currentText: event.target.value,
79-
selectionStart: event.target.selectionStart ?? event.target.value.length,
80-
selectionEnd: event.target.selectionEnd ?? event.target.value.length,
81-
}
82-
: editableElement
83-
? contentEditableSelection(editableElement)
84-
: null
8545
const admission = assessTextPaste({
8646
pastedText: text,
8747
maxPastedBytes,
8848
maxPastedCharacters,
89-
...(projectedValue
90-
? {
91-
...projectedValue,
92-
maxResultBytes: maxPastedBytes,
93-
maxResultCharacters: maxPastedCharacters,
94-
}
95-
: {}),
9649
})
9750
if (admission.accepted) return
9851

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -593,18 +593,11 @@ export const TextEditor = memo(function TextEditor({
593593

594594
const handleEditorPasteCapture = (event: ReactClipboardEvent<HTMLDivElement>) => {
595595
const pastedText = event.clipboardData.getData('text/plain')
596-
const editor = monacoEditorRef.current
597-
const model = editor?.getModel()
598-
const selection = editor?.getSelection()
599-
if (!pastedText || !model || !selection) return
596+
if (!pastedText) return
600597

601598
const admission = assessTextPaste({
602599
pastedText,
603600
maxPastedBytes: PASTE_LIMITS.TEXT_EDITOR_BYTES,
604-
currentText: model.getValue(),
605-
selectionStart: model.getOffsetAt(selection.getStartPosition()),
606-
selectionEnd: model.getOffsetAt(selection.getEndPosition()),
607-
maxResultBytes: PASTE_LIMITS.TEXT_EDITOR_BYTES,
608601
})
609602
if (admission.accepted) return
610603

apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/prompt-editor/prompt-editor.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,6 @@ export function PromptEditor({
257257
placeholder={placeholder}
258258
aria-label={ariaLabel}
259259
rows={1}
260-
maxLength={PASTE_LIMITS.CHAT_CHARACTERS}
261260
className={cn(
262261
TEXTAREA_BASE_CLASSES,
263262
usePlainTextMode && '!text-[var(--text-primary)]',

apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secrets-manager/secrets-manager.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const logger = createLogger('SecretsManager')
3939

4040
const GRID_COLS = 'grid grid-cols-[minmax(0,1fr)_8px_minmax(0,1fr)_auto] items-center'
4141
const COL_SPAN_ALL = 'col-span-4'
42-
const MAX_ENV_PASTE_ROWS = 1_000
42+
const MAX_ENV_PASTE_ROWS = 10_000
4343

4444
/** Copies a secret's name and confirms with a toast. */
4545
function copyName(key: string) {

apps/sim/lib/api/contracts/file-doc.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const mergeFileDocBodySchema = z.object({
4444
/**
4545
* Base64-encoded `Y.encodeStateAsUpdate` of the live document as the relay currently holds it. The
4646
* bound is generous headroom over a max-size collab doc's Yjs state (base64-inflated), not a tuned
47-
* limit — collab is gated to ≤256 KB documents client-side.
47+
* limit — collaborative documents are gated to 5 MiB of markdown client-side.
4848
*/
4949
docState: z
5050
.string()
@@ -89,7 +89,8 @@ export const persistFileDocBodySchema = z.object({
8989
userId: z.string().min(1, 'userId is required'),
9090
/**
9191
* Base64-encoded `Y.encodeStateAsUpdate` of the live document as the relay currently holds it — the
92-
* bound matches the merge contract (generous base64 headroom over a ≤256 KB collab doc's Yjs state).
92+
* bound matches the merge contract (generous base64 headroom over a 5 MiB collab document's Yjs
93+
* state).
9394
*/
9495
docState: z
9596
.string()

packages/utils/src/paste.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
assessTextPaste,
44
countPasteRows,
55
formatPasteLimit,
6+
PASTE_LIMITS,
67
utf8ByteLength,
78
utf8ByteLengthRange,
89
} from './paste'
@@ -80,3 +81,10 @@ it('formats binary paste limits', () => {
8081
expect(formatPasteLimit(1_000_000)).toBe('1 MB')
8182
expect(formatPasteLimit(5 * 1024 * 1024)).toBe('5 MiB')
8283
})
84+
85+
it('keeps non-contract paste ceilings in crash-only territory', () => {
86+
expect(PASTE_LIMITS.DEFAULT_BYTES).toBe(32 * 1024 * 1024)
87+
expect(PASTE_LIMITS.TEXT_EDITOR_BYTES).toBe(32 * 1024 * 1024)
88+
expect(PASTE_LIMITS.TERMINAL_BYTES).toBe(8 * 1024 * 1024)
89+
expect(PASTE_LIMITS.STRUCTURED_BYTES).toBe(32 * 1024 * 1024)
90+
})

packages/utils/src/paste.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
export const PASTE_LIMITS = {
2-
/** Fallback for controls without a more specific downstream contract. */
3-
DEFAULT_BYTES: 1_000_000,
4-
/** Matches the realtime collaboration transport's single-message boundary. */
5-
RICH_MARKDOWN_BYTES: 1_000_000,
6-
/** Matches the existing inline CSV editor boundary. */
7-
TEXT_EDITOR_BYTES: 5 * 1024 * 1024,
2+
/** Crash-only fallback for controls without a more specific downstream contract. */
3+
DEFAULT_BYTES: 32 * 1024 * 1024,
4+
/** Matches the existing collaborative-document seed boundary. */
5+
RICH_MARKDOWN_BYTES: 5 * 1024 * 1024,
6+
/** Stays below the 50 MiB JSON request boundary while allowing genuinely large source files. */
7+
TEXT_EDITOR_BYTES: 32 * 1024 * 1024,
88
/** Matches the deployed chat request contract. */
99
CHAT_CHARACTERS: 1_000_000,
1010
/** A Unicode scalar can occupy at most four UTF-8 bytes. */
1111
CHAT_BYTES: 4_000_000,
12-
/** Keeps a single terminal input below the Socket.IO single-message boundary. */
13-
TERMINAL_BYTES: 1_000_000,
14-
/** Allows a sizeable grid while row and cell limits provide the primary bound. */
15-
STRUCTURED_BYTES: 5 * 1024 * 1024,
12+
/** Crash-only bound; admitted input is streamed to the PTY in 64 KiB chunks. */
13+
TERMINAL_BYTES: 8 * 1024 * 1024,
14+
/** The server's row ceiling remains the primary table bound. */
15+
STRUCTURED_BYTES: 32 * 1024 * 1024,
1616
} as const
1717

1818
export const PASTE_RENDER_THRESHOLDS = {

0 commit comments

Comments
 (0)