Description
Fixes INT-1991 / Plain ticket T-10531 (myagiea.com).
After Pipedrive OAuth completes successfully, customers passing callback_url to connected_accounts.initiate(...) were being redirected to Pipedrive's app-settings page (https://<tenant>.pipedrive.com/settings/marketplace/app/<clientId>/app-settings) instead of their own URL. The customer's URL was silently dropped.
Root cause
resolvePipedriveCallbackRedirectUrl (added in #8482) returned callbackRedirectUrl ?? userRedirectUri. Because getPipedriveConnectionDetails builds a non-empty callbackRedirectUrl from api_domain on every successful Pipedrive token response, the ?? operator always took the left branch and the customer's userRedirectUri (carrying their SDK callback_url) never won.
The function is used by two flows that look identical at this code location:
| Flow | userRedirectUri shape | Desired winner |
|---|
| Marketplace install (#8482) | A Pipedrive app-settings URL (validated by install_session.ts:42) | callbackRedirectUrl — token-derived tenant, for cross-tenant correctness |
Customer-app (SDK callback_url) | Customer's URL (anything, e.g. https://myapp.com/done) | userRedirectUri — that's the SDK contract |
The author's tests only modeled the marketplace flow (both inputs *.pipedrive.com), so the customer-app regression slipped through review.
The fix
Discriminate by URL shape using isPipedriveAppSettingsUrl — the same validator the marketplace flow already relies on for session.return_url:
userRedirectUri IS a Pipedrive app-settings URL → marketplace flow → keep prior behavior.
userRedirectUri is anything else → customer-app flow → honor it.
This preserves the cross-tenant marketplace correctness encoded by the existing test (both inputs are app-settings URLs, so the first branch fires) while fixing the customer-app regression.
Considered alternatives
- Flip the priority (
userRedirectUri ?? callbackRedirectUrl) — breaks the marketplace cross-tenant case where install starts on tenant-A but user authorizes tenant-B; would send them to tenant-A's app-settings page instead of the one they actually authorized.
- Branch on
toolkit_slug === 'pipedrive' higher up — doesn't discriminate, both flows are Pipedrive.
isPipedriveAppSettingsUrl is the right discriminator because URL shape is the actual semantic difference between the two flows.
How did I test this PR
- Unit tests: added 3 new cases in
pipedrive.test.ts:
- Customer-app flow with non-Pipedrive
userRedirectUri → expects customer URL (the T-10531 regression test).
- Customer-app flow with
callbackRedirectUrl only (no customer URL) → falls back to callback-derived.
- Both inputs missing → returns
undefined.
- Renamed the two existing tests with a
marketplace flow: prefix so the two flows are clearly distinguished. Both keep passing without behavior change.
- Type-check: clean on the modified files (
pnpm tsc --noEmit shows no Pipedrive-related errors).
- Lint: clean (
pnpm lint on both files reports 0 warnings, 0 errors).
- End-to-end manual repro of the broken behavior was confirmed by @suraj before this fix —
connected_accounts.initiate(..., callback_url="https://google.com") redirected to the Pipedrive tenant app-settings page. After this fix, the customer URL is honored.
Out of scope (separate follow-ups)
- The
/marketplace/oauth/authorize route (vs /oauth/authorize) and the "App already installed → Continue" Pipedrive UX customers see during re-auth. These are Pipedrive-side consequences of using a Public/Marketplace app for customer-app flows. Functionally non-blocking (users click through), but worth addressing structurally by separating Public-app (marketplace install) from Private-app (customer-app) credentials. Will file as a separate ticket.
- The broader structural pattern (slug-gated branches inside shared OAuth code with precedence rules scattered across
handleOAuthCallback.ts:354, pipedrive.ts:128, and handleOAuthCallback.ts:695-700) that allowed this class of bug. A centralized precedence resolver + cross-toolkit invariant test would prevent similar bugs in other toolkits. Separate ticket.
🤖 Generated with Claude Code