Zendesk Sunshine OAuth Hardening Implementation Plan
Execution mode: autonomous, test-driven, surgical changes only.Goal: Make Zendesk Sunshine Conversations Marketplace OAuth work for a brand-new customer, preserve the existing API-key flow for legacy customers, allow a legacy customer to opt into OAuth and safely fall back after disconnect, and harden both Zendesk OAuth callbacks before PR #2087 is merged. Compatibility contract: Legacy settings remain valid and keep using Basic authentication unless an OAuth token exists. OAuth never overwrites legacy API keys or the legacy webhook id. A fresh Marketplace install creates the minimum Sunshine settings row with empty credential placeholders and its own webhook record. Disconnect removes only OAuth-owned state; it removes the placeholder settings row only when no legacy credentials exist. Architecture: Use a discriminated Sunshine auth type (
legacy API key or oauth bearer) and attach Zendesk Marketplace headers only to OAuth requests. The OAuth callback consumes a one-time, initiating-user-bound state record, exchanges the code using Zendesk’s documented JSON request, resolves token metadata, provisions or reuses the integration-scoped me webhook, and writes the token plus settings atomically. Webhook acceptance selects the OAuth webhook while OAuth is active and automatically falls back to the stored legacy webhook when OAuth is disconnected. Marketplace routing recognizes the OAuth client id in activeSwitchboardIntegration.integrationType and continues recognizing legacy switchboard ids.
Testing: Add database-backed route/service tests for one-time state, initiating-session binding, encrypted token persistence, OAuth-only settings, hybrid preservation, and disconnect fallback. Add HTTP-client contract tests around a real local HTTP server (no SDK mocks/spies/stubs) to prove token-exchange JSON and Marketplace headers. Keep existing live Zendesk Ticketing OAuth E2E coverage and add a live Sunshine Marketplace smoke test only when the required Marketplace credentials are available in the test environment.
Task 1: Reconcile current main
- Merge current
origin/maininto the feature branch. - Resolve the Zendesk SDK/service/handoff one-function-per-file migrations in favor of main’s module layout.
- Port only the OAuth-aware credential resolution into the replacement modules.
- Run typecheck before behavior changes.
Task 2: Harden OAuth state for Ticketing and Sunshine
Files:backend/src/zendesk-integration-v2/zendesk-ticketing-oauth.service.tsbackend/src/zendesk-integration-v2/dto/zendesk-ticketing-oauth.dto.tsbackend/src/zendesk-integration-v2/zendesk-v2.controller.tsbackend/src/zendesk-integration/sunshine/sunshine-oauth.service.tsbackend/src/zendesk-integration/sunshine/sunshine-oauth.dto.tsbackend/src/zendesk-integration/sunshine/sunshine-oauth.controller.ts- focused route/service specs beside those modules
- Write failing tests showing a callback cannot be reused and cannot be completed without the initiating dashboard session/user.
- Store a short-lived state nonce keyed to org and user when
/startis called. - Require dashboard authentication on callback, verify the callback user/session and current permission on the state-bound org, and atomically consume the state nonce. Do not depend on a selected-org header surviving the cross-site redirect.
- Keep the existing signed state and Ticketing PKCE protection.
Task 3: Model OAuth-only Sunshine settings without weakening the legacy schema
Files:- the unshipped Sunshine OAuth migration in this PR
backend/src/db/opencx.ts(regenerated)backend/src/zendesk-integration/sunshine/sunshine-api.tsbackend/src/zendesk-integration/sunshine/sunshine-client.tsbackend/src/zendesk-integration/sunshine/sunshine.service.ts
- Write failing DB-backed tests for a fresh OAuth-only row and a legacy row that remains byte-for-byte credential-compatible.
- Keep
api_key_idandapi_key_secretstructurally required and use empty strings only for an OAuth-only placeholder row; never overwrite a real credential. - Introduce a strict discriminated auth union so an API call always has exactly one complete auth mode.
- Make
getSettingsthrow a reconnect error for an installed-but-unusable OAuth-only connection instead of falling through to Basic with empty values.
Task 4: Implement Zendesk Marketplace OAuth exactly
Files:backend/src/zendesk-integration/sunshine/sunshine-oauth.service.tsbackend/src/zendesk-integration/sunshine/sunshine-api.tsbackend/src/zendesk-integration/sunshine/sunshine-client.ts- Sunshine OAuth specs
- Write failing HTTP contract tests for JSON token exchange and the three Marketplace headers.
- Exchange the authorization code with
application/jsonand required Marketplace headers. - Add Marketplace headers to every bearer-authenticated Sunshine API request.
- List
/integrations/me/webhooks, reuse the exact OpenCX target if present, or create one withconversation:message,conversation:create, andswitchboard:releaseControltriggers. - Fail the callback before persistence when token metadata or webhook provisioning fails.
Task 5: Persist installs atomically and protect secrets
Files:- both Zendesk OAuth repositories and services
- the unshipped Sunshine OAuth migration for Sunshine OAuth webhook ownership
- DB-backed OAuth specs
- Write failing tests showing raw token columns do not contain plaintext and callback DB writes roll back together.
- Encrypt access/refresh tokens at repository write boundaries and decrypt at read boundaries.
- Store the Marketplace webhook id on the OAuth token row, not the legacy Sunshine settings row.
- Upsert the OAuth token and minimum settings row in one DB transaction after remote provisioning succeeds.
- Remove silent Redis error swallowing; log best-effort cleanup failures.
Task 6: Preserve legacy behavior and enable opt-in/fallback
Files:backend/src/zendesk-integration/zendesk.controller.ts- Sunshine OAuth/service/controller specs
dashboard/apps/dashboard/app/(authenticated)/(dashboard)/(settings)/_components/integrations/ZendeskV2Integration.tsx
- Write failing tests for legacy-only, OAuth-only, hybrid OAuth-active, and hybrid-disconnected behavior.
- Accept only the OAuth-owned webhook while OAuth is active; accept the preserved legacy webhook after disconnect.
- Treat
activeSwitchboardIntegration.integrationType === configured OAuth client idas assigned to OpenCX, while preserving legacy switchboard-id/name matching. - On OAuth disconnect, keep a real legacy settings row and delete only an OAuth-only placeholder row.
- Enable the Messaging OAuth button without prior legacy setup and explain the required Zendesk Admin Center channel assignment.
Task 7: Verification, review, and publication
- Regenerate DB/OpenAPI/dashboard schema artifacts with project commands.
- Run focused RED/GREEN specs, relevant existing Sunshine/Zendesk specs, migration up/down checks,
pnpm tsgo, and targeted lint/format checks using Node 24. - Review the full PR diff for correctness, security, performance, project conventions, and real E2E test quality; fix every actionable issue.
- Commit with Conventional Commits, push the existing branch without force, reply to and resolve addressed review threads, and monitor GitHub checks.