Skip to content

Commit 6ccec84

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(quickbooks): address integration review findings
1 parent 743dbe3 commit 6ccec84

15 files changed

Lines changed: 145 additions & 38 deletions

File tree

apps/docs/content/docs/en/integrations/quickbooks.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ Sparse-update a Service or Non-inventory item in QuickBooks Online
394394
| --------- | ---- | -------- | ----------- |
395395
| `itemId` | string | Yes | ID of the item to update |
396396
| `syncToken` | string | Yes | Current item sync token |
397+
| `itemType` | string | Yes | Current item type: service or non_inventory |
397398
| `name` | string | No | Replacement item name |
398399
| `incomeAccountId` | string | No | Replacement income account ID |
399400
| `description` | string | No | Replacement sales description |

apps/sim/blocks/blocks/quickbooks.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,8 @@ export const QuickBooksBlock: BlockConfig<QuickBooksResponse> = {
356356
{ label: 'Service', id: 'service' },
357357
{ label: 'Non-inventory', id: 'non_inventory' },
358358
],
359-
condition: { field: 'operation', value: 'quickbooks_create_item' },
360-
required: { field: 'operation', value: 'quickbooks_create_item' },
359+
condition: { field: 'operation', value: [...ITEM_OPERATIONS] },
360+
required: { field: 'operation', value: [...ITEM_OPERATIONS] },
361361
value: () => 'service',
362362
},
363363
{

apps/sim/lib/core/security/redaction.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,19 @@ describe('redactSensitiveValues', () => {
198198
expect(result).not.toContain('key123456')
199199
})
200200

201+
it.concurrent('should preserve workflow-state tokens in serialized JSON', () => {
202+
const result = redactSensitiveValues(
203+
'{"SyncToken":"3","nextPageToken":"page-2","accessToken":"secret","password":"don\'t leak"}'
204+
)
205+
206+
expect(JSON.parse(result)).toEqual({
207+
SyncToken: '3',
208+
nextPageToken: 'page-2',
209+
accessToken: REDACTED_MARKER,
210+
password: REDACTED_MARKER,
211+
})
212+
})
213+
201214
it.concurrent('should not modify safe strings', () => {
202215
const input = 'This is a normal string with no secrets'
203216
const result = redactSensitiveValues(input)

apps/sim/lib/core/security/redaction.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,21 +68,11 @@ const SENSITIVE_VALUE_PATTERNS: Array<{
6868
pattern: /\b(sk|pk|api|key)[_-][A-Za-z0-9\-._]{20,}\b/gi,
6969
replacement: REDACTED_MARKER,
7070
},
71-
// JSON-style password fields: password: "value" or password: 'value'
72-
{
73-
pattern: /password['":\s]*['"][^'"]+['"]/gi,
74-
replacement: `password: "${REDACTED_MARKER}"`,
75-
},
76-
// JSON-style token fields: token: "value" or token: 'value'
77-
{
78-
pattern: /token['":\s]*['"][^'"]+['"]/gi,
79-
replacement: `token: "${REDACTED_MARKER}"`,
80-
},
81-
// JSON-style api_key fields: api_key: "value" or api-key: "value"
82-
{
83-
pattern: /api[_-]?key['":\s]*['"][^'"]+['"]/gi,
84-
replacement: `api_key: "${REDACTED_MARKER}"`,
85-
},
71+
]
72+
73+
const STRING_FIELD_PATTERNS = [
74+
/(^|[{,\s])(["']?)([A-Za-z0-9_-]+)\2(\s*:\s*)("(?:\\.|[^"\\])*")/gm,
75+
/(^|[{,\s])(["']?)([A-Za-z0-9_-]+)\2(\s*:\s*)('(?:\\.|[^'\\])*')/gm,
8676
]
8777

8878
export function isSensitiveKey(key: string): boolean {
@@ -104,6 +94,13 @@ export function redactSensitiveValues(value: string): string {
10494
}
10595

10696
let result = value
97+
for (const pattern of STRING_FIELD_PATTERNS) {
98+
result = result.replace(pattern, (match, prefix, keyQuote, key, separator, quotedValue) =>
99+
isSensitiveKey(key)
100+
? `${prefix}${keyQuote}${key}${keyQuote}${separator}${quotedValue[0]}${REDACTED_MARKER}${quotedValue[0]}`
101+
: match
102+
)
103+
}
107104
for (const { pattern, replacement } of SENSITIVE_VALUE_PATTERNS) {
108105
result = result.replace(pattern, replacement)
109106
}

apps/sim/lib/oauth/oauth.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,7 +1016,8 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
10161016
services: {
10171017
quickbooks: {
10181018
name: 'QuickBooks',
1019-
description: 'Read company, vendor, purchase order, and bill data from QuickBooks Online.',
1019+
description:
1020+
'Access company data and manage customers, vendors, and items in QuickBooks Online.',
10201021
providerId: 'quickbooks',
10211022
icon: QuickBooksIcon,
10221023
baseProviderIcon: QuickBooksIcon,

apps/sim/lib/oauth/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
8787
profile: 'Access profile information',
8888
email: 'Access email address',
8989
'com.intuit.quickbooks.accounting':
90-
'Read accounting data from the connected QuickBooks Online company',
90+
'Access and manage accounting data in the connected QuickBooks Online company',
9191

9292
// Notion scopes
9393
'database.read': 'Read database',

apps/sim/tools/error-extractors.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* 2. Add the ID to ErrorExtractorId constant at the bottom of this file
2020
*/
2121

22+
import { formatQuickBooksFaultDetail, sanitizeQuickBooksFaultData } from '@/lib/quickbooks/fault'
2223
import { parseGraphErrorFromData } from '@/tools/microsoft_excel/utils'
2324

2425
export interface ErrorInfo {
@@ -378,5 +379,3 @@ export const ErrorExtractorId = {
378379
PLAIN_TEXT_DATA: 'plain-text-data',
379380
HTTP_STATUS_TEXT: 'http-status-text',
380381
} as const
381-
382-
import { formatQuickBooksFaultDetail, sanitizeQuickBooksFaultData } from '@/lib/quickbooks/fault'

apps/sim/tools/quickbooks/create_item.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,13 @@ import {
1212
getQuickBooksToolHeaders,
1313
optionalQuickBooksString,
1414
quickBooksReference,
15+
quickBooksWritableItemType,
1516
requiredQuickBooksString,
1617
transformQuickBooksMutationResponse,
1718
validateQuickBooksOptionalNumber,
1819
} from '@/tools/quickbooks/utils'
1920
import type { ToolConfig } from '@/tools/types'
2021

21-
const QUICKBOOKS_ITEM_TYPES = {
22-
service: 'Service',
23-
non_inventory: 'NonInventory',
24-
} as const
25-
2622
export const quickbooksCreateItemTool: ToolConfig<
2723
QuickBooksCreateItemParams,
2824
QuickBooksMutationResponse<QuickBooksItem>
@@ -110,8 +106,7 @@ export const quickbooksCreateItemTool: ToolConfig<
110106
method: 'POST',
111107
headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'),
112108
body: (params) => {
113-
const type = QUICKBOOKS_ITEM_TYPES[params.itemType]
114-
if (!type) throw new Error(`Unsupported writable QuickBooks item type: ${params.itemType}`)
109+
const type = quickBooksWritableItemType(params.itemType)
115110
const purchaseDescription = optionalQuickBooksString(params.purchaseDescription)
116111
const purchaseCost = validateQuickBooksOptionalNumber(params.purchaseCost, 'purchaseCost')
117112
if (

apps/sim/tools/quickbooks/create_vendor.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
quickBooksEmailAddress,
1515
quickBooksPhoneNumber,
1616
requiredQuickBooksString,
17+
sanitizeQuickBooksVendor,
1718
transformQuickBooksMutationResponse,
1819
} from '@/tools/quickbooks/utils'
1920
import type { ToolConfig } from '@/tools/types'
@@ -127,7 +128,11 @@ export const quickbooksCreateVendorTool: ToolConfig<
127128
maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES,
128129
},
129130
transformResponse: (response) =>
130-
transformQuickBooksMutationResponse<QuickBooksVendor>(response, 'Vendor'),
131+
transformQuickBooksMutationResponse<QuickBooksVendor>(
132+
response,
133+
'Vendor',
134+
sanitizeQuickBooksVendor
135+
),
131136
outputs: {
132137
record: {
133138
type: 'json',

apps/sim/tools/quickbooks/quickbooks.test.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,35 @@ describe('QuickBooks master-data reader', () => {
368368
expect(byIdResult.output.item).not.toHaveProperty('BankAccountNumber')
369369
})
370370

371+
it('removes vendor tax identifiers from list and by-ID output', async () => {
372+
const vendor = {
373+
Id: '3',
374+
SyncToken: '1',
375+
DisplayName: 'Sanitized Vendor',
376+
TaxIdentifier: 'sensitive-tax-id',
377+
}
378+
const vendorParams: QuickBooksReadMasterDataParams = {
379+
...listParams,
380+
recordType: 'vendor',
381+
}
382+
383+
const listResult = await quickbooksReadMasterDataTool.transformResponse!(
384+
Response.json({ QueryResponse: { Vendor: [vendor] } }),
385+
vendorParams
386+
)
387+
expect(listResult.output.items?.[0]).toEqual({
388+
Id: '3',
389+
SyncToken: '1',
390+
DisplayName: 'Sanitized Vendor',
391+
})
392+
393+
const byIdResult = await quickbooksReadMasterDataTool.transformResponse!(
394+
Response.json({ Vendor: vendor }),
395+
{ ...vendorParams, readMode: 'by_id', recordId: '3' }
396+
)
397+
expect(byIdResult.output.item).not.toHaveProperty('TaxIdentifier')
398+
})
399+
371400
it('rejects missing IDs, unknown types and unknown modes before a request', () => {
372401
const requestUrl = quickbooksReadMasterDataTool.request.url as (
373402
params: QuickBooksReadMasterDataParams
@@ -504,6 +533,28 @@ describe('QuickBooks customer and vendor mutations', () => {
504533
expect(() => parseQuickBooksAddress('{"city":123}', 'billingAddress')).toThrow(
505534
'must be a string'
506535
)
536+
expect(() => parseQuickBooksAddress('{}', 'billingAddress')).toThrow('at least one')
537+
})
538+
539+
it('removes vendor tax identifiers from mutation output', async () => {
540+
const response = {
541+
Vendor: {
542+
Id: '21',
543+
SyncToken: '0',
544+
DisplayName: 'Sanitized Vendor',
545+
TaxIdentifier: 'sensitive-tax-id',
546+
},
547+
time: 'test-time',
548+
}
549+
550+
for (const tool of [quickbooksCreateVendorTool, quickbooksUpdateVendorTool]) {
551+
const result = await tool.transformResponse!(Response.json(response))
552+
expect(result.output.record).toEqual({
553+
Id: '21',
554+
SyncToken: '0',
555+
DisplayName: 'Sanitized Vendor',
556+
})
557+
}
507558
})
508559
})
509560

@@ -567,6 +618,7 @@ describe('QuickBooks item mutations', () => {
567618
...authParams,
568619
itemId: '44',
569620
syncToken: '2',
621+
itemType: 'service',
570622
activeStatus: 'unchanged',
571623
unitPrice: 15.75,
572624
expenseAccountId: '80',
@@ -578,7 +630,13 @@ describe('QuickBooks item mutations', () => {
578630
UnitPrice: 15.75,
579631
ExpenseAccountRef: { value: '80' },
580632
})
581-
expect(quickbooksUpdateItemTool.params).not.toHaveProperty('itemType')
633+
expect(quickbooksUpdateItemTool.params.itemType).toMatchObject({ required: true })
634+
expect(() =>
635+
quickbooksUpdateItemTool.request.body!({
636+
...params,
637+
itemType: 'inventory' as QuickBooksUpdateItemParams['itemType'],
638+
})
639+
).toThrow('Unsupported writable')
582640
})
583641

584642
it('returns the native mutation record and convenient identifiers', async () => {
@@ -779,7 +837,7 @@ describe('QuickBooks tool and block boundaries', () => {
779837
})
780838
expect(subBlocks.itemType.condition).toEqual({
781839
field: 'operation',
782-
value: 'quickbooks_create_item',
840+
value: ['quickbooks_create_item', 'quickbooks_update_item'],
783841
})
784842
})
785843
})

0 commit comments

Comments
 (0)