Skip to content

Commit cbc74bf

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(quickbooks): validate refreshed execution context
1 parent 29bfcc5 commit cbc74bf

4 files changed

Lines changed: 52 additions & 7 deletions

File tree

apps/sim/lib/oauth/oauth.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,26 @@ describe('OAuth Token Refresh', () => {
483483
}
484484
)
485485

486+
it.concurrent('should preserve numeric string token expiry values', async () => {
487+
const mockFetch = vi.fn().mockResolvedValue(
488+
Response.json({
489+
access_token: 'new_access_token',
490+
expires_in: '7200',
491+
})
492+
)
493+
494+
const result = await withMockFetch(mockFetch, () =>
495+
refreshOAuthToken('google', 'old_refresh_token')
496+
)
497+
498+
expect(result).toEqual({
499+
ok: true,
500+
accessToken: 'new_access_token',
501+
expiresIn: 7200,
502+
refreshToken: 'old_refresh_token',
503+
})
504+
})
505+
486506
it.concurrent('should handle providers that return new refresh tokens', async () => {
487507
const refreshToken = 'old_refresh_token'
488508
const newRefreshToken = 'new_refresh_token'

apps/sim/lib/oauth/oauth.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,10 +1885,13 @@ export async function refreshOAuthToken(
18851885
}
18861886

18871887
const expiresInCandidate = data.expires_in ?? data.expiresIn
1888-
const expiresIn =
1889-
typeof expiresInCandidate === 'number' && Number.isFinite(expiresInCandidate)
1888+
const parsedExpiresIn =
1889+
typeof expiresInCandidate === 'number'
18901890
? expiresInCandidate
1891-
: 3600
1891+
: typeof expiresInCandidate === 'string' && expiresInCandidate.trim()
1892+
? Number(expiresInCandidate)
1893+
: Number.NaN
1894+
const expiresIn = Number.isFinite(parsedExpiresIn) ? parsedExpiresIn : 3600
18921895

18931896
if (!accessToken) {
18941897
logger.warn('No access token found in refresh response', {

apps/sim/tools/quickbooks/get_company_info.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,20 @@ export const quickbooksGetCompanyInfoTool: ToolConfig<
5959
retry: { enabled: false },
6060
maxResponseBytes: QUICKBOOKS_MAX_RESPONSE_BYTES,
6161
},
62-
transformResponse: async (response) => {
62+
transformResponse: async (response, params) => {
63+
const realmId = normalizeQuickBooksRealmId(params?.realmId ?? '')
6364
const data = await parseQuickBooksJson<CompanyInfoEnvelope>(
6465
response,
6566
'QuickBooks CompanyInfo response'
6667
)
6768
if (!data.CompanyInfo || typeof data.CompanyInfo !== 'object') {
6869
throw new Error('QuickBooks CompanyInfo response is missing CompanyInfo')
6970
}
71+
if (String(data.CompanyInfo.Id ?? '').trim() !== realmId) {
72+
throw new Error(
73+
'QuickBooks CompanyInfo response returned a different company. Reconnect the QuickBooks credential.'
74+
)
75+
}
7076
return {
7177
success: true,
7278
output: {

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,8 @@ describe('QuickBooks tool boundaries', () => {
340340
CompanyInfo: { Id: '123456789', CompanyName: 'Sanitized Company' },
341341
time: 'test-time',
342342
})
343-
)
343+
),
344+
authParams
344345
)
345346
expect(result.output).toEqual({
346347
company: { Id: '123456789', CompanyName: 'Sanitized Company' },
@@ -351,11 +352,25 @@ describe('QuickBooks tool boundaries', () => {
351352
it('rejects an empty CompanyInfo wrapper', async () => {
352353
await expect(
353354
quickbooksGetCompanyInfoTool.transformResponse!(
354-
new Response(JSON.stringify({ time: 'test-time' }))
355+
new Response(JSON.stringify({ time: 'test-time' })),
356+
authParams
355357
)
356358
).rejects.toThrow('missing CompanyInfo')
357359
})
358360

361+
it('rejects CompanyInfo for a different realm', async () => {
362+
await expect(
363+
quickbooksGetCompanyInfoTool.transformResponse!(
364+
new Response(
365+
JSON.stringify({
366+
CompanyInfo: { Id: '999', CompanyName: 'Different Sanitized Company' },
367+
})
368+
),
369+
authParams
370+
)
371+
).rejects.toThrow('different company')
372+
})
373+
359374
it('recognizes a QuickBooks Fault in an HTTP 200 CompanyInfo response', async () => {
360375
await expect(
361376
quickbooksGetCompanyInfoTool.transformResponse!(
@@ -372,7 +387,8 @@ describe('QuickBooks tool boundaries', () => {
372387
},
373388
}),
374389
{ status: 200 }
375-
)
390+
),
391+
authParams
376392
)
377393
).rejects.toThrow(
378394
'QuickBooks request failed with HTTP 200. 3200: Authentication failed: Sanitized fault detail'

0 commit comments

Comments
 (0)