Skip to content

Commit 6d0ddbf

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
Merge remote-tracking branch 'origin/staging' into investigate/oracle-fusion-erp-integration
2 parents b3d91b4 + f050dc0 commit 6d0ddbf

16 files changed

Lines changed: 303 additions & 54 deletions

apps/sim/connectors/gmail/gmail.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/document
44
import { DEFAULT_MAX_THREADS, gmailConnectorMeta } from '@/connectors/gmail/meta'
55
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
66
import {
7+
BoundedLines,
8+
CONNECTOR_TEXT_DOCUMENT_MAX_BYTES,
79
htmlToPlainText,
810
joinTagArray,
911
parseDefaultedUnlimitedSafeInteger,
@@ -323,21 +325,17 @@ function formatThread(thread: GmailThread): {
323325
}
324326
const labelIds = [...labelIdSet]
325327

326-
const lines: string[] = []
327-
lines.push(`Subject: ${subject}`)
328-
lines.push(`From: ${from}`)
328+
const lines = new BoundedLines()
329+
lines.push(`Subject: ${subject}`, `From: ${from}`)
329330
if (to) lines.push(`To: ${to}`)
330-
lines.push(`Messages: ${messages.length}`)
331-
lines.push('')
331+
lines.push(`Messages: ${messages.length}`, '')
332332

333333
for (const msg of messages) {
334334
const msgFrom = getHeader(msg.payload, 'From') || 'Unknown'
335335
const msgDate = getHeader(msg.payload, 'Date') || ''
336336
const body = msg.payload ? extractBody(msg.payload) : ''
337337

338-
lines.push(`--- ${msgFrom} (${msgDate}) ---`)
339-
lines.push(body.trim())
340-
lines.push('')
338+
if (!lines.push(`--- ${msgFrom} (${msgDate}) ---`, body.trim(), '')) break
341339
}
342340

343341
const firstDate = firstMessage.internalDate
@@ -348,7 +346,7 @@ function formatThread(thread: GmailThread): {
348346
: undefined
349347

350348
return {
351-
content: lines.join('\n').trim(),
349+
content: lines.join().trim(),
352350
subject,
353351
metadata: {
354352
from,
@@ -412,6 +410,7 @@ function threadToStub(thread: {
412410
title: thread.snippet || 'Untitled Thread',
413411
content: '',
414412
contentDeferred: true,
413+
estimatedBytes: CONNECTOR_TEXT_DOCUMENT_MAX_BYTES,
415414
mimeType: 'text/plain',
416415
sourceUrl: threadUrl(thread.id),
417416
contentHash: `gmail:${thread.id}:${thread.historyId ?? ''}`,

apps/sim/connectors/google-chat/google-chat.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
SPACES_PAGE_SIZE,
1010
} from '@/connectors/google-chat/meta'
1111
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
12-
import { parseTagDate } from '@/connectors/utils'
12+
import { BoundedLines, CONNECTOR_TEXT_DOCUMENT_MAX_BYTES, parseTagDate } from '@/connectors/utils'
1313

1414
const logger = createLogger('GoogleChatConnector')
1515

@@ -218,6 +218,7 @@ function spaceToStub(
218218
title: spaceTitle(space),
219219
content: '',
220220
contentDeferred: true,
221+
estimatedBytes: CONNECTOR_TEXT_DOCUMENT_MAX_BYTES,
221222
mimeType: 'text/plain',
222223
sourceUrl: space.spaceUri,
223224
contentHash: buildContentHash(space, maxMessages, lookbackDays, syncContext),
@@ -323,37 +324,28 @@ function senderLabel(sender: ChatUser | undefined): string {
323324
* that belongs in a knowledge base is synced through the Google Drive connector
324325
* instead, which already handles size caps, OCR, and format parsing.
325326
*/
326-
function formatSpaceContent(space: Space, messages: ChatMessage[]): string {
327-
const parts: string[] = [`Space: ${spaceTitle(space)}`]
327+
function formatSpaceContent(
328+
space: Space,
329+
messages: ChatMessage[]
330+
): { content: string; messageCount: number } {
331+
const parts = new BoundedLines()
332+
parts.push(`Space: ${spaceTitle(space)}`)
328333
const description = space.spaceDetails?.description?.trim()
329334
if (description) parts.push(`Description: ${description}`)
330335
const guidelines = space.spaceDetails?.guidelines?.trim()
331336
if (guidelines) parts.push(`Guidelines: ${guidelines}`)
332337

333-
const lines: string[] = []
338+
let messageCount = 0
334339
for (const message of messages) {
335340
const text = message.text?.trim() || message.fallbackText?.trim()
336341
if (!text) continue
342+
if (messageCount === 0) parts.push('', '--- Messages ---')
337343
const timestamp = message.createTime ?? ''
338-
lines.push(`[${timestamp}] ${senderLabel(message.sender)}: ${text}`)
339-
}
340-
341-
if (lines.length > 0) {
342-
parts.push('')
343-
parts.push('--- Messages ---')
344-
parts.push(...lines)
344+
if (!parts.push(`[${timestamp}] ${senderLabel(message.sender)}: ${text}`)) break
345+
messageCount += 1
345346
}
346347

347-
return parts.join('\n')
348-
}
349-
350-
/** Number of messages that actually contributed text to the transcript. */
351-
function countIndexedMessages(messages: ChatMessage[]): number {
352-
let count = 0
353-
for (const message of messages) {
354-
if (message.text?.trim() || message.fallbackText?.trim()) count++
355-
}
356-
return count
348+
return { content: parts.join(), messageCount }
357349
}
358350

359351
export const googleChatConnector: ConnectorConfig = {
@@ -476,12 +468,12 @@ export const googleChatConnector: ConnectorConfig = {
476468
* leave a previously indexed transcript in place after the space was cleared
477469
* or `lookbackDays` was tightened past every message.
478470
*/
479-
const messageCount = countIndexedMessages(messages)
471+
const { content, messageCount } = formatSpaceContent(space, messages)
480472
const stub = spaceToStub(space, maxMessages, lookbackDays, syncContext)
481473

482474
return {
483475
...stub,
484-
content: formatSpaceContent(space, messages),
476+
content,
485477
contentDeferred: false,
486478
metadata: { ...stub.metadata, messageCount },
487479
}

apps/sim/connectors/outlook/outlook.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { fetchWithRetry, VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/document
44
import { DEFAULT_MAX_CONVERSATIONS, outlookConnectorMeta } from '@/connectors/outlook/meta'
55
import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types'
66
import {
7+
BoundedLines,
8+
CONNECTOR_TEXT_DOCUMENT_MAX_BYTES,
79
htmlToPlainText,
810
isListingScopeUnavailableError,
911
listingRequestError,
@@ -563,24 +565,20 @@ function formatConversation(
563565
const from = formatRecipient(first.from)
564566
const to = first.toRecipients?.map(formatRecipient).join(', ') || ''
565567

566-
const lines: string[] = []
567-
lines.push(`Subject: ${subject}`)
568-
lines.push(`From: ${from}`)
568+
const lines = new BoundedLines()
569+
lines.push(`Subject: ${subject}`, `From: ${from}`)
569570
if (to) lines.push(`To: ${to}`)
570-
lines.push(`Messages: ${sorted.length}`)
571-
lines.push('')
571+
lines.push(`Messages: ${sorted.length}`, '')
572572

573573
for (const msg of sorted) {
574574
const msgFrom = formatRecipient(msg.from)
575575
const msgDate = msg.receivedDateTime || ''
576576
const body = extractBodyText(msg.body)
577577

578-
lines.push(`--- ${msgFrom} (${msgDate}) ---`)
579-
lines.push(body.trim())
580-
lines.push('')
578+
if (!lines.push(`--- ${msgFrom} (${msgDate}) ---`, body.trim(), '')) break
581579
}
582580

583-
const content = lines.join('\n').trim()
581+
const content = lines.join().trim()
584582
if (!content) return null
585583

586584
const categories = new Set<string>()
@@ -805,6 +803,7 @@ export const outlookConnector: ConnectorConfig = {
805803
title: subject,
806804
content: '',
807805
contentDeferred: true,
806+
estimatedBytes: CONNECTOR_TEXT_DOCUMENT_MAX_BYTES,
808807
mimeType: 'text/plain',
809808
sourceUrl,
810809
contentHash: `outlook:${convId}:${lastDate}`,

apps/sim/connectors/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ export interface ExternalDocument {
6363
skippedRetryContentHash?: string
6464
/** When true, content is empty and will be fetched via getDocument for new/changed docs only */
6565
contentDeferred?: boolean
66+
/**
67+
* How large the deferred content is expected to be, in bytes, when the
68+
* listing cannot know exactly. Bounds how many deferred documents hydrate
69+
* at once: without it a deferred document is assumed to be as large as the
70+
* whole in-flight budget and hydrates alone, which turns a mailbox crawl
71+
* into one thread at a time.
72+
*/
73+
estimatedBytes?: number
6674
/**
6775
* When set, the document was intentionally not indexed (e.g. it exceeds the
6876
* connector's size limit). The sync engine records it as a `failed` document

apps/sim/connectors/utils.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { typeformConnector } from '@/connectors/typeform/typeform'
6363
import {
6464
appendPendingMicrosoftGraphFolders,
6565
assertMicrosoftGraphNextLink,
66+
BoundedLines,
6667
ConnectorFileTooLargeError,
6768
ConnectorListingScopeUnavailableError,
6869
decodeMicrosoftGraphTraversalCursor,
@@ -1639,3 +1640,26 @@ describe('isSkippableMicrosoftGraphFolderError', () => {
16391640
expect(isSkippableMicrosoftGraphFolderError(new Error('500'), perMember, false)).toBe(false)
16401641
})
16411642
})
1643+
1644+
describe('BoundedLines', () => {
1645+
it('joins everything when the text fits', () => {
1646+
const lines = new BoundedLines(64)
1647+
expect(lines.push('Subject: hi', '')).toBe(true)
1648+
expect(lines.push('--- a ---', 'body')).toBe(true)
1649+
expect(lines.join()).toBe('Subject: hi\n\n--- a ---\nbody')
1650+
})
1651+
1652+
it('refuses a record that would cross the ceiling, whole, and says so in the output', () => {
1653+
const lines = new BoundedLines(20)
1654+
expect(lines.push('first')).toBe(true)
1655+
expect(lines.push('--- header ---', 'a long body')).toBe(false)
1656+
expect(lines.push('x')).toBe(false)
1657+
expect(lines.join()).toBe('first\n[Truncated: the indexed text reached the size limit]')
1658+
})
1659+
1660+
it('counts encoded bytes, not characters', () => {
1661+
const lines = new BoundedLines(6)
1662+
expect(lines.push('éé')).toBe(true)
1663+
expect(lines.push('é')).toBe(false)
1664+
})
1665+
})

apps/sim/connectors/utils.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -795,3 +795,50 @@ export function isSkippableMicrosoftGraphFolderError(
795795
): boolean {
796796
return !isRootFolder && isListingScopeUnavailableError(error) && isPerMemberListing(syncContext)
797797
}
798+
799+
/**
800+
* Ceiling for a document a connector assembles from many source records (a
801+
* mail thread, a chat transcript) rather than downloads as one file. Formatters
802+
* enforce it through `BoundedLines`, and listings advertise it through
803+
* `ExternalDocument.estimatedBytes`, so the sync engine plans hydration around
804+
* a bound it can rely on: five such documents fit its 64 MiB in-flight budget
805+
* and hydrate together instead of one at a time.
806+
*/
807+
export const CONNECTOR_TEXT_DOCUMENT_MAX_BYTES = 12 * 1024 * 1024
808+
809+
const TRUNCATION_NOTICE = '[Truncated: the indexed text reached the size limit]'
810+
811+
/**
812+
* Accumulates newline-joined text under a byte ceiling. A record is appended
813+
* whole or not at all, so a truncated document never ends mid-message, and the
814+
* output carries a notice when something was left out.
815+
*/
816+
export class BoundedLines {
817+
private readonly lines: string[] = []
818+
private bytes = 0
819+
private truncated = false
820+
821+
constructor(private readonly maxBytes = CONNECTOR_TEXT_DOCUMENT_MAX_BYTES) {}
822+
823+
/**
824+
* Appends the lines together when they fit; otherwise marks the document
825+
* truncated, appends nothing, and returns false so the caller stops.
826+
*/
827+
push(...lines: string[]): boolean {
828+
if (this.truncated) return false
829+
let size = 0
830+
for (const line of lines) size += Buffer.byteLength(line, 'utf8') + 1
831+
if (this.bytes + size > this.maxBytes) {
832+
this.truncated = true
833+
return false
834+
}
835+
this.lines.push(...lines)
836+
this.bytes += size
837+
return true
838+
}
839+
840+
/** Joins the accepted lines, ending with the truncation notice when a push was refused. */
841+
join(): string {
842+
return this.truncated ? [...this.lines, TRUNCATION_NOTICE].join('\n') : this.lines.join('\n')
843+
}
844+
}

apps/sim/lib/api/contracts/credential-groups.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
credentialGroupAccessResponseSchema,
66
credentialGroupEnrollmentDetailSchema,
77
credentialGroupEnrollmentListQuerySchema,
8+
credentialGroupOAuthCallbackQuerySchema,
89
credentialGroupSchema,
910
inviteCredentialGroupEnrollmentsBodySchema,
1011
sharedCredentialGroupOAuthCallbackContract,
@@ -292,4 +293,22 @@ describe('credential group contracts', () => {
292293
}).success
293294
).toBe(false)
294295
})
296+
297+
it('accepts an Atlassian-sized authorization code', () => {
298+
const parsed = credentialGroupOAuthCallbackQuerySchema.safeParse({
299+
state: `cg_${'a'.repeat(36)}`,
300+
code: 'a'.repeat(4096),
301+
})
302+
303+
expect(parsed.success).toBe(true)
304+
})
305+
306+
it('still rejects an unbounded authorization code', () => {
307+
const parsed = credentialGroupOAuthCallbackQuerySchema.safeParse({
308+
state: `cg_${'a'.repeat(36)}`,
309+
code: 'a'.repeat(8193),
310+
})
311+
312+
expect(parsed.success).toBe(false)
313+
})
295314
})

apps/sim/lib/api/contracts/credential-groups.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { z } from 'zod'
2-
import { workflowIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives'
2+
import {
3+
MAX_OAUTH_CODE_LENGTH,
4+
workflowIdSchema,
5+
workspaceIdSchema,
6+
} from '@/lib/api/contracts/primitives'
37
import { defineRouteContract } from '@/lib/api/contracts/types'
48
import {
59
CREDENTIAL_GROUP_MCP_SERVER_LIMIT,
@@ -234,7 +238,7 @@ export const startCredentialGroupMcpOAuthParamsSchema =
234238
export const credentialGroupOAuthCallbackQuerySchema = z
235239
.object({
236240
state: z.string().min(1, 'OAuth state is required').max(512),
237-
code: z.string().min(1).max(2048).optional(),
241+
code: z.string().min(1).max(MAX_OAUTH_CODE_LENGTH, 'Authorization code is too long').optional(),
238242
error: z.string().min(1).max(256).optional(),
239243
error_description: z.string().max(1000).optional(),
240244
})

apps/sim/lib/api/contracts/oauth-connections.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from 'zod'
2-
import { workspaceIdSchema } from '@/lib/api/contracts/primitives'
2+
import { MAX_OAUTH_CODE_LENGTH, workspaceIdSchema } from '@/lib/api/contracts/primitives'
33
import type {
44
ContractBody,
55
ContractBodyInput,
@@ -223,7 +223,6 @@ export const trelloCallbackContract = defineRouteContract({
223223
})
224224

225225
const MAX_OAUTH_RETURN_URL_LENGTH = 2048
226-
const MAX_OAUTH_CODE_LENGTH = 8192
227226
const MAX_OAUTH_STATE_LENGTH = 256
228227
const MAX_OAUTH_ERROR_LENGTH = 2048
229228

apps/sim/lib/api/contracts/primitives.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,17 @@ export function withMissingFieldMessage<TSchema extends z.ZodString>(
237237
*/
238238
export const MAX_ID_LENGTH = 128
239239

240+
/**
241+
* Bound for an OAuth `code` callback parameter.
242+
*
243+
* Authorization codes have no length ceiling in RFC 6749, and providers differ by
244+
* orders of magnitude: Slack's are tens of characters while Atlassian returns a
245+
* signed JWT that routinely exceeds 2KB. The bound exists to keep an unbounded
246+
* string out of a token exchange, so it is sized above the largest real code
247+
* rather than around any one provider.
248+
*/
249+
export const MAX_OAUTH_CODE_LENGTH = 8192
250+
240251
/**
241252
* Builds a required, non-empty string schema whose message covers **both**
242253
* failure modes.

0 commit comments

Comments
 (0)