Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
482 changes: 439 additions & 43 deletions apps/docs/content/docs/en/integrations/quickbooks.mdx

Large diffs are not rendered by default.

650 changes: 571 additions & 79 deletions apps/sim/blocks/blocks/quickbooks.ts

Large diffs are not rendered by default.

79 changes: 79 additions & 0 deletions apps/sim/lib/core/security/redaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ describe('isSensitiveKey', () => {
expect(isSensitiveKey('refresh_token')).toBe(true)
expect(isSensitiveKey('auth_token')).toBe(true)
expect(isSensitiveKey('accessToken')).toBe(true)
expect(isSensitiveKey('sessionToken')).toBe(true)
expect(isSensitiveKey('webIdentityToken')).toBe(true)
expect(isSensitiveKey('verificationToken')).toBe(true)
expect(isSensitiveKey('githubToken')).toBe(true)
})

it.concurrent('should match secret variations', () => {
Expand Down Expand Up @@ -110,6 +114,22 @@ describe('isSensitiveKey', () => {
})

describe('non-sensitive keys (no false positives)', () => {
it.concurrent('should preserve workflow-state tokens that do not grant access', () => {
expect(isSensitiveKey('syncToken')).toBe(false)
expect(isSensitiveKey('SyncToken')).toBe(false)
expect(isSensitiveKey('nextSyncToken')).toBe(false)
expect(isSensitiveKey('pageToken')).toBe(false)
expect(isSensitiveKey('nextPageToken')).toBe(false)
expect(isSensitiveKey('scroll_token')).toBe(false)
expect(isSensitiveKey('continuationToken')).toBe(false)
expect(isSensitiveKey('nextContinuationToken')).toBe(false)
expect(isSensitiveKey('cursorToken')).toBe(false)
expect(isSensitiveKey('nextToken')).toBe(false)
expect(isSensitiveKey('clientRequestToken')).toBe(false)
expect(isSensitiveKey('idempotencyToken')).toBe(false)
expect(isSensitiveKey('subjectFromWebIdentityToken')).toBe(false)
})

it.concurrent('should not match keys with sensitive words as prefix only', () => {
expect(isSensitiveKey('tokenCount')).toBe(false)
expect(isSensitiveKey('tokenizer')).toBe(false)
Expand Down Expand Up @@ -178,6 +198,39 @@ describe('redactSensitiveValues', () => {
expect(result).not.toContain('key123456')
})

it.concurrent('should redact dotted and parenthesized sensitive fields', () => {
const result = redactSensitiveValues(
`stripe.api_key: "stripe-secret" (password: 'password-value') service.SyncToken: "3"`
)

expect(result).toBe(
`stripe.api_key: "${REDACTED_MARKER}" (password: '${REDACTED_MARKER}') service.SyncToken: "3"`
)
})

it.concurrent('should redact equals-style sensitive fields', () => {
const result = redactSensitiveValues(
`password="password-value" token='token-value' api_key="api-key-value" SyncToken="3"`
)

expect(result).toBe(
`password="${REDACTED_MARKER}" token='${REDACTED_MARKER}' api_key="${REDACTED_MARKER}" SyncToken="3"`
)
})

it.concurrent('should preserve workflow-state tokens in serialized JSON', () => {
const result = redactSensitiveValues(
'{"SyncToken":"3","nextPageToken":"page-2","accessToken":"secret","password":"don\'t leak"}'
)

expect(JSON.parse(result)).toEqual({
SyncToken: '3',
nextPageToken: 'page-2',
accessToken: REDACTED_MARKER,
password: REDACTED_MARKER,
})
})

it.concurrent('should not modify safe strings', () => {
const input = 'This is a normal string with no secrets'
const result = redactSensitiveValues(input)
Expand Down Expand Up @@ -230,6 +283,32 @@ describe('redactApiKeys', () => {
expect(result.config.normalField).toBe('normal-value')
})

it.concurrent('should preserve non-secret token fields while redacting credentials', () => {
const result = redactApiKeys({
syncToken: '3',
nextPageToken: 'page-2',
nextToken: 'next-page',
continuationToken: 'continue-page',
clientRequestToken: 'idempotency-key',
record: { Id: '42', SyncToken: '3' },
accessToken: 'access-secret',
sessionToken: 'session-secret',
verificationToken: 'verification-secret',
})

expect(result).toEqual({
syncToken: '3',
nextPageToken: 'page-2',
nextToken: 'next-page',
continuationToken: 'continue-page',
clientRequestToken: 'idempotency-key',
record: { Id: '42', SyncToken: '3' },
accessToken: REDACTED_MARKER,
sessionToken: REDACTED_MARKER,
verificationToken: REDACTED_MARKER,
})
})

it.concurrent('should redact sensitive keys in arrays', () => {
const arr = [{ apiKey: 'secret-key-1' }, { apiKey: 'secret-key-2' }]

Expand Down
43 changes: 26 additions & 17 deletions apps/sim/lib/core/security/redaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,19 @@ import { filterUserFileForDisplay, isUserFile } from '@/lib/core/utils/user-file
export const REDACTED_MARKER = '[REDACTED]'
export const TRUNCATED_MARKER = '[TRUNCATED]'

const BYPASS_REDACTION_KEYS = new Set(['nextPageToken'])
/**
* Token-named fields that carry workflow state rather than authorization.
*
* Keep this allowlist semantic and narrow: unknown `*token` fields remain
* redacted by default, while pagination cursors, synchronization versions, and
* idempotency keys stay visible so users can pass them into subsequent steps.
*/
const NON_SENSITIVE_TOKEN_KEY_PATTERNS: RegExp[] = [
/(?:page|pagination|continuation|cursor|scroll|sync)[_-]?token$/i,
/^(?:next|previous|after|before)[_-]?token$/i,
/^(?:client[_-]?request|idempotency)[_-]?token$/i,
/^subjectFromWebIdentityToken$/i,
]

/** Keys that contain large binary/encoded data that should be truncated in logs */
const LARGE_DATA_KEYS = new Set(['base64'])
Expand Down Expand Up @@ -56,25 +68,15 @@ const SENSITIVE_VALUE_PATTERNS: Array<{
pattern: /\b(sk|pk|api|key)[_-][A-Za-z0-9\-._]{20,}\b/gi,
replacement: REDACTED_MARKER,
},
// JSON-style password fields: password: "value" or password: 'value'
{
pattern: /password['":\s]*['"][^'"]+['"]/gi,
replacement: `password: "${REDACTED_MARKER}"`,
},
// JSON-style token fields: token: "value" or token: 'value'
{
pattern: /token['":\s]*['"][^'"]+['"]/gi,
replacement: `token: "${REDACTED_MARKER}"`,
},
// JSON-style api_key fields: api_key: "value" or api-key: "value"
{
pattern: /api[_-]?key['":\s]*['"][^'"]+['"]/gi,
replacement: `api_key: "${REDACTED_MARKER}"`,
},
]

const STRING_FIELD_PATTERNS = [
/(^|[^A-Za-z0-9_.-])(["']?)([A-Za-z0-9_.-]+)\2(\s*[:=]\s*)("(?:\\.|[^"\\])*")/gm,
/(^|[^A-Za-z0-9_.-])(["']?)([A-Za-z0-9_.-]+)\2(\s*[:=]\s*)('(?:\\.|[^'\\])*')/gm,
]

export function isSensitiveKey(key: string): boolean {
if (BYPASS_REDACTION_KEYS.has(key)) {
if (NON_SENSITIVE_TOKEN_KEY_PATTERNS.some((pattern) => pattern.test(key))) {
return false
}
const lowerKey = key.toLowerCase()
Expand All @@ -92,6 +94,13 @@ export function redactSensitiveValues(value: string): string {
}

let result = value
for (const pattern of STRING_FIELD_PATTERNS) {
result = result.replace(pattern, (match, prefix, keyQuote, key, separator, quotedValue) =>
isSensitiveKey(key)
? `${prefix}${keyQuote}${key}${keyQuote}${separator}${quotedValue[0]}${REDACTED_MARKER}${quotedValue[0]}`
: match
)
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
BillLeoutsakosvl346 marked this conversation as resolved.
for (const { pattern, replacement } of SENSITIVE_VALUE_PATTERNS) {
result = result.replace(pattern, replacement)
}
Expand Down
34 changes: 29 additions & 5 deletions apps/sim/lib/integrations/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -14564,8 +14564,8 @@
"type": "quickbooks",
"slug": "quickbooks",
"name": "QuickBooks",
"description": "Read procurement data from QuickBooks Online",
"longDescription": "Connect one QuickBooks Online company and read its company profile, vendors, purchase orders, and bills. The connected company is selected during OAuth and cannot be overridden by workflow input.",
"description": "Read and manage QuickBooks Online company and master data",
"longDescription": "Connect one QuickBooks Online company to read company, master-data, purchase-order, and bill records and to create or update customers, vendors, and basic products or services.",
"bgColor": "#2CA01C",
"iconName": "QuickBooksIcon",
"docsUrl": "https://docs.sim.ai/integrations/quickbooks",
Expand All @@ -14575,8 +14575,32 @@
"description": "Get information about the connected QuickBooks Online company"
},
{
"name": "List Vendors",
"description": "List vendors in the connected QuickBooks Online company"
"name": "Read Master Data",
"description": "List or read one account, customer, vendor, item, or employee"
},
{
"name": "Create Customer",
"description": "Create a customer in the connected QuickBooks Online company"
},
{
"name": "Update Customer",
"description": "Sparse-update a customer in the connected QuickBooks Online company"
},
{
"name": "Create Vendor",
"description": "Create a vendor in the connected QuickBooks Online company"
},
{
"name": "Update Vendor",
"description": "Sparse-update a vendor in the connected QuickBooks Online company"
},
{
"name": "Create Item",
"description": "Create a Service or Non-inventory item in QuickBooks Online"
},
{
"name": "Update Item",
"description": "Sparse-update supported fields on an item without changing its type"
},
{
"name": "List Purchase Orders",
Expand All @@ -14587,7 +14611,7 @@
"description": "List bills in the connected QuickBooks Online company"
}
],
"operationCount": 4,
"operationCount": 10,
"triggers": [],
"triggerCount": 0,
"authType": "oauth",
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/lib/oauth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
services: {
quickbooks: {
name: 'QuickBooks',
description: 'Read company, vendor, purchase order, and bill data from QuickBooks Online.',
description:
'Access company data and manage customers, vendors, and items in QuickBooks Online.',
providerId: 'quickbooks',
icon: QuickBooksIcon,
baseProviderIcon: QuickBooksIcon,
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/oauth/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
profile: 'Access profile information',
email: 'Access email address',
'com.intuit.quickbooks.accounting':
'Read accounting data from the connected QuickBooks Online company',
'Access and manage accounting data in the connected QuickBooks Online company',

// Notion scopes
'database.read': 'Read database',
Expand Down
3 changes: 1 addition & 2 deletions apps/sim/tools/error-extractors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
*/

import { parseGraphErrorFromData } from '@/tools/microsoft_excel/utils'
import { formatQuickBooksFaultDetail, sanitizeQuickBooksFaultData } from '@/tools/quickbooks/fault'

export interface ErrorInfo {
status?: number
Expand Down Expand Up @@ -378,5 +379,3 @@ export const ErrorExtractorId = {
PLAIN_TEXT_DATA: 'plain-text-data',
HTTP_STATUS_TEXT: 'http-status-text',
} as const

import { formatQuickBooksFaultDetail, sanitizeQuickBooksFaultData } from '@/tools/quickbooks/fault'
2 changes: 1 addition & 1 deletion apps/sim/tools/generated/tool-ids.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/sim/tools/generated/tool-metadata.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/sim/tools/generated/tool-outputs.ts

Large diffs are not rendered by default.

Loading