Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthroughAdds a new Swift SDK implementation and examples, expands the pnpm workspace to include sdks, and introduces a full Swift runtime (API client, token stores, models, client/server apps), tests, spec docs, and a publish workflow. Changes
Sequence Diagram(s)sequenceDiagram
participant App as Client App
participant API as APIClient
participant TS as TokenStore
participant Srv as Auth Server
App->>API: sendRequest(path, method, body, authenticated)
API->>TS: getAccessToken()
TS-->>API: accessToken?
alt access token present
API->>Srv: HTTP request (with access token)
alt 2xx
Srv-->>API: 2xx response
API-->>App: return data
else 401 invalid_access_token
Srv-->>API: 401 invalid_access_token
API->>API: refreshTokenIfNeeded()
API->>TS: getRefreshToken()
TS-->>API: refreshToken
API->>Srv: POST /api/v1/auth/oauth/token (refresh)
alt refresh success
Srv-->>API: new tokens
API->>TS: setTokens(newAccess, newRefresh)
API->>Srv: retry original request with new access token
Srv-->>API: response
API-->>App: return data
else refresh failed
API->>TS: clearTokens()
API-->>App: throw auth error
end
else other error
Srv-->>API: error response
API-->>App: throw parsed error
end
else no access token
API->>Srv: HTTP request (no auth)
Srv-->>API: response
API-->>App: return data or error
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ❌ 3❌ Failed checks (1 warning, 2 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
<!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Updated internal environment detection mechanism for OAuth flows. Insecure HTTP requests are now allowed when running outside of production environments, rather than only during testing scenarios. No changes to public APIs. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
There was a problem hiding this comment.
Pull request overview
This pull request adds comprehensive SDK specifications and a Swift SDK implementation for Stack Auth. The specifications define the structure and behavior for SDKs across multiple languages, while the Swift implementation demonstrates the first concrete SDK following these specs.
Changes:
- Added detailed SDK specification files covering users, teams, authentication, permissions, payments, and more
- Implemented a complete Swift SDK with client and server app support
- Added comprehensive test suite for the Swift SDK
- Included example applications for iOS and macOS
- Updated workspace configuration to include SDK packages
Reviewed changes
Copilot reviewed 54 out of 56 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| sdks/spec/src/types/users/*.spec.md | User type specifications (base, current, server) |
| sdks/spec/src/types/teams/*.spec.md | Team type specifications including member profiles |
| sdks/spec/src/types/permissions/*.spec.md | Permission system specifications |
| sdks/spec/src/types/payments/*.spec.md | Payment and item management specs |
| sdks/spec/src/types/notifications/*.spec.md | Notification category specifications |
| sdks/spec/src/types/contact-channels/*.spec.md | Contact channel specifications |
| sdks/spec/src/types/common/*.spec.md | Common types (sessions, API keys) |
| sdks/spec/src/types/auth/*.spec.md | OAuth connection specifications |
| sdks/spec/src/apps/*.spec.md | Client and server app specifications |
| sdks/spec/src/_utilities.spec.md | Utility patterns and conventions |
| sdks/spec/README.md | Specification documentation |
| sdks/implementations/swift/Sources/StackAuth/*.swift | Swift SDK implementation |
| sdks/implementations/swift/Tests/StackAuthTests/*.swift | Comprehensive test suite |
| sdks/implementations/swift/Examples/* | iOS and macOS example apps |
| sdks/implementations/swift/Package.swift | Swift package configuration |
| pnpm-workspace.yaml | Workspace configuration update |
Files not reviewed (2)
- pnpm-lock.yaml: Language not supported
- sdks/implementations/swift/Examples/StackAuthiOS/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Greptile SummaryThis PR introduces a comprehensive SDK specification framework and a complete Swift SDK implementation for Stack Auth. The specification defines API patterns, authentication flows, error handling, and token management that can guide implementations across multiple programming languages. The Swift SDK provides native iOS/macOS support with proper platform integrations like Keychain storage and Key Changes:
Implementation Quality:
Confidence Score: 5/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant App as iOS/macOS App
participant SDK as StackClientApp
participant Store as TokenStore
participant API as APIClient
participant WebView as ASWebAuthenticationSession
participant Server as Stack Auth Backend
Note over App,Server: OAuth Authentication
App->>SDK: signInWithOAuth(provider)
SDK->>SDK: Generate PKCE params
SDK->>Server: Build auth URL
SDK->>WebView: Open web session
WebView->>Server: Navigate to auth page
Server-->>WebView: Show provider login
WebView->>WebView: User logs in
WebView->>Server: Complete OAuth flow
Server-->>WebView: Return to app
WebView-->>SDK: Callback received
SDK->>Server: Exchange for session
Server-->>SDK: Session data
SDK->>Store: Save session
SDK-->>App: Success
Note over App,Server: Session Refresh
App->>SDK: getUser()
SDK->>Store: Get session
Store-->>SDK: Session data
SDK->>API: Make authenticated request
API->>Server: API call
Server-->>API: 401 expired
API->>API: Lock refresh (mutex)
API->>Server: Refresh session
Server-->>API: New session
API->>Store: Update session
API->>Server: Retry request
Server-->>API: Success
API-->>SDK: Data
SDK-->>App: CurrentUser
Note over App,Server: Credential Sign-Up
App->>SDK: signUpWithCredential()
SDK->>API: Create account
API->>Server: POST sign-up
Server-->>API: Session data
API->>Store: Save securely
SDK-->>App: Complete
|
There was a problem hiding this comment.
Actionable comments posted: 20
🤖 Fix all issues with AI agents
In `@sdks/implementations/swift/Examples/StackAuthiOS/README.md`:
- Around line 20-32: The README uses a potentially nonexistent simulator name
"iPhone 17 Pro" in the xcodebuild destination; update the examples to a more
stable device name such as "iPhone 15 Pro" or show a generic pattern (e.g.,
'platform=iOS Simulator,name=iPhone 15 Pro' or 'platform=iOS
Simulator,name=iPhone') so the xcodebuild commands in the README build/run
section work across environments.
In `@sdks/implementations/swift/README.md`:
- Around line 123-129: Update the "Error handling" cell for JavaScript in the
comparison table to reflect standard JS practices: replace "Result types" with
"Promises (try/catch or .catch())" so the row reads JavaScript: Promises
(try/catch or .catch()), Swift: `throws`; edit the table entry shown under the
"Error handling" heading accordingly.
- Around line 14-18: The README's Swift Package dependency URL
"https://github.com/stack-auth/stack-swift" is invalid; update the dependency
declaration to point to the correct GitHub repository for the Swift SDK by
replacing the package URL string in the dependencies array (the .package(url:
"...", from: "1.0.0") entry) with the verified repository URL for the project,
and confirm the package name and minimum version match the upstream Swift
package manifest.
- Around line 30-40: The example uses `user` outside its `if let user = try
await stack.getUser()` scope and calls `user.signOut()` incorrectly; fix by
either move the sign-out into the `if let` scope (call `try await
user.signOut()` inside the block) or call the stack-level sign out via `try
await stack.signOut()` after ensuring a user exists, updating the snippet around
`stack.signInWithCredential`, `stack.getUser()`, and the signOut call
accordingly.
In `@sdks/implementations/swift/Sources/StackAuth/APIClient.swift`:
- Around line 122-129: The 429 handling in sendWithRetry currently retries
indefinitely; update sendWithRetry to enforce a maximum retry limit (e.g., add a
maxAttempts constant or parameter and check the attempt count) and, inside the
429 branch, before sleeping/recursing, verify attempt < maxAttempts and
otherwise throw a descriptive error or return the failure; reference the
sendWithRetry(request:authenticated:attempt:) implementation and mirror the same
max-retry check used for network errors so 429 no longer causes infinite
retries.
- Line 38: The code force-unwraps URL(string: "\(baseUrl)/api/v1\(path)") in
APIClient.swift which can crash; replace the force-unwrap with a safe
construction and throw or return an error when invalid—e.g., use URLComponents
or guard let url = URL(string: ...) else { throw
APIClientError.invalidURL(baseUrl: baseUrl, path: path) } (or propagate an
existing error type used by APIClient's request method) so callers receive a
recoverable error instead of a crash.
In `@sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift`:
- Around line 73-79: The two setSelectedTeam methods call
update(selectedTeamId:) but never refresh the local selectedTeam property; make
selectedTeam a mutable var (if not already) and after awaiting
update(selectedTeamId:) assign selectedTeam: in setSelectedTeam(_ team: Team?)
simply set selectedTeam = team after the update; in setSelectedTeam(id teamId:
String?) after the update set selectedTeam = userData?.teams?.first { $0.id ==
teamId } ?? nil (or otherwise resolve the Team by id from the cached userData),
keeping the async/try flow and using the existing update(selectedTeamId:) call
to persist the change.
- Line 7: The CurrentUser model declares selectedTeam as immutable (public let
selectedTeam: Team?) while setSelectedTeam(...) updates server state, so change
selectedTeam to a mutable property (public var selectedTeam: Team?) in
CurrentUser and ensure the setSelectedTeam methods (setSelectedTeam) also update
this local property after a successful server response (or alternatively trigger
a re-fetch of the CurrentUser via the existing fetch/current user method);
update any initializers or decoding logic in CurrentUser to use var and keep
thread-safety/notification behavior consistent if needed.
In `@sdks/implementations/swift/Sources/StackAuth/Models/Permission.swift`:
- Around line 9-11: ProjectPermission lacks a public memberwise initializer; add
a public init mirroring TeamPermission to allow external construction.
Specifically, add a public init(id: String) { self.id = id } to the
ProjectPermission struct so callers can instantiate ProjectPermission publicly,
consistent with TeamPermission.
- Around line 4-6: The TeamPermission struct is public but lacks a public
initializer, preventing external code from instantiating it; add an explicit
public initializer to TeamPermission (e.g., init(id: String)) so consumers can
construct instances, by updating the TeamPermission type to include a public
init(id: String) initializer that assigns the id property.
In `@sdks/implementations/swift/Sources/StackAuth/Models/ServerUser.swift`:
- Around line 156-159: The query strings in listContactChannels(),
hasPermission(), and listPermissions() are built via unsafe string
interpolation; change each to construct the URL path using URLComponents and
URLQueryItem so user_id, permission_id, team_id, and recursive are
percent-encoded (e.g., build a base path like "/contact-channels" or
"/has-permission" then add query items and use urlComponents.url!.absoluteString
or path from that when calling client.sendRequest). Replace the inline "\(id)" /
"\(permissionId)" / "\(teamId)" / "\(recursive)" usages in the
client.sendRequest calls with the encoded URL from URLComponents to ensure
reserved characters are escaped.
In `@sdks/implementations/swift/Sources/StackAuth/Models/Session.swift`:
- Around line 13-34: The init(from json: [String: Any]) initializer in
Session.swift currently uses fragile `as? Int64` casts for `created_at` and
`last_used_at`, which can fail when JSON numbers are `NSNumber` and yield epoch
(1970) fallbacks; change parsing to read these fields as `NSNumber` (or
`Double`) and use `doubleValue` (or `double`) to compute
Date(timeIntervalSince1970: value / 1000.0), remove the unnecessary `*_millis`
fallback checks, and apply the same robust pattern to optional `last_used_at`
handling so `lastUsedAt` is nil when absent instead of defaulting to epoch; keep
existing `isImpersonation`, `isCurrentSession`, and `geoInfo` logic unchanged.
In `@sdks/implementations/swift/Sources/StackAuth/Models/Team.swift`:
- Around line 129-138: The createApiKey method is sending the wrong expiry field
name ("expires_at") causing the server to ignore it; update the request body in
Team.createApiKey to use the server contract key "expires_at_millis" and
continue to send the Int64 milliseconds value
(Int64(expiresAt.timeIntervalSince1970 * 1000)) when expiresAt is non-nil so the
server reads the expiration correctly.
In `@sdks/implementations/swift/Tests/StackAuthTests/TeamTests.swift`:
- Around line 10-15: The test createTeamWithDisplayName should early-return when
the backend is unavailable; add the minimal guard pattern at the top of the
test: await a tiny sleep then call guard await TestConfig.isBackendAvailable()
else { return } to skip the test gracefully. Insert this check immediately after
the test starts (before using TestConfig.createClientApp,
TestConfig.uniqueEmail, or calling app.signUpWithCredential) in the
TeamTests.swift test methods (e.g., createTeamWithDisplayName) so CI/local runs
don't fail when the backend is down.
In `@sdks/implementations/swift/Tests/StackAuthTests/TestConfig.swift`:
- Around line 20-33: The health check in isBackendAvailable currently treats any
statusCode < 500 as "available" which allows 4xx responses to pass; change the
logic in isBackendAvailable to only return true for successful 2xx responses by
checking the HTTPURLResponse.statusCode is in the 200...299 range (e.g.,
statusCode >= 200 && statusCode < 300) and return false for any other status
codes or errors.
In `@sdks/implementations/swift/Tests/StackAuthTests/UserManagementTests.swift`:
- Around line 398-404: Replace the manual StackClientApp instantiation with the
test helper: instead of constructing StackClientApp(...) using
testProjectId/testPublishableClientKey/baseUrl and tokenStore: .explicit(...),
call TestConfig.createClientApp(tokenStore: .explicit(accessToken:
tokens.accessToken, refreshToken: tokens.refreshToken)) so the test uses the
shared setup used elsewhere; ensure you remove the noAutomaticPrefetch parameter
(or rely on TestConfig defaults) and keep the explicit tokenStore passed
through.
In `@sdks/spec/src/_utilities.spec.md`:
- Around line 139-148: Update the error-code example to use the documented
UPPERCASE_WITH_UNDERSCORES format: replace the lowercase
"multi_factor_authentication_required" example with
"MULTI_FACTOR_AUTHENTICATION_REQUIRED" so it matches the rule and the
implementation (known-errors.tsx). Locate the example in the spec (the code
property in the error example around line showing `code:
"multi_factor_authentication_required"`) and change only the string value to
"MULTI_FACTOR_AUTHENTICATION_REQUIRED".
In `@sdks/spec/src/apps/server-app.spec.md`:
- Around line 308-334: The createOAuthProvider spec is inconsistent about error
behavior—update the createOAuthProvider documentation (referencing
createOAuthProvider, ServerOAuthProvider, and
OAuthProviderAccountIdAlreadyUsedForSignIn) to explicitly state whether the
endpoint can error: either append "Does not error." if the method returns an
error object instead of throwing, or replace the current return line with "May
error" (and ensure the OAuthProviderAccountIdAlreadyUsedForSignIn error is
listed under Errors). Keep the phrasing consistent with other methods in the
file so implementers know whether to expect thrown errors or only returned error
objects.
In `@sdks/spec/src/types/payments/item.spec.md`:
- Around line 106-122: Update the example code block under the "Example Usage
(pseudocode)" section to include a language identifier after the opening triple
backticks (e.g., ```python, ```javascript or ```typescript) so the snippet
containing calls like server.getItem, item.increaseQuantity,
item.tryDecreaseQuantity, and references to
item.nonNegativeQuantity/item.quantity is rendered with proper syntax
highlighting; pick the language that best matches the pseudocode (or use
`python`/`javascript`) and insert it immediately after the opening ``` of the
code fence.
In `@sdks/spec/src/types/users/current-user.spec.md`:
- Around line 7-10: The unordered list under the "Also includes:" heading is
indented by two spaces which violates MD007; remove the leading two spaces so
the list items "- Auth methods (signOut, getAccessToken, etc.)" and "- Customer
methods (payments/customer.spec.md)" are left-aligned directly under "Also
includes:" (no leading spaces) so the markdown list starts at column zero.
🧹 Nitpick comments (19)
sdks/spec/src/types/notifications/notification-category.spec.md (1)
27-42: Clarify error and response behavior for subscribe/unsubscribe.“Does not error” is likely inaccurate for authenticated POSTs (e.g., auth failure, invalid category_id, rate limits). Please document expected status codes and response shape, plus any error cases (even if idempotent).
sdks/implementations/swift/Sources/StackAuth/Models/Permission.swift (2)
4-11: Consider addingCodable,Equatable, andHashableconformance.These models will likely be deserialized from API responses and used in collections or comparisons. Adding these conformances improves usability.
Proposed enhancement
-public struct TeamPermission: Sendable { +public struct TeamPermission: Sendable, Codable, Equatable, Hashable { public let id: String + + public init(id: String) { + self.id = id + } } -public struct ProjectPermission: Sendable { +public struct ProjectPermission: Sendable, Codable, Equatable, Hashable { public let id: String + + public init(id: String) { + self.id = id + } }
1-1:Foundationimport may be unnecessary.
Stringis part of the Swift standard library. Unless otherFoundationtypes are planned for this file, the import can be removed.sdks/implementations/swift/Sources/StackAuth/Models/Project.swift (2)
9-11: Consider throwing on missing required fields instead of defaulting to empty strings.Using empty string defaults for
idanddisplayNamesilently masks API parsing failures. If the API returns malformed data, consumers won't know these fields are invalid until runtime issues occur.♻️ Alternative: failable initializer
- init(from json: [String: Any]) { - self.id = json["id"] as? String ?? "" - self.displayName = json["display_name"] as? String ?? "" + init?(from json: [String: Any]) { + guard let id = json["id"] as? String, + let displayName = json["display_name"] as? String else { + return nil + } + self.id = id + self.displayName = displayName
85-87: Consider adding a public initializer forOAuthProviderConfig.Without a public initializer, consumers cannot create instances for testing or programmatic use.
♻️ Add public init
public struct OAuthProviderConfig: Sendable { public let id: String + + public init(id: String) { + self.id = id + } }sdks/implementations/swift/Sources/StackAuth/APIClient.swift (2)
76-76: Prefer non-optionalDatainitializer.Per SwiftLint,
"{}".data(using: .utf8)returns an optional. UseData("{}".utf8)instead which is non-optional and cleaner.♻️ Proposed fix
- request.httpBody = "{}".data(using: .utf8) + request.httpBody = Data("{}".utf8)
250-256: JSON parsing swallows the underlying error.Using
try?loses the original parse error details, making debugging difficult. Consider preserving the error information.♻️ Preserve parse error
func parseJSON<T>(_ data: Data) throws -> T { - guard let json = try? JSONSerialization.jsonObject(with: data) as? T else { - throw StackAuthError(code: "parse_error", message: "Failed to parse response") + do { + guard let json = try JSONSerialization.jsonObject(with: data) as? T else { + throw StackAuthError(code: "parse_error", message: "Response type mismatch") + } + return json + } catch let error as StackAuthError { + throw error + } catch { + throw StackAuthError(code: "parse_error", message: "Failed to parse response: \(error.localizedDescription)") } - return json }sdks/implementations/swift/Sources/StackAuth/Models/ContactChannel.swift (2)
14-22: Defensive parsing with empty string fallback foridcould lead to invalid API calls.If the JSON lacks an
idfield,self.idbecomes"", resulting in API paths like/contact-channels/which will fail or behave unexpectedly. Consider throwing an error or using a failable initializer when required fields are missing.💡 Suggested approach
- init(client: APIClient, json: [String: Any]) { + init?(client: APIClient, json: [String: Any]) { + guard let id = json["id"] as? String, !id.isEmpty else { + return nil + } self.client = client - self.id = json["id"] as? String ?? "" + self.id = id self.value = json["value"] as? String ?? "" self.type = json["type"] as? String ?? "email" self.isPrimary = json["is_primary"] as? Bool ?? false self.isVerified = json["is_verified"] as? Bool ?? false self.usedForAuth = json["used_for_auth"] as? Bool ?? false }
41-46: Silent failure on response parsing leaves local state stale.If
JSONSerializationfails or the response structure is unexpected, the local properties retain their old values without any indication of failure. This is consistent with other models in the SDK but may cause subtle bugs where the client believes the update succeeded while local state diverges from the server.Consider logging a warning or documenting this behavior explicitly if intentional.
sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift (4)
287-298: URL query parameters should be URL-encoded.
team.idis directly interpolated into the URL without encoding. While team IDs are typically UUIDs, encoding ensures robustness.💡 Suggested improvement
if let team = team { - query.append("team_id=\(team.id)") + query.append("team_id=\(team.id.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? team.id)") }Consider adding a helper method in
APIClientfor building query strings safely.
124-127: Inconsistent error handling: list methods silently return empty arrays on parse failure.Methods like
listTeams(),listContactChannels(),getActiveSessions(),listPermissions(), andlistApiKeys()return empty arrays when JSON parsing fails, whilecreateTeam(),createContactChannel(), andcreateApiKey()throw errors. This inconsistency could mask real issues (network errors that throw are fine, but a 200 response with malformed JSON silently returns[]).Consider either documenting this behavior or throwing parse errors consistently.
Also applies to: 179-182, 222-225, 306-309, 323-326
156-159:createTeamautomatically selects the newly created team.This side effect may be unexpected. Consider documenting this behavior or making it optional.
132-135:getTeam(id:)should use the direct team endpoint instead of fetching all teams.This implementation fetches all user teams and filters client-side, which is inefficient for users with many teams. Use the direct endpoint
/teams/{teamId}likeStackServerApp.getTeam(id:)does (see line 150 in StackServerApp.swift).sdks/implementations/swift/Tests/StackAuthTests/ContactChannelTests.swift (1)
87-111: Ensure server‑created users are always cleaned up.
If an assertion fails before the explicit delete, users can be left behind. Consider a teardown/cleanup block that always runs (e.g., adefer-style cleanup or a Testing teardown hook).♻️ Example cleanup pattern (apply similarly to other server tests)
- let user = try await app.createUser(email: email) + let user = try await app.createUser(email: email) + defer { Task { try? await user.delete() } } let channels = try await user.listContactChannels() `#expect`(!channels.isEmpty) @@ - // Clean up - try await user.delete() + // Clean up handled by deferAlso applies to: 113-127, 129-181
sdks/implementations/swift/Tests/StackAuthTests/TokenTests.swift (1)
189-217: Clarify the “same store” test intent.
This test uses explicit tokens rather than a shared store; consider renaming the description (or switching to a shared backend) to match behavior.♻️ Suggested rename
- `@Test`("Should share tokens between app instances with same store") + `@Test`("Should share tokens between app instances with explicit tokens")sdks/implementations/swift/Examples/StackAuthMacOS/StackAuthMacOS/StackAuthMacOSApp.swift (1)
261-360: Consider main‑actor isolation for the view model.
Async tasks update observable UI state; main‑actor isolation avoids background‑thread mutations if any task hops off the main actor.♻️ Suggested adjustment
-@Observable -class SDKTestViewModel { +@MainActor +@Observable +class SDKTestViewModel {sdks/implementations/swift/Tests/StackAuthTests/UserManagementTests.swift (1)
24-25: Consider using guard-let or optional binding instead of force unwrapping.Lines 25, 119, and 131 use force unwrapping (
!) after nil checks. While functionally safe, this pattern is less idiomatic Swift and can mask issues if the code is refactored.♻️ Suggested improvement using guard-let
- `#expect`(id != nil) - `#expect`(!id!.isEmpty) + guard let id = id else { + Issue.record("id should not be nil") + return + } + `#expect`(!id.isEmpty)sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift (2)
588-588: Remove redundant optional initialization.SwiftLint correctly identifies that initializing an optional to
nilis redundant in Swift.♻️ Fix redundant initialization
- var restrictedReason: User.RestrictedReason? = nil + var restrictedReason: User.RestrictedReason?
626-641: Consider filtering null values from auth headers JSON.The
getAuthHeadersmethod includesnilvalues in the JSON object. Depending on how the backend interprets this, you may want to exclude null values to produce a cleaner header like{"accessToken":"..."}instead of{"accessToken":"...","refreshToken":null}.♻️ Optional: Filter nil values
public func getAuthHeaders() async -> [String: String] { let accessToken = await client.getAccessToken() let refreshToken = await client.getRefreshToken() - let json: [String: Any?] = [ - "accessToken": accessToken, - "refreshToken": refreshToken - ] + var json: [String: Any] = [:] + if let accessToken = accessToken { + json["accessToken"] = accessToken + } + if let refreshToken = refreshToken { + json["refreshToken"] = refreshToken + } if let data = try? JSONSerialization.data(withJSONObject: json), let string = String(data: data, encoding: .utf8) {
| let clientApp = StackClientApp( | ||
| projectId: testProjectId, | ||
| publishableClientKey: testPublishableClientKey, | ||
| baseUrl: baseUrl, | ||
| tokenStore: .explicit(accessToken: tokens.accessToken, refreshToken: tokens.refreshToken), | ||
| noAutomaticPrefetch: true | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "let testProjectId|var testProjectId|let testPublishableClientKey|var testPublishableClientKey|let baseUrl|var baseUrl" --type swiftRepository: stack-auth/stack-auth
Length of output: 987
🏁 Script executed:
rg -n "TestConfig\s*\.|createClientApp" --type swiftRepository: stack-auth/stack-auth
Length of output: 32344
🏁 Script executed:
cat -n sdks/implementations/swift/Tests/StackAuthTests/UserManagementTests.swift | sed -n '380,420p'Repository: stack-auth/stack-auth
Length of output: 1462
Use TestConfig.createClientApp() to maintain consistency with other tests in the file.
Rather than manually constructing StackClientApp with raw globals (testProjectId, testPublishableClientKey, baseUrl), use the test helper method which accepts a tokenStore parameter. This can be called as TestConfig.createClientApp(tokenStore: .explicit(accessToken: tokens.accessToken, refreshToken: tokens.refreshToken)) to achieve the same result while maintaining consistency with the rest of the test suite.
🤖 Prompt for AI Agents
In `@sdks/implementations/swift/Tests/StackAuthTests/UserManagementTests.swift`
around lines 398 - 404, Replace the manual StackClientApp instantiation with the
test helper: instead of constructing StackClientApp(...) using
testProjectId/testPublishableClientKey/baseUrl and tokenStore: .explicit(...),
call TestConfig.createClientApp(tokenStore: .explicit(accessToken:
tokens.accessToken, refreshToken: tokens.refreshToken)) so the test uses the
shared setup used elsewhere; ensure you remove the noAutomaticPrefetch parameter
(or rely on TestConfig defaults) and keep the explicit tokenStore passed
through.
| ## createOAuthProvider(options) | ||
|
|
||
| Arguments: | ||
| options.userId: string | ||
| options.accountId: string | ||
| options.providerConfigId: string | ||
| options.email: string | ||
| options.allowSignIn: bool | ||
| options.allowConnectedAccounts: bool | ||
|
|
||
| Returns: ServerOAuthProvider (on success) | ||
|
|
||
| Request: | ||
| POST /api/v1/users/{userId}/oauth-providers [server-only] | ||
| Body: { | ||
| account_id: string, | ||
| provider_config_id: string, | ||
| email: string, | ||
| allow_sign_in: bool, | ||
| allow_connected_accounts: bool | ||
| } | ||
|
|
||
| Errors: | ||
| OAuthProviderAccountIdAlreadyUsedForSignIn | ||
| code: "oauth_provider_account_id_already_used_for_sign_in" | ||
| message: "This OAuth account is already linked to another user for sign-in." | ||
|
|
There was a problem hiding this comment.
Clarify error handling documentation.
The createOAuthProvider section states "Returns: ServerOAuthProvider (on success)" and documents an error case (OAuthProviderAccountIdAlreadyUsedForSignIn), but most other methods in this spec end with "Does not error." This inconsistency may confuse SDK implementers about whether this method can throw/error.
Please either:
- Add "Does not error" if this returns an error object instead of throwing
- Or explicitly state "May error" and document the error behavior consistently
🤖 Prompt for AI Agents
In `@sdks/spec/src/apps/server-app.spec.md` around lines 308 - 334, The
createOAuthProvider spec is inconsistent about error behavior—update the
createOAuthProvider documentation (referencing createOAuthProvider,
ServerOAuthProvider, and OAuthProviderAccountIdAlreadyUsedForSignIn) to
explicitly state whether the endpoint can error: either append "Does not error."
if the method returns an error object instead of throwing, or replace the
current return line with "May error" (and ensure the
OAuthProviderAccountIdAlreadyUsedForSignIn error is listed under Errors). Keep
the phrasing consistent with other methods in the file so implementers know
whether to expect thrown errors or only returned error objects.
| ## Example Usage (pseudocode) | ||
|
|
||
| ``` | ||
| // Granting credits | ||
| item = server.getItem({ userId: "...", itemId: "api-credits" }) | ||
| await item.increaseQuantity(100) | ||
|
|
||
| // Consuming credits (with overdraft protection) | ||
| success = await item.tryDecreaseQuantity(10) | ||
| if not success: | ||
| throw InsufficientCredits("Not enough credits") | ||
|
|
||
| // Checking balance | ||
| item = user.getItem("api-credits") | ||
| print(f"Available: {item.nonNegativeQuantity}") | ||
| print(f"Actual balance: {item.quantity}") // might be negative | ||
| ``` |
There was a problem hiding this comment.
Add language identifier to code block.
The example code block starting at Line 108 should specify a language for proper syntax highlighting and rendering.
📝 Suggested fix
-```
+```python
// Granting credits
item = server.getItem({ userId: "...", itemId: "api-credits" })Or use an appropriate language identifier (e.g., javascript, typescript, swift) based on the intended pseudocode language.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## Example Usage (pseudocode) | |
| ``` | |
| // Granting credits | |
| item = server.getItem({ userId: "...", itemId: "api-credits" }) | |
| await item.increaseQuantity(100) | |
| // Consuming credits (with overdraft protection) | |
| success = await item.tryDecreaseQuantity(10) | |
| if not success: | |
| throw InsufficientCredits("Not enough credits") | |
| // Checking balance | |
| item = user.getItem("api-credits") | |
| print(f"Available: {item.nonNegativeQuantity}") | |
| print(f"Actual balance: {item.quantity}") // might be negative | |
| ``` | |
| ## Example Usage (pseudocode) | |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
108-108: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In `@sdks/spec/src/types/payments/item.spec.md` around lines 106 - 122, Update the
example code block under the "Example Usage (pseudocode)" section to include a
language identifier after the opening triple backticks (e.g., ```python,
```javascript or ```typescript) so the snippet containing calls like
server.getItem, item.increaseQuantity, item.tryDecreaseQuantity, and references
to item.nonNegativeQuantity/item.quantity is rendered with proper syntax
highlighting; pick the language that best matches the pseudocode (or use
`python`/`javascript`) and insert it immediately after the opening ``` of the
code fence.
| } | ||
|
|
||
| // Exchange code for tokens | ||
| let tokenUrl = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")! |
There was a problem hiding this comment.
Force unwrap of URL construction in OAuth token exchange could crash with invalid baseUrl. Should use guard let instead.
View Details
📝 Patch Details
diff --git a/sdks/implementations/swift/Sources/StackAuth/APIClient.swift b/sdks/implementations/swift/Sources/StackAuth/APIClient.swift
index fe5d3b20..382bcd95 100644
--- a/sdks/implementations/swift/Sources/StackAuth/APIClient.swift
+++ b/sdks/implementations/swift/Sources/StackAuth/APIClient.swift
@@ -193,7 +193,11 @@ actor APIClient {
}
// Build token refresh request
- let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")!
+ guard let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token") else {
+ // Refresh failed - clear tokens
+ await tokenStore.clearTokens()
+ return false
+ }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
diff --git a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
index 4c472e79..ac8066e6 100644
--- a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
+++ b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
@@ -142,9 +142,12 @@ public actor StackClientApp {
let callbackUrl = redirectUrl ?? urls.oauthCallback
- var components = URLComponents(string: "\(baseUrl)/api/v1/auth/oauth/authorize/\(provider.lowercased())")!
+ guard let components = URLComponents(string: "\(baseUrl)/api/v1/auth/oauth/authorize/\(provider.lowercased())") else {
+ throw StackAuthError(code: "invalid_url", message: "Failed to construct OAuth authorization URL from base: \(baseUrl)")
+ }
+ var mutableComponents = components
let publishableKey = await client.publishableClientKey
- components.queryItems = [
+ mutableComponents.queryItems = [
URLQueryItem(name: "client_id", value: projectId),
URLQueryItem(name: "client_secret", value: publishableKey),
URLQueryItem(name: "redirect_uri", value: callbackUrl),
@@ -160,11 +163,11 @@ public actor StackClientApp {
// Add access token if user is already logged in
if let accessToken = await client.getAccessToken() {
- components.queryItems?.append(URLQueryItem(name: "token", value: accessToken))
+ mutableComponents.queryItems?.append(URLQueryItem(name: "token", value: accessToken))
}
- guard let url = components.url else {
- throw StackAuthError(code: "invalid_url", message: "Failed to construct OAuth URL")
+ guard let url = mutableComponents.url else {
+ throw StackAuthError(code: "invalid_url", message: "Failed to construct OAuth authorization URL")
}
return OAuthUrlResult(url: url, state: actualState, codeVerifier: actualCodeVerifier)
@@ -236,7 +239,9 @@ public actor StackClientApp {
}
// Exchange code for tokens
- let tokenUrl = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")!
+ guard let tokenUrl = URL(string: "\(baseUrl)/api/v1/auth/oauth/token") else {
+ throw OAuthError(code: "invalid_url", message: "Failed to construct token exchange URL from base: \(baseUrl)")
+ }
var request = URLRequest(url: tokenUrl)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
Analysis
Force unwrap of URL construction in OAuth token exchange can crash with invalid baseUrl
What fails: StackClientApp.getOAuthUrl() and StackClientApp.callOAuthCallback() use force unwrap (!) on failable URL(string:) and URLComponents(string:) initializers. If an invalid baseUrl is provided, the app crashes with "Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value" during OAuth flows.
How to reproduce:
// Create client with invalid baseUrl containing spaces
let app = StackClientApp(
projectId: "test",
publishableClientKey: "test-key",
baseUrl: "https://invalid url.com" // Invalid: contains space
)
// Crash on OAuth attempt
do {
_ = try await app.getOAuthUrl(provider: "google")
// Runtime crash: "Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value"
} catch { }Result: App terminates with fatal error. The force unwrap fails because URL(string:) is a failable initializer that returns nil for strings that don't conform to RFC 3986, such as URLs with spaces, invalid characters, or malformed schemes.
Expected: Should throw a catchable StackAuthError or OAuthError with descriptive message instead of crashing. This matches the pattern already used in APIClient.sendRequest() line 38 which uses safe unwrapping: guard let url = URL(string: "\(baseUrl)/api/v1\(path)") else { throw StackAuthError(...) }
Fix implemented:
- Line 145: Changed
URLComponents(string:)!force unwrap to safe guard unwrap ingetOAuthUrl() - Line 239: Changed
URL(string:)!force unwrap to safe guard unwrap incallOAuthCallback() - APIClient.swift line 196: Changed
URL(string:)!force unwrap to safe guard unwrap in token refresh flow for consistency
All force unwraps replaced with defensive guard let statements that throw appropriate errors with context about which URL construction failed and what baseUrl was provided.
| } | ||
|
|
||
| // Build token refresh request | ||
| let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")! |
There was a problem hiding this comment.
Force unwrap of URL construction in token refresh could crash with invalid baseUrl. Should use guard let instead to be consistent with error handling elsewhere in the code.
View Details
📝 Patch Details
diff --git a/sdks/implementations/swift/Sources/StackAuth/APIClient.swift b/sdks/implementations/swift/Sources/StackAuth/APIClient.swift
index fe5d3b20..8e82124c 100644
--- a/sdks/implementations/swift/Sources/StackAuth/APIClient.swift
+++ b/sdks/implementations/swift/Sources/StackAuth/APIClient.swift
@@ -193,7 +193,9 @@ actor APIClient {
}
// Build token refresh request
- let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")!
+ guard let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token") else {
+ throw StackAuthError(code: "INVALID_URL", message: "Failed to construct token refresh URL from base: \(baseUrl)")
+ }
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
diff --git a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
index 4c472e79..98aab0cf 100644
--- a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
+++ b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
@@ -236,7 +236,9 @@ public actor StackClientApp {
}
// Exchange code for tokens
- let tokenUrl = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")!
+ guard let tokenUrl = URL(string: "\(baseUrl)/api/v1/auth/oauth/token") else {
+ throw OAuthError(code: "invalid_url", message: "Failed to construct token exchange URL from base: \(baseUrl)")
+ }
var request = URLRequest(url: tokenUrl)
request.httpMethod = "POST"
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
Analysis
Force unwrap of URL construction in token refresh could crash with invalid baseUrl
What fails: APIClient.refreshTokenIfNeeded() at line 196 and StackClientApp.callOAuthCallback() at line 239 use force unwrap (!) when constructing OAuth token URLs, which will crash if the baseUrl is invalid or contains characters that cannot be encoded into a URL.
How to reproduce:
// This will crash:
let app = StackClientApp(
projectId: "test-project",
publishableClientKey: "test-key",
baseUrl: "https://invalid url.com" // baseUrl with space character
)
// Then any OAuth flow or token refresh will trigger:
// Fatal error: unexpectedly found nil while unwrapping an Optional valueResult: Force unwrap crashes the app immediately with a generic "unexpectedly found nil" fatal error rather than a handled exception.
Expected: Should use safe unwrapping with guard let and throw a proper error (consistent with line 38 of APIClient.swift), allowing callers to handle invalid configuration gracefully.
Related code pattern: Line 38 of APIClient.swift already uses safe unwrapping:
guard let url = URL(string: "\(baseUrl)/api/v1\(path)") else {
throw StackAuthError(code: "INVALID_URL", message: "Failed to construct request URL from base: \(baseUrl) and path: \(path)")
}There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In @.github/workflows/swift-sdk-publish.yaml:
- Around line 31-49: The tag-check step (id: check-tag) currently treats any
non-200 response as "tag missing" which masks auth/rate-limit/server errors;
update the logic that inspects HTTP_STATUS (for TAG="v${{
steps.version.outputs.version }}") so that: if HTTP_STATUS == 200 set
exists=true, else if HTTP_STATUS == 404 set exists=false, otherwise log the HTTP
status and error context and exit non-zero to fail fast (do not write exists
output) so auth/5xx problems stop the workflow instead of attempting a publish.
In `@sdks/implementations/swift/Sources/StackAuth/APIClient.swift`:
- Around line 195-197: Replace the force-unwrap of URL(string:
"\(baseUrl)/api/v1/auth/oauth/token") in the token refresh request construction
with the same safe pattern used in the existing request(...) method: use a guard
let (or if let) to safely unwrap URL(string:), and return/throw a descriptive
error if URL creation fails; update the variables 'url' and 'request' in the
token refresh code path (the token refresh request construction block) to use
the newly-unwrapped URL and ensure any callers handle the propagated error.
In `@sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift`:
- Around line 262-273: The current header serialization in CurrentUser.swift
builds a [String: Any?] with optionals which causes JSONSerialization to fail
and return "{}" when any token is nil; update the header-building logic (the
block that creates the json dictionary and returns ["x-stack-auth": ...]) to
only include non-nil token entries (e.g., construct a [String: String] or a
[String: Any] by conditionally inserting "accessToken" and "refreshToken" when
they are non-nil) so JSONSerialization always serializes a valid object; also
ensure a sensible fallback by returning a JSON string that contains the present
token(s) (or at minimum the access token if available) rather than "{}" when
serialization somehow fails.
In `@sdks/spec/src/_utilities.spec.md`:
- Around line 12-15: Fix the markdown by normalizing the list indentation under
the "Construct API URL" stanza and formatting the bare URL as inline code or
wrapped in angle brackets: make each bullet start at the same indent level
(e.g., aligned directly under the first bullet) and change the bare URL to
`https://api.stack-auth.com` (or `<https://api.stack-auth.com>`) and ensure the
example URL is also backticked like `https://api.stack-auth.com/api/v1/users/me`
so MD007/MD034 are satisfied.
- Around line 166-173: The fenced code block defining TokenStoreInit is missing
a language tag; update the opening fence for the block that contains the
TokenStoreInit union type to include the TypeScript language (change the opening
``` to ```ts) so the block is recognized as TS/MD040-compliant and enables
proper syntax highlighting for the TokenStoreInit declaration.
♻️ Duplicate comments (2)
sdks/implementations/swift/Sources/StackAuth/APIClient.swift (1)
173-186: Prevent re-entrant refresh races by settingisRefreshingbefore the firstawait.
The await ontokenStore.getRefreshToken()can yield, letting another caller enter and start a second refresh.🛠️ Suggested fix
- guard let refreshToken = await tokenStore.getRefreshToken() else { - return false - } - - isRefreshing = true - defer { + isRefreshing = true + defer { isRefreshing = false for waiter in refreshWaiters { waiter.resume() } refreshWaiters.removeAll() } + + guard let refreshToken = await tokenStore.getRefreshToken() else { + return false + }sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift (1)
7-79: selectedTeam stays stale after setSelectedTeam.Line 7 declares
selectedTeamas immutable, and Lines 73–79 don’t update local state after the server call. This reproduces the earlier state-divergence bug.🐛 Suggested fix
- public let selectedTeam: Team? + public private(set) var selectedTeam: Team?public func setSelectedTeam(_ team: Team?) async throws { try await update(selectedTeamId: team?.id) + self.selectedTeam = team } public func setSelectedTeam(id teamId: String?) async throws { try await update(selectedTeamId: teamId) + self.selectedTeam = userData.teams?.first { $0.id == teamId } }
🧹 Nitpick comments (1)
sdks/implementations/swift/Sources/StackAuth/Models/Session.swift (1)
48-54: UseNSNumber.doubleValuefor latitude/longitude parsing.
JSONSerializationyieldsNSNumber;as? Doublecan be brittle with integer-valued coords. Align with thecreated_athandling for consistency.♻️ Suggested tweak
- self.latitude = json["latitude"] as? Double - self.longitude = json["longitude"] as? Double + self.latitude = (json["latitude"] as? NSNumber)?.doubleValue + self.longitude = (json["longitude"] as? NSNumber)?.doubleValue
| - name: Check if tag already exists in target repo | ||
| id: check-tag | ||
| run: | | ||
| TAG="v${{ steps.version.outputs.version }}" | ||
| echo "Checking if tag $TAG exists in stack-auth/swift-sdk-prerelease..." | ||
|
|
||
| # Use the GitHub API to check if the tag exists | ||
| HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ | ||
| -H "Authorization: Bearer ${{ secrets.SWIFT_SDK_PUBLISH_TOKEN }}" \ | ||
| -H "Accept: application/vnd.github+json" \ | ||
| "https://api.github.com/repos/stack-auth/swift-sdk-prerelease/git/refs/tags/$TAG") | ||
|
|
||
| if [ "$HTTP_STATUS" = "200" ]; then | ||
| echo "Tag $TAG already exists, skipping publish" | ||
| echo "exists=true" >> $GITHUB_OUTPUT | ||
| else | ||
| echo "Tag $TAG does not exist, will publish" | ||
| echo "exists=false" >> $GITHUB_OUTPUT | ||
| fi |
There was a problem hiding this comment.
Fail fast on non-404 API errors during tag check.
Right now 401/403/5xx are treated as “tag missing,” which can mask auth/rate-limit failures and attempt a publish anyway.
🛠️ Suggested fix
- if [ "$HTTP_STATUS" = "200" ]; then
- echo "Tag $TAG already exists, skipping publish"
- echo "exists=true" >> $GITHUB_OUTPUT
- else
- echo "Tag $TAG does not exist, will publish"
- echo "exists=false" >> $GITHUB_OUTPUT
- fi
+ if [ "$HTTP_STATUS" = "200" ]; then
+ echo "Tag $TAG already exists, skipping publish"
+ echo "exists=true" >> $GITHUB_OUTPUT
+ elif [ "$HTTP_STATUS" = "404" ]; then
+ echo "Tag $TAG does not exist, will publish"
+ echo "exists=false" >> $GITHUB_OUTPUT
+ else
+ echo "Failed to check tag (HTTP $HTTP_STATUS)"
+ exit 1
+ fi🤖 Prompt for AI Agents
In @.github/workflows/swift-sdk-publish.yaml around lines 31 - 49, The tag-check
step (id: check-tag) currently treats any non-200 response as "tag missing"
which masks auth/rate-limit/server errors; update the logic that inspects
HTTP_STATUS (for TAG="v${{ steps.version.outputs.version }}") so that: if
HTTP_STATUS == 200 set exists=true, else if HTTP_STATUS == 404 set exists=false,
otherwise log the HTTP status and error context and exit non-zero to fail fast
(do not write exists output) so auth/5xx problems stop the workflow instead of
attempting a publish.
| // Build token refresh request | ||
| let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")! | ||
| var request = URLRequest(url: url) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd sdks/implementations/swift/Sources/StackAuth && head -200 APIClient.swift | tail -50Repository: stack-auth/stack-auth
Length of output: 2076
🏁 Script executed:
cd sdks/implementations/swift/Sources/StackAuth && wc -l APIClient.swiftRepository: stack-auth/stack-auth
Length of output: 84
🏁 Script executed:
cd sdks/implementations/swift/Sources/StackAuth && sed -n '180,210p' APIClient.swiftRepository: stack-auth/stack-auth
Length of output: 1267
🏁 Script executed:
cd sdks/implementations/swift/Sources/StackAuth && grep -n "baseUrl" APIClient.swift | head -20Repository: stack-auth/stack-auth
Length of output: 494
Replace force-unwrap with proper error handling for token refresh URL construction.
The force-unwrap at line 196 will crash if baseUrl is malformed. While baseUrl is normalized to remove trailing slashes, it can still contain invalid URL characters or be completely invalid, causing URL(string:) to return nil.
The codebase already uses proper error handling for identical URL construction at line 38–39 in the request method. Apply the same pattern here:
Suggested fix
- let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")!
+ guard let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token") else {
+ throw StackAuthError(code: "INVALID_URL", message: "Failed to construct token refresh URL from base: \(baseUrl)")
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Build token refresh request | |
| let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")! | |
| var request = URLRequest(url: url) | |
| // Build token refresh request | |
| guard let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token") else { | |
| throw StackAuthError(code: "INVALID_URL", message: "Failed to construct token refresh URL from base: \(baseUrl)") | |
| } | |
| var request = URLRequest(url: url) |
🤖 Prompt for AI Agents
In `@sdks/implementations/swift/Sources/StackAuth/APIClient.swift` around lines
195 - 197, Replace the force-unwrap of URL(string:
"\(baseUrl)/api/v1/auth/oauth/token") in the token refresh request construction
with the same safe pattern used in the existing request(...) method: use a guard
let (or if let) to safely unwrap URL(string:), and return/throw a descriptive
error if URL creation fails; update the variables 'url' and 'request' in the
token refresh code path (the token refresh request construction block) to use
the newly-unwrapped URL and ensure any callers handle the propagated error.
sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift
Outdated
Show resolved
Hide resolved
| Construct API URL: `{baseUrl}/api/v1{path}` | ||
| - baseUrl defaults to "https://api.stack-auth.com" | ||
| - Remove trailing slash from final URL | ||
| - Example: `https://api.stack-auth.com/api/v1/users/me` |
There was a problem hiding this comment.
Fix list indentation and the bare URL to satisfy markdownlint.
MD007/MD034 flags here. Align list bullets and format the URL.
✅ Suggested fix
-Construct API URL: `{baseUrl}/api/v1{path}`
- - baseUrl defaults to "https://api.stack-auth.com"
- - Remove trailing slash from final URL
- - Example: `https://api.stack-auth.com/api/v1/users/me`
+Construct API URL: `{baseUrl}/api/v1{path}`
+- baseUrl defaults to `https://api.stack-auth.com`
+- Remove trailing slash from final URL
+- Example: `https://api.stack-auth.com/api/v1/users/me`📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Construct API URL: `{baseUrl}/api/v1{path}` | |
| - baseUrl defaults to "https://api.stack-auth.com" | |
| - Remove trailing slash from final URL | |
| - Example: `https://api.stack-auth.com/api/v1/users/me` | |
| Construct API URL: `{baseUrl}/api/v1{path}` | |
| - baseUrl defaults to `https://api.stack-auth.com` | |
| - Remove trailing slash from final URL | |
| - Example: `https://api.stack-auth.com/api/v1/users/me` |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
13-13: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
13-13: Bare URL used
(MD034, no-bare-urls)
14-14: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
15-15: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
🤖 Prompt for AI Agents
In `@sdks/spec/src/_utilities.spec.md` around lines 12 - 15, Fix the markdown by
normalizing the list indentation under the "Construct API URL" stanza and
formatting the bare URL as inline code or wrapped in angle brackets: make each
bullet start at the same indent level (e.g., aligned directly under the first
bullet) and change the bare URL to `https://api.stack-auth.com` (or
`<https://api.stack-auth.com>`) and ensure the example URL is also backticked
like `https://api.stack-auth.com/api/v1/users/me` so MD007/MD034 are satisfied.
| } catch { | ||
| return handleNoUser(or: or) | ||
| } | ||
| } | ||
|
|
||
| private func handleNoUser(or: GetUserOr) -> CurrentUser? { | ||
| switch or { | ||
| case .returnNull, .anonymous: | ||
| return nil | ||
| case .redirect: | ||
| // Can't redirect in Swift | ||
| return nil | ||
| case .throw: | ||
| // Already thrown | ||
| return nil | ||
| } |
There was a problem hiding this comment.
The getUser() method silently converts errors to nil when or: .throw is specified, violating the contract that errors should be thrown in this case.
View Details
📝 Patch Details
diff --git a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
index 4c472e79..23002be4 100644
--- a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
+++ b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
@@ -496,28 +496,38 @@ public actor StackClientApp {
)
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
- return nil
+ return try handleNoUser(or: or)
}
let user = CurrentUser(client: client, json: json)
// Check if we should return this user
if await user.isAnonymous && !includeAnonymous {
- return handleNoUser(or: or)
+ return try handleNoUser(or: or)
}
if await user.isRestricted && !effectiveIncludeRestricted {
- return handleNoUser(or: or)
+ return try handleNoUser(or: or)
}
return user
+ } catch is StackAuthErrorProtocol {
+ // If it's already a StackAuthError and we should throw, re-throw it
+ if or == .throw {
+ throw UserNotSignedInError()
+ }
+ return try handleNoUser(or: or)
} catch {
- return handleNoUser(or: or)
+ // For other errors, convert to UserNotSignedInError if throwing
+ if or == .throw {
+ throw UserNotSignedInError()
+ }
+ return try handleNoUser(or: or)
}
}
- private func handleNoUser(or: GetUserOr) -> CurrentUser? {
+ private func handleNoUser(or: GetUserOr) throws -> CurrentUser? {
switch or {
case .returnNull, .anonymous:
return nil
@@ -525,8 +535,7 @@ public actor StackClientApp {
// Can't redirect in Swift
return nil
case .throw:
- // Already thrown
- return nil
+ throw UserNotSignedInError()
}
}
Analysis
getUser() silently converts errors to nil when or: .throw is specified
What fails: StackClientApp.getUser(or: .throw) returns nil instead of throwing UserNotSignedInError when:
- API request to /users/me fails (network error, 401 after token refresh, 5xx server errors)
- User object is filtered out as anonymous/restricted
How to reproduce:
let app = StackClientApp(projectId: "...", publishableClientKey: "...")
// With valid tokens but API fails or returns restricted user:
do {
let user = try await app.getUser(or: .throw)
// Returns nil instead of throwing
} catch {
// Never reaches here
}Result: Returns nil silently instead of throwing UserNotSignedInError
Expected: Per client-app.spec.md lines 287-290, errors should only occur "when or='throw'". The function should throw UserNotSignedInError in all failure scenarios when or: .throw is specified.
Root cause:
- handleNoUser(or:) function returned nil for all cases instead of throwing when or == .throw
- API request errors were caught and passed to handleNoUser() instead of being handled based on the or parameter
- User filtering (anonymous/restricted) called handleNoUser() without throwing
| fi | ||
|
|
||
| # Create and push tag | ||
| TAG="v${{ steps.version.outputs.version }}" | ||
| git tag "$TAG" | ||
| git push origin main --tags | ||
|
|
||
| echo "Successfully published Swift SDK v${{ steps.version.outputs.version }}" |
There was a problem hiding this comment.
| fi | |
| # Create and push tag | |
| TAG="v${{ steps.version.outputs.version }}" | |
| git tag "$TAG" | |
| git push origin main --tags | |
| echo "Successfully published Swift SDK v${{ steps.version.outputs.version }}" | |
| # Create and push tag only after successful commit | |
| TAG="v${{ steps.version.outputs.version }}" | |
| git tag "$TAG" | |
| git push origin main --tags | |
| echo "Successfully published Swift SDK v${{ steps.version.outputs.version }}" | |
| fi |
The git push command always executes and pushes tags even when no changes were committed, which could result in orphaned tags or failed pushes to an empty repository state.
View Details
Analysis
Git tag created and pushed even when no commit was made
What fails: In .github/workflows/swift-sdk-publish.yaml, the "Commit and push to target repo" step creates and pushes a git tag even when no changes were staged for commit, resulting in tags being created without corresponding new commits.
How to reproduce:
- In a clean target repository clone, if
git add -Astages no changes (files already match what should be published),git diff --staged --quietreturns exit code 0 - The if condition enters the true branch and echoes "No changes to commit"
- However, lines 87-90 (
git tag,git push origin main --tags) are outside the if/else block - These commands execute unconditionally, creating a tag without a corresponding commit
Result: Tag is created and pushed pointing to the existing HEAD commit, even though no new commit was made. This can occur when:
- Version number is incremented in package.json but Swift SDK files haven't actually changed
- The workflow is triggered by the path filter but copies identical files to the target repo
Expected behavior: Tag creation and push should only occur after a successful commit. According to GitHub Actions bash script best practices, operations that depend on a prior step should be conditional on that step's success.
Fix applied: Moved git tag and git push origin main --tags commands inside the else block so they only execute after a successful commit is created.
| public func signOut() async throws { | ||
| // Ignore errors - session may already be invalid | ||
| _ = try? await client.sendRequest( | ||
| path: "/auth/sessions/current", | ||
| method: "DELETE", | ||
| authenticated: true | ||
| ) | ||
| await client.clearTokens() | ||
| } |
There was a problem hiding this comment.
Same issue as StackClientApp: the signOut() function declares async throws but silently ignores errors with try?.
View Details
📝 Patch Details
diff --git a/sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift b/sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift
index 9d1d01df..3d7f78d8 100644
--- a/sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift
+++ b/sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift
@@ -237,7 +237,7 @@ public actor CurrentUser {
// MARK: - Auth Methods
- public func signOut() async throws {
+ public func signOut() async {
// Ignore errors - session may already be invalid
_ = try? await client.sendRequest(
path: "/auth/sessions/current",
diff --git a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
index 83947aef..97208a1f 100644
--- a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
+++ b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
@@ -604,7 +604,7 @@ public actor StackClientApp {
// MARK: - Sign Out
- public func signOut() async throws {
+ public func signOut() async {
_ = try? await client.sendRequest(
path: "/auth/sessions/current",
method: "DELETE",
Analysis
Misleading API contract in signOut() functions - async throws declares throwing but suppresses all errors
What fails: CurrentUser.signOut() and StackClientApp.signOut() declare async throws in their signatures but use try? to silently suppress all errors from client.sendRequest(), never propagating any errors to callers.
How to reproduce:
// Both of these compile without requiring try/catch, even though signature declares throws:
await currentUser.signOut() // No try/catch required despite async throws
await stackClientApp.signOut() // No try/catch required despite async throwsResult: Functions can be called without try keyword, contradicting the throws declaration. Callers see async throws and expect error propagation, but all errors are silently suppressed with try?.
Expected: Per Swift error handling best practices, when a function catches all errors internally with try? and never propagates them, the function should not declare throws in its signature. The signature should be async without throws.
Fix applied: Removed throws from both function signatures:
CurrentUser.signOut(): Changed fromasync throwstoasyncStackClientApp.signOut(): Changed fromasync throwstoasync
This aligns the API contract with the actual error-handling behavior - callers are no longer misled by a throws declaration when errors are actually suppressed internally.
| public func signOut() async throws { | ||
| _ = try? await client.sendRequest( | ||
| path: "/auth/sessions/current", | ||
| method: "DELETE", | ||
| authenticated: true | ||
| ) | ||
| await client.clearTokens() | ||
| } |
There was a problem hiding this comment.
The signOut() function declares async throws in its signature but uses try? to silently ignore errors, meaning it never actually throws despite the API contract.
View Details
📝 Patch Details
diff --git a/sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift b/sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift
index 9d1d01df..37c24b08 100644
--- a/sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift
+++ b/sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift
@@ -238,8 +238,7 @@ public actor CurrentUser {
// MARK: - Auth Methods
public func signOut() async throws {
- // Ignore errors - session may already be invalid
- _ = try? await client.sendRequest(
+ _ = try await client.sendRequest(
path: "/auth/sessions/current",
method: "DELETE",
authenticated: true
diff --git a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
index 83947aef..912fdd89 100644
--- a/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
+++ b/sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift
@@ -605,7 +605,7 @@ public actor StackClientApp {
// MARK: - Sign Out
public func signOut() async throws {
- _ = try? await client.sendRequest(
+ _ = try await client.sendRequest(
path: "/auth/sessions/current",
method: "DELETE",
authenticated: true
Analysis
API Contract Violation: signOut() declares throws but uses try? to suppress errors
What fails: Both StackClientApp.signOut() (line 607 in StackClientApp.swift) and CurrentUser.signOut() (line 240 in CurrentUser.swift) declare async throws in their signatures but use try? to silently suppress errors from the API call, causing the functions to never actually throw despite the API contract promising they may.
How to reproduce:
// Callers expect this might throw based on the signature
do {
try await app.signOut()
} catch {
// This catch block will NEVER execute
print("Sign out failed: \(error)")
}Result: The function never throws an error, even if the DELETE request fails. The catch block is dead code. This is consistent with similar functions like revokeSession() which properly use try await and DO propagate errors.
Expected: According to Swift error handling documentation, the throws keyword is an explicit part of a function's API contract. Functions declaring throws must either:
- Actually throw errors (using
tryinstead oftry?), OR - Declare no
throwscapability (justasync)
Using try? while declaring throws violates this contract and misleads callers about error handling expectations.
Fix: Changed try? to try in both functions to properly propagate errors from client.sendRequest(), making the API contract match the implementation.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@sdks/implementations/swift/Sources/StackAuth/APIClient.swift`:
- Around line 126-128: The comparison against the x-stack-known-error header
uses the lowercase string "invalid_access_token", so the token refresh branch
never runs; update the equality check in APIClient.swift where you read
httpResponse.value(forHTTPHeaderField: "x-stack-known-error") (the errorCode
variable) to compare against "INVALID_ACCESS_TOKEN" (uppercase snake case) so
the refresh token logic triggers when the backend returns that header.
In `@sdks/spec/src/_utilities.spec.md`:
- Around line 24-31: The markdown list has inconsistent indentation (MD007):
align all bullets and their sub-bullets consistently by adjusting the
indentation for the lines starting with "x-stack-override-error-status:
\"true\"" and "x-stack-random-nonce: <random-string>" so they use the same
leading spaces as the other list items (and ensure their explanatory sub-lines
for "client"/"server"/cache buster are indented consistently relative to their
parent bullet); locate the entries referencing "client"/"StackClientApp",
"server"/"StackServerApp", "x-stack-override-error-status", and
"x-stack-random-nonce" and make the list bullet indentation uniform across the
block.
♻️ Duplicate comments (10)
sdks/spec/src/_utilities.spec.md (1)
12-15: Duplicate: fix list indentation and bare URL.This was already raised in a previous review for these lines. Please apply the earlier suggested fix.
sdks/implementations/swift/Sources/StackAuth/APIClient.swift (1)
212-214: Force unwrap on URL construction can crash.This force unwrap was flagged in multiple past reviews but remains unaddressed. If
baseUrlcontains invalid characters, this will crash. Apply the same safe unwrapping pattern used insendRequestat line 55-57.🐛 Proposed fix
// Build token refresh request - let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")! + guard let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token") else { + await tokenStore.clearTokens() + return false + } var request = URLRequest(url: url)sdks/implementations/swift/Sources/StackAuth/Models/CurrentUser.swift (3)
7-7:selectedTeamis immutable butsetSelectedTeam()modifies server state.
selectedTeamis declared aslet, so callingsetSelectedTeam()updates the server but leaves the local property unchanged. This creates state divergence wherecurrentUser.selectedTeamdiffers from the server's selected team.🐛 Proposed fix
- public let selectedTeam: Team? + public private(set) var selectedTeam: Team?Then update
setSelectedTeammethods to also update the local property after the server update succeeds.
73-79:setSelectedTeammethods don't update localselectedTeamproperty.These methods call
update(selectedTeamId:)but never refresh the localselectedTeam. Theupdate()method only updatesuserData, notselectedTeam.🐛 Proposed fix for both methods
public func setSelectedTeam(_ team: Team?) async throws { try await update(selectedTeamId: team?.id) + self.selectedTeam = team // Requires changing selectedTeam to var } public func setSelectedTeam(id teamId: String?) async throws { try await update(selectedTeamId: teamId) + // If selectedTeam is var, resolve from teams list: + // self.selectedTeam = teamId.flatMap { id in try? await listTeams().first { $0.id == id } } }
240-248:signOut()declaresthrowsbut suppresses errors withtry?.The function signature declares
async throwsbut usestry?to silently ignore all errors. This misleads callers about error handling expectations. Either propagate errors withtryor removethrowsfrom the signature.🐛 Proposed fix: remove throws if errors should be suppressed
- public func signOut() async throws { + public func signOut() async { // Ignore errors - session may already be invalid _ = try? await client.sendRequest(sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift (5)
189-189: Force unwrap on URLComponents construction can crash.This was flagged in past reviews. If
baseUrlcontains invalid characters, the force unwrap will crash. Use safe unwrapping with guard.🐛 Proposed fix
- var components = URLComponents(string: "\(baseUrl)/api/v1/auth/oauth/authorize/\(provider.lowercased())")! + guard var components = URLComponents(string: "\(baseUrl)/api/v1/auth/oauth/authorize/\(provider.lowercased())") else { + throw StackAuthError(code: "invalid_url", message: "Failed to construct OAuth authorization URL from base: \(baseUrl)") + } let publishableKey = await client.publishableClientKey
228-266: OAuth session may not be retained, causing potential hang.The
ASWebAuthenticationSessionis created insidewithCheckedThrowingContinuationbut is a local variable. Aftersession.start()is called and the closure scope exits, ARC may deallocate the session before the callback fires. Apple's documentation requires callers to retain the session until completion. Additionally, ifstart()returnsfalse, the continuation is never resumed.🐛 Proposed fix: retain session and check start() result
+ // Add property to retain the session + private var activeAuthSession: ASWebAuthenticationSession? + `@MainActor` public func signInWithOAuth( provider: String, presentationContextProvider: ASWebAuthenticationPresentationContextProviding? = nil ) async throws { let oauth = try await getOAuthUrl(provider: provider) let callbackScheme = "stackauth-\(projectId)" try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in let session = ASWebAuthenticationSession( url: oauth.url, callbackURLScheme: callbackScheme ) { [weak self] callbackUrl, error in + self?.activeAuthSession = nil // Release after callback // ... existing completion handler } + self.activeAuthSession = session // Retain session session.prefersEphemeralWebBrowserSession = false `#if` os(iOS) || os(macOS) if let provider = presentationContextProvider { session.presentationContextProvider = provider } `#endif` - session.start() + if !session.start() { + self.activeAuthSession = nil + continuation.resume(throwing: StackAuthError(code: "oauth_session_failed", message: "Failed to start OAuth session")) + } } }
283-283: Force unwrap on URL construction can crash.This force unwrap in
callOAuthCallbackwas flagged in past reviews but remains. Apply safe unwrapping consistent with the rest of the codebase.🐛 Proposed fix
// Exchange code for tokens - let tokenUrl = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")! + guard let tokenUrl = URL(string: "\(baseUrl)/api/v1/auth/oauth/token") else { + throw OAuthError(code: "invalid_url", message: "Failed to construct token exchange URL from base: \(baseUrl)") + } var request = URLRequest(url: tokenUrl)
559-575:getUser(or: .throw)silently returns nil instead of throwing.When
or == .throwis specified, errors and user filtering (anonymous/restricted checks) callhandleNoUser()which always returnsnilinstead of throwingUserNotSignedInError(). The.throwcase should actually throw an error, not return nil.🐛 Proposed fix
- private func handleNoUser(or: GetUserOr) -> CurrentUser? { + private func handleNoUser(or: GetUserOr) throws -> CurrentUser? { switch or { case .returnNull, .anonymous: return nil case .redirect: // Can't redirect in Swift return nil case .throw: - // Already thrown - return nil + throw UserNotSignedInError() } }Then update call sites to use
try handleNoUser(or: or).
651-658:signOut()declaresthrowsbut suppresses errors withtry?.Same issue as in
CurrentUser.signOut(). The function signature promises it may throw, buttry?silently suppresses all errors. Either propagate errors or removethrows.🐛 Proposed fix
- public func signOut() async throws { + public func signOut() async { _ = try? await client.sendRequest(
🧹 Nitpick comments (2)
sdks/implementations/swift/Sources/StackAuth/APIClient.swift (1)
93-96: Consider using non-optional Data initializer.Per static analysis, prefer
Data(string.utf8)over"{}".data(using: .utf8)for guaranteed non-nil conversion.♻️ Suggested refactor
} else if method == "POST" || method == "PATCH" || method == "PUT" { // POST/PATCH/PUT requests need a body even if empty request.setValue("application/json", forHTTPHeaderField: "Content-Type") - request.httpBody = "{}".data(using: .utf8) + request.httpBody = Data("{}".utf8) }sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift (1)
632-632: Remove redundant nil initialization.Per static analysis, initializing an optional variable with nil is redundant in Swift.
♻️ Suggested refactor
- var restrictedReason: User.RestrictedReason? = nil + var restrictedReason: User.RestrictedReason?
| if let errorCode = httpResponse.value(forHTTPHeaderField: "x-stack-known-error"), | ||
| errorCode == "invalid_access_token" { | ||
| // Try to refresh token |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check error code conventions in Errors.swift
rg -n "case \"" sdks/implementations/swift/Sources/StackAuth/Errors.swift | head -20Repository: stack-auth/stack-auth
Length of output: 599
🏁 Script executed:
#!/bin/bash
# Search for x-stack-known-error header usage and related error codes
rg -n "x-stack-known-error|invalid_access_token" sdks/implementations/swift/Sources/StackAuth/ -A 2 -B 2Repository: stack-auth/stack-auth
Length of output: 1490
🏁 Script executed:
#!/bin/bash
# Check if there's any documentation or API definition that defines error codes
find sdks/implementations/swift -type f \( -name "*.swift" -o -name "*.md" \) | xargs rg -l "error.*code\|invalid_access_token" | head -10Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for other x-stack header usages to understand format convention
rg -n "x-stack-" sdks/implementations/swift/Sources/StackAuth/ -iRepository: stack-auth/stack-auth
Length of output: 3031
🏁 Script executed:
#!/bin/bash
# Check if there's any backend or API documentation
find . -type f \( -name "*.md" -o -name "*.json" -o -name "*.yaml" -o -name "*.yml" \) | xargs rg -l "invalid.*access.*token\|x-stack-known-error" 2>/dev/null | head -10Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for any test files that might show expected API responses
find sdks/implementations/swift -type f -name "*Test*" -o -name "*test*" | head -20Repository: stack-auth/stack-auth
Length of output: 610
🏁 Script executed:
#!/bin/bash
# Check TokenTests.swift for token refresh and error handling
cat -n sdks/implementations/swift/Tests/StackAuthTests/TokenTests.swift | head -100Repository: stack-auth/stack-auth
Length of output: 4222
🏁 Script executed:
#!/bin/bash
# Check ErrorTests.swift
cat -n sdks/implementations/swift/Tests/StackAuthTests/ErrorTests.swiftRepository: stack-auth/stack-auth
Length of output: 10666
🏁 Script executed:
#!/bin/bash
# Search for any backend/server code that might define error codes
find . -type f \( -name "*.ts" -o -name "*.js" -o -name "*.go" -o -name "*.java" \) | xargs rg -l "invalid_access_token\|invalid-access-token" 2>/dev/null | head -5Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look for backend/server code structure
find . -type d -name "backend" -o -name "server" -o -name "apps" 2>/dev/null | head -20Repository: stack-auth/stack-auth
Length of output: 357
🏁 Script executed:
#!/bin/bash
# Search across the entire repo for x-stack-known-error header being set
rg -n "x-stack-known-error" . --type-list | head -20Repository: stack-auth/stack-auth
Length of output: 454
🏁 Script executed:
#!/bin/bash
# Search for files that might set this header (look for error middleware, handlers, etc.)
rg -n "invalid.?access.?token|x-stack-known-error" . -t ts -t js -t go -t java 2>/dev/null | head -30Repository: stack-auth/stack-auth
Length of output: 3349
🏁 Script executed:
#!/bin/bash
# Look for where error codes are defined in backend
find . -type f \( -name "*.ts" -o -name "*.js" \) -path "*/backend/*" -o -path "*/server/*" | xargs rg -l "INVALID_ACCESS_TOKEN\|invalid_access_token" 2>/dev/null | head -10Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for error code definitions in backend
rg -n "INVALID_ACCESS_TOKEN|invalid_access_token" apps/backend -t ts -t js -iRepository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look for token refresh and error handling in backend
rg -n "access.*token.*error\|refreshAccessToken\|refreshToken" apps/backend -t ts -t js -A 3 | head -50Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Check the e2e test helpers to see if there's any reference to invalid access token
cat -n apps/e2e/tests/backend/backend-helpers.ts | grep -A 5 -B 5 "invalid access token"Repository: stack-auth/stack-auth
Length of output: 3322
🏁 Script executed:
#!/bin/bash
# Search for all error codes defined in backend to understand the format
rg -n '"[A-Z_]+".*error' apps/backend -t ts -t js | head -30Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for where x-stack-known-error header is set in backend
rg -n "x-stack-known-error" apps/backend -t ts -t js -B 5 -A 5 | head -60Repository: stack-auth/stack-auth
Length of output: 660
🏁 Script executed:
#!/bin/bash
# Look for error code constants/enums in backend
find apps/backend -name "*.ts" -o -name "*.js" | xargs rg -l "ERROR|error.*code" | head -10Repository: stack-auth/stack-auth
Length of output: 242
🏁 Script executed:
#!/bin/bash
# Check if there's an errors or constants file
find apps/backend -type f \( -name "*error*" -o -name "*constant*" \) -name "*.ts"Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for token-related error handling broadly
rg -n "token.*error\|refresh.*token\|invalid.*token" apps/backend -t ts -A 2 -B 2 | head -80Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for where responses set headers with error codes
rg -n "header.*x-stack-known-error\|setHeader.*x-stack-known-error\|headers.*x-stack" apps/backend -t ts -A 2 -B 2 | head -40Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look for JSON error responses in backend
rg -n "code.*:.*ERROR\|\"code\":" apps/backend -t ts | head -40Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Find the main API handler/middleware
find apps/backend -type f -name "*auth*" -o -name "*middleware*" -o -name "*handler*" | grep -E "\.(ts|js)$" | head -15Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for where access token validation happens
rg -n "access.*token\|AccessToken" apps/backend/src -t ts | head -50Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look at the client interface which handles x-stack-known-error
cat -n packages/stack-shared/src/interface/client-interface.ts | sed -n '430,450p'Repository: stack-auth/stack-auth
Length of output: 916
🏁 Script executed:
#!/bin/bash
# Search for error code definitions in stack-shared
rg -n "INVALID_ACCESS_TOKEN\|invalid_access_token" packages/stack-shared -t ts -B 3 -A 3Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look for error definitions in stack-shared
find packages/stack-shared/src -type f -name "*error*" | head -10Repository: stack-auth/stack-auth
Length of output: 150
🏁 Script executed:
#!/bin/bash
# Check for API response types that define error codes
rg -n "errorCode\|error.*code\|ErrorCode" packages/stack-shared/src -t ts | head -40Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Look at the known-errors file to see error code format
cat -n packages/stack-shared/src/known-errors.tsx | head -100Repository: stack-auth/stack-auth
Length of output: 4199
🏁 Script executed:
#!/bin/bash
# Search for invalid access token in known-errors
rg -n "invalid.*access.*token\|INVALID_ACCESS_TOKEN" packages/stack-shared/src/known-errors.tsx -iRepository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Get all error codes defined in known-errors
rg -n "errorCode\s*=\s*" packages/stack-shared/src/known-errors.tsx -A 1Repository: stack-auth/stack-auth
Length of output: 295
🏁 Script executed:
#!/bin/bash
# Look at more of the known-errors file to see error definitions
cat -n packages/stack-shared/src/known-errors.tsx | tail -200Repository: stack-auth/stack-auth
Length of output: 7275
🏁 Script executed:
#!/bin/bash
# Search for InvalidAccessToken error definition
rg -n "InvalidAccessToken\|INVALID_ACCESS_TOKEN" packages/stack-shared/src/known-errors.tsx -B 2 -A 5Repository: stack-auth/stack-auth
Length of output: 47
🏁 Script executed:
#!/bin/bash
# Search for InvalidAccessToken in the entire codebase
rg -n "InvalidAccessToken" packages/stack-shared/src -t tsRepository: stack-auth/stack-auth
Length of output: 948
🏁 Script executed:
#!/bin/bash
# Search for the actual error code used
rg -n "\"INVALID_ACCESS_TOKEN\"" . -t ts -t jsRepository: stack-auth/stack-auth
Length of output: 139
🏁 Script executed:
#!/bin/bash
# Look for token errors specifically
rg -n "Token.*=" packages/stack-shared/src/known-errors.tsx | grep -i "access\|token"Repository: stack-auth/stack-auth
Length of output: 878
🏁 Script executed:
#!/bin/bash
# Look at the InvalidAccessToken error definition
cat -n packages/stack-shared/src/known-errors.tsx | sed -n '511,520p'Repository: stack-auth/stack-auth
Length of output: 388
Fix error code case to match backend API response.
The comparison uses lowercase "invalid_access_token", but the backend returns "INVALID_ACCESS_TOKEN" (UPPERCASE_SNAKE_CASE) in the x-stack-known-error header. Change line 127 to:
errorCode == "INVALID_ACCESS_TOKEN"Without this fix, the token refresh logic will never trigger on invalid access token errors.
🤖 Prompt for AI Agents
In `@sdks/implementations/swift/Sources/StackAuth/APIClient.swift` around lines
126 - 128, The comparison against the x-stack-known-error header uses the
lowercase string "invalid_access_token", so the token refresh branch never runs;
update the equality check in APIClient.swift where you read
httpResponse.value(forHTTPHeaderField: "x-stack-known-error") (the errorCode
variable) to compare against "INVALID_ACCESS_TOKEN" (uppercase snake case) so
the refresh token logic triggers when the backend returns that header.
| - "client" for StackClientApp | ||
| - "server" for StackServerApp (also include server key header) | ||
| x-stack-override-error-status: "true" | ||
| - Tells server to return errors as 200 with x-stack-actual-status header | ||
| - This works around some platforms that intercept non-200 responses | ||
| x-stack-random-nonce: <random-string> | ||
| - Cache buster to prevent framework caching (e.g., Next.js) | ||
| - Generate a new random string for each request |
There was a problem hiding this comment.
Fix list indentation to satisfy MD007.
The sub-bullets are indented by two spaces; markdownlint expects aligned list bullets at the same level.
✅ Suggested fix
- - "client" for StackClientApp
- - "server" for StackServerApp (also include server key header)
+ - "client" for StackClientApp
+ - "server" for StackServerApp (also include server key header)
...
- - Tells server to return errors as 200 with x-stack-actual-status header
- - This works around some platforms that intercept non-200 responses
+ - Tells server to return errors as 200 with x-stack-actual-status header
+ - This works around some platforms that intercept non-200 responses
...
- - Cache buster to prevent framework caching (e.g., Next.js)
- - Generate a new random string for each request
+ - Cache buster to prevent framework caching (e.g., Next.js)
+ - Generate a new random string for each request📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - "client" for StackClientApp | |
| - "server" for StackServerApp (also include server key header) | |
| x-stack-override-error-status: "true" | |
| - Tells server to return errors as 200 with x-stack-actual-status header | |
| - This works around some platforms that intercept non-200 responses | |
| x-stack-random-nonce: <random-string> | |
| - Cache buster to prevent framework caching (e.g., Next.js) | |
| - Generate a new random string for each request | |
| - "client" for StackClientApp | |
| - "server" for StackServerApp (also include server key header) | |
| x-stack-override-error-status: "true" | |
| - Tells server to return errors as 200 with x-stack-actual-status header | |
| - This works around some platforms that intercept non-200 responses | |
| x-stack-random-nonce: <random-string> | |
| - Cache buster to prevent framework caching (e.g., Next.js) | |
| - Generate a new random string for each request |
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
24-24: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
25-25: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
27-27: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
28-28: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
30-30: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
31-31: Unordered list indentation
Expected: 0; Actual: 2
(MD007, ul-indent)
🤖 Prompt for AI Agents
In `@sdks/spec/src/_utilities.spec.md` around lines 24 - 31, The markdown list has
inconsistent indentation (MD007): align all bullets and their sub-bullets
consistently by adjusting the indentation for the lines starting with
"x-stack-override-error-status: \"true\"" and "x-stack-random-nonce:
<random-string>" so they use the same leading spaces as the other list items
(and ensure their explanatory sub-lines for "client"/"server"/cache buster are
indented consistently relative to their parent bullet); locate the entries
referencing "client"/"StackClientApp", "server"/"StackServerApp",
"x-stack-override-error-status", and "x-stack-random-nonce" and make the list
bullet indentation uniform across the block.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| # Create and push tag | ||
| TAG="v${{ steps.version.outputs.version }}" | ||
| git tag "$TAG" | ||
| git push origin main --tags |
There was a problem hiding this comment.
Workflow creates version tag without code changes
Medium Severity
The workflow creates and pushes a version tag even when there are no code changes to commit. If someone bumps the version in package.json without modifying any Swift SDK code, the tag check passes (new version doesn't exist), files are copied (with package.json removed), no changes are detected, the commit is skipped, but the tag is still created and pushed. This results in multiple version tags pointing to identical code, violating semantic versioning expectations.
| if actualStatus == 401 && authenticated { | ||
| // Check if it's an invalid access token error | ||
| if let errorCode = httpResponse.value(forHTTPHeaderField: "x-stack-known-error"), | ||
| errorCode == "invalid_access_token" { |
There was a problem hiding this comment.
Error code case mismatch prevents token refresh
High Severity
The token refresh check compares error code against lowercase "invalid_access_token", but the codebase documents that API error codes are UPPERCASE_WITH_UNDERSCORES (all other error codes like "EMAIL_PASSWORD_MISMATCH" use this format). This case mismatch means the equality check will never match, and automatic token refresh will never trigger for 401 errors with expired access tokens.
Additional Locations (1)
|
📝 Documentation updates detected! New suggestion: Document Swift SDK (Preview) |
Note
Adds initial Swift SDK and tooling.
sdks/implementations/swiftpackage with core sources (APIClient, error types, models likeContactChannel, etc.) and READMEstack-auth/swift-sdk-prereleaseon main; ignores and SPM lockfiles addedsdks/*paths; E2E test harness detects SDK implementations and runs their testsWritten by Cursor Bugbot for commit 1b493ca. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
Documentation
Tests
Chores
✏️ Tip: You can customize this high-level summary in your review settings.