Skip to content

Commit 12599c8

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix: harden Oracle Fusion response handling
1 parent 3ffdc29 commit 12599c8

4 files changed

Lines changed: 39 additions & 11 deletions

File tree

apps/sim/lib/internal/oracle-fusion-financials/client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
secureFetchWithPinnedIP,
77
validateUrlWithDNS,
88
} from '@/lib/core/security/input-validation.server'
9+
import { redactExactSensitiveValues } from '@/lib/core/security/redaction'
910
import { consumeOrCancelBody } from '@/lib/core/utils/stream-limits'
1011
import { normalizeOracleFusionApplicationOrigin } from '@/lib/credentials/client-credential-accounts/descriptors'
1112
import type { OracleFusionAuthInput } from '@/lib/internal/oracle-fusion-financials/schema'
@@ -55,7 +56,7 @@ function sanitizeOracleError(body: string, accessToken: string, status: number):
5556
// Non-JSON proxy pages are intentionally not reflected to tool callers.
5657
}
5758
const unique = [...new Set(messages)]
58-
const safe = truncate(unique.join(' — ').replaceAll(accessToken, '[REDACTED]'), 1_000)
59+
const safe = truncate(redactExactSensitiveValues(unique.join(' — '), [accessToken]), 1_000)
5960
return safe || `Oracle Fusion Financials request failed with HTTP ${status}`
6061
}
6162

apps/sim/lib/internal/oracle-fusion-financials/oracle-fusion-financials.test.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -305,23 +305,33 @@ describe('Oracle Fusion Financials provider', () => {
305305
})
306306

307307
it('stops after two retries and surfaces a sanitized Oracle error', async () => {
308+
const accessToken = 'short/lived+access~token='
309+
const encodedAccessToken = encodeURIComponent(accessToken)
310+
const formEncodedAccessToken = new URLSearchParams({ value: accessToken })
311+
.toString()
312+
.slice('value='.length)
308313
mockSecureFetch
309314
.mockResolvedValueOnce(response(504, { title: 'gateway timeout' }))
310315
.mockResolvedValueOnce(response(503, { title: 'unavailable' }))
311316
.mockResolvedValueOnce(
312317
response(429, {
313-
title: `Token ${AUTH.accessToken}`,
314-
detail: 'Request temporarily throttled',
318+
title: `Token ${encodedAccessToken}`,
319+
detail: `Token ${formEncodedAccessToken}`,
320+
message: 'access_token=provider-detail-canary&scope=read',
315321
})
316322
)
317323

318-
const error = await requestOracleFusionJson(AUTH, {
319-
path: '/fscmRestApi/resources/11.13.18.05/invoices',
320-
}).catch((caught) => caught)
324+
const error = await requestOracleFusionJson(
325+
{ ...AUTH, accessToken },
326+
{ path: '/fscmRestApi/resources/11.13.18.05/invoices' }
327+
).catch((caught) => caught)
321328
expect(error).toBeInstanceOf(OracleFusionFinancialsProviderError)
322329
expect(error).toMatchObject({ status: 429 })
323330
expect((error as Error).message).toContain('[REDACTED]')
324-
expect((error as Error).message).not.toContain(AUTH.accessToken)
331+
expect((error as Error).message).not.toContain(accessToken)
332+
expect((error as Error).message).not.toContain(encodedAccessToken)
333+
expect((error as Error).message).not.toContain(formEncodedAccessToken)
334+
expect((error as Error).message).not.toContain('provider-detail-canary')
325335
expect(mockSecureFetch).toHaveBeenCalledTimes(3)
326336
})
327337

@@ -353,6 +363,7 @@ describe('Oracle Fusion Financials provider', () => {
353363

354364
it('rejects malformed Oracle list envelopes and projected field types', async () => {
355365
mockSecureFetch
366+
.mockResolvedValueOnce(response(200, { count: 0, hasMore: false, limit: 50, offset: 0 }))
356367
.mockResolvedValueOnce(
357368
response(200, { items: [], count: '0', hasMore: false, limit: 50, offset: 0 })
358369
)
@@ -361,6 +372,16 @@ describe('Oracle Fusion Financials provider', () => {
361372
)
362373
.mockResolvedValueOnce(response(200, page([{ CheckId: 'not-a-number' }])))
363374

375+
await expect(
376+
executeOracleFusionFinancialsOperation(
377+
'oracle_fusion_financials_list_payables_payments',
378+
AUTH
379+
)
380+
).rejects.toMatchObject({
381+
name: 'OracleFusionFinancialsProviderError',
382+
status: 502,
383+
message: 'Oracle Fusion Financials returned an unexpected response shape',
384+
})
364385
await expect(
365386
executeOracleFusionFinancialsOperation(
366387
'oracle_fusion_financials_list_payables_payments',

apps/sim/lib/internal/oracle-fusion-financials/schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ export const oracleFusionPaymentSchema = z
203203

204204
export const oracleFusionListEnvelopeSchema = z
205205
.object({
206-
items: z.array(z.unknown()).max(100).default([]),
206+
items: z.array(z.unknown()).max(100),
207207
count: z.number().int().nonnegative().max(100),
208208
hasMore: z.boolean(),
209209
limit: z.number().int().positive().max(100),

apps/sim/lib/selectors/server/providers/oracle-fusion-financials.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,9 +211,15 @@ describe('Oracle Fusion Financials invoice selector', () => {
211211
mocks.request.mockRejectedValue(
212212
new OracleFusionFinancialsProviderError('provider-secret-canary', 401)
213213
)
214-
await expect(attachment.execute(args({ kind: 'list' }), prepared())).rejects.toEqual(
215-
expect.objectContaining({ name: 'SelectorConnectionUnavailableError' })
216-
)
214+
const authenticationError = await attachment
215+
.execute(args({ kind: 'list' }), prepared())
216+
.catch((error) => error)
217+
expect(authenticationError).toMatchObject({
218+
name: 'SelectorConnectionUnavailableError',
219+
message: 'Connection unavailable',
220+
status: 401,
221+
})
222+
expect((authenticationError as Error).message).not.toContain('provider-secret-canary')
217223

218224
mocks.request.mockRejectedValue(
219225
new OracleFusionFinancialsProviderError('provider-secret-canary', 404)

0 commit comments

Comments
 (0)