Skip to content

fix(auth)!: return VerifyOTPResponse from verifyOTP instead of AuthResponse - #1088

Merged
grdsdev merged 15 commits into
mainfrom
fix/sdk-1022-verifyotp-email-change-parity
Aug 13, 2026
Merged

grdsdev merged 15 commits into
mainfrom
fix/sdk-1022-verifyotp-email-change-parity

Conversation

@grdsdev

@grdsdev grdsdev commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

verifyOTP(type: .emailChange) threw an uncaught DecodingError when GoTrue's /verify endpoint returned { msg, code } — the body sent for the first of the two confirmations required by a secure email change. AuthResponse could only decode a Session or a User, so any other shape crashed instead of returning a usable result.

An earlier version of this fix gave AuthResponse itself a .none case and made user optional. But AuthResponse is shared by signUp and the Passkey methods, neither of which can ever produce that shape — every caller of those unrelated methods would have had to handle a case only verifyOTP can trigger. Verified against the supabase/auth source directly: only verifyOTP(type: .emailChange)'s first confirmation can ever produce this body. signUp can return either a session or a plain user (confirmation pending); the other verifyOTP types and the Passkey completion endpoint always return a session.

Changes

  • Sources/Auth/Types.swift: verifyOTP and its overloads now return a new VerifyOTPResponse type instead of AuthResponse. It has exactly the two shapes GoTrue's /verify can produce for it: .session(Session) and .emailChangeConfirmationPending(EmailChangeConfirmation), the latter carrying the message/code GoTrue actually sends (decoded losslessly, instead of being discarded). AuthResponse itself is unchanged from its currently-released shape.
  • V3_MIGRATION.md: new root migration guide for this upcoming major version, covering every module. Documents this breaking change with before/after examples.
  • .claude/skills/writing-migration-guides/: new skill capturing the migration-guide format and convention (modeled on supabase-flutter's MIGRATION.md), since every breaking change now requires one (see AGENTS.md).
  • sdk-compliance.yaml: registered VerifyOTPResponse / EmailChangeConfirmation as supporting symbols under auth.sign_in.verify_otp.
  • Tests: unit coverage for VerifyOTPResponse decoding against mocked fixtures, plus a new integration test (verifyOTPForSecureEmailChange) that drives the real two-step confirmation flow via admin.generateLink, since the shared integration-test project runs with auth.email.enable_confirmations = false and can't reach this code path.
  • Tests/IntegrationTests/supabase-secure-email-change/: a second, minimal local Supabase project with just that one setting flipped, so the new integration test runs against real GoTrue behavior without affecting the rest of the suite.
  • .github/workflows/ci.yml / AGENTS.md: wired the isolated project into CI as its own step, and documented running it locally.

Breaking change

verifyOTP and its overloads (verifyOTP(email:token:type:...), verifyOTP(phone:token:type:...), verifyOTP(tokenHash:type:)) now return VerifyOTPResponse instead of AuthResponse. See V3_MIGRATION.md for the full migration steps.

Root cause

internal/api/verify.go in supabase/auth: GoTrue returns a bare {msg, code} body (not a Session/User shape) specifically for the first of the two confirmations required by a secure email change (mail.EmailChangeVerification, when SecureEmailChangeEnabled and EmailChangeConfirmStatus == zeroConfirmation). AuthResponse.init(from:) had no case for that shape and threw.

Test plan

  • Unit: Tests/AuthTests/AuthResponseTests.swiftVerifyOTPResponse decodes the {msg,code} shape into .emailChangeConfirmationPending; AuthResponse correctly throws on it rather than silently swallowing it
  • Unit: Tests/AuthTests/AuthClientTests.swiftverifyOTPForEmailChangeSingleConfirmation
  • Integration: Tests/IntegrationTests/AuthClientIntegrationTests.swiftverifyOTPForSecureEmailChange, exercises the real two-step confirmation end-to-end against a live GoTrue instance (including a real bug found along the way in admin/generate_link's response for email_change_new)
  • Full unit suite green (swift test --filter "AuthTests|SupabaseTests", 256 tests)
  • Integration Tests (Linux) CI job green
  • validate / Check public API against capability matrix CI job green
  • DocC build, spell-check, format check all clean

Linear

Closes SDK-1022

@grdsdev
grdsdev requested a review from a team as a code owner July 7, 2026 08:50
grdsdev added a commit that referenced this pull request Jul 7, 2026
…ne doc

Addresses code review feedback on #1088 (SDK-1022): the import ordering
in AuthClientTests.swift had regressed to a non-swift-format-canonical
layout, and the `.none` doc didn't make clear it's a catch-all for any
unrecognized response shape, not just the email-change confirmation case.
@coveralls

coveralls commented Jul 7, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 31686113079

Coverage increased (+0.2%) to 84.829%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 17 of 17 lines across 2 files are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 10474
Covered Lines: 8885
Line Coverage: 84.83%
Coverage Strength: 1025189.14 hits per line

💛 - Coveralls

@grdsdev
grdsdev changed the base branch from main to chore/v3-beta-prerelease-config August 12, 2026 16:17
grdsdev added a commit that referenced this pull request Aug 12, 2026
…ne doc

Addresses code review feedback on #1088 (SDK-1022): the import ordering
in AuthClientTests.swift had regressed to a non-swift-format-canonical
layout, and the `.none` doc didn't make clear it's a catch-all for any
unrecognized response shape, not just the email-change confirmation case.
@grdsdev
grdsdev force-pushed the fix/sdk-1022-verifyotp-email-change-parity branch from 1a03e36 to 8df4718 Compare August 12, 2026 16:30
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 97caf233-067c-4bf4-baf3-55fc95f39943

📥 Commits

Reviewing files that changed from the base of the PR and between e934c2c and 3ec946f.

📒 Files selected for processing (1)
  • Tests/IntegrationTests/AuthClientIntegrationTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/IntegrationTests/AuthClientIntegrationTests.swift

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added email-change OTP responses with confirmation status, message, and code.
    • Added support for sign-up responses requiring email confirmation.
  • Breaking Changes

    • verifyOTP now returns VerifyOTPResponse instead of AuthResponse.
    • Update integrations to handle session results and pending email-change confirmations.
  • Documentation

    • Added V3 migration guidance for the verifyOTP response change.
    • Documented requirements for recording future breaking changes.

Walkthrough

The PR changes all verifyOTP overloads to return VerifyOTPResponse. The response supports session results and pending email-change confirmations with a message and code. Session persistence and signed-in events remain unchanged. Tests cover sign-up confirmation, email-change verification, response decoding, and secure email changes. The PR adds V3 migration guidance and documents the repository workflow for breaking-change migration guides.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthClient
  participant AuthAPI
  Client->>AuthClient: Call verifyOTP
  AuthClient->>AuthAPI: Send OTP verification request
  AuthAPI-->>AuthClient: Return session or confirmation payload
  AuthClient->>AuthClient: Decode VerifyOTPResponse
  AuthClient-->>Client: Return verification result
Loading

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Base automatically changed from chore/v3-beta-prerelease-config to main August 12, 2026 16:57
grdsdev added a commit that referenced this pull request Aug 12, 2026
…ne doc

Addresses code review feedback on #1088 (SDK-1022): the import ordering
in AuthClientTests.swift had regressed to a non-swift-format-canonical
layout, and the `.none` doc didn't make clear it's a catch-all for any
unrecognized response shape, not just the email-change confirmation case.
@grdsdev
grdsdev force-pushed the fix/sdk-1022-verifyotp-email-change-parity branch from 9af3aa6 to 600d4bb Compare August 12, 2026 17:03

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Tests/IntegrationTests/AuthClientIntegrationTests.swift`:
- Line 239: Update the comparison in the second-user sign-up test to use
`#require` on secondUserSession.user and obtain its non-optional id before
asserting it differs from user.id. Keep the existing distinct-ID assertion while
ensuring a missing second user fails the test rather than satisfying it.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 91798a58-0f1b-49c0-a525-c39dc5e797f9

📥 Commits

Reviewing files that changed from the base of the PR and between f6687c5 and 600d4bb.

📒 Files selected for processing (8)
  • .claude/skills/writing-migration-guides/SKILL.md
  • AGENTS.md
  • Sources/Auth/Types.swift
  • Tests/AuthTests/AuthClientTests.swift
  • Tests/AuthTests/AuthResponseTests.swift
  • Tests/AuthTests/Resources/email-change-single-confirmation.json
  • Tests/IntegrationTests/AuthClientIntegrationTests.swift
  • V3_MIGRATION.md

Comment thread Tests/IntegrationTests/AuthClientIntegrationTests.swift Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@V3_MIGRATION.md`:
- Around line 37-48: Update the verifyOTP migration example to switch over both
VerifyOTPResponse cases instead of accessing response.session unconditionally.
In the email-change confirmation-pending case, preserve and surface
EmailChangeConfirmation.message and code; retain the existing session user-email
handling for the successful verification case.
- Around line 40-48: Update the “Before” migration example to access
AuthResponse.user non-optionally as response.user.email, while preserving
optional chaining on VerifyOTPResponse.session in the “After” example.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 27900e93-8631-440b-949d-c781e7416d0b

📥 Commits

Reviewing files that changed from the base of the PR and between 600d4bb and 2893d8b.

📒 Files selected for processing (7)
  • Sources/Auth/AuthClient.swift
  • Sources/Auth/Types.swift
  • Tests/AuthTests/AuthClientTests.swift
  • Tests/AuthTests/AuthResponseTests.swift
  • Tests/AuthTests/Resources/signup-response.json
  • Tests/IntegrationTests/AuthClientIntegrationTests.swift
  • V3_MIGRATION.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/AuthTests/AuthClientTests.swift

Comment thread V3_MIGRATION.md
Comment thread V3_MIGRATION.md
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Capability matrix drift detected

The following capabilities are marked implemented in swift but have no registered symbols to verify:

  • auth.passkey.register_passkey (no symbols list — cannot confirm implementation exists)
  • auth.passkey.sign_in_with_passkey (no symbols list — cannot confirm implementation exists)
  • client.authentication_integration.third_party_auth (no symbols list — cannot confirm implementation exists)
  • client.authentication_integration.cross_client_token_sync (no symbols list — cannot confirm implementation exists)
  • client.authentication_integration.oauth_flow_type (no symbols list — cannot confirm implementation exists)
  • client.authentication_integration.session_url_detection (no symbols list — cannot confirm implementation exists)
  • client.session_management.custom_storage (no symbols list — cannot confirm implementation exists)
  • client.session_management.persist_session (no symbols list — cannot confirm implementation exists)
  • client.request_configuration.global_headers (no symbols list — cannot confirm implementation exists)
  • client.observability.trace_propagation (no symbols list — cannot confirm implementation exists)
  • database.query.select (no symbols list — cannot confirm implementation exists)
  • database.query.schema_selection (no symbols list — cannot confirm implementation exists)
  • database.query.rpc (no symbols list — cannot confirm implementation exists)
  • database.mutate.insert (no symbols list — cannot confirm implementation exists)
  • database.mutate.update (no symbols list — cannot confirm implementation exists)
  • database.mutate.upsert (no symbols list — cannot confirm implementation exists)
  • database.mutate.delete (no symbols list — cannot confirm implementation exists)
  • database.mutate.select_after_mutation (no symbols list — cannot confirm implementation exists)
  • database.using_filters.eq (no symbols list — cannot confirm implementation exists)
  • database.using_filters.neq (no symbols list — cannot confirm implementation exists)
  • database.using_filters.gt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.gte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.lt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.lte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.like (no symbols list — cannot confirm implementation exists)
  • database.using_filters.ilike (no symbols list — cannot confirm implementation exists)
  • database.using_filters.is (no symbols list — cannot confirm implementation exists)
  • database.using_filters.in (no symbols list — cannot confirm implementation exists)
  • database.using_filters.contains (no symbols list — cannot confirm implementation exists)
  • database.using_filters.contained_by (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_gt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_gte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_lt (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_lte (no symbols list — cannot confirm implementation exists)
  • database.using_filters.range_adjacent (no symbols list — cannot confirm implementation exists)
  • database.using_filters.overlaps (no symbols list — cannot confirm implementation exists)
  • database.using_filters.text_search (no symbols list — cannot confirm implementation exists)
  • database.using_filters.match (no symbols list — cannot confirm implementation exists)
  • database.using_filters.not (no symbols list — cannot confirm implementation exists)
  • database.using_filters.or (no symbols list — cannot confirm implementation exists)
  • database.using_filters.raw (no symbols list — cannot confirm implementation exists)
  • database.using_filters.regex (no symbols list — cannot confirm implementation exists)
  • database.using_filters.regex_icase (no symbols list — cannot confirm implementation exists)
  • database.using_filters.is_distinct (no symbols list — cannot confirm implementation exists)
  • database.using_filters.like_all (no symbols list — cannot confirm implementation exists)
  • database.using_filters.like_any (no symbols list — cannot confirm implementation exists)
  • database.using_filters.ilike_all (no symbols list — cannot confirm implementation exists)
  • database.using_filters.ilike_any (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.order (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.limit (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.range (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.single_row (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.strip_nulls (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.format_csv (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.format_geojson (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.max_affected_rows (no symbols list — cannot confirm implementation exists)
  • database.using_modifiers.request_cancellation (no symbols list — cannot confirm implementation exists)
  • database.configuration.auto_retry (no symbols list — cannot confirm implementation exists)
  • functions.invocation.invoke (no symbols list — cannot confirm implementation exists)
  • functions.invocation.set_auth_token (no symbols list — cannot confirm implementation exists)
  • functions.invocation.method_override (no symbols list — cannot confirm implementation exists)
  • functions.invocation.streaming_response (no symbols list — cannot confirm implementation exists)
  • functions.invocation.request_cancellation (no symbols list — cannot confirm implementation exists)
  • realtime.client.connect (no symbols list — cannot confirm implementation exists)
  • realtime.client.disconnect (no symbols list — cannot confirm implementation exists)
  • realtime.client.get_channels (no symbols list — cannot confirm implementation exists)
  • realtime.client.remove_channel (no symbols list — cannot confirm implementation exists)
  • realtime.client.remove_all_channels (no symbols list — cannot confirm implementation exists)
  • realtime.client.connection_state (no symbols list — cannot confirm implementation exists)
  • realtime.client.listen_heartbeats (no symbols list — cannot confirm implementation exists)
  • realtime.client.set_auth_token (no symbols list — cannot confirm implementation exists)
  • realtime.client.channel (no symbols list — cannot confirm implementation exists)
  • realtime.channel.subscribe (no symbols list — cannot confirm implementation exists)
  • realtime.channel.unsubscribe (no symbols list — cannot confirm implementation exists)
  • realtime.channel.broadcast (no symbols list — cannot confirm implementation exists)
  • realtime.channel.broadcast_http (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.postgres_changes (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.subscribe_presence (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.private_channel (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.broadcast_self (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.broadcast_ack (no symbols list — cannot confirm implementation exists)
  • realtime.subscriptions.broadcast_replay (no symbols list — cannot confirm implementation exists)
  • realtime.presence.track (no symbols list — cannot confirm implementation exists)
  • realtime.presence.untrack (no symbols list — cannot confirm implementation exists)
  • realtime.presence.presence_key (no symbols list — cannot confirm implementation exists)
  • realtime.configuration.custom_websocket_transport (no symbols list — cannot confirm implementation exists)
  • realtime.configuration.reconnect_backoff (no symbols list — cannot confirm implementation exists)
  • realtime.configuration.heartbeat_interval (no symbols list — cannot confirm implementation exists)
  • realtime.configuration.access_token_callback (no symbols list — cannot confirm implementation exists)
  • realtime.configuration.deferred_disconnect (no symbols list — cannot confirm implementation exists)
  • realtime.configuration.custom_logger (no symbols list — cannot confirm implementation exists)
  • realtime.configuration.binary_protocol (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.get_bucket (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.list_file_buckets (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.update_bucket (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.delete_file_bucket (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.empty_bucket (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.access_bucket (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.upload (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.download (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.move (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.copy (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.remove (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.create_signed_url (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.create_signed_urls (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.create_signed_upload_url (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.upload_with_signed_url (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.update_file (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.file_exists (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.file_info (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.copy_cross_bucket (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.move_cross_bucket (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.upload_with_metadata (no symbols list — cannot confirm implementation exists)
  • storage.file_buckets.url_cache_nonce (no symbols list — cannot confirm implementation exists)

These may have been renamed, removed, or never registered. Please update the capability matrix.
See: https://github.com/supabase/sdk/blob/main/docs/capability-matrix.md

grdsdev added 14 commits August 13, 2026 06:20
…r user

verifyOTP(type: .emailChange) previously threw a DecodingError when
GoTrue's /verify endpoint returned the `{ msg, code }` body sent for the
first of the two confirmations required by a secure email change,
since AuthResponse could only decode a Session or a User.

AuthResponse gains a `.none` case for this shape, and `user` is now
`User?` instead of `User` so `(session: nil, user: nil)` is
representable.

BREAKING CHANGE: `AuthResponse.user` is now `User?` instead of `User`.
Update call sites that access `response.user` directly to handle the
optional (e.g. `response.user?.email`).

Linear: SDK-1022
…ne doc

Addresses code review feedback on #1088 (SDK-1022): the import ordering
in AuthClientTests.swift had regressed to a non-swift-format-canonical
layout, and the `.none` doc didn't make clear it's a catch-all for any
unrecognized response shape, not just the email-change confirmation case.
The tests re-created while resolving the rebase conflict onto main still
used XCTest assertions and a duplicated import, left over from before
main's migration to Swift Testing.
…reaking changes

AuthResponse.user changed from User to User? in this release; document
the migration and encode the requirement in AGENTS.md so future
breaking changes ship with a guide.
Rename AuthResponse Migration Guide.md to Auth Migration Guide.md and
restructure it as a per-module doc with a section per breaking change,
so future Auth breaking changes get a new section instead of a new
file. Update the AGENTS.md convention to match.
The previous commit renamed the file but left the old single-change
title and heading levels; nest the AuthResponse change under its own
section so future Auth breaking changes append sections instead of
new files, matching the updated AGENTS.md convention.
… versioned naming

Replace the AGENTS.md migration-guide paragraph with a
writing-migration-guides skill (.claude/skills/), modeled on
supabase-flutter's MIGRATION.md: name the exact symbol that changed,
explain why, show before/after code, and call out whether a break is
a compile error or silent.

Rename Auth Migration Guide.md to Auth V3 Migration Guide.md to adopt
the <Module> V<N> Migration Guide.md convention, tying the file to the
major version it targets.
Replace the per-module docs/migrations/<Module> V<N> Migration Guide.md
convention with a single root V<N>_MIGRATION.md covering every module,
matching how release notes actually ship (one major-version bump at a
time, not one per module). Update the writing-migration-guides skill
to match.
The writing-migration-guides skill only covers how to write the entry
once you know one is needed; nothing stated that breaking changes
require one in the first place. State the requirement next to the
existing breaking-change commit convention.
…sponse

GoTrue's /verify endpoint returns a bare `{ msg, code }` body for the
first of the two confirmations required by a secure email change, a
shape AuthResponse can't represent since it only holds a Session or a
User. AuthResponse is shared with signUp and the Passkey methods,
neither of which can ever produce that shape, so growing it there
would have leaked a case only verifyOTP can trigger onto every caller.

Revert AuthResponse to its original Session/User-only shape (user is
non-optional again) and give verifyOTP its own VerifyOTPResponse type
with the two shapes it can actually return: .session and
.emailChangeConfirmationPending(EmailChangeConfirmation), the latter
carrying the message/code GoTrue actually sends instead of discarding
it.

Verified against the supabase/auth source: only
mail.EmailChangeVerification's first confirmation ever emits the bare
body; signUp, the other verifyOTP types, and the Passkey completion
endpoint always return a session or a plain user shape.

BREAKING CHANGE: verifyOTP and its overloads now return
VerifyOTPResponse instead of AuthResponse. See V3_MIGRATION.md.

Linear: SDK-1022
The "Before" line still showed AuthResponse.user as optional, stale
from an earlier iteration of this design; AuthResponse.user is
non-optional. The "After" line only accessed .session, never showing
the .emailChangeConfirmationPending case the whole change is about.
…e test

Two CI failures on this PR:

- "Check public API against capability matrix": VerifyOTPResponse and
  EmailChangeConfirmation were never registered. Add them as
  supporting_symbols under auth.sign_in.verify_otp.

- "Integration Tests (Linux)": verifyOTPForSecureEmailChange failed
  because GOTRUE_MAILER_AUTOCONFIRM=true in the shared integration
  test project skips GoTrue's whole single-confirmation branch in
  emailChangeVerify, so the first confirmation completes the change
  immediately instead of returning the pending shape. Every other
  integration test relies on that autoconfirm setting to avoid
  confirming emails, so flipping it project-wide isn't an option.

Add a second, minimal Supabase project
(Tests/IntegrationTests/supabase-secure-email-change) with only that
one setting flipped, and run this one test against it in its own CI
step, skipping it in the main run. Documented in AGENTS.md.
…enHash

Confirmed against the running local stack: GoTrue's admin/generate_link
always returns hashedToken computed from the request's current email
(crypto.GenerateTokenHash(params.Email, otp)), even for
type=email_change_new, where the token actually stored server-side
(EmailChangeTokenNew) is computed from the new email instead. The
hashedToken field is simply wrong for that one case; emailOTP is not
affected since GoTrue rehashes it against the email passed to
/verify. Switch to the email+token overload for that confirmation.
v14.1.0's binary XCFramework releases are unreachable from GitHub
right now (downloadError("Could not connect to the server.")),
failing package resolution on every xcodebuild (legacy) job. Bump
past it to 18.1.0, which resolves and downloads cleanly.
@grdsdev
grdsdev force-pushed the fix/sdk-1022-verifyotp-email-change-parity branch from 2cc6b43 to c4bcffb Compare August 13, 2026 09:20
@grdsdev grdsdev changed the title fix(auth)!: return AuthResponse.none when GoTrue returns no session or user fix(auth)!: return VerifyOTPResponse from verifyOTP instead of AuthResponse Aug 13, 2026
@grdsdev
grdsdev requested a review from spydon August 13, 2026 09:24
@grdsdev
grdsdev merged commit dd5e825 into main Aug 13, 2026
33 checks passed
@grdsdev
grdsdev deleted the fix/sdk-1022-verifyotp-email-change-parity branch August 13, 2026 09:44
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.

3 participants