auth0-swift-major-migration

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Auth0.swift v3 Migration

Auth0.swift v3 迁移指南

Migrates an existing Auth0.swift v2 integration to v3. Every code change is gated on a search that confirms the project actually calls the affected API — if the project never uses
CredentialsManager
, no
CredentialsManager
code is touched. Changes follow the project's existing architecture and Apple platform conventions.
将现有的Auth0.swift v2集成迁移至v3版本。所有代码变更都会基于搜索结果确认项目是否实际调用了受影响的API——如果项目从未使用
CredentialsManager
,则不会修改任何与
CredentialsManager
相关的代码。变更将遵循项目现有的架构和Apple平台规范。

When NOT to Use

不适用场景

  • 全新Auth0集成(无现有Auth0.swift):使用auth0-swift
  • 小版本/补丁更新(例如2.17 → 2.18):运行
    pod update Auth0
    或更新SPM即可,无需迁移
  • Android应用:使用auth0-android
  • React Native / Expo:使用auth0-react-nativeauth0-expo

Prerequisites

前置条件

  • Existing Auth0.swift v2 integration
  • Xcode installed; project builds cleanly on the current version
  • Project under git version control with a clean working tree

  • 已集成Auth0.swift v2版本
  • 已安装Xcode;当前版本下项目可正常构建
  • 项目处于git版本控制下,且工作区干净

Migration Workflow

迁移流程

Agent instruction: Execute every step in order. The goal is a green build with the smallest correct changeset. Each code-change step is gated by the Step 4 file-reading audit — if the API was not found in the project's source files, skip the entire step for that area. Never add code the project doesn't already call.

Agent指令: 按顺序执行每一步。目标是通过最少的正确变更实现构建成功。每个代码变更步骤都受第4步的文件读取审计限制——如果在项目源码中未找到对应API,则跳过该步骤的所有内容。绝对不要添加项目未调用的代码。

Step 1 — Pre-flight & Safety Backup

步骤1 — 预检与安全备份

bash
undefined
bash
undefined

1a. Verify clean working tree — stop if there are uncommitted changes

1a. 验证工作区是否干净——如果存在未提交的变更则停止操作

git status --porcelain

If the output is non-empty, ask the user:
> *"You have uncommitted changes. Should I stash them before proceeding (`git stash`), or would you like to commit first?"*

```bash
git status --porcelain

如果输出非空,询问用户:
> *“您有未提交的变更。我应该先暂存它们(`git stash`),还是您先提交这些变更?”*

```bash

1b. Create a safety branch the user can reset to at any time

1b. 创建一个安全分支,用户可随时重置到该分支

git checkout -b auth0-v3-migration-backup git checkout -

```bash
git checkout -b auth0-v3-migration-backup git checkout -

```bash

1c. Pick an available simulator, then confirm the project builds before touching anything

1c. 选择一个可用的模拟器,然后确认项目在未做任何修改前可正常构建

SIM=$(xcrun simctl list devices available -j
| python3 -c "import sys,json; d=json.load(sys.stdin);
phones=[dev for devs in d['devices'].values() for dev in devs
if 'iPhone' in dev.get('name','') and dev.get('isAvailable')];
print(phones[0]['name'] if phones else 'iPhone 16')") xcodebuild build
-scheme <SCHEME>
-destination "platform=iOS Simulator,name=${SIM}"
2>&1 | tail -5

If the build fails, stop. Ask the user to fix the existing issues first.

---
SIM=$(xcrun simctl list devices available -j
| python3 -c "import sys,json; d=json.load(sys.stdin);
phones=[dev for devs in d['devices'].values() for dev in devs
if 'iPhone' in dev.get('name','') and dev.get('isAvailable')];
print(phones[0]['name'] if phones else 'iPhone 16')") xcodebuild build
-scheme <SCHEME>
-destination "platform=iOS Simulator,name=${SIM}"
2>&1 | tail -5

如果构建失败,停止操作。请用户先修复现有问题。

---

Step 2 — Detect Current & Target Versions

步骤2 — 检测当前版本与目标版本

Detect the current Auth0.swift version from the project's dependency files:
bash
undefined
从项目的依赖文件中检测当前Auth0.swift版本:
bash
undefined

Check Package.resolved first (most reliable)

优先检查Package.resolved(最可靠)

find . -name "Package.resolved" | xargs grep -A3 '"auth0/Auth0.swift"|Auth0.swift"' 2>/dev/null | grep '"version"'
find . -name "Package.resolved" | xargs grep -A3 '"auth0/Auth0.swift"|Auth0.swift"' 2>/dev/null | grep '"version"'

Fallback: Podfile.lock

备选:Podfile.lock

grep "^ - Auth0 " Podfile.lock 2>/dev/null
grep "^ - Auth0 " Podfile.lock 2>/dev/null

Fallback: Cartfile.resolved

备选:Cartfile.resolved

grep "auth0/Auth0.swift" Cartfile.resolved 2>/dev/null
grep "auth0/Auth0.swift" Cartfile.resolved 2>/dev/null

Fallback: Package.swift

备选:Package.swift

grep -A2 'auth0/Auth0.swift' Package.swift 2>/dev/null

**Resolve the target version.** There are two paths:

**Path A — the user passed a target version argument (`$ARGUMENTS`):**

Validate it against the published releases before using it. It must pass **all three** checks:

```bash
grep -A2 'auth0/Auth0.swift' Package.swift 2>/dev/null

**确定目标版本**。有两种路径:

**路径A — 用户传入了目标版本参数(`$ARGUMENTS`):**

在使用前先验证其是否符合已发布的版本。必须通过以下**全部三项**检查:

```bash

List all published Auth0.swift v3 release tags

列出所有已发布的Auth0.swift v3版本标签

curl -s https://api.github.com/repos/auth0/Auth0.swift/releases | python3 -c " import sys, json releases = json.load(sys.stdin) v3 = [r for r in releases if r['tag_name'].startswith('3') and not r['draft']] for r in v3: print(r['tag_name']) "

1. **Exists** — the requested tag appears in the published release list above.
2. **Correct major** — the tag is within the **v3** major line (starts with `3`). A `2.x` or any other major is not valid; reject it.
3. **Not a downgrade** — the tag is newer than the version detected in the project.

> **On any check failing, STOP and ask the user.** Do not silently fall back. For example:
> - *"`3.9.9` isn't a published Auth0.swift release. Published v3 releases are: `3.0.0-beta.2`, … . Please pass a valid v3 tag, or omit the argument to auto-resolve the latest v3 release."*
> - *"`2.10.0` is a v2 release, not v3. This skill migrates to v3. Pass a v3 tag (e.g. `3.0.0-beta.2`) or omit the argument."*
> - *"`3.0.0-beta.1` is older than the `3.0.0-beta.2` already in your project — that's a downgrade. Pass a newer v3 tag or omit the argument."*

**Path B — no argument: auto-resolve the latest v3 release (including pre-releases):**

```bash
curl -s https://api.github.com/repos/auth0/Auth0.swift/releases | python3 -c " import sys, json releases = json.load(sys.stdin) v3 = [r for r in releases if r['tag_name'].startswith('3') and not r['draft']] for r in v3: print(r['tag_name']) "

1. **存在性** — 请求的标签出现在上述已发布版本列表中。
2. **主版本正确** — 标签属于**v3**主版本系列(以`3`开头)。`2.x`或其他主版本无效,需拒绝。
3. **非降级** — 标签版本比项目中检测到的当前版本更新。

> **如果任何一项检查失败,立即停止并询问用户**。不要静默回退。例如:
> - *“`3.9.9`不是已发布的Auth0.swift版本。已发布的v3版本包括:`3.0.0-beta.2`,……。请传入有效的v3标签,或省略参数以自动选择最新的v3版本。”*
> - *“`2.10.0`是v2版本,不是v3版本。该工具仅用于迁移到v3版本。请传入v3标签(例如`3.0.0-beta.2`)或省略参数。”*
> - *“`3.0.0-beta.1`比您项目中已有的`3.0.0-beta.2`版本旧——这属于降级操作。请传入更新的v3标签或省略参数。”*

**路径B — 无参数:自动选择最新的v3版本(包括预发布版本):**

```bash

Newest v3.x release tag (stable or pre-release), most recent first

最新的v3.x版本标签(稳定版或预发布版),按发布时间倒序排列

curl -s https://api.github.com/repos/auth0/Auth0.swift/releases | python3 -c " import sys, json releases = json.load(sys.stdin) v3 = [r for r in releases if r['tag_name'].startswith('3') and not r['draft']] if v3: print(v3[0]['tag_name']) else: print('') "

Record the result as `<TARGET_TAG>` and use it in every subsequent step.

> **If `<TARGET_TAG>` is a pre-release** (contains `-beta`, `-rc`, etc.), inform the user before continuing:
> *"The latest v3 release is `<TARGET_TAG>` (a pre-release). I'll migrate to that. You can pin a different tag by passing it as an argument: `auth0-swift-major-migration <tag>`."*
>
> **If no v3 release exists** (the resolver returns empty), stop and tell the user there is no published v3 release to migrate to.


---
curl -s https://api.github.com/repos/auth0/Auth0.swift/releases | python3 -c " import sys, json releases = json.load(sys.stdin) v3 = [r for r in releases if r['tag_name'].startswith('3') and not r['draft']] if v3: print(v3[0]['tag_name']) else: print('') "

将结果记录为`<TARGET_TAG>`,并在后续所有步骤中使用。

> **如果`<TARGET_TAG>`是预发布版本**(包含`-beta`、`-rc`等),在继续前告知用户:
> *“最新的v3版本是`<TARGET_TAG>`(预发布版)。我将迁移到该版本。您可以通过传入参数指定其他标签:`auth0-swift-major-migration <tag>`。”*
>
> **如果不存在v3版本**(解析结果为空),停止操作并告知用户没有可迁移的已发布v3版本。


---

Step 3 — Fetch & Read the v3 SDK Source

步骤3 — 获取并读取v3 SDK源码

Fetch the actual Swift source for the target tag. The signatures here are the authoritative reference for every change made in Step 6.
bash
TAG=<TARGET_TAG>   # the version the developer chose in Step 2, e.g. 3.0.0-beta.2
获取目标标签对应的Swift源码。这里的签名是第6步中所有变更的权威参考。
bash
TAG=<TARGET_TAG>   # 开发者在步骤2中选择的版本,例如3.0.0-beta.2

List all public Swift files in the SDK

列出SDK中所有公开的Swift文件

curl -s "https://api.github.com/repos/auth0/Auth0.swift/git/trees/${TAG}?recursive=1"
| python3 -c " import sys, json for item in json.load(sys.stdin).get('tree', []): if item['path'].startswith('Auth0/') and item['path'].endswith('.swift'): print(item['path']) "
curl -s "https://api.github.com/repos/auth0/Auth0.swift/git/trees/${TAG}?recursive=1"
| python3 -c " import sys, json for item in json.load(sys.stdin).get('tree', []): if item['path'].startswith('Auth0/') and item['path'].endswith('.swift'): print(item['path']) "

Fetch core public API files

获取核心公开API文件

for FILE in WebAuth.swift CredentialsManager.swift Authentication.swift
Credentials.swift UserProfile.swift Requestable.swift
CredentialsStorage.swift CredentialsManagerError.swift WebAuthError.swift; do URL="https://raw.githubusercontent.com/auth0/Auth0.swift/${TAG}/Auth0/${FILE}" CONTENT=$(curl -sf "$URL") [ -n "$CONTENT" ] && echo "=== $FILE ===" && echo "$CONTENT" done
for FILE in WebAuth.swift CredentialsManager.swift Authentication.swift
Credentials.swift UserProfile.swift Requestable.swift
CredentialsStorage.swift CredentialsManagerError.swift WebAuthError.swift; do URL="https://raw.githubusercontent.com/auth0/Auth0.swift/${TAG}/Auth0/${FILE}" CONTENT=$(curl -sf "$URL") [ -n "$CONTENT" ] && echo "=== $FILE ===" && echo "$CONTENT" done

MFA files live in a subdirectory

MFA文件位于子目录中

for FILE in MFA/MFAClient.swift MFA/MFAErrors.swift; do URL="https://raw.githubusercontent.com/auth0/Auth0.swift/${TAG}/Auth0/${FILE}" CONTENT=$(curl -sf "$URL") [ -n "$CONTENT" ] && echo "=== $FILE ===" && echo "$CONTENT" done

Read the fetched source and note:
- Every public method signature that changed (return type, parameters, `throws` added)
- Types that were renamed or removed
- Protocol requirements that changed
- Default parameter values that changed

This is the ground truth. Every change in Step 6 must match a real signature in these files.

---
for FILE in MFA/MFAClient.swift MFA/MFAErrors.swift; do URL="https://raw.githubusercontent.com/auth0/Auth0.swift/${TAG}/Auth0/${FILE}" CONTENT=$(curl -sf "$URL") [ -n "$CONTENT" ] && echo "=== $FILE ===" && echo "$CONTENT" done

读取获取到的源码并记录:
- 所有变更的公开方法签名(返回类型、参数、新增`throws`)
- 重命名或移除的类型
- 变更的协议要求
- 变更的默认参数值

这是最根本的依据。第6步中的所有变更必须与这些文件中的实际签名一致。

---

Step 4 — Audit Which Auth0 APIs the Project Uses

步骤4 — 审计项目使用的Auth0 API

Find all Swift files that import Auth0 — these are the scope of the migration:
bash
grep -rl "import Auth0" --include="*.swift" .
Read every file from that list. Do not grep for specific API patterns — read the full source so you can see exactly how
Auth0
,
webAuth
,
authentication
,
credentialsManager
, and any Auth0 types are used, including calls with domain/clientId parameters, chained builder calls, and any custom conformances.
For each file, identify:
What to look forWhy it matters
Any call to
webAuth()
,
webAuth(domain:)
,
webAuth(domain:clientId:)
§6.1 –
clearSession
rename; §6.14 – default scope
Any call to
.clearSession(
§6.1 — rename to
logout
Switch/catch on
WebAuthError
with explicit case names
§6.2 — removed and new cases
DispatchQueue.main.async
or
MainActor.run
wrapping an Auth0 callback
§6.3 — removable in v3
Any stored
Request<…>
type annotation (not just chained
.start(…)
)
§6.4 — type changed to
Requestable
Test mocks conforming to
Authentication
,
MFAClient
, or
Requestable
§6.4 — return type +
@MainActor
update
Any call to
credentialsManager.store(
§6.5 — Bool → throws
Any call to
credentialsManager.clear()
or
credentialsManager.clear(forAudience:
§6.6 — Bool → throws (both overloads)
Any access to
credentialsManager.user
(property, not method)
§6.7 — replaced by
userProfile()
method
Any call to
credentialsManager.revoke(
§6.8 — new error paths
Any type annotation or declaration using
UserInfo
§6.9 — renamed to
UserProfile
Any access to
.expiresIn
on a
Credentials
-like object
§6.10 — renamed to
expiresAt
Any type conforming to
CredentialsStorage
§6.11 — method signatures changed
Any call to
Auth0.users(
or
Auth0.users(token:
§6.12 — Management client removed
login(withOTP:
,
login(withOOBCode:
,
login(withRecoveryCode:
,
multifactorChallenge(
§6.13 — MFA methods removed
Any call to
webAuth()
that does not chain
.scope(
§6.14 — default scope changed
Any call to
credentialsManager.credentials(
without explicit
minTTL:
parameter
§6.15 — default minTTL changed from 0 to 60 seconds
Build a checklist: "This project uses: [list]" and "This project does NOT use: [list]". Only work through the §6.x sections that appear in the "uses" list. Skip the rest entirely.

找到所有导入Auth0的Swift文件——这些是迁移的范围:
bash
grep -rl "import Auth0" --include="*.swift" .
读取该列表中的每个文件。不要仅通过grep查找特定API模式——要读取完整源码,以便准确了解
Auth0
webAuth
authentication
credentialsManager
以及任何Auth0类型的使用方式,包括带有domain/clientId参数的调用、链式构建器调用和任何自定义协议实现。
针对每个文件,识别以下内容:
检查内容重要性
任何对
webAuth()
webAuth(domain:)
webAuth(domain:clientId:)
的调用
§6.1 –
clearSession
重命名;§6.14 – 默认scope
任何对
.clearSession(
的调用
§6.1 — 重命名为
logout
WebAuthError
使用明确枚举值的switch/catch语句
§6.2 — 移除和新增的枚举值
使用
DispatchQueue.main.async
MainActor.run
包裹Auth0回调
§6.3 — v3中可移除
任何存储的
Request<…>
类型注解(不仅仅是链式调用
.start(…)
)
§6.4 — 类型变更为
Requestable
符合
Authentication
MFAClient
Requestable
协议的测试mock
§6.4 — 返回类型 +
@MainActor
更新
任何对
credentialsManager.store(
的调用
§6.5 — Bool返回值改为throws
任何对
credentialsManager.clear()
credentialsManager.clear(forAudience:
的调用
§6.6 — Bool返回值改为throws(两个重载方法)
credentialsManager.user
的访问(属性,而非方法)
§6.7 — 替换为
userProfile()
方法
任何对
credentialsManager.revoke(
的调用
§6.8 — 新增错误路径
任何使用
UserInfo
的类型注解或声明
§6.9 — 重命名为
UserProfile
Credentials
类对象的
.expiresIn
属性的访问
§6.10 — 重命名为
expiresAt
任何符合
CredentialsStorage
协议的类型
§6.11 — 方法签名变更
任何对
Auth0.users(
Auth0.users(token:
的调用
§6.12 — 移除Management客户端
login(withOTP:
login(withOOBCode:
login(withRecoveryCode:
multifactorChallenge(
§6.13 — 移除MFA方法
任何未链式调用
.scope(
webAuth()
调用
§6.14 — 默认scope变更
任何未显式指定
minTTL:
参数的
credentialsManager.credentials(
调用
§6.15 — 默认minTTL从0秒改为60秒
创建一个检查清单:“项目使用:[列表]”“项目未使用:[列表]”。仅处理“使用”列表中对应的§6.x部分。完全跳过其余部分。

Step 5 — Update the SDK Dependency

步骤5 — 更新SDK依赖

Apply only the matching package manager.
Use the
<TARGET_TAG>
chosen in Step 2. For stable releases (
3.x.y
with no suffix), use a range specifier. For pre-releases (
3.x.y-beta.z
), pin the exact tag — package managers treat pre-release versions as out-of-range for
~>
/
from:
rules.
Swift Package Manager (Package.swift):
swift
// Stable v3 — range specifier picks up all 3.x.y patches
.package(url: "https://github.com/auth0/Auth0.swift", from: "3.0.0")

// Pre-release / specific beta — exact tag required
.package(url: "https://github.com/auth0/Auth0.swift", exact: "3.0.0-beta.2")
Then resolve:
bash
swift package resolve
CocoaPods (Podfile):
ruby
undefined
仅应用匹配的包管理器配置。
使用步骤2中选择的
<TARGET_TAG>
。对于稳定版本(无后缀的
3.x.y
),使用范围指定符。对于预发布版本(
3.x.y-beta.z
),需固定精确标签——包管理器会将预发布版本视为超出
~>
/
from:
规则的范围。
Swift Package Manager (Package.swift):
swift
// 稳定v3版本 — 范围指定符会自动获取所有3.x.y补丁版本
.package(url: "https://github.com/auth0/Auth0.swift", from: "3.0.0")

// 预发布/特定beta版本 — 需要精确标签
.package(url: "https://github.com/auth0/Auth0.swift", exact: "3.0.0-beta.2")
然后执行解析:
bash
swift package resolve
CocoaPods (Podfile):
ruby
// 稳定v3版本
pod 'Auth0', '~> 3.0'

// 预发布/特定beta版本 — 固定精确版本
pod 'Auth0', '3.0.0-beta.2'
然后执行:
bash
pod update Auth0
Carthage (Cartfile):
plaintext
// 稳定v3版本
github "auth0/Auth0.swift" ~> 3.0

// 预发布/特定beta版本 — 固定精确标签
github "auth0/Auth0.swift" "3.0.0-beta.2"
然后执行:
bash
carthage update Auth0.swift --use-xcframeworks
Xcode管理的SPM(根目录无
Package.swift
):
  • 稳定版: 文件 → 包 → 更新到最新包版本,然后验证版本规则为“从3.0.0开始的下一个主版本”。
  • 预发布/特定beta版本: 文件 → 包 → 更新到最新包版本不会解析beta版本,除非依赖已固定精确版本。告知用户将版本规则改为“精确版本”并输入
    3.0.0-beta.2
    (或所选标签)。
此时不要构建——先应用所有已知的代码变更。

Stable v3

步骤6 — 应用破坏性变更

pod 'Auth0', '~> 3.0'
Agent指令: 仅处理步骤4文件读取审计中匹配的§6.x部分。跳过项目未使用的API对应的所有章节——不要修改这些文件。
严格按照所示内容应用每个变更。不要修改周边代码、重命名变量、格式化代码或现代化未迁移的代码。匹配项目现有的风格:完成回调→完成回调,async/await→async/await,Combine→Combine。

Pre-release / specific beta — pin the exact version

6.1 —
WebAuth.clearSession()
WebAuth.logout()

pod 'Auth0', '3.0.0-beta.2'

Then:
```bash
pod update Auth0
Carthage (Cartfile):
plaintext
undefined
适用场景: 步骤4在项目源码中找到任何对
.clearSession(
的调用。
clearSession(federated:)
方法已重命名为
logout(federated:)
。参数及其默认值保持不变。
完成回调:
swift
// v2
Auth0.webAuth().clearSession { result in
    switch result {
    case .success: handleLogoutSuccess()
    case .failure(let error): handleError(error)
    }
}

// v3
Auth0.webAuth().logout { result in
    switch result {
    case .success: handleLogoutSuccess()
    case .failure(let error): handleError(error)
    }
}
async/await:
swift
// v2
try await Auth0.webAuth().clearSession()

// v3
try await Auth0.webAuth().logout()
Combine:
swift
// v2
Auth0.webAuth().clearSession()
    .sink(receiveCompletion: { ... }, receiveValue: { ... })
    .store(in: &cancellables)

// v3
Auth0.webAuth().logout()
    .sink(receiveCompletion: { ... }, receiveValue: { ... })
    .store(in: &cancellables)
带有
federated: true
参数:
参数名称不变——仅重命名方法:
swift
// v2
try await Auth0.webAuth().clearSession(federated: true)

// v3
try await Auth0.webAuth().logout(federated: true)

Stable v3

6.2 —
WebAuthError
— 移除和新增枚举值(针对穷举
switch
语句)

github "auth0/Auth0.swift" ~> 3.0
适用场景: 步骤4在项目源码中找到任何对
WebAuthError
使用明确枚举值的
switch
catch
语句。
v3版本中移除了两个
WebAuthError
枚举值。如果项目对
WebAuthError
使用穷举
switch
(或显式匹配这些枚举值),构建会失败。
新增了三个枚举值以暴露之前隐藏的失败场景。
已移除的枚举值(匹配时会编译失败):
v2枚举值v3行为
.invalidInvitationURL
已移除——现在会以
.unknown
形式暴露
.pkceNotAllowed
已移除——现在会以
.unknown
形式暴露
新增的枚举值(现在会出现在
catch
/
switch
块中):
v3枚举值触发场景
.authenticationFailed
服务端失败:密码错误、需要MFA、账户锁定等
.codeExchangeFailed
令牌交换失败:网络问题、无效授权、后端错误
.credentialsManagerError
登录/登出后,Credentials管理器存储或清除凭据失败;可通过
.cause
访问底层错误
迁移操作——从switch语句中删除已移除的枚举值:
swift
// v2 — 包含已不存在枚举值的穷举switch
Auth0.webAuth().start { result in
    switch result {
    case .success(let credentials):
        handle(credentials)
    case .failure(let error):
        switch error {
        case .userCancelled:
            break  // 用户取消操作——无需处理
        case .pkceNotAllowed:
            // ❌ v3中编译错误——移除该枚举值
            showConfigError("PKCE not allowed")
        default:
            showError(error)
        }
    }
}

// v3 — 删除已移除的枚举值;按需处理新增的枚举值
Auth0.webAuth().start { result in
    switch result {
    case .success(let credentials):
        handle(credentials)
    case .failure(let error):
        switch error {
        case .userCancelled:
            break  // 用户取消操作——无需处理
        case .authenticationFailed:
            // 服务端拒绝登录——显示合适的提示信息
            showError("登录失败,请检查您的凭据。")
        case .codeExchangeFailed:
            // 令牌交换失败——网络或服务端问题
            showError("出现错误,请重试。")
        case .credentialsManagerError:
            // 登录成功但无法存储凭据
            // 用户在内存中已认证,但下次启动需重新登录
            // 可通过error.cause访问底层错误(WebAuthError.cause: Error?)
            reportToMonitoring(error.cause)
            showError("无法保存您的会话。")
        default:
            showError(error)
        }
    }
}
如果项目使用async/await并捕获特定枚举值:
swift
// v2
do {
    let credentials = try await Auth0.webAuth().start()
    handle(credentials)
} catch WebAuthError.userCancelled {
    break
} catch WebAuthError.pkceNotAllowed {
    // ❌ v3中编译错误——移除该catch块
    showConfigError()
} catch {
    showError(error)
}

// v3 — 删除已移除的枚举值;如果项目需要处理新增枚举值则添加
 do {
    let credentials = try await Auth0.webAuth().start()
    handle(credentials)
} catch WebAuthError.userCancelled {
    break
} catch WebAuthError.authenticationFailed {
    showError("登录失败,请检查您的凭据。")
} catch WebAuthError.codeExchangeFailed {
    showError("出现错误,请重试。")
} catch {
    showError(error)
}
新增的
.authenticationFailed
.codeExchangeFailed
枚举值不需要显式处理——
default:
分支已能捕获它们。仅当项目需要为这些失败场景显示不同UI或上报遥测数据时,才添加显式处理分支。

Pre-release / specific beta — pin the exact tag

6.3 — 移除WebAuth和CredentialsManager回调周围冗余的主线程调度

github "auth0/Auth0.swift" "3.0.0-beta.2"

Then:
```bash
carthage update Auth0.swift --use-xcframeworks
Xcode-managed SPM (no
Package.swift
at root):
  • Stable: File → Packages → Update to Latest Package Versions, then verify the version rule is Up to Next Major from 3.0.0.
  • Pre-release / specific beta: File → Packages → Update to Latest Package Versions won't resolve a beta unless the dependency already pins an exact version. Tell the user to change the version rule to Exact Version and enter
    3.0.0-beta.2
    (or the chosen tag).
Do not build yet — apply all known code changes first.

适用场景: 步骤4找到使用
DispatchQueue.main.async
MainActor.run
包裹Auth0回调体的代码。
在v3版本中,所有完成回调、Combine发布者和async/await方法都会在主线程返回结果(它们被标记为
@MainActor
)。不再需要将回调体包裹在
DispatchQueue.main.async { }
await MainActor.run { }
中,可以移除这些调度代码。
完成回调——移除调度包装器:
swift
// v2 — 手动调度到主线程
credentialsManager.credentials { result in
    DispatchQueue.main.async {
        switch result {
        case .success(let credentials):
            self.accessToken = credentials.accessToken
            self.isAuthenticated = true
        case .failure(let error):
            self.authError = error
        }
    }
}

// v3 — 回调已在主线程返回
credentialsManager.credentials { result in
    switch result {
    case .success(let credentials):
        self.accessToken = credentials.accessToken
        self.isAuthenticated = true
    case .failure(let error):
        self.authError = error
    }
}
async/await——移除MainActor.run包装器:
swift
// v2
let credentials = try await Auth0.webAuth().start()
await MainActor.run {
    self.isAuthenticated = true
}

// v3 — start()是@MainActor方法;await后已处于主线程
let credentials = try await Auth0.webAuth().start()
self.isAuthenticated = true
仅移除用于保护Auth0回调体的调度包装器。如果
DispatchQueue.main.async
块还包含无关的UI操作,仅移除与Auth0回调相关的部分。

Step 6 — Apply Breaking Changes

6.4 —
Authentication
/
MFAClient
方法返回
Requestable
而非
Request
— 应用代码和测试mock

Agent instruction: Work through only the §6.x sections that matched during the Step 4 file-reading audit. Skip every section whose API the project does not use — do not touch those files.
Apply each change exactly as shown. Do not alter surrounding code, rename variables, reformat, or modernise code that isn't being migrated. Match the project's existing style: completion handler → completion handler, async/await → async/await, Combine → Combine.

适用场景: 步骤4找到以下任一场景:(a) 应用代码中存在存储的
Request<…>
类型注解;(b) 测试/mock文件中存在符合
Authentication
MFAClient
Requestable
协议的类型。
在v3版本中,所有
Authentication
MFAClient
方法返回协议类型而非具体的
Request
结构体:
  • 返回凭据的方法(login、codeExchange、renew、ssoExchange等)现在返回
    any TokenRequestable<T, E>
  • 所有其他方法(signup、resetPassword、userInfo、jwks等)现在返回
    any Requestable<T, E>
对应用代码的影响: 直接链式调用
.start(_:)
的调用点——绝大多数场景——无需修改即可编译。唯一会报错的应用代码是存在存储的
Request<>
类型注解的情况:
swift
// v2 — 将请求存储为带类型的变量
let request: Request<Credentials, AuthenticationError> = Auth0
    .authentication()
    .login(usernameOrEmail: email, password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience, scope: scope)
request.start { result in ... }

// v3 — 将类型注解更新为协议类型
// 对于返回凭据的方法:
let request: any TokenRequestable<Credentials, AuthenticationError> = Auth0
    .authentication()
    .login(usernameOrEmail: email, password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience, scope: scope)
request.start { result in ... }

// 对于非凭据方法(signup、resetPassword、userInfo、jwks):
let request: any Requestable<DatabaseUser, AuthenticationError> = Auth0
    .authentication()
    .signup(email: email, password: password, connection: connection)
request.start { result in ... }

// 最常见的模式——直接链式调用,无需注解,无需修改:
Auth0.authentication()
    .login(usernameOrEmail: email, password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience, scope: scope)
    .start { result in ... }  // ✅ 无需修改
现在返回
any TokenRequestable
的返回凭据方法(完整列表):
  • login(email:code:audience:scope:)
  • login(phoneNumber:code:audience:scope:)
  • login(usernameOrEmail:password:realmOrConnection:audience:scope:)
  • loginDefaultDirectory(withUsername:password:audience:scope:)
  • login(appleAuthorizationCode:fullName:profile:audience:scope:)
  • login(facebookSessionAccessToken:profile:audience:scope:)
  • login(passkey:challenge:connection:audience:scope:organization:)
    — 两个重载方法(使用passkey登录+注册)
  • codeExchange(withCode:codeVerifier:redirectURI:)
  • renew(withRefreshToken:audience:scope:)
  • ssoExchange(withRefreshToken:)
  • customTokenExchange(subjectToken:subjectTokenType:audience:scope:organization:parameters:)
  • MFAClient.verify(otp:mfaToken:)
    ,
    verify(oobCode:bindingCode:mfaToken:)
    ,
    verify(recoveryCode:mfaToken:)
对测试目标的影响——自定义
Authentication
mock:
如果项目的测试目标存在符合
Authentication
MFAClient
协议的mock或stub,需要进行两项修改:
  1. 返回类型:
    Request<T, E>
    改为
    any TokenRequestable<T, E>
    (凭据方法)或
    any Requestable<T, E>
    (其他方法)
  2. start(_:)
    回调:
    添加
    @MainActor
    以匹配更新后的
    Requestable
    协议要求
swift
// v2 — 测试中的mock Authentication协议实现
class MockAuthentication: Authentication {
    var credentialsResult: Result<Credentials, AuthenticationError> = .failure(.init(info: [:], statusCode: 0))

    func login(usernameOrEmail username: String,
               password: String,
               realmOrConnection realm: String,
               audience: String?,
               scope: String) -> Request<Credentials, AuthenticationError> {
        // ❌ v3中编译错误——Request不再是返回类型
        return Request(session: URLSession.shared, ...) // v2内部实现——不再可用
    }
}

// v2 — 用作stub的mock Requestable
struct MockRequest<T, E: Auth0Error>: Requestable {
    let result: Result<T, E>
    func start(_ callback: @escaping (Result<T, E>) -> Void) {
        // ❌ 缺少@MainActor——不符合v3的Requestable协议
        callback(result)
    }
}

// v3 — 更新后的mock
struct MockRequest<T, E: Auth0Error>: Requestable {
    let result: Result<T, E>
    // ✅ 添加@MainActor以匹配协议;通过Task调度满足@MainActor隔离要求
    func start(_ callback: @escaping @MainActor (Result<T, E>) -> Void) {
        Task { @MainActor in callback(result) }
    }
}

// v3 — 更新后的Authentication mock,返回正确的协议类型
class MockAuthentication: Authentication {
    var credentialsResult: Result<Credentials, AuthenticationError> = .failure(.init(info: [:], statusCode: 0))

    func login(usernameOrEmail username: String,
               password: String,
               realmOrConnection realm: String,
               audience: String?,
               scope: String) -> any TokenRequestable<Credentials, AuthenticationError> {
        // ✅ 返回MockTokenRequest,而非Request
        return MockTokenRequest(result: credentialsResult)
    }
}

// v3 — TokenRequestable mock(用于返回凭据的方法)
struct MockTokenRequest<T, E: Auth0Error>: TokenRequestable {
    typealias ResultType = T
    typealias ErrorType = E

    let result: Result<T, E>

    func start(_ callback: @escaping @MainActor (Result<T, E>) -> Void) {
        Task { @MainActor in callback(result) }
    }

    // TokenRequestable新增了这些声明验证构建器方法——返回self即可
    func validateClaims() -> any TokenRequestable<T, E> { self }
    func withLeeway(_ leeway: Int) -> any TokenRequestable<T, E> { self }
    func withIssuer(_ issuer: String) -> any TokenRequestable<T, E> { self }
    func withNonce(_ nonce: String?) -> any TokenRequestable<T, E> { self }
    func withMaxAge(_ maxAge: Int?) -> any TokenRequestable<T, E> { self }
    func withOrganization(_ organization: String?) -> any TokenRequestable<T, E> { self }
}
上面的
MockTokenRequest
stub通过返回
self
来实现所有
TokenRequestable
构建器方法。在大多数测试中,
validateClaims()
with*
修饰符不会被调用,因此返回
self
是正确的。如果特定测试需要验证声明验证行为,则需正确实现这些方法。

6.1 —
WebAuth.clearSession()
WebAuth.logout()

6.5 —
CredentialsManager.store(credentials:)
— Bool返回值改为throws

Applies if: Step 4 found any call to
.clearSession(
in the project's source files.
The
clearSession(federated:)
method was renamed to
logout(federated:)
. The parameter and its default value are unchanged.
Completion handler:
swift
// v2
Auth0.webAuth().clearSession { result in
    switch result {
    case .success: handleLogoutSuccess()
    case .failure(let error): handleError(error)
    }
}

// v3
Auth0.webAuth().logout { result in
    switch result {
    case .success: handleLogoutSuccess()
    case .failure(let error): handleError(error)
    }
}
async/await:
swift
// v2
try await Auth0.webAuth().clearSession()

// v3
try await Auth0.webAuth().logout()
Combine:
swift
// v2
Auth0.webAuth().clearSession()
    .sink(receiveCompletion: { ... }, receiveValue: { ... })
    .store(in: &cancellables)

// v3
Auth0.webAuth().logout()
    .sink(receiveCompletion: { ... }, receiveValue: { ... })
    .store(in: &cancellables)
With
federated: true
:
The parameter name is the same — just rename the method:
swift
// v2
try await Auth0.webAuth().clearSession(federated: true)

// v3
try await Auth0.webAuth().logout(federated: true)

适用场景: 步骤4在项目源码中找到任何对
credentialsManager.store(credentials:
的调用。
store(credentials:)
之前返回
Bool
。在v3版本中,失败时会抛出错误,成功时返回
Void
如果项目检查了返回值:
swift
// v2
if credentialsManager.store(credentials: credentials) {
    print("存储成功")
} else {
    print("存储失败")
}

// v3 — 使用do-catch;将错误映射到项目现有的错误处理逻辑
do {
    try credentialsManager.store(credentials: credentials)
} catch {
    // 替换为项目已有的日志/错误处理逻辑
    handleError(error)
}
如果项目忽略了返回值:
swift
// v2 — 静默忽略返回值
_ = credentialsManager.store(credentials: credentials)

// v3 — try?同样会忽略错误;如果项目之前未处理失败则使用该方式
try? credentialsManager.store(credentials: credentials)
当项目有可接入的错误处理模式时,优先使用
do-catch
而非
try?
。仅当需要保留有意的静默忽略行为时才使用
try?

6.2 —
WebAuthError
— removed and new cases in exhaustive
switch
statements

6.6 —
CredentialsManager.clear()
clear(forAudience:scope:)
— Bool返回值改为throws

Applies if: Step 4 found any
switch
or
catch
on
WebAuthError
with explicit case names in the project's source files.
Two
WebAuthError
cases were removed in v3. If the project has an exhaustive
switch
over
WebAuthError
(or explicitly matches these cases), the build will fail.
Three new cases were added to surface previously hidden failures.
Removed cases (will no longer compile if matched):
v2 casev3 behaviour
.invalidInvitationURL
Removed — now surfaces as
.unknown
.pkceNotAllowed
Removed — now surfaces as
.unknown
New cases (can now appear in
catch
/
switch
blocks):
v3 caseWhen it fires
.authenticationFailed
Server-side failure: wrong password, MFA required, account locked, etc.
.codeExchangeFailed
Token exchange failed: network issue, invalid grant, backend error
.credentialsManagerError
Credentials manager failed to store or clear credentials after login/logout; access the underlying error via
.cause
Migration — remove the deleted cases from switch statements:
swift
// v2 — exhaustive switch including cases that no longer exist
Auth0.webAuth().start { result in
    switch result {
    case .success(let credentials):
        handle(credentials)
    case .failure(let error):
        switch error {
        case .userCancelled:
            break  // user dismissed — no action needed
        case .pkceNotAllowed:
            // ❌ compile error in v3 — remove this case
            showConfigError("PKCE not allowed")
        default:
            showError(error)
        }
    }
}

// v3 — remove the deleted cases; handle the new ones where appropriate
Auth0.webAuth().start { result in
    switch result {
    case .success(let credentials):
        handle(credentials)
    case .failure(let error):
        switch error {
        case .userCancelled:
            break  // user dismissed — no action needed
        case .authenticationFailed:
            // server rejected the login — show an appropriate message
            showError("Login failed. Please check your credentials.")
        case .codeExchangeFailed:
            // token exchange failed — network or server issue
            showError("Something went wrong. Please try again.")
        case .credentialsManagerError:
            // login succeeded but credentials could not be stored
            // the user is authenticated in memory but will need to log in again next launch
            // access the underlying error via error.cause (WebAuthError.cause: Error?)
            reportToMonitoring(error.cause)
            showError("Could not save your session.")
        default:
            showError(error)
        }
    }
}
If the project uses async/await and catches specific cases:
swift
// v2
do {
    let credentials = try await Auth0.webAuth().start()
    handle(credentials)
} catch WebAuthError.userCancelled {
    break
} catch WebAuthError.pkceNotAllowed {
    // ❌ compile error in v3 — remove this catch
    showConfigError()
} catch {
    showError(error)
}

// v3 — remove deleted cases; add new ones if the project should handle them
do {
    let credentials = try await Auth0.webAuth().start()
    handle(credentials)
} catch WebAuthError.userCancelled {
    break
} catch WebAuthError.authenticationFailed {
    showError("Login failed. Please check your credentials.")
} catch WebAuthError.codeExchangeFailed {
    showError("Something went wrong. Please try again.")
} catch {
    showError(error)
}
The new cases
.authenticationFailed
and
.codeExchangeFailed
are not required to be handled explicitly — a
default:
branch already catches them. Only add explicit cases if the project wants to show different UI or telemetry for those failures.

适用场景: 步骤4在项目源码中找到任何对
credentialsManager.clear()
credentialsManager.clear(forAudience:
的调用。
两个重载方法之前都返回
Bool
。在v3版本中,两个方法都会抛出错误:
  • clear() throws
    — 清除主存储的凭据
  • clear(forAudience:scope:) throws
    — 清除特定受众的API凭据
swift
// v2
_ = credentialsManager.clear()
_ = credentialsManager.clear(forAudience: "https://api.example.com")

// v3
try? credentialsManager.clear()
try? credentialsManager.clear(forAudience: "https://api.example.com")
// 或者,如果项目需要处理错误:
do {
    try credentialsManager.clear()
} catch {
    handleError(error)
}

6.3 — Remove redundant main-thread dispatch around WebAuth and CredentialsManager callbacks

6.7 —
CredentialsManager.user
属性 →
userProfile()
抛出方法

Applies if: Step 4 found
DispatchQueue.main.async
or
MainActor.run
wrapping an Auth0 callback body.
In v3, all completion-handler callbacks, Combine publishers, and async/await methods deliver results on the main thread (they are
@MainActor
). Wrapping callback bodies in
DispatchQueue.main.async { }
or
await MainActor.run { }
is no longer necessary and can be removed.
Completion handler callback — remove the dispatch wrapper:
swift
// v2 — dispatch to main manually
credentialsManager.credentials { result in
    DispatchQueue.main.async {
        switch result {
        case .success(let credentials):
            self.accessToken = credentials.accessToken
            self.isAuthenticated = true
        case .failure(let error):
            self.authError = error
        }
    }
}

// v3 — callback already arrives on main thread
credentialsManager.credentials { result in
    switch result {
    case .success(let credentials):
        self.accessToken = credentials.accessToken
        self.isAuthenticated = true
    case .failure(let error):
        self.authError = error
    }
}
async/await — remove the MainActor.run wrapper:
swift
// v2
let credentials = try await Auth0.webAuth().start()
await MainActor.run {
    self.isAuthenticated = true
}

// v3 — start() is @MainActor; already on main thread after the await
let credentials = try await Auth0.webAuth().start()
self.isAuthenticated = true
Only remove dispatch wrappers that are solely protecting Auth0 callback bodies. If a
DispatchQueue.main.async
block also dispatches unrelated UI work, remove only what's attributable to the Auth0 callback.

适用场景: 步骤4在项目源码中找到任何对
credentialsManager.user
的属性访问(而非方法调用)。
user: UserInfo?
计算属性已被替换为
userProfile() throws -> UserProfile?
(另见§6.9的类型重命名)。
swift
// v2 — 属性访问,返回UserInfo?
func currentUser() -> UserInfo? {
    return credentialsManager.user
}

// v3 — 抛出方法调用,返回UserProfile?
func currentUser() -> UserProfile? {
    return try? credentialsManager.userProfile()
}

// v3 — 如果项目需要暴露错误:
func loadUser() throws {
    let profile = try credentialsManager.userProfile()
    self.userProfile = profile
}

6.4 —
Authentication
/
MFAClient
methods return
Requestable
instead of
Request
— app code and test mocks

6.8 —
CredentialsManager
异步方法 — 抛出存储带来的新错误路径

Applies if: Step 4 found either (a) a stored
Request<…>
type annotation in app code, or (b) test/mock files with types conforming to
Authentication
,
MFAClient
, or
Requestable
.
In v3, all
Authentication
and
MFAClient
methods return protocol types rather than the concrete
Request
struct:
  • Credential-returning methods (login, codeExchange, renew, ssoExchange, etc.) now return
    any TokenRequestable<T, E>
  • All other methods (signup, resetPassword, userInfo, jwks, etc.) now return
    any Requestable<T, E>
Impact on app code: Call sites that chain directly to
.start(_:)
— the overwhelming majority — compile without any change. The only app code that breaks is a stored
Request<>
type annotation:
swift
// v2 — storing the request in a typed variable
let request: Request<Credentials, AuthenticationError> = Auth0
    .authentication()
    .login(usernameOrEmail: email, password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience, scope: scope)
request.start { result in ... }

// v3 — update the type annotation to the protocol type
// For credential-returning methods:
let request: any TokenRequestable<Credentials, AuthenticationError> = Auth0
    .authentication()
    .login(usernameOrEmail: email, password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience, scope: scope)
request.start { result in ... }

// For non-credential methods (signup, resetPassword, userInfo, jwks):
let request: any Requestable<DatabaseUser, AuthenticationError> = Auth0
    .authentication()
    .signup(email: email, password: password, connection: connection)
request.start { result in ... }

// Most common pattern — chaining directly, no annotation needed, no change required:
Auth0.authentication()
    .login(usernameOrEmail: email, password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience, scope: scope)
    .start { result in ... }  // ✅ unchanged
Credential-returning methods that now return
any TokenRequestable
(full list):
  • login(email:code:audience:scope:)
  • login(phoneNumber:code:audience:scope:)
  • login(usernameOrEmail:password:realmOrConnection:audience:scope:)
  • loginDefaultDirectory(withUsername:password:audience:scope:)
  • login(appleAuthorizationCode:fullName:profile:audience:scope:)
  • login(facebookSessionAccessToken:profile:audience:scope:)
  • login(passkey:challenge:connection:audience:scope:organization:)
    — two overloads (sign in + sign up with passkey)
  • codeExchange(withCode:codeVerifier:redirectURI:)
  • renew(withRefreshToken:audience:scope:)
  • ssoExchange(withRefreshToken:)
  • customTokenExchange(subjectToken:subjectTokenType:audience:scope:organization:parameters:)
  • MFAClient.verify(otp:mfaToken:)
    ,
    verify(oobCode:bindingCode:mfaToken:)
    ,
    verify(recoveryCode:mfaToken:)
Impact on test targets — custom
Authentication
mocks:
If the project's test target has a mock or stub conforming to the
Authentication
or
MFAClient
protocol, two changes are required:
  1. Return type: Change
    Request<T, E>
    to
    any TokenRequestable<T, E>
    (credential methods) or
    any Requestable<T, E>
    (other methods)
  2. start(_:)
    callback:
    Add
    @MainActor
    to match the updated
    Requestable
    protocol requirement
swift
// v2 — mock Authentication conformance in tests
class MockAuthentication: Authentication {
    var credentialsResult: Result<Credentials, AuthenticationError> = .failure(.init(info: [:], statusCode: 0))

    func login(usernameOrEmail username: String,
               password: String,
               realmOrConnection realm: String,
               audience: String?,
               scope: String) -> Request<Credentials, AuthenticationError> {
        // ❌ compile error in v3 — Request is no longer the return type
        return Request(session: URLSession.shared, ...) // v2 internal — no longer works
    }
}

// v2 — mock Requestable used as stub
struct MockRequest<T, E: Auth0Error>: Requestable {
    let result: Result<T, E>
    func start(_ callback: @escaping (Result<T, E>) -> Void) {
        // ❌ @MainActor missing — does not conform to v3 Requestable
        callback(result)
    }
}

// v3 — updated mock
struct MockRequest<T, E: Auth0Error>: Requestable {
    let result: Result<T, E>
    // ✅ Add @MainActor to match the protocol; dispatch via Task to satisfy @MainActor isolation
    func start(_ callback: @escaping @MainActor (Result<T, E>) -> Void) {
        Task { @MainActor in callback(result) }
    }
}

// v3 — updated Authentication mock returning the correct protocol type
class MockAuthentication: Authentication {
    var credentialsResult: Result<Credentials, AuthenticationError> = .failure(.init(info: [:], statusCode: 0))

    func login(usernameOrEmail username: String,
               password: String,
               realmOrConnection realm: String,
               audience: String?,
               scope: String) -> any TokenRequestable<Credentials, AuthenticationError> {
        // ✅ Return MockTokenRequest, not Request
        return MockTokenRequest(result: credentialsResult)
    }
}

// v3 — TokenRequestable mock (for credential-returning methods)
struct MockTokenRequest<T, E: Auth0Error>: TokenRequestable {
    typealias ResultType = T
    typealias ErrorType = E

    let result: Result<T, E>

    func start(_ callback: @escaping @MainActor (Result<T, E>) -> Void) {
        Task { @MainActor in callback(result) }
    }

    // TokenRequestable adds these claim-validation builder methods — return self
    func validateClaims() -> any TokenRequestable<T, E> { self }
    func withLeeway(_ leeway: Int) -> any TokenRequestable<T, E> { self }
    func withIssuer(_ issuer: String) -> any TokenRequestable<T, E> { self }
    func withNonce(_ nonce: String?) -> any TokenRequestable<T, E> { self }
    func withMaxAge(_ maxAge: Int?) -> any TokenRequestable<T, E> { self }
    func withOrganization(_ organization: String?) -> any TokenRequestable<T, E> { self }
}
The
MockTokenRequest
stub above stubs out all
TokenRequestable
builder methods by returning
self
. In most tests,
validateClaims()
and the
with*
modifiers are never called, so returning
self
is correct. If a specific test verifies claim validation behaviour, implement those methods properly.

适用场景: 步骤4在项目源码中找到任何对
credentialsManager.revoke(
的调用。
由于
CredentialsManager
存储方法现在会抛出错误,多个异步方法新增了之前被静默忽略的失败路径。最显著的是
revoke()
方法。仅更新项目实际编写的错误处理代码——已使用
default:
分支的调用点无需修改。
revoke()
现在可能出现的新错误:
新错误触发场景处理方式
.noCredentials
getEntry
抛出错误——存储中无凭据,无需撤销
视为已登出;导航到登录页面
.revokeFailed
撤销刷新令牌的网络请求失败令牌可能仍在服务端有效;显示错误信息
.clearFailed
撤销成功但Keychain删除失败视为已登出——服务端令牌已失效
swift
// v2 — 仅可能出现.revokeFailed;无凭据时会静默返回.success
credentialsManager.revoke { result in
    switch result {
    case .success:
        navigateToLogin()
    case .failure(let error):
        showError(error)  // 仅.revokeFailed会进入此处
    }
}

// v3 — 新增错误会暴露;如果项目检查特定错误则更新switch
credentialsManager.revoke { result in
    switch result {
    case .success:
        navigateToLogin()
    case .failure(let error):
        switch error {
        case .noCredentials:
            // 无存储的凭据——实际上已登出
            navigateToLogin()
        case .revokeFailed:
            // 服务端撤销失败——刷新令牌可能仍有效
            showError("无法撤销您的会话,请重试。")
        case .clearFailed:
            // 服务端令牌已撤销但Keychain删除失败
            // 视为已登出——令牌在服务端已失效
            navigateToLogin()
        default:
            showError(error)
        }
    }
}
credentials()
renew()
apiCredentials()
ssoCredentials()
现在可能出现的新错误:
新错误触发场景
.noCredentials
getEntry
抛出错误(例如Keychain项未找到)——之前被
try?
静默忽略
.renewFailed
刷新令牌续期请求失败——网络错误、无效/过期的刷新令牌
.storeFailed
保存续期后的凭据时Keychain写入失败
仅当项目现有的
catch
/
failure
处理程序需要区分这些场景时才需要关注。如果使用通用的回退逻辑,则无需修改。
swift
// v3 — 如果项目需要区分存储失败和网络失败:
credentialsManager.credentials { result in
    switch result {
    case .success(let credentials):
        use(credentials)
    case .failure(let error):
        switch error {
        case .noCredentials, .renewFailed:
            // 凭据缺失或续期失败——强制重新登录
            navigateToLogin()
        case .storeFailed:
            // 续期成功但无法保存——本次会话内存中的凭据有效
            // 用户下次启动需重新登录
            reportToMonitoring(error)
            use(/* 如果可用,使用最后已知的凭据 */)
        default:
            showError(error)
        }
    }
}
仅当项目当前对
CredentialsManagerError
使用
switch
且需要区分处理这些场景时,才添加这些新的
case
分支。
default:
分支已能正确处理这些错误,无需修改。

6.5 —
CredentialsManager.store(credentials:)
— Bool return → throws

6.9 —
UserInfo
UserProfile
类型重命名

Applies if: Step 4 found any call to
credentialsManager.store(credentials:
in the project's source files.
store(credentials:)
previously returned
Bool
. In v3 it throws on failure and returns
Void
on success.
If the project checked the return value:
swift
// v2
if credentialsManager.store(credentials: credentials) {
    print("Stored successfully")
} else {
    print("Store failed")
}

// v3 — use do-catch; map the error into the project's existing error handler
do {
    try credentialsManager.store(credentials: credentials)
} catch {
    // replace with whatever logging/error handling the project already uses
    handleError(error)
}
If the project discarded the return value:
swift
// v2 — silently discarded
_ = credentialsManager.store(credentials: credentials)

// v3 — try? discards the error the same way; use if the project didn't handle failures before
try? credentialsManager.store(credentials: credentials)
Prefer
do-catch
over
try?
when the project has an error-handling pattern to route into. Use
try?
only to preserve intentional silent-discard behaviour.

适用场景: 步骤4在项目源码中找到任何引用
UserInfo
的类型注解、函数签名或变量声明。
UserInfo
类型已重命名为
UserProfile
。更新所有引用
UserInfo
的类型注解、函数签名和变量声明。
swift
// v2
var currentUser: UserInfo?
func showProfile(_ profile: UserInfo) { ... }
func fetchUser() -> UserInfo? { ... }

// v3
var currentUser: UserProfile?
func showProfile(_ profile: UserProfile) { ... }
func fetchUser() -> UserProfile? { ... }
如果项目调用
Auth0.authentication().userInfo(withAccessToken:)
,方法名称不变但返回类型已变更:
swift
// v2 — 返回Request<UserInfo, AuthenticationError>
Auth0.authentication()
    .userInfo(withAccessToken: accessToken)
    .start { (result: Result<UserInfo, AuthenticationError>) in ... }

// v3 — 返回Request<UserProfile, AuthenticationError>
Auth0.authentication()
    .userInfo(withAccessToken: accessToken)
    .start { (result: Result<UserProfile, AuthenticationError>) in ... }

6.6 —
CredentialsManager.clear()
and
clear(forAudience:scope:)
— Bool return → throws

6.10 —
Credentials.expiresIn
Credentials.expiresAt

Applies if: Step 4 found any call to
credentialsManager.clear()
or
credentialsManager.clear(forAudience:
in the project's source files.
Both overloads previously returned
Bool
. In v3 both throw:
  • clear() throws
    — clears the main stored credentials
  • clear(forAudience:scope:) throws
    — clears API credentials for a specific audience
swift
// v2
_ = credentialsManager.clear()
_ = credentialsManager.clear(forAudience: "https://api.example.com")

// v3
try? credentialsManager.clear()
try? credentialsManager.clear(forAudience: "https://api.example.com")
// or, if the project handles errors:
do {
    try credentialsManager.clear()
} catch {
    handleError(error)
}

适用场景: 步骤4在项目源码中找到任何对
Credentials
APICredentials
SSOCredentials
对象的
.expiresIn
属性的访问。
Credentials
APICredentials
SSOCredentials
上的
expiresIn: Date
属性已重命名为
expiresAt: Date
。底层JSON键不变;仅Swift属性名称变更。
swift
// v2
let expiry: Date = credentials.expiresIn

// v3
let expiry: Date = credentials.expiresAt

6.7 —
CredentialsManager.user
property →
userProfile()
throwing method

6.11 —
CredentialsStorage
自定义实现 — 方法现在会抛出错误

Applies if: Step 4 found any access to
credentialsManager.user
as a property (not a method call) in the project's source files.
The
user: UserInfo?
computed property was replaced by
userProfile() throws -> UserProfile?
(see also §6.9 for the type rename).
swift
// v2 — property access, returns UserInfo?
func currentUser() -> UserInfo? {
    return credentialsManager.user
}

// v3 — method call that throws, returns UserProfile?
func currentUser() -> UserProfile? {
    return try? credentialsManager.userProfile()
}

// v3 — if the project needs to surface errors:
func loadUser() throws {
    let profile = try credentialsManager.userProfile()
    self.userProfile = profile
}

适用场景: 步骤4在项目源码中找到符合
CredentialsStorage
协议的类型。如果项目仅使用
SimpleKeychain
实例则跳过——默认存储无需修改。
仅适用于项目提供自定义
CredentialsStorage
实现的场景(即符合该协议的类型——而非仅使用默认的
SimpleKeychain
实例)。如果项目仅使用
SimpleKeychain
实例则跳过。
协议已从返回Bool/Data?改为抛出方法,并新增了必填的
deleteAllEntries()
方法。
swift
// v2 — 协议实现
final class AppKeychain: CredentialsStorage {
    func getEntry(forKey key: String) -> Data? {
        return Keychain.shared.read(key: key)
    }

    func setEntry(_ data: Data, forKey key: String) -> Bool {
        return Keychain.shared.write(data, forKey: key)
    }

    func deleteEntry(forKey key: String) -> Bool {
        return Keychain.shared.delete(key: key)
    }
}

// v3 — 方法抛出错误;新增deleteAllEntries()方法
final class AppKeychain: CredentialsStorage {
    func getEntry(forKey key: String) throws -> Data {
        guard let data = Keychain.shared.read(key: key) else {
            throw CredentialsManagerError.noCredentials
        }
        return data
    }

    func setEntry(_ data: Data, forKey key: String) throws {
        guard Keychain.shared.write(data, forKey: key) else {
            throw CredentialsManagerError.storeFailed
        }
    }

    func deleteEntry(forKey key: String) throws {
        guard Keychain.shared.delete(key: key) else {
            throw CredentialsManagerError.revokeFailed
        }
    }

    func deleteAllEntries() throws {
        Keychain.shared.deleteAll()
    }
}
CredentialsStorage
协议声明其方法为
throws
但无特定错误类型——您可以抛出任何
Error
。上面的示例仅为说明使用
CredentialsManagerError
枚举值;您的实现应抛出适合您存储后端的错误类型。如果选择复用这些枚举值,请在步骤3获取的SDK源码中验证
CredentialsManagerError
枚举值名称。

6.8 —
CredentialsManager
async methods — new error paths from throwing storage

6.12 — 移除Management客户端

Applies if: Step 4 found any call to
credentialsManager.revoke(
in the project's source files.
Because
CredentialsManager
storage methods now throw, several async methods gain new failure paths that were previously silently swallowed. The most significant is
revoke()
. Only update error-handling code that the project actually writes — call sites that already use a
default:
branch need no change.
New errors that can now surface from
revoke()
:
New errorWhen it firesWhat to do
.noCredentials
getEntry
threw — no credentials in storage, nothing to revoke
Treat as already logged out; navigate to login
.revokeFailed
Network call to revoke the refresh token failedThe token may still be active on the server; show an error
.clearFailed
Revocation succeeded but Keychain delete failedTreat as logged out — the token is no longer valid server-side
swift
// v2 — only .revokeFailed was possible; missing credentials returned .success silently
credentialsManager.revoke { result in
    switch result {
    case .success:
        navigateToLogin()
    case .failure(let error):
        showError(error)  // only .revokeFailed reached here
    }
}

// v3 — new cases surface; update the switch if the project checks specific cases
credentialsManager.revoke { result in
    switch result {
    case .success:
        navigateToLogin()
    case .failure(let error):
        switch error {
        case .noCredentials:
            // nothing was stored — already effectively logged out
            navigateToLogin()
        case .revokeFailed:
            // server revocation failed — refresh token may still be active
            showError("Could not revoke your session. Please try again.")
        case .clearFailed:
            // token revoked server-side but Keychain delete failed
            // treat as logged out — token is no longer valid
            navigateToLogin()
        default:
            showError(error)
        }
    }
}
New errors that can now surface from
credentials()
,
renew()
,
apiCredentials()
,
ssoCredentials()
:
New errorWhen it fires
.noCredentials
getEntry
throws (e.g., Keychain item not found) — previously swallowed by
try?
.renewFailed
Refresh token renewal request failed — network error, invalid/expired refresh token
.storeFailed
Keychain write fails when saving renewed credentials
These only matter if the project's existing
catch
/
failure
handler needs to distinguish these cases. If it uses a generic fallback, no change is needed.
swift
// v3 — if the project wants to distinguish storage failures from network failures:
credentialsManager.credentials { result in
    switch result {
    case .success(let credentials):
        use(credentials)
    case .failure(let error):
        switch error {
        case .noCredentials, .renewFailed:
            // credentials missing or refresh failed — force re-login
            navigateToLogin()
        case .storeFailed:
            // renewed successfully but couldn't save — credentials valid in memory this session
            // user will be asked to log in again on next launch
            reportToMonitoring(error)
            use(/* last known credentials if available */)
        default:
            showError(error)
        }
    }
}
Only add these new
case
branches if the project currently has a
switch
on
CredentialsManagerError
that would benefit from handling them differently. A
default:
branch already handles them correctly without any change.

适用场景: 步骤4在项目源码中找到任何对
Auth0.users(
Auth0.users(token:
的调用。
Auth0.users(token:)
和整个
Users
管理客户端已在v3版本中从SDK移除。不要静默删除任何调用点——添加
TODO
注释并在迁移总结中说明。
swift
// v2 — 应用中的直接Management API调用
Auth0
    .users(token: managementToken)
    .patch(userId, userPatch: UserPatchAttributes(name: newName))
    .start { result in
        switch result {
        case .success: print("更新成功")
        case .failure(let error): print(error)
        }
    }

// v3 — Management客户端已移除;添加TODO并保留意图
// TODO: Auth0.swift v3已移除Management客户端。
// 请将此替换为调用您自己的后端端点,该端点
// 使用机器到机器令牌调用Auth0 Management API。
// 绝对不要在客户端应用中嵌入Management API令牌。
// 参考:https://auth0.com/docs/secure/tokens/access-tokens/management-api-access-tokens
需要后端工作——在步骤9的总结中记录。

6.9 —
UserInfo
UserProfile
type rename

6.13 — 从
Authentication
中移除MFA方法 → 迁移到
MFAClient

Applies if: Step 4 found any type annotation, function signature, or variable declaration referencing
UserInfo
in the project's source files.
The
UserInfo
type was renamed to
UserProfile
. Update every type annotation, function signature, and variable declaration that references
UserInfo
.
swift
// v2
var currentUser: UserInfo?
func showProfile(_ profile: UserInfo) { ... }
func fetchUser() -> UserInfo? { ... }

// v3
var currentUser: UserProfile?
func showProfile(_ profile: UserProfile) { ... }
func fetchUser() -> UserProfile? { ... }
If the project calls
Auth0.authentication().userInfo(withAccessToken:)
, the method name is unchanged but the return type changed:
swift
// v2 — returns Request<UserInfo, AuthenticationError>
Auth0.authentication()
    .userInfo(withAccessToken: accessToken)
    .start { (result: Result<UserInfo, AuthenticationError>) in ... }

// v3 — returns Request<UserProfile, AuthenticationError>
Auth0.authentication()
    .userInfo(withAccessToken: accessToken)
    .start { (result: Result<UserProfile, AuthenticationError>) in ... }

适用场景: 步骤4在项目源码中找到任何对
login(withOTP:
login(withOOBCode:
login(withRecoveryCode:
multifactorChallenge(
的调用——或符合
MFAClient
协议的测试mock。
Authentication
协议上的四个MFA方法已在v3版本中移除。它们被专用的
MFAClient
协议替代,可通过
Auth0.mfa()
访问:
v2 (
Authentication
)
v3 (
MFAClient
)
authentication().login(withOTP: otp, mfaToken: token)
mfa().verify(otp: otp, mfaToken: token)
authentication().login(withOOBCode: code, mfaToken: token, bindingCode: binding)
mfa().verify(oobCode: code, bindingCode: binding, mfaToken: token)
authentication().login(withRecoveryCode: code, mfaToken: token)
mfa().verify(recoveryCode: code, mfaToken: token)
authentication().multifactorChallenge(mfaToken: token, types: types, authenticatorId: id)
mfa().challenge(with: id, mfaToken: token)
mfaToken
本身
仍来自相同的地方——当
error.isMultifactorRequired == true
时,
AuthenticationError
会通过
error.mfaRequiredErrorPayload?.mfaToken
返回该令牌。

OTP(TOTP认证器应用):
swift
// v2
Auth0.authentication()
    .login(withOTP: otpCode, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error): showError(error)
        }
    }

// v3 — verify返回any TokenRequestable<Credentials, MFAVerifyError>
Auth0.mfa()
    .verify(otp: otpCode, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error): showError(error)
        }
    }

// async/await
let credentials = try await Auth0.mfa().verify(otp: otpCode, mfaToken: mfaToken).start()

OOB(短信/邮件验证码):
swift
// v2
Auth0.authentication()
    .login(withOOBCode: oobCode, mfaToken: mfaToken, bindingCode: bindingCode)
    .start { result in ... }

// v3 — 参数顺序变更:oobCode在前,bindingCode在后
Auth0.mfa()
    .verify(oobCode: oobCode, bindingCode: bindingCode, mfaToken: mfaToken)
    .start { result in ... }

恢复码:
swift
// v2
Auth0.authentication()
    .login(withRecoveryCode: recoveryCode, mfaToken: mfaToken)
    .start { result in ... }

// v3
Auth0.mfa()
    .verify(recoveryCode: recoveryCode, mfaToken: mfaToken)
    .start { result in ... }

MFA挑战(请求发送OOB验证码):
swift
// v2
Auth0.authentication()
    .multifactorChallenge(mfaToken: mfaToken,
                          types: ["oob"],
                          authenticatorId: authenticatorId)
    .start { result in ... }

// v3 — 移除types参数;直接传入authenticatorId
Auth0.mfa()
    .challenge(with: authenticatorId, mfaToken: mfaToken)
    .start { result in ... }

处理MFA所需错误以获取mfaToken(v2和v3中不变):
swift
Auth0.authentication()
    .login(usernameOrEmail: email,
           password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience,
           scope: scope)
    .start { result in
        switch result {
        case .success(let credentials):
            storeCredentials(credentials)
        case .failure(let error) where error.isMultifactorRequired:
            // mfaToken在v2和v3中的提取方式相同
            if let mfaToken = error.mfaRequiredErrorPayload?.mfaToken {
                presentMFAChallenge(mfaToken: mfaToken)
            }
        case .failure(let error):
            showError(error)
        }
    }

错误类型变更:
AuthenticationError
MFAVerifyError
MFAClient
上的verify方法返回
any TokenRequestable<Credentials, MFAVerifyError>
。如果项目之前在MFA失败处理程序中匹配特定
AuthenticationError
枚举值,需将其映射到
MFAVerifyError
swift
// v2 — MFA失败以AuthenticationError形式返回
Auth0.authentication()
    .login(withOTP: otp, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error as AuthenticationError):
            if error.isMultifactorCodeInvalid {
                showError("验证码无效,请重试。")
            } else {
                showError(error.debugDescription)
            }
        }
    }

// v3 — 失败以MFAVerifyError形式返回;获取MFAErrors.swift查看所有枚举值
Auth0.mfa()
    .verify(otp: otp, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error):
            // 查看目标SDK版本中Auth0/MFA/MFAErrors.swift里的MFAVerifyError枚举值
            // 获取可用的精确枚举值名称
            showError(error.debugDescription)
        }
    }
从目标标签(步骤3)获取
Auth0/MFA/MFAErrors.swift
并读取
MFAVerifyError
枚举值,以映射项目当前处理的特定错误。不要猜测错误枚举值名称——从源码中读取。

MFAClient
的测试mock:
如果项目的测试目标存在符合
MFAClient
协议的mock,需更新方法返回类型并在
start(_:)
中添加
@MainActor
(与§6.4中
Authentication
mock的模式相同):
swift
// v3 — 测试中的mock MFAClient
struct MockMFAClient: MFAClient {
    var verifyResult: Result<Credentials, MFAVerifyError>

    func verify(otp: String,
                mfaToken: String) -> any TokenRequestable<Credentials, MFAVerifyError> {
        return MockTokenRequest(result: verifyResult)
    }

    func verify(oobCode: String,
                bindingCode: String?,
                mfaToken: String) -> any TokenRequestable<Credentials, MFAVerifyError> {
        return MockTokenRequest(result: verifyResult)
    }

    func verify(recoveryCode: String,
                mfaToken: String) -> any TokenRequestable<Credentials, MFAVerifyError> {
        return MockTokenRequest(result: verifyResult)
    }

    func challenge(with authenticatorId: String,
                   mfaToken: String) -> any Requestable<MFAChallenge, MfaChallengeError> {
        // 从目标标签获取MFAClient.swift以找到MFAChallenge的初始化方法,
        // 然后构造真实的测试数据或返回.failure(如果测试不涉及该路径)
        return MockRequest(result: .failure(/* MFAErrors.swift中的MfaChallengeError枚举值 */))
    }
    // 使用相同模式实现剩余的MFAClient要求
}
使用§6.4中的
MockTokenRequest
MockRequest
结构体。
MFAClient
协议还要求实现
getAuthenticators
enroll(mfaToken:phoneNumber:)
enroll(mfaToken:)
enroll(mfaToken:email:)
——使用相同方式stub这些方法,返回类型参考
MFAClient.swift
在步骤9的总结中列出所有迁移的MFA流程,并要求用户端到端重新测试每个MFA流程(OTP、OOB、恢复码、挑战请求)以匹配租户配置。

6.10 —
Credentials.expiresIn
Credentials.expiresAt

6.14 — 默认scope现在包含
offline_access

Applies if: Step 4 found any access to
.expiresIn
on a
Credentials
,
APICredentials
, or
SSOCredentials
object.
The
expiresIn: Date
property on
Credentials
,
APICredentials
, and
SSOCredentials
was renamed to
expiresAt: Date
. The underlying JSON key is unchanged; only the Swift property name changed.
swift
// v2
let expiry: Date = credentials.expiresIn

// v3
let expiry: Date = credentials.expiresAt

适用场景: 步骤4找到任何对
webAuth()
webAuth(domain:)
webAuth(domain:clientId:)
的调用——但仅适用于未链式调用
.scope(…)
的调用链。读取文件中的实际调用点以确认是否存在
.scope(
;不要仅通过grep查找——调用链可能跨多行。
在v3版本中,默认scope从
"openid profile email"
变更为
"openid profile email offline_access"
。依赖默认配置且不需要刷新令牌的应用应添加显式的
.scope()
调用:
swift
// v2 — 默认scope: "openid profile email"(无刷新令牌)
Auth0.webAuth()
    .audience("https://api.example.com")
    .start { result in ... }

// v3 — 默认scope包含offline_access(会返回刷新令牌)
// 如果要保持v2的行为(无刷新令牌),需显式添加.scope():
Auth0.webAuth()
    .audience("https://api.example.com")
    .scope("openid profile email")  // 显式设置——不包含offline_access
    .start { result in ... }

// 如果欢迎使用刷新令牌(推荐——支持静默续期):
// 无需修改;新的默认配置是有意设计的。
无论选择哪种路径,都要在步骤9的总结中说明这是一个行为变更——如果要颁发刷新令牌,Auth0租户必须允许该应用的离线访问。

6.11 —
CredentialsStorage
custom implementation — methods now throw

6.15 —
CredentialsManager.credentials()
— 默认
minTTL
从0秒改为60秒

Applies if: Step 4 found a type conforming to
CredentialsStorage
in the project's source files. Skip if the project only passes a
SimpleKeychain
instance — the default storage needs no change.
Only applies if the project provides a custom
CredentialsStorage
implementation (i.e., a type conforming to the protocol — not just using the default
SimpleKeychain
). Skip if the project only passes a
SimpleKeychain
instance.
The protocol changed from Bool/Data? returns to throwing methods, and added a new required
deleteAllEntries()
.
swift
// v2 — protocol conformance
final class AppKeychain: CredentialsStorage {
    func getEntry(forKey key: String) -> Data? {
        return Keychain.shared.read(key: key)
    }

    func setEntry(_ data: Data, forKey key: String) -> Bool {
        return Keychain.shared.write(data, forKey: key)
    }

    func deleteEntry(forKey key: String) -> Bool {
        return Keychain.shared.delete(key: key)
    }
}

// v3 — methods throw; deleteAllEntries() required
final class AppKeychain: CredentialsStorage {
    func getEntry(forKey key: String) throws -> Data {
        guard let data = Keychain.shared.read(key: key) else {
            throw CredentialsManagerError.noCredentials
        }
        return data
    }

    func setEntry(_ data: Data, forKey key: String) throws {
        guard Keychain.shared.write(data, forKey: key) else {
            throw CredentialsManagerError.storeFailed
        }
    }

    func deleteEntry(forKey key: String) throws {
        guard Keychain.shared.delete(key: key) else {
            throw CredentialsManagerError.revokeFailed
        }
    }

    func deleteAllEntries() throws {
        Keychain.shared.deleteAll()
    }
}
The
CredentialsStorage
protocol declares its methods as
throws
with no specific error type — you can throw any
Error
. The example above uses
CredentialsManagerError
cases for illustration only; your implementation should throw an error type that makes sense for your storage backend. Verify the
CredentialsManagerError
case names in the SDK source fetched in Step 3 if you choose to reuse them.

适用场景: 步骤4找到任何未显式指定
minTTL:
参数的
credentialsManager.credentials(
调用。
在v3版本中,
CredentialsManager.credentials(withScope:minTTL:parameters:headers:callback:)
的默认
minTTL
0
改为
60
。这意味着凭据管理器现在会在令牌实际过期前60秒就认为令牌已过期——并触发静默刷新,而不是仅在令牌已过期时才刷新。
这是一个静默行为变更:应用无需修改即可编译,但令牌续期的时机比之前更早。
swift
// v2 — credentials()仅在令牌实际过期时触发续期(minTTL默认值:0)
credentialsManager.credentials { result in
    switch result {
    case .success(let credentials): use(credentials)
    case .failure(let error): handleError(error)
    }
}

// v3 — credentials()在令牌过期前60秒触发续期(minTTL默认值:60)
// 如果该行为可接受(大多数应用推荐),无需修改代码。
// 要显式恢复v2的行为:
credentialsManager.credentials(minTTL: 0) { result in
    switch result {
    case .success(let credentials): use(credentials)
    case .failure(let error): handleError(error)
    }
}
对于大多数应用,新的默认配置更优——在令牌过期前稍早续期可避免飞行中请求使用中途过期的访问令牌的情况。仅当应用有特定理由需要在精确过期时间续期时,才显式设置
minTTL: 0
在步骤9的总结中说明这是一个行为注意事项

6.12 — Management client removed

步骤7 — 更新依赖并构建

Applies if: Step 4 found any call to
Auth0.users(
or
Auth0.users(token:
in the project's source files.
Auth0.users(token:)
and the entire
Users
management client were removed from the SDK in v3. Do not silently delete any call sites — add a
TODO
comment and surface this in the migration summary.
swift
// v2 — direct Management API call in the app
Auth0
    .users(token: managementToken)
    .patch(userId, userPatch: UserPatchAttributes(name: newName))
    .start { result in
        switch result {
        case .success: print("Updated")
        case .failure(let error): print(error)
        }
    }

// v3 — Management client removed; add TODO and preserve intent
// TODO: Auth0.swift v3 removed the Management client.
// Replace this with a call to your own backend endpoint, which
// calls the Auth0 Management API using a machine-to-machine token.
// NEVER embed a Management API token in the client app.
// See: https://auth0.com/docs/secure/tokens/access-tokens/management-api-access-tokens
This requires backend work — record it in the Step 9 summary.

bash
undefined

6.13 — MFA methods removed from
Authentication
→ migrate to
MFAClient

尝试构建——预期会有剩余调用点的错误

Applies if: Step 4 found any call to
login(withOTP:
,
login(withOOBCode:
,
login(withRecoveryCode:
, or
multifactorChallenge(
— or test mocks conforming to
MFAClient
— in the project's source files.
The four MFA methods on the
Authentication
protocol were removed in v3. They are replaced by the dedicated
MFAClient
protocol, accessible via
Auth0.mfa()
:
v2 (
Authentication
)
v3 (
MFAClient
)
authentication().login(withOTP: otp, mfaToken: token)
mfa().verify(otp: otp, mfaToken: token)
authentication().login(withOOBCode: code, mfaToken: token, bindingCode: binding)
mfa().verify(oobCode: code, bindingCode: binding, mfaToken: token)
authentication().login(withRecoveryCode: code, mfaToken: token)
mfa().verify(recoveryCode: code, mfaToken: token)
authentication().multifactorChallenge(mfaToken: token, types: types, authenticatorId: id)
mfa().challenge(with: id, mfaToken: token)
The
mfaToken
itself
still comes from the same place — an
AuthenticationError
where
error.isMultifactorRequired == true
returns the token via
error.mfaRequiredErrorPayload?.mfaToken
.

OTP (TOTP authenticator app):
swift
// v2
Auth0.authentication()
    .login(withOTP: otpCode, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error): showError(error)
        }
    }

// v3 — verify returns any TokenRequestable<Credentials, MFAVerifyError>
Auth0.mfa()
    .verify(otp: otpCode, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error): showError(error)
        }
    }

// async/await
let credentials = try await Auth0.mfa().verify(otp: otpCode, mfaToken: mfaToken).start()

OOB (SMS / email code):
swift
// v2
Auth0.authentication()
    .login(withOOBCode: oobCode, mfaToken: mfaToken, bindingCode: bindingCode)
    .start { result in ... }

// v3 — parameter order changed: oobCode first, bindingCode second
Auth0.mfa()
    .verify(oobCode: oobCode, bindingCode: bindingCode, mfaToken: mfaToken)
    .start { result in ... }

Recovery code:
swift
// v2
Auth0.authentication()
    .login(withRecoveryCode: recoveryCode, mfaToken: mfaToken)
    .start { result in ... }

// v3
Auth0.mfa()
    .verify(recoveryCode: recoveryCode, mfaToken: mfaToken)
    .start { result in ... }

MFA challenge (request an OOB code to be sent):
swift
// v2
Auth0.authentication()
    .multifactorChallenge(mfaToken: mfaToken,
                          types: ["oob"],
                          authenticatorId: authenticatorId)
    .start { result in ... }

// v3 — types parameter removed; pass authenticatorId directly
Auth0.mfa()
    .challenge(with: authenticatorId, mfaToken: mfaToken)
    .start { result in ... }

Handling the MFA required error to obtain the mfaToken (unchanged between v2 and v3):
swift
Auth0.authentication()
    .login(usernameOrEmail: email,
           password: password,
           realmOrConnection: "Username-Password-Authentication",
           audience: audience,
           scope: scope)
    .start { result in
        switch result {
        case .success(let credentials):
            storeCredentials(credentials)
        case .failure(let error) where error.isMultifactorRequired:
            // mfaToken extracted the same way in both v2 and v3
            if let mfaToken = error.mfaRequiredErrorPayload?.mfaToken {
                presentMFAChallenge(mfaToken: mfaToken)
            }
        case .failure(let error):
            showError(error)
        }
    }

Error type changed:
AuthenticationError
MFAVerifyError
The verify methods on
MFAClient
return
any TokenRequestable<Credentials, MFAVerifyError>
. If the project previously matched specific
AuthenticationError
cases in MFA failure handlers, map them onto
MFAVerifyError
:
swift
// v2 — MFA failures came as AuthenticationError
Auth0.authentication()
    .login(withOTP: otp, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error as AuthenticationError):
            if error.isMultifactorCodeInvalid {
                showError("Invalid code. Please try again.")
            } else {
                showError(error.debugDescription)
            }
        }
    }

// v3 — failures come as MFAVerifyError; fetch MFAErrors.swift for all cases
Auth0.mfa()
    .verify(otp: otp, mfaToken: mfaToken)
    .start { result in
        switch result {
        case .success(let credentials): storeCredentials(credentials)
        case .failure(let error):
            // Check the MFAVerifyError cases in Auth0/MFA/MFAErrors.swift
            // for the exact case names available in the target SDK version
            showError(error.debugDescription)
        }
    }
Fetch
Auth0/MFA/MFAErrors.swift
from the target tag (Step 3) and read the
MFAVerifyError
cases to map any specific error handling the project currently does. Do not guess error case names — read them from the source.

Test mocks for
MFAClient
:
If the project's test target has a mock conforming to
MFAClient
, update method return types and add
@MainActor
to
start(_:)
(same pattern as §6.4 for
Authentication
mocks):
swift
// v3 — mock MFAClient in tests
struct MockMFAClient: MFAClient {
    var verifyResult: Result<Credentials, MFAVerifyError>

    func verify(otp: String,
                mfaToken: String) -> any TokenRequestable<Credentials, MFAVerifyError> {
        return MockTokenRequest(result: verifyResult)
    }

    func verify(oobCode: String,
                bindingCode: String?,
                mfaToken: String) -> any TokenRequestable<Credentials, MFAVerifyError> {
        return MockTokenRequest(result: verifyResult)
    }

    func verify(recoveryCode: String,
                mfaToken: String) -> any TokenRequestable<Credentials, MFAVerifyError> {
        return MockTokenRequest(result: verifyResult)
    }

    func challenge(with authenticatorId: String,
                   mfaToken: String) -> any Requestable<MFAChallenge, MfaChallengeError> {
        // Fetch MFAClient.swift from the target tag to find MFAChallenge's initializer,
        // then construct a real fixture or return .failure for tests that don't exercise this path
        return MockRequest(result: .failure(/* MfaChallengeError case from MFAErrors.swift */))
    }
    // implement remaining MFAClient requirements using the same pattern
}
Use the
MockTokenRequest
and
MockRequest
structs from §6.4. The
MFAClient
protocol also requires
getAuthenticators
,
enroll(mfaToken:phoneNumber:)
,
enroll(mfaToken:)
, and
enroll(mfaToken:email:)
— stub them the same way, using the return types from
MFAClient.swift
.
List all migrated MFA flows in the Step 9 summary and ask the user to re-test every MFA flow end-to-end (OTP, OOB, recovery code, challenge request) against their tenant configuration.

xcodebuild build
-scheme <SCHEME>
-destination "platform=iOS Simulator,name=${SIM}"
2>&1

针对每个错误:

1. 读取错误信息并定位源码行
2. 将其匹配到步骤6中的某个API变更
3. 验证修复内容与步骤3获取的实际SDK签名一致
4. 按照项目现有风格应用修复
5. 重新构建

**常见错误→原因映射:**

| Xcode错误 | 可能原因 |
|---|---|
| `has no member 'clearSession'` | §6.1 — 重命名为`logout` |
| `error enum element 'pkceNotAllowed' not found in type`或`'invalidInvitationURL' not found` | §6.2 — 从switch中删除已移除的`WebAuthError`枚举值 |
| `cannot convert return expression of type 'Request<...>'`在mock中 | §6.4 — 更新mock返回类型为`any TokenRequestable<T,E>`或`any Requestable<T,E>` |
| `does not conform to protocol 'Requestable'`(`start`缺少`@MainActor`) | §6.4 — 在mock的`start(_:)`回调中添加`@MainActor` |
| `has no member 'user'` on CredentialsManager | §6.7 — 改为`userProfile()` |
| `cannot find type 'UserInfo'` | §6.9 — 重命名为`UserProfile` |
| `has no member 'expiresIn'` | §6.10 — 重命名为`expiresAt` |
| `cannot convert value of type 'Bool'` on store/clear | §6.5/§6.6 — 添加do-catch或try? |
| `does not conform to protocol 'CredentialsStorage'` | §6.11 — 更新协议方法 + 添加deleteAllEntries |
| `call can throw, but is not marked with 'try'` | 包裹在do-catch中或添加try? |
| `sending '...' risks causing data races` | 仅当项目使用Swift 6语言模式或`SWIFT_STRICT_CONCURRENCY=complete`时出现;在现有actor模型内解决——不属于迁移错误 |

**限制:** 最多进行**10次构建-修复循环**。如果10次尝试后构建仍失败,停止操作并向用户显示剩余错误及上下文——不要猜测。

---

6.14 — Default scope now includes
offline_access

步骤8 — 运行测试并验证

Applies if: Step 4 found any call to
webAuth()
,
webAuth(domain:)
, or
webAuth(domain:clientId:)
— but only for call chains that do not already have a
.scope(…)
modifier. Read the actual call site in the file to confirm whether
.scope(
is present; do not grep — the call chain may span multiple lines.
In v3, the default scope changed from
"openid profile email"
to
"openid profile email offline_access"
. Apps that relied on the default and do not want a refresh token should add an explicit
.scope()
call:
swift
// v2 — default scope: "openid profile email" (no refresh token)
Auth0.webAuth()
    .audience("https://api.example.com")
    .start { result in ... }

// v3 — default scope includes offline_access (refresh token returned)
// If you want to keep the v2 behaviour (no refresh token), add .scope() explicitly:
Auth0.webAuth()
    .audience("https://api.example.com")
    .scope("openid profile email")  // explicit — no offline_access
    .start { result in ... }

// If refresh tokens are welcome (recommended — enables silent renewal):
// No change needed; the new default is intentional.
Surface this as a behavioural change in the Step 9 summary regardless of which path is chosen — the Auth0 tenant must permit offline access for this app if refresh tokens are to be issued.

bash
undefined

6.15 —
CredentialsManager.credentials()
— default
minTTL
changed from 0 to 60 seconds

如果存在测试套件则运行(复用步骤1中的$SIM)

Applies if: Step 4 found any call to
credentialsManager.credentials(
without an explicit
minTTL:
parameter.
In v3,
CredentialsManager.credentials(withScope:minTTL:parameters:headers:callback:)
defaults
minTTL
to
60
instead of
0
. This means the credentials manager will now consider tokens expired — and trigger a silent refresh — 60 seconds before their actual expiry, rather than only when they are already expired.
This is a silent behavioural change: the app still compiles without changes, but token renewal now happens earlier than before.
swift
// v2 — credentials() triggers renewal only when token is actually expired (minTTL default: 0)
credentialsManager.credentials { result in
    switch result {
    case .success(let credentials): use(credentials)
    case .failure(let error): handleError(error)
    }
}

// v3 — credentials() triggers renewal 60 seconds before expiry (minTTL default: 60)
// No code change needed if this behaviour is acceptable (recommended for most apps).
// To restore the v2 behaviour explicitly:
credentialsManager.credentials(minTTL: 0) { result in
    switch result {
    case .success(let credentials): use(credentials)
    case .failure(let error): handleError(error)
    }
}
For most apps the new default is preferable — renewing tokens slightly before expiry avoids races where an in-flight request uses an access token that expires mid-request. Only set
minTTL: 0
explicitly if the app has a specific reason to renew only at exact expiry.
Surface this as a behavioural note in the Step 9 summary.

xcodebuild test
-scheme <SCHEME>
-destination "platform=iOS Simulator,name=${SIM}"
2>&1 | tail -30

由相同API变更导致的测试失败(错误的类型名称、缺失的方法)应使用与步骤7相同的规则修复。需要超出API更新的逻辑变更的测试失败应标记给用户。

```bash

Step 7 — Update the Dependency & Build

总结变更差异

bash
undefined
git diff --stat

---

Attempt a build — expect errors for any remaining call sites

步骤9 — 迁移总结

xcodebuild build
-scheme <SCHEME>
-destination "platform=iOS Simulator,name=${SIM}"
2>&1

For each error:

1. Read the error and locate the source line
2. Match it to one of the API changes in Step 6
3. Verify the fix matches the actual SDK signature fetched in Step 3
4. Apply the fix in keeping with the project's existing style
5. Rebuild

**Common error → cause mapping:**

| Xcode error | Likely cause |
|---|---|
| `has no member 'clearSession'` | §6.1 — rename to `logout` |
| `error enum element 'pkceNotAllowed' not found in type` or `'invalidInvitationURL' not found` | §6.2 — remove deleted `WebAuthError` cases from switch |
| `cannot convert return expression of type 'Request<...>'` in mock | §6.4 — update mock return type to `any TokenRequestable<T,E>` or `any Requestable<T,E>` |
| `does not conform to protocol 'Requestable'` (missing `@MainActor` on `start`) | §6.4 — add `@MainActor` to `start(_:)` callback in mock |
| `has no member 'user'` on CredentialsManager | §6.7 — change to `userProfile()` |
| `cannot find type 'UserInfo'` | §6.9 — rename to `UserProfile` |
| `has no member 'expiresIn'` | §6.10 — rename to `expiresAt` |
| `cannot convert value of type 'Bool'` on store/clear | §6.5/§6.6 — add do-catch or try? |
| `does not conform to protocol 'CredentialsStorage'` | §6.11 — update protocol methods + add deleteAllEntries |
| `call can throw, but is not marked with 'try'` | wrap in do-catch or add try? |
| `sending '...' risks causing data races` | only appears when the project uses Swift 6 language mode or `SWIFT_STRICT_CONCURRENCY=complete`; resolve within the existing actor model — not a migration error |

**Limit:** Up to **10 build-fix cycles**. If the build still fails after 10 attempts, stop and show the remaining errors to the user with context — do not guess.

---
呈现简洁的总结,涵盖以下内容:
1. 已应用的变更(按API领域分组;列出每个领域修改的文件)
2. 需要手动检查的内容
  • 所有错误处理变更——确认新错误类型已正确路由
  • 所有使用
    try?
    忽略错误的场景(项目之前忽略Bool返回值)——询问是否需要显式错误处理
  • offline_access
    默认scope变更——确认租户已配置允许离线访问,或确认显式scope调用正确
3. 后端/配置后续工作(仅在触发时列出)
  • WebAuthError枚举值变更(§6.2): 列出从switch语句中删除的已移除枚举值和添加的新枚举值。注意
    .authenticationFailed
    .codeExchangeFailed
    可能需要修改面向用户的文案。
  • mocks中
    Request
    Requestable
    (§6.4):
    列出更新的测试mock文件。注意任何使用
    return self
    stub的
    TokenRequestable
    构建器方法——确认这对涉及的测试是正确的。
  • 新增错误路径(§6.8): 列出项目调用的CredentialsManager异步方法,并说明现在可能出现的新错误:
    • revoke()
      .noCredentials
      (无凭据可撤销)、
      .revokeFailed
      (服务端撤销失败)、
      .clearFailed
      (令牌已撤销但Keychain删除失败)
    • credentials()
      /
      renew()
      /
      apiCredentials()
      /
      ssoCredentials()
      .noCredentials
      (Keychain项未找到)、
      .renewFailed
      (刷新令牌续期失败)、
      .storeFailed
      (续期后的凭据无法保存)
    • 确认每个场景的失败处理是否正确导航或暴露错误。
  • 移除Management客户端(§6.12): 列出使用
    TODO
    stub的具体操作。说明用户必须在安全后端实现的内容。
  • 移除MFA方法(§6.13): 列出需要更新到
    MFAClient
    的MFA流程。要求用户端到端重新测试MFA。
  • 默认scope变更(§6.14): 说明是否已显式添加
    .scope()
    或接受了新的
    offline_access
    默认配置。确认租户已配置允许离线访问。
  • 默认minTTL变更(§6.15): 说明
    credentialsManager.credentials()
    现在会在令牌过期前60秒续期,而非精确过期时间。确认该行为可接受或已显式设置
    minTTL: 0
4. 未应用的可选改进(简要列出;绝不自动应用)
  • CredentialsManager
    上的新
    clearAll()
    方法——一键清除所有凭据
  • 新的
    MFAClient
    API——如果项目使用MFA且旧方法已移除
  • DPoP(Demonstrating Proof of Possession)支持——如果API要求发送方约束令牌
  • Passkey登录/注册API(iOS 16.6+、macOS 13.5+)
  • ssoCredentials()
    ——如果需要SSO凭据交换
5. 询问用户 是否要提交迁移变更、探索任何可选改进,或一起查看特定文件。
安全提醒: 不要在总结输出中包含令牌、密钥、客户端凭据或Keychain值。

Step 8 — Run Tests & Verify

详细参考

bash
undefined
  • 迁移流程 — 跨版本跳转、回滚、CocoaPods/Carthage边缘情况、Swift版本兼容性
  • 安全检查清单 — 迁移前后必须保持的安全准则

Run the test suite if one exists (reuse $SIM from Step 1)

常见错误

xcodebuild test
-scheme <SCHEME>
-destination "platform=iOS Simulator,name=${SIM}"
2>&1 | tail -30

Test failures caused by the same API changes (wrong type name, missing method) should be fixed using the same rules as Step 7. Test failures that require logic changes beyond API updates should be flagged for the user.

```bash
错误做法正确做法
步骤4未在项目中找到对应API仍应用§6.x部分步骤4的文件读取是唯一依据。未找到则完全跳过该部分
仅使用grep判断API是否被使用Grep会遗漏多行调用链、带有
domain:clientId:
参数的调用和变量别名。读取实际文件
项目未使用
CredentialsManager
却修改相关代码
仅迁移项目实际调用的内容
移除非Auth0代码周围的
DispatchQueue.main
包装器
仅移除Auth0回调体内的调度包装器
静默删除Management API调用点添加
// TODO:
并在总结中说明——删除调用会破坏功能
静默删除旧MFA调用点同上——添加
TODO
并在总结中说明
基于假设知识而非获取的SDK源码应用变更每个修复必须追溯到步骤3获取的文件中的签名
当开发者选择beta标签时使用
from: "3.0.0"
稳定范围指定符无法解析beta版本;预发布版本使用
exact: "<TAG>"
在脏工作区启动迁移始终先验证
git status --porcelain
为空
未应用已知变更直接构建先应用所有已知变更,再构建以捕获剩余错误
超过10次构建失败循环仍继续停止操作并向用户显示剩余错误
跳过迁移总结始终生成完整总结——用户需要这些信息

Summarise the diff

相关技能

git diff --stat

---

Step 9 — Migration Summary

参考链接

Present a concise summary covering:
1. Changes applied (grouped by API area; list files touched per area)
2. Needs manual review
  • Every error-handling change — confirm the new error types are routed correctly
  • Every
    try?
    used to discard errors where the project previously discarded a
    Bool
    — ask if explicit error handling is wanted
  • The
    offline_access
    default scope change — confirm the tenant is configured to allow it, or confirm the explicit scope call is correct
3. Backend / configuration follow-up (only if triggered)
  • WebAuthError cases changed (§6.2): List which removed cases were deleted from switch statements and which new cases were added. Note that
    .authenticationFailed
    and
    .codeExchangeFailed
    may benefit from user-facing copy changes.
  • Request
    Requestable
    in mocks (§6.4):
    List which test mock files were updated. Note any
    TokenRequestable
    builder methods that were stubbed with
    return self
    — confirm this is correct for the tests involved.
  • New error paths (§6.8): List which CredentialsManager async methods the project calls and note the new errors that can now surface:
    • revoke()
      .noCredentials
      (nothing to revoke),
      .revokeFailed
      (server revocation failed),
      .clearFailed
      (token revoked but Keychain delete failed)
    • credentials()
      /
      renew()
      /
      apiCredentials()
      /
      ssoCredentials()
      .noCredentials
      (Keychain item not found),
      .renewFailed
      (refresh token renewal failed),
      .storeFailed
      (renewed credentials could not be saved)
    • Confirm the failure handling for each case navigates or surfaces errors correctly.
  • Management client removed (§6.12): List the specific operations that were stubbed with
    TODO
    . Describe what the user must implement on a secure backend.
  • MFA methods removed (§6.13): List which MFA flows need updating to
    MFAClient
    . Ask the user to re-test MFA end-to-end.
  • Default scope change (§6.14): Note whether
    .scope()
    was added explicitly or the new
    offline_access
    default was accepted. Confirm the tenant is configured to allow offline access.
  • Default minTTL change (§6.15): Note that
    credentialsManager.credentials()
    now renews tokens 60 seconds before expiry instead of at exact expiry. Confirm this is acceptable or that
    minTTL: 0
    was set explicitly.
4. Optional improvements not applied (list briefly; never auto-apply)
  • New
    clearAll()
    method on
    CredentialsManager
    — clears all credentials in one call
  • New
    MFAClient
    API — if the project uses MFA and the old methods were already removed
  • DPoP (Demonstrating Proof of Possession) support — if the API requires sender-constrained tokens
  • Passkey login/signup APIs (iOS 16.6+, macOS 13.5+)
  • ssoCredentials()
    — if SSO credential exchange is needed
5. Ask the user if they'd like to commit the migration changes, explore any optional improvement, or step through specific files together.
Security reminder: Never include tokens, secrets, client credentials, or Keychain values in the summary output.

安全提示: 不要在构建日志或终端输出中回显令牌、客户端密钥或凭据。不要将密钥提交到版本控制系统。

Detailed References

  • Migration Process — Multi-version jumps, rollback, CocoaPods/Carthage edge cases, Swift version compatibility
  • Security Checklist — Invariants that must hold before and after migration

Common Mistakes

MistakeCorrect approach
Applying a §6.x section when Step 4 didn't find that API in the projectStep 4 file-reading is the gate. Not found = skip the section entirely
Using grep alone to decide if an API is usedGrep misses multi-line call chains, calls with
domain:clientId:
params, and variable aliases. Read the actual files
Touching
CredentialsManager
when the project doesn't use it
Only migrate what the project actually calls
Removing
DispatchQueue.main
wrappers around non-Auth0 code
Only remove dispatch wrappers that are solely inside an Auth0 callback body
Silently deleting Management API call sitesAdd
// TODO:
and surface in the summary — removing the call breaks functionality
Silently deleting old MFA call sitesSame as above — add
TODO
and note in the summary
Applying changes based on assumed knowledge, not the fetched SDK sourceEvery fix must trace to a signature in the files fetched in Step 3
Pinning
from: "3.0.0"
when the developer chose a beta tag
Stable range specifiers won't resolve betas; use
exact: "<TAG>"
for pre-releases
Starting migration on a dirty working treeAlways verify
git status --porcelain
is empty first
Skipping straight to build without applying known changes firstApply all known changes first, then build to catch remainders
Continuing past 10 failed build cyclesStop and show the user the remaining errors
Skipping the migration summaryAlways produce the full summary — the user needs it

Related Skills


References

Security: Never echo tokens, client secrets, or credentials in build logs or terminal output. Never commit secrets to version control.