Skip to content

refactor(core): share the oauth state failure union and fold callback narrowing - #4624

Merged
tyler-dane merged 1 commit into
mainfrom
claude/kind-dirac-pbjgs8
Oct 20, 2026
Merged

tyler-dane merged 1 commit into
mainfrom
claude/kind-dirac-pbjgs8

Conversation

@tyler-dane

Copy link
Copy Markdown
Contributor

What and why

Technical-debt cleanup in the OAuth callback analytics added by #4588 and partly reworked by #4617. No behavior change for any outcome the mapper reports today.

A duplicated type that could only drift. #4588 added oauthCallbackFailureReason to packages/core with a hand-copied "malformed" | "invalidSignature" | "expired" | "stateMismatch" union mirroring OAuthStateFailure in packages/sync/src/oauth/oauth-state.ts. Core cannot import sync, so nothing tied the two together: adding a fifth state-verification failure in sync would leave the analytics mapper unable to accept it, and widening the inline copy by hand would let the two definitions disagree silently. This moves the union into connection.contracts beside the other consent-flow contracts, as a Zod enum like its ConnectionStateReason sibling, and has both the verifier and the analytics mapper read it from there. One definition, correct dependency direction.

state and code were each narrowed from unknown twice, with different idioms (typeof x === "string" ? x : null in the declined branch, typeof x === "string" && x.length > 0 in the error branch). Each callback value is now narrowed once up front through asNonEmptyString. This is behavior-preserving: oauthFailedReasonFromCallbackQuery already tests state and code falsily, so "" took the absent path under both idioms. The inconsistency was the kind that eventually becomes a bug, not one today.

isTokenExchangeFailure and providerErrorFromExchange repeated the same object guard and the same cast, and were only ever called together on the same input. They fold into one tokenExchangeFailure, whose result now routes through withProviderError like every other branch in the function. That also drops the one inconsistency with real effect: the token-exchange branch built its result literal by hand, so a ProviderAuthError carrying no reason sent an explicit provider_error: undefined to PostHog instead of omitting the property.

Added regression tests for the folded paths: a reasonless exchange failure, a non-ProviderAuthError on the exchange path, blank state/code, and the invalidSignature/expired mapping.

Verify

bun run verify --strict
Selected packages: core, sync
Checks run: test:core, test:sync:fast, type-check, lint, knip
Checks skipped: (none)
VERDICT: PASS

Generated by Claude Code

… narrowing

#4588 added `oauthCallbackFailureReason` in core with a hand-copied
`"malformed" | "invalidSignature" | "expired" | "stateMismatch"` union
mirroring `OAuthStateFailure` in sync. Core cannot import sync, so the two
could only drift: adding a fifth verification failure would leave the
analytics mapper silently unable to accept it.

Move the union to `connection.contracts` next to the other consent-flow
contracts, as a Zod enum like its `ConnectionStateReason` sibling, and have
both the verifier and the analytics mapper read it from there.

Also tidy two smells in the same mapper:

- `state` and `code` were each narrowed from `unknown` twice, with different
  idioms (`?? null` in one branch, `.length > 0` in another). Narrow each
  callback value once up front via `asNonEmptyString`. Behavior is unchanged:
  `oauthFailedReasonFromCallbackQuery` already tests these falsily, so a
  blank value took the absent path either way.
- `isTokenExchangeFailure` and `providerErrorFromExchange` repeated the same
  object guard and cast and were only ever called together. Fold them into
  one `tokenExchangeFailure` and route the result through `withProviderError`
  like every other branch, so a reasonless failure omits `provider_error`
  rather than sending an explicit `undefined` to PostHog.

bun run verify --strict
VERDICT: PASS

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GSTPkN9FL7t93wAj8442Y3
@tyler-dane tyler-dane added the agent-automerge Agent-loop finished; merge-guard may squash-merge label Oct 20, 2026 — with Claude
@tyler-dane
tyler-dane enabled auto-merge October 20, 2026 10:07
@github-actions

Copy link
Copy Markdown
Contributor

Boot bundle: 509.8 KB gzip (+0.0 KB vs main)

@tyler-dane
tyler-dane added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit fce63a2 Oct 20, 2026
30 checks passed
@tyler-dane
tyler-dane deleted the claude/kind-dirac-pbjgs8 branch October 20, 2026 10:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-automerge Agent-loop finished; merge-guard may squash-merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants