feat(quickbooks): add credential-scoped OAuth tools and triggers - #6099
feat(quickbooks): add credential-scoped OAuth tools and triggers#6099BillLeoutsakosvl346 wants to merge 21 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryHigh Risk Overview OAuth and credentials are extended so one connected company is fixed at callback time: the auth route validates The PR introduces workflow tools and API routes beyond read-only lists—master data CRUD, sales and purchasing transactions (create/update/void where supported), accounting entries, financial reports, transaction email, PDF download, and attachment upload/download—with bounded file handling and SSRF-guarded attachment URLs in the document download path. Reviewed by Cursor Bugbot for commit 78c0b29. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryAdds a credential-scoped QuickBooks Online integration.
Confidence Score: 5/5The PR appears safe to merge based on this follow-up review. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| apps/sim/app/api/auth/oauth2/authorize/route.ts | Adds QuickBooks-specific authorization using credential-scoped client configuration and signed, same-origin return state. |
| apps/sim/app/api/auth/oauth2/callback/quickbooks/route.ts | Adds a session-bound QuickBooks callback that validates state and realm identity before completing the connection. |
| apps/sim/lib/credentials/application/complete-quickbooks-connection.ts | Exchanges the authorization code, validates the selected company, and persists credential-scoped OAuth state. |
| apps/sim/lib/oauth/quickbooks.ts | Implements QuickBooks token exchange, refresh, revocation, profile validation, and account identity helpers. |
| apps/sim/lib/internal/quickbooks/execute-tool.ts | Adds the server-side dispatch boundary for QuickBooks accounting and document operations. |
| apps/sim/tools/quickbooks/client.ts | Adds bounded QuickBooks API request construction and response handling. |
| packages/db/migrations/0322_friendly_white_tiger.sql | Adds persistence for encrypted credential-scoped OAuth client configuration. |
Sequence Diagram
sequenceDiagram
participant User
participant Sim as Sim OAuth API
participant Intuit
participant Credential as Credential Store
participant Tool as QuickBooks Tool
User->>Sim: Save encrypted client configuration
Sim->>Intuit: Redirect with signed state
Intuit-->>Sim: Authorization code and realm ID
Sim->>Intuit: Exchange code and validate company
Sim->>Credential: Store tokens, realm, environment, and client configuration
Tool->>Credential: Resolve credential-scoped authorization
Tool->>Intuit: Execute company-scoped operation
Intuit-->>Tool: Bounded provider response
Reviews (4): Last reviewed commit: "feat(quickbooks): support credential-sco..." | Re-trigger Greptile
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 33d9afb. Configure here.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 29bfcc5. Configure here.
|
@cursor review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 19e00eb. Configure here.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 00fc671. Configure here.
|
@cursor review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 970d3b1. Configure here.
80b3121 to
2405a19
Compare
…tegration # Conflicts: # apps/docs/content/docs/integrations/quickbooks.mdx # apps/sim/app/api/auth/oauth/disconnect/route.test.ts # apps/sim/app/api/auth/oauth/token/route.ts # apps/sim/lib/api/contracts/selectors/oauth.ts # apps/sim/lib/auth/connectors/providers.ts # apps/sim/lib/oauth/token-resolution.ts # apps/sim/tools/generated/tool-ids.ts # apps/sim/tools/generated/tool-metadata.ts # apps/sim/tools/generated/tool-outputs.ts
…tegration # Conflicts: # apps/sim/tools/generated/tool-ids.ts # apps/sim/tools/generated/tool-metadata.ts # apps/sim/tools/generated/tool-outputs.ts
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
* feat(quickbooks): complete webhook trigger matrix * fix(quickbooks): document webhook event selectors * fix(quickbooks): clarify webhook setup --------- Co-authored-by: Bill Leoutsakos <billleoutsakos@Mac.localdomain>
There was a problem hiding this comment.
11 issues found across 151 files
Confidence score: 2/5
packages/db/schema.tsstores QuickBooks client configuration at the account level, so connecting the same company through different Intuit apps can overwrite credentials used by another connection—scope the configuration per credential or connection.apps/sim/hooks/queries/oauth/oauth-connections.tsbypasses the QuickBooks-specific draft flow on desktop, causing connections to fail, whileconnected-credential-detail.tsxlacks return context for reliable reauthorization feedback—route desktop through the QuickBooks draft flow and preserve callback context.apps/sim/lib/credentials/oauth-accounts.tsrevokes all grants before local validation and deletion completes, leaving revoked tokens attached to undeleted rows after a partial failure—reorder the operation or add compensating cleanup.apps/sim/tools/quickbooks/update_customer_payment.tsandupdate_refund_receipt.tscannot reliably apply documented allocation or line replacement semantics, while the related request/error paths also misclassify invalid inputs and nested QuickBooks faults—handle empty allocations and native line IDs explicitly, validate provider-specific fields, and inspectQueryResponse.Faultbefore falling back to status-only errors.
Not reviewed (too large): apps/sim/blocks/blocks/quickbooks.ts (~3,242 lines), apps/docs/content/docs/integrations/quickbooks.mdx (~2,961 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/sim/lib/api/contracts/v2/credentials.ts">
<violation number="1" location="apps/sim/lib/api/contracts/v2/credentials.ts:242">
P2: When a v2 caller omits the config for QuickBooks or sends it to another provider, this optional field passes contract validation and the draft creator returns a generic 500. Validate the provider/config relationship as request validation or a classified orchestration error before creating the draft.</violation>
</file>
<file name="apps/sim/tools/quickbooks/fault.ts">
<violation number="1" location="apps/sim/tools/quickbooks/fault.ts:52">
P2: When `Fault.Error` contains many entries, `flatMap` materializes every sanitized entry before `slice` keeps five. Append only the first five entries while counting later valid entries as omitted so a large provider fault cannot allocate an intermediate array proportional to the full response.</violation>
</file>
<file name="packages/db/schema.ts">
<violation number="1" location="packages/db/schema.ts:102">
P1: When the same QuickBooks company is connected with different Intuit apps, this account-level column shares one client configuration across all credentials referencing that account. The later connection overwrites the value used for refresh and revocation, so the other credential can use the wrong client credentials; store the config on the workspace credential or make account identity include the client configuration and preserve that scope through refresh and revocation.</violation>
</file>
<file name="apps/sim/hooks/queries/oauth/oauth-connections.ts">
<violation number="1" location="apps/sim/hooks/queries/oauth/oauth-connections.ts:192">
P1: On desktop, this branch never runs because the bridge returns first. The desktop fallback uses generic `oauth2.link`, so QuickBooks cannot use its draft-scoped client configuration and the connection fails; add a QuickBooks-specific desktop authorize route while preserving the loopback callback.</violation>
</file>
<file name="apps/sim/lib/credentials/api/route-policies.ts">
<violation number="1" location="apps/sim/lib/credentials/api/route-policies.ts:30">
P2: When a disconnect request exceeds the account or credential bound, this branch returns `503` with `Retry-After` for a request that cannot succeed by retrying. Use a distinct bounded-input error and reserve this response for transient provider revocation failures.</violation>
</file>
<file name="apps/sim/lib/api/contracts/credentials.ts">
<violation number="1" location="apps/sim/lib/api/contracts/credentials.ts:335">
P2: When a caller omits this field for QuickBooks or sends it for another provider, the request passes validation and then returns 500 from `createConnectDraft` instead of a 400 validation error. Make this body schema provider-aware: require the config for QuickBooks and reject it for other providers.</violation>
</file>
<file name="apps/sim/lib/credentials/oauth-accounts.ts">
<violation number="1" location="apps/sim/lib/credentials/oauth-accounts.ts:173">
P1: When disconnecting multiple QuickBooks accounts, this pre-revokes every grant before validating and deleting any local rows. A later provider or local failure leaves previously revoked tokens attached to undeleted credentials, so retries can no longer complete the disconnect. Make the operation resumable and idempotent by validating all local failure conditions first, tracking per-account revocation progress, and treating already-revoked tokens as completed during cleanup.</violation>
</file>
<file name="apps/sim/tools/error-extractors.ts">
<violation number="1" location="apps/sim/tools/error-extractors.ts:425">
P2: When QuickBooks returns a non-2xx query failure with `QueryResponse.Fault`, this extractor ignores the nested fault and reports only the HTTP status. Inspect `QueryResponse` as a fallback after checking the top-level fault so invalid-query errors retain their actionable detail.</violation>
</file>
<file name="apps/sim/tools/quickbooks/update_customer_payment.ts">
<violation number="1" location="apps/sim/tools/quickbooks/update_customer_payment.ts:102">
P1: When `unapplyOmittedInvoices` is true without a nonempty allocation list, the update preserves the existing allocations instead of unapplying omitted invoices. Handle an empty list as `Line: []` or reject the flag unless an allocation list is supplied, and keep the tool contract consistent with that behavior.</violation>
</file>
<file name="apps/sim/tools/quickbooks/update_refund_receipt.ts">
<violation number="1" location="apps/sim/tools/quickbooks/update_refund_receipt.ts:125">
P1: When `lines` is supplied, this tool cannot guarantee the documented replacement behavior because sparse QuickBooks line updates require native line IDs to update or remove existing lines. Use a read/full-update flow that preserves line IDs, or expose line IDs and implement the provider’s deletion semantics before sending this request.</violation>
</file>
<file name="apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx">
<violation number="1" location="apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx:216">
P2: When QuickBooks reauthorization is launched from this page, no return context identifies the new OAuth attempt. The callback therefore provides no reliable success/error toast or update event, and a stale context from another flow can redirect the user to the wrong page. Write or replace the integrations return context when this reauthorization is actually submitted, and clear/handle it on failure.</violation>
</file>
| @@ -99,6 +99,7 @@ export const account = pgTable( | |||
| refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), | |||
| scope: text('scope'), | |||
| password: text('password'), | |||
| encryptedOAuthClientConfig: text('encrypted_oauth_client_config'), | |||
There was a problem hiding this comment.
P1: When the same QuickBooks company is connected with different Intuit apps, this account-level column shares one client configuration across all credentials referencing that account. The later connection overwrites the value used for refresh and revocation, so the other credential can use the wrong client credentials; store the config on the workspace credential or make account identity include the client configuration and preserve that scope through refresh and revocation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/schema.ts, line 102:
<comment>When the same QuickBooks company is connected with different Intuit apps, this account-level column shares one client configuration across all credentials referencing that account. The later connection overwrites the value used for refresh and revocation, so the other credential can use the wrong client credentials; store the config on the workspace credential or make account identity include the client configuration and preserve that scope through refresh and revocation.</comment>
<file context>
@@ -99,6 +99,7 @@ export const account = pgTable(
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
scope: text('scope'),
password: text('password'),
+ encryptedOAuthClientConfig: text('encrypted_oauth_client_config'),
createdAt: timestamp('created_at').notNull(),
updatedAt: timestamp('updated_at').notNull(),
</file context>
| return { success: true } | ||
| } | ||
|
|
||
| if (providerId === 'quickbooks') { |
There was a problem hiding this comment.
P1: On desktop, this branch never runs because the bridge returns first. The desktop fallback uses generic oauth2.link, so QuickBooks cannot use its draft-scoped client configuration and the connection fails; add a QuickBooks-specific desktop authorize route while preserving the loopback callback.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/hooks/queries/oauth/oauth-connections.ts, line 192:
<comment>On desktop, this branch never runs because the bridge returns first. The desktop fallback uses generic `oauth2.link`, so QuickBooks cannot use its draft-scoped client configuration and the connection fails; add a QuickBooks-specific desktop authorize route while preserving the loopback callback.</comment>
<file context>
@@ -189,6 +189,17 @@ export function useConnectOAuthService() {
return { success: true }
}
+ if (providerId === 'quickbooks') {
+ if (!draftId) {
+ throw new Error('QuickBooks authorization requires a credential connection draft.')
</file context>
| const clientConfig = await decryptQuickBooksOAuthClientConfig( | ||
| targetAccount.encryptedOAuthClientConfig | ||
| ) | ||
| await revokeQuickBooksToken(token, clientConfig, quickBooksDisconnectSignal) |
There was a problem hiding this comment.
P1: When disconnecting multiple QuickBooks accounts, this pre-revokes every grant before validating and deleting any local rows. A later provider or local failure leaves previously revoked tokens attached to undeleted credentials, so retries can no longer complete the disconnect. Make the operation resumable and idempotent by validating all local failure conditions first, tracking per-account revocation progress, and treating already-revoked tokens as completed during cleanup.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/credentials/oauth-accounts.ts, line 173:
<comment>When disconnecting multiple QuickBooks accounts, this pre-revokes every grant before validating and deleting any local rows. A later provider or local failure leaves previously revoked tokens attached to undeleted credentials, so retries can no longer complete the disconnect. Make the operation resumable and idempotent by validating all local failure conditions first, tracking per-account revocation progress, and treating already-revoked tokens as completed during cleanup.</comment>
<file context>
@@ -123,14 +138,55 @@ export async function disconnectOAuthAccounts(params: DisconnectOAuthAccountsPar
+ const clientConfig = await decryptQuickBooksOAuthClientConfig(
+ targetAccount.encryptedOAuthClientConfig
+ )
+ await revokeQuickBooksToken(token, clientConfig, quickBooksDisconnectSignal)
+ } catch (error) {
+ throw new OAuthProviderRevocationError('QuickBooks', error)
</file context>
| description: | ||
| 'Bounded invoice allocations to apply. Each entry sets the amount applied to that invoice; invoices already applied on the payment and not listed here keep their current amounts', | ||
| }, | ||
| unapplyOmittedInvoices: { |
There was a problem hiding this comment.
P1: When unapplyOmittedInvoices is true without a nonempty allocation list, the update preserves the existing allocations instead of unapplying omitted invoices. Handle an empty list as Line: [] or reject the flag unless an allocation list is supplied, and keep the tool contract consistent with that behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/tools/quickbooks/update_customer_payment.ts, line 102:
<comment>When `unapplyOmittedInvoices` is true without a nonempty allocation list, the update preserves the existing allocations instead of unapplying omitted invoices. Handle an empty list as `Line: []` or reject the flag unless an allocation list is supplied, and keep the tool contract consistent with that behavior.</comment>
<file context>
@@ -0,0 +1,128 @@
+ description:
+ 'Bounded invoice allocations to apply. Each entry sets the amount applied to that invoice; invoices already applied on the payment and not listed here keep their current amounts',
+ },
+ unapplyOmittedInvoices: {
+ type: 'boolean',
+ required: false,
</file context>
| url: (params) => buildQuickBooksEntityUrl(params, 'refundreceipt').toString(), | ||
| method: 'POST', | ||
| headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'), | ||
| body: (params) => buildQuickBooksUpdateSalesDocumentBody(params), |
There was a problem hiding this comment.
P1: When lines is supplied, this tool cannot guarantee the documented replacement behavior because sparse QuickBooks line updates require native line IDs to update or remove existing lines. Use a read/full-update flow that preserves line IDs, or expose line IDs and implement the provider’s deletion semantics before sending this request.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/tools/quickbooks/update_refund_receipt.ts, line 125:
<comment>When `lines` is supplied, this tool cannot guarantee the documented replacement behavior because sparse QuickBooks line updates require native line IDs to update or remove existing lines. Use a read/full-update flow that preserves line IDs, or expose line IDs and implement the provider’s deletion semantics before sending this request.</comment>
<file context>
@@ -0,0 +1,138 @@
+ url: (params) => buildQuickBooksEntityUrl(params, 'refundreceipt').toString(),
+ method: 'POST',
+ headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'),
+ body: (params) => buildQuickBooksUpdateSalesDocumentBody(params),
+ retry: { enabled: false },
+ },
</file context>
| const sanitizedErrors = errors.flatMap((entry) => { | ||
| if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [] | ||
| const value = entry as Record<string, unknown> | ||
| const sanitized = Object.fromEntries( | ||
| QUICKBOOKS_FAULT_FIELDS.flatMap((key) => { | ||
| const field = typeof value[key] === 'string' ? value[key].trim() : '' | ||
| return field ? [[key, truncate(field, QUICKBOOKS_MAX_FAULT_FIELD_CHARS, '')]] : [] | ||
| }) | ||
| ) | ||
| return Object.keys(sanitized).length > 0 ? [sanitized] : [] | ||
| }) | ||
| if (sanitizedErrors.length === 0) return null | ||
|
|
||
| const priorOmitted = (fault as Record<string, unknown>).omittedErrorCount | ||
| const carriedOmitted = typeof priorOmitted === 'number' && priorOmitted > 0 ? priorOmitted : 0 | ||
| const omittedErrorCount = | ||
| carriedOmitted + Math.max(0, sanitizedErrors.length - QUICKBOOKS_MAX_FAULT_ERRORS) | ||
|
|
||
| return { | ||
| Fault: { | ||
| Error: sanitizedErrors.slice(0, QUICKBOOKS_MAX_FAULT_ERRORS), | ||
| ...(omittedErrorCount > 0 ? { omittedErrorCount } : {}), | ||
| }, | ||
| } |
There was a problem hiding this comment.
P2: When Fault.Error contains many entries, flatMap materializes every sanitized entry before slice keeps five. Append only the first five entries while counting later valid entries as omitted so a large provider fault cannot allocate an intermediate array proportional to the full response.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/tools/quickbooks/fault.ts, line 52:
<comment>When `Fault.Error` contains many entries, `flatMap` materializes every sanitized entry before `slice` keeps five. Append only the first five entries while counting later valid entries as omitted so a large provider fault cannot allocate an intermediate array proportional to the full response.</comment>
<file context>
@@ -0,0 +1,117 @@
+ const errors = (fault as Record<string, unknown>).Error
+ if (!Array.isArray(errors)) return null
+
+ const sanitizedErrors = errors.flatMap((entry) => {
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return []
+ const value = entry as Record<string, unknown>
</file context>
| const sanitizedErrors = errors.flatMap((entry) => { | |
| if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [] | |
| const value = entry as Record<string, unknown> | |
| const sanitized = Object.fromEntries( | |
| QUICKBOOKS_FAULT_FIELDS.flatMap((key) => { | |
| const field = typeof value[key] === 'string' ? value[key].trim() : '' | |
| return field ? [[key, truncate(field, QUICKBOOKS_MAX_FAULT_FIELD_CHARS, '')]] : [] | |
| }) | |
| ) | |
| return Object.keys(sanitized).length > 0 ? [sanitized] : [] | |
| }) | |
| if (sanitizedErrors.length === 0) return null | |
| const priorOmitted = (fault as Record<string, unknown>).omittedErrorCount | |
| const carriedOmitted = typeof priorOmitted === 'number' && priorOmitted > 0 ? priorOmitted : 0 | |
| const omittedErrorCount = | |
| carriedOmitted + Math.max(0, sanitizedErrors.length - QUICKBOOKS_MAX_FAULT_ERRORS) | |
| return { | |
| Fault: { | |
| Error: sanitizedErrors.slice(0, QUICKBOOKS_MAX_FAULT_ERRORS), | |
| ...(omittedErrorCount > 0 ? { omittedErrorCount } : {}), | |
| }, | |
| } | |
| const sanitizedErrors: Array<Record<string, string>> = [] | |
| let omittedErrorCount = 0 | |
| for (const entry of errors) { | |
| if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue | |
| const value = entry as Record<string, unknown> | |
| const sanitized = Object.fromEntries( | |
| QUICKBOOKS_FAULT_FIELDS.flatMap((key) => { | |
| const field = typeof value[key] === 'string' ? value[key].trim() : '' | |
| return field ? [[key, truncate(field, QUICKBOOKS_MAX_FAULT_FIELD_CHARS, '')]] : [] | |
| }) | |
| ) | |
| if (Object.keys(sanitized).length === 0) continue | |
| if (sanitizedErrors.length < QUICKBOOKS_MAX_FAULT_ERRORS) { | |
| sanitizedErrors.push(sanitized) | |
| } else { | |
| omittedErrorCount += 1 | |
| } | |
| } | |
| if (sanitizedErrors.length === 0) return null | |
| const priorOmitted = (fault as Record<string, unknown>).omittedErrorCount | |
| const carriedOmitted = typeof priorOmitted === 'number' && priorOmitted > 0 ? priorOmitted : 0 | |
| omittedErrorCount += carriedOmitted | |
| return { | |
| Fault: { | |
| Error: sanitizedErrors, | |
| ...(omittedErrorCount > 0 ? { omittedErrorCount } : {}), | |
| }, | |
| } |
| export const internalCredentialErrorPolicy = extendInternalErrorPolicy( | ||
| internalOrchestrationErrorPolicy, | ||
| (error) => { | ||
| if (error instanceof OAuthProviderRevocationError) { |
There was a problem hiding this comment.
P2: When a disconnect request exceeds the account or credential bound, this branch returns 503 with Retry-After for a request that cannot succeed by retrying. Use a distinct bounded-input error and reserve this response for transient provider revocation failures.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/credentials/api/route-policies.ts, line 30:
<comment>When a disconnect request exceeds the account or credential bound, this branch returns `503` with `Retry-After` for a request that cannot succeed by retrying. Use a distinct bounded-input error and reserve this response for transient provider revocation failures.</comment>
<file context>
@@ -26,6 +27,13 @@ export const credentialValidationParseOptions = {
export const internalCredentialErrorPolicy = extendInternalErrorPolicy(
internalOrchestrationErrorPolicy,
(error) => {
+ if (error instanceof OAuthProviderRevocationError) {
+ return internalErrorResponse(
+ 503,
</file context>
| displayName: z.string().min(1), | ||
| description: z.string().trim().max(500).optional(), | ||
| credentialId: z.string().min(1).optional(), | ||
| oauthClientConfig: quickBooksOAuthClientConfigSchema.optional(), |
There was a problem hiding this comment.
P2: When a caller omits this field for QuickBooks or sends it for another provider, the request passes validation and then returns 500 from createConnectDraft instead of a 400 validation error. Make this body schema provider-aware: require the config for QuickBooks and reject it for other providers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/api/contracts/credentials.ts, line 335:
<comment>When a caller omits this field for QuickBooks or sends it for another provider, the request passes validation and then returns 500 from `createConnectDraft` instead of a 400 validation error. Make this body schema provider-aware: require the config for QuickBooks and reject it for other providers.</comment>
<file context>
@@ -302,12 +302,37 @@ export const workspaceCredentialMemberSchema = z.object({
displayName: z.string().min(1),
description: z.string().trim().max(500).optional(),
credentialId: z.string().min(1).optional(),
+ oauthClientConfig: quickBooksOAuthClientConfigSchema.optional(),
})
</file context>
| examples: ['QuickBooks Online Accounting API'], | ||
| extract: (errorInfo) => { | ||
| const status = errorInfo?.status | ||
| const fault = sanitizeQuickBooksFaultData(errorInfo?.data) |
There was a problem hiding this comment.
P2: When QuickBooks returns a non-2xx query failure with QueryResponse.Fault, this extractor ignores the nested fault and reports only the HTTP status. Inspect QueryResponse as a fallback after checking the top-level fault so invalid-query errors retain their actionable detail.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/tools/error-extractors.ts, line 425:
<comment>When QuickBooks returns a non-2xx query failure with `QueryResponse.Fault`, this extractor ignores the nested fault and reports only the HTTP status. Inspect `QueryResponse` as a fallback after checking the top-level fault so invalid-query errors retain their actionable detail.</comment>
<file context>
@@ -415,6 +416,30 @@ const ERROR_EXTRACTORS: ErrorExtractorConfig[] = [
+ examples: ['QuickBooks Online Accounting API'],
+ extract: (errorInfo) => {
+ const status = errorInfo?.status
+ const fault = sanitizeQuickBooksFaultData(errorInfo?.data)
+ if (!fault) return null
+
</file context>
| const fault = sanitizeQuickBooksFaultData(errorInfo?.data) | |
| const data = errorInfo?.data | |
| const fault = | |
| sanitizeQuickBooksFaultData(data) ?? | |
| (data && typeof data === 'object' && !Array.isArray(data) | |
| ? sanitizeQuickBooksFaultData((data as Record<string, unknown>).QueryResponse) | |
| : null) |
| ? () => setReconnectOpen(true) | ||
| : handleReconnectOAuth | ||
| : credential.providerId === 'quickbooks' | ||
| ? () => setReconnectOpen(true) |
There was a problem hiding this comment.
P2: When QuickBooks reauthorization is launched from this page, no return context identifies the new OAuth attempt. The callback therefore provides no reliable success/error toast or update event, and a stale context from another flow can redirect the user to the wrong page. Write or replace the integrations return context when this reauthorization is actually submitted, and clear/handle it on failure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx, line 216:
<comment>When QuickBooks reauthorization is launched from this page, no return context identifies the new OAuth attempt. The callback therefore provides no reliable success/error toast or update event, and a stale context from another flow can redirect the user to the wrong page. Write or replace the integrations return context when this reauthorization is actually submitted, and clear/handle it on failure.</comment>
<file context>
@@ -211,7 +212,9 @@ export function ConnectedCredentialDetail({
? () => setReconnectOpen(true)
- : handleReconnectOAuth
+ : credential.providerId === 'quickbooks'
+ ? () => setReconnectOpen(true)
+ : handleReconnectOAuth
}
</file context>
…tegration # Conflicts: # apps/sim/tools/generated/tool-metadata.ts # apps/sim/tools/generated/tool-outputs.ts
Summary
Type of Change
Testing
Checklist