关于 auth0-swift-major-migration
用于将现有 Auth0.swift 集成从 v2 迁移到 v3 的技能。它先审查 API 使用情况,只应用相关变更,并遵循项目既有架构和约定。
主要功能
- 01审查 Swift 文件中的 Auth0 API 使用
- 02检查目标版本与依赖
- 03仅应用相关的破坏性变更
- 04保留既有代码风格和架构
- 05构建检查与迁移安全步骤
适用场景
- 01将 iOS 项目从 Auth0.swift v2 迁移到 v3
- 02只更新项目实际使用的 API
- 03准备干净且可回退的迁移
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ auth0/ agent-skills --skill auth0-swift-major-migration安装后在 Claude Code 中输入/auth0-swift-major-migration确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/process.mdsecurity.mdtests/benchmark-config.jsonevals.jsongraders.jsongraders.tspackage.jsonprompt.mdrun-evals.mjs详细介绍
Auth0.swift v3 迁移
将现有的 Auth0.swift v2 集成迁移到 v3。每项代码更改都必须先通过搜索确认项目确实调用了受影响的 API——如果项目从未使用 CredentialsManager,就不会改动任何 CredentialsManager 代码。更改会遵循项目现有的架构和 Apple 平台惯例。
不适用场景
- 全新集成 Auth0(没有现有的 Auth0.swift):使用 auth0-swift
- 次要版本/补丁版本更新(例如 2.17 → 2.18):运行
pod update Auth0或更新 SPM,无需迁移 - Android 应用:使用 auth0-android
- React Native / Expo:使用 auth0-react-native 或 auth0-expo
前置条件
- 已有 Auth0.swift v2 集成
- 已安装 Xcode;项目在当前版本下可以成功构建
- 项目使用 git 进行版本控制,且工作区干净
迁移工作流
Agent 指令: 按顺序执行每一步。目标是在改动尽可能小且正确的前提下,让构建成功。每个代码更改步骤都必须通过第 4 步的文件读取审查——如果在项目源文件中未找到该 API,则跳过该部分的整个步骤。绝不添加项目原本没有调用的代码。
步骤 1 — 预检与安全备份
# 1a. Verify clean working tree — stop if there are uncommitted changes
git status --porcelain
如果输出不为空,请询问用户:
“你有未提交的更改。要继续之前先将它们暂存起来(
git stash),还是希望先提交?”
# 1b. Create a safety branch the user can reset to at any time
git checkout -b auth0-v3-migration-backup
git checkout -
# 1c. Pick an available simulator, then confirm the project builds before touching anything
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
如果构建失败,请停止。先让用户修复现有问题。
步骤 2 — 检测当前版本和目标版本
从项目的依赖文件中检测当前的 Auth0.swift 版本:
# Check Package.resolved first (most reliable)
find . -name "Package.resolved" | xargs grep -A3 '"auth0/Auth0.swift"\|Auth0.swift"' 2>/dev/null | grep '"version"'
# Fallback: Podfile.lock
grep "^ - Auth0 " Podfile.lock 2>/dev/null
# Fallback: Cartfile.resolved
grep "auth0/Auth0.swift" Cartfile.resolved 2>/dev/null
# Fallback: Package.swift
grep -A2 'auth0/Auth0.swift' Package.swift 2>/dev/null
确定目标版本。 有两种方式:
方式 A — 用户传入了目标版本参数($ARGUMENTS):
使用前,先根据已发布的版本验证该版本。它必须通过以下全部三项检查:
# List all published Auth0.swift v3 release tags
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'])
"
- 版本存在 — 请求的标签出现在上方已发布的版本列表中。
- 主版本正确 — 标签属于 v3 主版本系列(以
3开头)。2.x或任何其他主版本都无效;请拒绝。 - 不是降级 — 该标签比项目中检测到的版本更新。
任何一项检查失败,都要停止并询问用户。 不要默默地使用默认版本。例如:
- “
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 版本(包括预发布版本):
# Newest v3.x release tag (stable or pre-release), most recent first
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 版本可供迁移。
步骤 3 — 获取并阅读 v3 SDK 源码
获取目标标签对应的实际 Swift 源码。此处的签名是第 6 步中所有更改的权威依据。
TAG=<TARGET_TAG> # the version the developer chose in Step 2, e.g. 3.0.0-beta.2
# List all public Swift files in the SDK
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
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
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 步中的每项更改都必须与这些文件中的实际签名相符。
第 4 步 — 审查项目使用了哪些 Auth0 API
查找所有导入 Auth0 的 Swift 文件——这些文件就是迁移范围:
grep -rl "import Auth0" --include="*.swift" .
阅读列表中的每个文件。 不要搜索特定的 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 使用显式 case 名称的 switch/catch | §6.2 — 已移除和新增的 case |
包装 Auth0 回调的 DispatchQueue.main.async 或 MainActor.run | §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 小节,其余部分全部跳过。
第 5 步 — 更新 SDK 依赖项
只应用对应的包管理器。
使用第 2 步中选定的 <TARGET_TAG>。对于稳定版(3.x.y,无后缀),使用范围指定符。对于预发布版本(3.x.y-beta.z),固定到确切标签——包管理器会将预发布版本视为超出 ~> / from: 规则范围。
Swift Package Manager(Package.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")
然后解析:
swift package resolve
CocoaPods(Podfile):
# Stable v3
pod 'Auth0', '~> 3.0'
# Pre-release / specific beta — pin the exact version
pod 'Auth0', '3.0.0-beta.2'
然后:
pod update Auth0
Carthage(Cartfile):
# Stable v3
github "auth0/Auth0.swift" ~> 3.0
# Pre-release / specific beta — pin the exact tag
github "auth0/Auth0.swift" "3.0.0-beta.2"
然后:
carthage update Auth0.swift --use-xcframeworks
由 Xcode 管理的 SPM(根目录下没有 Package.swift):
- 稳定版: 依次点击 File → Packages → Update to Latest Package Versions,然后确认版本规则为从 3.0.0 开始的 Up to Next Major。
- 预发布版 / 指定 beta: 除非依赖项已固定到确切版本,否则 File → Packages → Update to Latest Package Versions 不会解析 beta。告知用户将版本规则更改为 Exact Version,并输入
3.0.0-beta.2(或选定的标签)。
暂时不要构建——先应用所有已知的代码更改。
第 6 步 — 应用破坏性变更
Agent 指令: 只处理第 4 步文件阅读审查中匹配的 §6.x 小节。跳过项目未使用其 API 的所有小节——不要修改这些文件。
严格按照所示内容应用每项更改。不要修改周边代码、重命名变量、重新格式化,也不要现代化与迁移无关的代码。遵循项目现有风格:完成处理程序 → 完成处理程序,async/await → async/await,Combine → Combine。
6.1 — WebAuth.clearSession() → WebAuth.logout()
**适用条件:**第 4 步在项目源文件中发现了对 .clearSession( 的调用。
clearSession(federated:) 方法已重命名为 logout(federated:)。参数及其默认值均未更改。
完成处理程序:
// 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:
// v2
try await Auth0.webAuth().clearSession()
// v3
try await Auth0.webAuth().logout()
Combine:
// v2
Auth0.webAuth().clearSession()
.sink(receiveCompletion: { ... }, receiveValue: { ... })
.store(in: &cancellables)
// v3
Auth0.webAuth().logout()
.sink(receiveCompletion: { ... }, receiveValue: { ... })
.store(in: &cancellables)
**使用 federated: true 时:**参数名称相同,只需重命名方法:
// v2
try await Auth0.webAuth().clearSession(federated: true)
// v3
try await Auth0.webAuth().logout(federated: true)
6.2 — WebAuthError — 穷尽式 switch 语句中已移除和新增的 case
**适用条件:**第 4 步在项目源文件中发现了针对 WebAuthError 的 switch 或 catch,且其中显式指定了 case 名称。
v3 中移除了两个 WebAuthError case。如果项目对 WebAuthError 使用穷尽式 switch(或显式匹配这些 case),构建将失败。
新增了三个 case,用于报告之前隐藏的失败。
已移除的 case(匹配时将无法再编译):
| v2 case | v3 中的行为 |
|---|---|
.invalidInvitationURL | 已移除 — 现在会显示为 .unknown |
.pkceNotAllowed | 已移除 — 现在会显示为 .unknown |
新增的 case(现在可能出现在 catch/switch 代码块中):
| v3 case | 触发时机 |
|---|---|
.authenticationFailed | 服务端失败:密码错误、需要 MFA、账户已锁定等 |
.codeExchangeFailed | 令牌交换失败:网络问题、授权无效、后端错误 |
.credentialsManagerError | 登录/登出后,凭据管理器存储或清除凭据失败;可通过 .cause 访问底层错误 |
迁移 — 从 switch 语句中移除已删除的 case:
// 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)
}
}
}
如果项目使用 async/await 并捕获特定 case:
// 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)
}
无需显式处理新增的
.authenticationFailed和.codeExchangeFailedcase;已有的default:分支可以捕获它们。只有当项目希望针对这些失败显示不同的 UI 或遥测信息时,才添加显式 case。
6.3 — 移除 WebAuth 和 CredentialsManager 回调周围多余的主线程调度
**适用条件:**第 4 步发现有 DispatchQueue.main.async 或 MainActor.run 包裹 Auth0 回调代码块。
在 v3 中,所有完成处理程序回调、Combine 发布者和 async/await 方法都会在主线程上返回结果(它们都标记为 @MainActor)。不再需要用 DispatchQueue.main.async { } 或 await MainActor.run { } 包裹回调代码块,可以将其移除。
完成处理程序回调 — 移除调度包装:
// 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 — 移除 MainActor.run 包装:
// 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
仅移除唯一用途是保护 Auth0 回调代码块的调度包装。如果
DispatchQueue.main.async代码块还调度了无关的 UI 工作,只移除与 Auth0 回调有关的部分。
6.4 — Authentication / MFAClient 方法返回 Requestable 而非 Request — 应用代码和测试 Mock
适用于: 步骤 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<> 类型注解会导致应用代码无法编译:
// 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
现在返回 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,则需要进行两项更改:
- 返回类型: 将
Request<T, E>改为any TokenRequestable<T, E>(凭据方法)或any Requestable<T, E>(其他方法) start(_:)回调: 添加@MainActor,以匹配更新后的Requestable协议要求
// 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 }
}
上面的
MockTokenRequestStub 通过返回self,为所有TokenRequestable构建器方法提供 Stub 实现。在大多数测试中,不会调用validateClaims()和with*修饰方法,因此返回self是正确的。如果某个特定测试要验证声明验证行为,请为这些方法提供正确实现。
6.5 — CredentialsManager.store(credentials:) — 返回 Bool → 抛出错误
适用于: 步骤 4 在项目源文件中发现对 credentialsManager.store(credentials: 的任何调用。
此前,store(credentials:) 返回 Bool。在 v3 中,该方法在失败时抛出错误,成功时返回 Void。
如果项目检查了返回值:
// 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)
}
如果项目忽略了返回值:
// 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)
如果项目已有可接入的错误处理模式,请优先使用
do-catch,而非try?。只有在需要保留有意静默忽略错误的行为时,才使用try?。
6.6 — CredentialsManager.clear() 和 clear(forAudience:scope:) — 返回 Bool → 抛出错误
适用于: 步骤 4 在项目源文件中发现对 credentialsManager.clear() 或 credentialsManager.clear(forAudience: 的任何调用。
此前,这两个重载都返回 Bool。在 v3 中,二者都会抛出错误:
clear() throws— 清除主要存储的凭据clear(forAudience:scope:) throws— 清除特定受众的 API 凭据
// 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)
}
6.7 — CredentialsManager.user 属性 → 会抛出错误的 userProfile() 方法
适用于: 步骤 4 在项目源文件中发现将 credentialsManager.user 作为属性(而非方法调用)访问的情况。
user: UserInfo? 计算属性已替换为 userProfile() throws -> UserProfile?(类型重命名另见 §6.9)。
// 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
}
6.8 — CredentialsManager 异步方法 — throwing 存储带来的新错误路径
适用条件: 第 4 步在项目源文件中发现了对 credentialsManager.revoke( 的调用。
由于 CredentialsManager 的存储方法现在会抛出错误,多个异步方法新增了此前被静默吞掉的失败路径。其中最重要的是 revoke()。仅更新项目实际编写的错误处理代码——已使用 default: 分支的调用点无需更改。
现在可能从 revoke() 中出现的新错误:
| 新错误 | 触发时机 | 处理方式 |
|---|---|---|
.noCredentials | getEntry 抛出错误——存储中没有凭证,无需撤销 | 视为已退出登录;导航至登录页面 |
.revokeFailed | 撤销刷新令牌的网络调用失败 | 令牌可能仍在服务端有效;显示错误 |
.clearFailed | 撤销成功,但 Keychain 删除失败 | 视为已退出登录——令牌在服务端已失效 |
// 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)
}
}
}
现在可能从 credentials()、renew()、apiCredentials()、ssoCredentials() 中出现的新错误:
| 新错误 | 触发时机 |
|---|---|
.noCredentials | getEntry 抛出错误(例如,未找到 Keychain 项)——此前被 try? 吞掉 |
.renewFailed | 刷新令牌续期请求失败——网络错误、刷新令牌无效或已过期 |
.storeFailed | 保存续期后的凭证时,Keychain 写入失败 |
只有在项目现有的 catch/failure 处理程序需要区分这些情况时,这些错误才需要处理。如果使用通用回退逻辑,则无需更改。
// 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)
}
}
}
只有当项目当前对
CredentialsManagerError使用switch,且区分处理这些错误确有帮助时,才添加这些新的case分支。已有的default:分支无需更改即可正确处理这些错误。
6.9 — UserInfo → UserProfile 类型重命名
适用条件: 第 4 步在项目源文件中发现任何引用 UserInfo 的类型注解、函数签名或变量声明。
UserInfo 类型已重命名为 UserProfile。请更新所有引用 UserInfo 的类型注解、函数签名和变量声明。
// 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:),方法名保持不变,但返回类型已更改:
// 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 ... }
6.10 — Credentials.expiresIn → Credentials.expiresAt
适用条件: 第 4 步发现对 Credentials、APICredentials 或 SSOCredentials 对象的 .expiresIn 访问。
Credentials、APICredentials 和 SSOCredentials 上的 expiresIn: Date 属性已重命名为 expiresAt: Date。底层 JSON 键保持不变;只有 Swift 属性名发生了更改。
// v2
let expiry: Date = credentials.expiresIn
// v3
let expiry: Date = credentials.expiresAt
6.11 — CredentialsStorage 自定义实现——方法现在会抛出错误
适用条件: 第 4 步在项目源文件中发现了符合 CredentialsStorage 的类型。如果项目仅传入 SimpleKeychain 实例,则跳过——默认存储无需更改。
仅适用于项目提供了自定义 CredentialsStorage 实现的情况(即符合该协议的类型,而不只是使用默认的 SimpleKeychain)。如果项目仅传入 SimpleKeychain 实例,则跳过。
该协议已从返回 Bool/Data? 改为会抛出错误的方法,并新增了必需的 deleteAllEntries()。
// 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()
}
}
CredentialsStorage协议将其方法声明为throws,且未指定具体错误类型——你可以抛出任何Error。上面的示例仅使用CredentialsManagerError的 case 作为说明;你的实现应抛出适合存储后端的错误类型。如果选择复用这些错误,请在第 3 步获取的 SDK 源码中确认CredentialsManagerError的 case 名称。
6.12 — 已移除管理客户端
**适用条件:**如果步骤 4 在项目源文件中发现对 Auth0.users( 或 Auth0.users(token: 的调用。
v3 中已从 SDK 移除 Auth0.users(token:) 和整个 Users 管理客户端。不要悄悄删除任何调用位置——添加 TODO 注释,并在迁移摘要中说明此事。
// 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
这需要后端工作——在步骤 9 的摘要中记录此事。
6.13 — 已从 Authentication 移除 MFA 方法 → 迁移到 MFAClient
**适用条件:**如果步骤 4 在项目源文件中发现对 login(withOTP:、login(withOOBCode:、login(withRecoveryCode: 或 multifactorChallenge( 的调用,或者发现符合 MFAClient 的测试模拟对象。
v3 移除了 Authentication 协议上的四个 MFA 方法。它们由专用的 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 身份验证器应用):
// 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(短信 / 电子邮件验证码):
// 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 ... }
恢复码:
// v2
Auth0.authentication()
.login(withRecoveryCode: recoveryCode, mfaToken: mfaToken)
.start { result in ... }
// v3
Auth0.mfa()
.verify(recoveryCode: recoveryCode, mfaToken: mfaToken)
.start { result in ... }
MFA 挑战(请求发送 OOB 验证码):
// 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 ... }
处理 MFA 必需错误以获取 mfaToken(v2 和 v3 之间没有变化):
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)
}
}
错误类型已更改:AuthenticationError → MFAVerifyError
MFAClient 上的验证方法返回 any TokenRequestable<Credentials, MFAVerifyError>。如果项目之前在 MFA 失败处理程序中匹配了特定的 AuthenticationError 情况,请将其映射到 MFAVerifyError:
// 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)
}
}
从目标标签(步骤 3)获取
Auth0/MFA/MFAErrors.swift,并查看其中的MFAVerifyError情况,以便映射项目当前执行的任何特定错误处理。不要猜测错误情况名称——请从源代码中读取。
MFAClient 的测试模拟对象:
如果项目的测试目标中有符合 MFAClient 的模拟对象,请更新方法返回类型,并为 start(_:) 添加 @MainActor(与 §6.4 中 Authentication 模拟对象的模式相同):
// 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
}
使用 §6.4 中的
MockTokenRequest和MockRequest结构体。MFAClient协议还要求实现getAuthenticators、enroll(mfaToken:phoneNumber:)、enroll(mfaToken:)和enroll(mfaToken:email:)——以相同方式为它们提供存根,并使用MFAClient.swift中的返回类型。
在步骤 9 的摘要中列出所有已迁移的 MFA 流程,并请用户针对其租户配置端到端重新测试每个 MFA 流程(OTP、OOB、恢复码、挑战请求)。
6.14 — 默认作用域现在包含 offline_access
适用条件: 步骤 4 找到了对 webAuth()、webAuth(domain:) 或 webAuth(domain:clientId:) 的调用,但仅限于尚未使用 .scope(…) 修饰符的调用链。阅读文件中的实际调用位置,确认是否存在 .scope(;不要使用 grep——调用链可能跨越多行。
在 v3 中,默认作用域从 "openid profile email" 更改为 "openid profile email offline_access"。依赖默认作用域且不需要刷新令牌的应用应添加显式的 .scope() 调用:
// 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.
无论选择哪种处理方式,都要在步骤 9 的摘要中将其列为行为变更——如果要为此应用签发刷新令牌,Auth0 租户必须允许离线访问。
6.15 — CredentialsManager.credentials() — 默认 minTTL 从 0 秒更改为 60 秒
适用条件: 步骤 4 找到了未显式指定 minTTL: 参数的 credentialsManager.credentials( 调用。
在 v3 中,CredentialsManager.credentials(withScope:minTTL:parameters:headers:callback:) 的默认 minTTL 从 0 更改为 60。这意味着凭据管理器现在会在令牌实际过期前 60 秒将其视为已过期,并触发静默刷新;此前只有在令牌已过期时才会刷新。
这是一个静默行为变更:应用无需修改即可编译,但令牌续期会比以前更早发生。
// 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)
}
}
对大多数应用而言,新默认值更合适——在令牌过期前稍早续期,可避免正在进行的请求使用在请求过程中到期的访问令牌所引发的竞态。只有在应用有特定理由、必须等到令牌恰好过期才续期时,才显式设置 minTTL: 0。
在步骤 9 的摘要中将其列为行为说明。
步骤 7 — 更新依赖项并构建
# Attempt a build — expect errors for any remaining call sites
xcodebuild build \
-scheme <SCHEME> \
-destination "platform=iOS Simulator,name=${SIM}" \
2>&1
对于每条错误:
- 阅读错误信息并定位源代码行
- 将其与步骤 6 中的一项 API 变更对应起来
- 确认修复符合步骤 3 中获取的实际 SDK 签名
- 按照项目现有风格应用修复
- 重新构建
常见错误 → 可能原因对照表:
| Xcode 错误 | 可能原因 |
|---|---|
has no member 'clearSession' | §6.1 — 重命名为 logout |
error enum element 'pkceNotAllowed' not found in type 或 'invalidInvitationURL' not found | §6.2 — 从 switch 中移除已删除的 WebAuthError 分支 |
mock 中出现 cannot convert return expression of type 'Request<...>' | §6.4 — 将 mock 返回类型更新为 any TokenRequestable<T,E> 或 any Requestable<T,E> |
does not conform to protocol 'Requestable'(start 缺少 @MainActor) | §6.4 — 为 mock 中的 start(_:) 回调添加 @MainActor |
CredentialsManager 上出现 has no member 'user' | §6.7 — 改为 userProfile() |
cannot find type 'UserInfo' | §6.9 — 重命名为 UserProfile |
has no member 'expiresIn' | §6.10 — 重命名为 expiresAt |
store/clear 上出现 cannot convert value of type 'Bool' | §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 次后构建仍然失败,请停止并向用户展示剩余错误及相关背景信息——不要猜测。
步骤 8 — 运行测试并验证
# 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
由相同 API 变更引起的测试失败(类型名称错误、缺少方法)应按照步骤 7 中的相同规则修复。需要进行超出 API 更新范围的逻辑变更才能修复的测试失败,应标记出来并告知用户。
# Summarise the diff
git diff --stat
第 9 步 — 迁移摘要
简要总结以下内容:
1. 已应用的更改(按 API 区域分组;列出每个区域涉及的文件)
2. 需要人工审核
- 每项错误处理更改——确认新的错误类型已正确处理
- 每个用于丢弃错误、且项目此前会丢弃
Bool的try?——询问是否需要显式处理错误 offline_access默认作用域更改——确认租户已配置为允许该作用域,或确认显式的作用域调用正确
3. 后端 / 配置后续工作(仅在触发时)
- WebAuthError 案例已更改(§6.2): 列出从 switch 语句中删除了哪些已移除的案例,以及添加了哪些新案例。说明
.authenticationFailed和.codeExchangeFailed的面向用户文案可能需要调整。 - 模拟对象中的
Request→Requestable(§6.4): 列出已更新的测试模拟对象文件。注明任何以return self实现存根的TokenRequestable构建器方法——确认这对相关测试是否正确。 - 新增错误路径(§6.8): 列出项目调用的 CredentialsManager 异步方法,并说明现在可能出现的新错误:
revoke()—.noCredentials(无凭据可撤销)、.revokeFailed(服务器撤销失败)、.clearFailed(令牌已撤销,但 Keychain 删除失败)credentials()/renew()/apiCredentials()/ssoCredentials()—.noCredentials(未找到 Keychain 项目)、.renewFailed(刷新令牌续期失败)、.storeFailed(无法保存续期后的凭据)- 确认每种情况的失败处理都能正确导航或显示错误。
- 已移除 Management client(§6.12): 列出哪些具体操作以
TODO作为存根。说明用户必须在安全后端实现什么。 - 已移除 MFA 方法(§6.13): 列出哪些 MFA 流程需要更新为使用
MFAClient。请用户端到端重新测试 MFA。 - 默认作用域更改(§6.14): 说明是否显式添加了
.scope(),或是否接受了新的offline_access默认值。确认租户已配置为允许离线访问。 - 默认 minTTL 更改(§6.15): 说明
credentialsManager.credentials()现在会在令牌过期前 60 秒续期,而不是等到令牌恰好过期时才续期。确认这是否可接受,或是否已显式设置minTTL: 0。
4. 未应用的可选改进(简要列出;绝不自动应用)
CredentialsManager上新增的clearAll()方法——一次清除所有凭据- 新的
MFAClientAPI——如果项目使用 MFA,且旧方法已移除 - DPoP(演示式持有证明)支持——如果 API 要求使用发送方约束令牌
- Passkey 登录 / 注册 API(iOS 16.6+、macOS 13.5+)
ssoCredentials()——如果需要 SSO 凭据交换
5. 询问用户是否要提交迁移更改、探索任何可选改进,或一起逐个检查特定文件。
安全提醒: 摘要中绝不包含令牌、密钥、客户端凭据或 Keychain 值。
详细参考资料
常见错误
| 错误 | 正确做法 |
|---|---|
| Step 4 未在项目中找到某个 API,却应用 §6.x 章节 | Step 4 的文件读取是判断依据。未找到 = 完全跳过该章节 |
| 仅凭 grep 判断是否使用了某个 API | grep 会漏掉多行调用链、带有 domain:clientId: 参数的调用以及变量别名。请阅读实际文件 |
项目未使用 CredentialsManager,却对其进行修改 | 只迁移项目实际调用的内容 |
移除非 Auth0 代码周围的 DispatchQueue.main 包装 | 只移除完全位于 Auth0 回调主体内的调度包装 |
| 悄悄删除 Management API 调用点 | 添加 // TODO: 并在摘要中说明——删除调用会破坏功能 |
| 悄悄删除旧版 MFA 调用点 | 同上——添加 TODO 并在摘要中说明 |
| 根据臆测而非获取的 SDK 源码应用更改 | 每项修复都必须能追溯到 Step 3 获取的文件中的函数签名 |
开发者选择了 beta 标签,却指定 from: "3.0.0" | 稳定版范围指定符无法解析 beta 版本;预发布版本请使用 exact: "<TAG>" |
| 在工作树有未提交更改时开始迁移 | 务必先确认 git status --porcelain 为空 |
| 未先应用已知更改,就直接开始构建 | 先应用所有已知更改,再构建以查找剩余问题 |
| 构建失败超过 10 轮后仍继续 | 停止并向用户展示剩余错误 |
| 跳过迁移摘要 | 务必提供完整摘要——用户需要这份摘要 |
相关技能
- auth0-swift — 从头开始集成新的 Auth0.swift
- auth0-android — Android 原生身份验证
参考资料
安全: 切勿在构建日志或终端输出中回显令牌、客户端密钥或凭据。切勿将密钥提交到版本控制中。