Skip to content

feat(quickbooks): add credential-scoped OAuth tools and triggers - #6099

Open
BillLeoutsakosvl346 wants to merge 21 commits into
stagingfrom
feat/quickbooks-integration
Open

feat(quickbooks): add credential-scoped OAuth tools and triggers#6099
BillLeoutsakosvl346 wants to merge 21 commits into
stagingfrom
feat/quickbooks-integration

Conversation

@BillLeoutsakosvl346

@BillLeoutsakosvl346 BillLeoutsakosvl346 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add 47 validated QuickBooks Online accounting, report, email, PDF, and attachment tools.
  • Add all 29 documented QuickBooks CloudEvents triggers with signature verification, company routing, bounded ingress, and durable dispatch.
  • Keep QuickBooks in the standard OAuth credential flow while encrypting each user supplied Intuit Client ID, Client Secret, environment, and webhook verifier token.
  • Derive the realm ID from the Intuit callback and verify the selected company before binding the credential.

Type of Change

  • New feature

Testing

  • Full monorepo suite passed, including 3,003 Sim test files and 41,577 passed Sim tests.
  • Changed integration slice passed: 40 files and 829 tests.
  • Lint, type-check, all 45 repository audits, generated artifact checks, and migration safety against staging passed.
  • A live Intuit sandbox handshake still requires a real user-owned Intuit app; provider exchanges and webhook delivery are covered with provider-faithful contract tests.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
docs Ready Ready Preview Sep 4, 2026 10:35pm UTC

Request Review

@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Touches OAuth, token revocation, and live QuickBooks company data with many write, void, email, and file-transfer actions; misconfiguration or bugs could affect customer accounting records or leak credentials.

Overview
Adds QuickBooks Online as a documented integration with a circular brand icon, docs nav entry, and a large generated quickbooks.mdx reference covering company binding, sparse updates, pagination, reports, PDFs, and attachments.

OAuth and credentials are extended so one connected company is fixed at callback time: the auth route validates realmId, scopes Better Auth handling with withQuickBooksCallbackRealm, disconnect revokes Intuit tokens before local deletion, and the token API returns realmId only for QuickBooks credentials (with reconnect guidance on malformed account IDs). Optional QUICKBOOKS_* env vars configure sandbox vs production.

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-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a credential-scoped QuickBooks Online integration.

  • Adds authorization, callback, token refresh, revocation, and company binding using encrypted per-credential Intuit client configuration.
  • Adds QuickBooks workflow blocks, internal execution operations, and 47 accounting and document tools.
  • Adds credential persistence, migration artifacts, provider metadata, UI connection flows, generated contracts, documentation, and focused tests.

Confidence Score: 5/5

The PR appears safe to merge based on this follow-up review.

No blocking failure remains.

Important Files Changed

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
Loading

Reviews (4): Last reviewed commit: "feat(quickbooks): support credential-sco..." | Re-trigger Greptile

Comment thread apps/sim/app/api/auth/oauth/token/route.ts
Comment thread apps/sim/tools/quickbooks/fault.ts
Comment thread apps/sim/tools/index.ts
@BillLeoutsakosvl346

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

Comment thread apps/sim/lib/oauth/oauth.ts Outdated
Comment thread apps/sim/tools/quickbooks/get_company_info.ts
@BillLeoutsakosvl346

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

Comment thread apps/sim/lib/oauth/oauth.ts Outdated
@BillLeoutsakosvl346

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread apps/sim/lib/auth/auth.ts Outdated
@BillLeoutsakosvl346

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@BillLeoutsakosvl346

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@BillLeoutsakosvl346

Copy link
Copy Markdown
Contributor Author

@cursor review

Comment thread apps/sim/lib/auth/auth.ts Outdated
@BillLeoutsakosvl346

Copy link
Copy Markdown
Contributor Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@waleedlatif1
waleedlatif1 force-pushed the feat/quickbooks-integration branch from 80b3121 to 2405a19 Compare August 28, 2026 18:12
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

…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
@waleedlatif1 waleedlatif1 changed the title feat(quickbooks): add read-only procurement integration feat(quickbooks): add complete credential-scoped OAuth integration Sep 4, 2026
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 issues found across 151 files

Confidence score: 2/5

  • packages/db/schema.ts stores 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.ts bypasses the QuickBooks-specific draft flow on desktop, causing connections to fail, while connected-credential-detail.tsx lacks return context for reliable reauthorization feedback—route desktop through the QuickBooks draft flow and preserve callback context.
  • apps/sim/lib/credentials/oauth-accounts.ts revokes 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.ts and update_refund_receipt.ts cannot 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 inspect QueryResponse.Fault before 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>

Re-trigger cubic

Comment thread packages/db/schema.ts
@@ -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'),

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

return { success: true }
}

if (providerId === 'quickbooks') {

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

const clientConfig = await decryptQuickBooksOAuthClientConfig(
targetAccount.encryptedOAuthClientConfig
)
await revokeQuickBooksToken(token, clientConfig, quickBooksDisconnectSignal)

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

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: {

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

url: (params) => buildQuickBooksEntityUrl(params, 'refundreceipt').toString(),
method: 'POST',
headers: (params) => getQuickBooksToolHeaders(params.accessToken, 'application/json'),
body: (params) => buildQuickBooksUpdateSalesDocumentBody(params),

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

Comment on lines +52 to +75
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 } : {}),
},
}

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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 } : {}),
},
}
Fix with cubic

export const internalCredentialErrorPolicy = extendInternalErrorPolicy(
internalOrchestrationErrorPolicy,
(error) => {
if (error instanceof OAuthProviderRevocationError) {

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

displayName: z.string().min(1),
description: z.string().trim().max(500).optional(),
credentialId: z.string().min(1).optional(),
oauthClientConfig: quickBooksOAuthClientConfigSchema.optional(),

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

examples: ['QuickBooks Online Accounting API'],
extract: (errorInfo) => {
const status = errorInfo?.status
const fault = sanitizeQuickBooksFaultData(errorInfo?.data)

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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)
Fix with cubic

? () => setReconnectOpen(true)
: handleReconnectOAuth
: credential.providerId === 'quickbooks'
? () => setReconnectOpen(true)

@cubic-dev-ai cubic-dev-ai Bot Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

…tegration

# Conflicts:
#	apps/sim/tools/generated/tool-metadata.ts
#	apps/sim/tools/generated/tool-outputs.ts
@waleedlatif1 waleedlatif1 changed the title feat(quickbooks): add complete credential-scoped OAuth integration feat(quickbooks): add credential-scoped OAuth tools and triggers Sep 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants