Skip to content

SDK specs#1117

Closed
N2D4 wants to merge 21 commits intodevfrom
spec-sdks
Closed

SDK specs#1117
N2D4 wants to merge 21 commits intodevfrom
spec-sdks

Conversation

@N2D4
Copy link
Contributor

@N2D4 N2D4 commented Jan 17, 2026


Note

Adds initial Swift SDK and tooling.

  • Swift SDK (SPM): New sdks/implementations/swift package with core sources (APIClient, error types, models like ContactChannel, etc.) and README
  • Examples: Interactive SwiftUI example apps for macOS and iOS demonstrating auth, users, teams, tokens, OAuth, and server ops
  • CI/Release: GitHub Action to publish to stack-auth/swift-sdk-prerelease on main; ignores and SPM lockfiles added
  • Repo config: Workspace includes sdks/* paths; E2E test harness detects SDK implementations and runs their tests

Written by Cursor Bugbot for commit 1b493ca. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Official Swift SDK: client & server auth APIs, token stores, models (users, teams, sessions, API keys, contact channels), OAuth flows, and interactive iOS/macOS example apps.
  • Documentation

    • Swift SDK README plus an extensive SDK specification covering public APIs, types, flows, and platform guidance.
  • Tests

    • Comprehensive unit and E2E suites for auth, OAuth, tokens, users, teams, contact channels, permissions, and error handling; CI E2E runner to discover and run SDK tests.
  • Chores

    • Workspace expanded to include SDK packages, added Swift example ignore rules, package manifests and lockfiles, and a workflow to publish the Swift SDK.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Jan 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
stack-backend Ready Ready Preview, Comment Jan 21, 2026 0:28am
stack-dashboard Ready Ready Preview, Comment Jan 21, 2026 0:28am
stack-demo Ready Ready Preview, Comment Jan 21, 2026 0:28am
stack-docs Ready Ready Preview, Comment Jan 21, 2026 0:28am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 17, 2026

Note

Other AI code review bot(s) detected

CodeRabbit 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.

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Workspace
pnpm-workspace.yaml
Adds sdks/* and sdks/implementations/* to workspace packages.
Swift package & pins
sdks/implementations/swift/Package.swift, sdks/implementations/swift/Package.resolved, sdks/implementations/swift/package.json
New Swift Package manifest, resolved dependency pins, and npm shim/scripts.
Publish workflow
.github/workflows/swift-sdk-publish.yaml
New GitHub Action to publish Swift SDK to prerelease repo (conditional on tag presence).
Examples & Xcode projects
sdks/implementations/swift/Examples/...
New macOS and iOS example apps, READMEs, Xcode/workspace files, and macOS Package.swift.
Example UIs & logging
sdks/implementations/swift/Examples/.../StackAuthMacOSApp.swift, .../StackAuthiOSApp.swift
Large SwiftUI demo apps with view models, logging, and interactive SDK test actions.
Core HTTP & auth client
sdks/implementations/swift/Sources/StackAuth/APIClient.swift
New actor-based APIClient with header handling, JSON bodies, 401 refresh coordination, 429/backoff retry, and error mapping.
Token storage
sdks/implementations/swift/Sources/StackAuth/TokenStore.swift
TokenStoreProtocol and backends (Keychain, Memory, Explicit, Null) plus TokenStore enum.
Client & Server entrypoints
sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift, .../StackServerApp.swift
New StackClientApp and StackServerApp actors exposing OAuth, credential/magic-link/MFA flows, user/team/session APIs, project access, and server-only ops.
Errors & mapping
sdks/implementations/swift/Sources/StackAuth/Errors.swift
New error protocol, concrete error types, and factory mapping from API error codes.
Models
sdks/implementations/swift/Sources/StackAuth/Models/*
Many new models/actors: User, CurrentUser, ServerUser, Team, ServerTeam, ContactChannel, Session/GeoInfo, ApiKey variants, Permission, Project, TeamInvitation, TeamUser, etc., with network-backed methods.
Tests & Test utilities
sdks/implementations/swift/Tests/StackAuthTests/*, sdks/implementations/swift/Tests/StackAuthTests/TestConfig.swift
Extensive async test suites covering auth flows, OAuth, errors, tokens, users, teams, contact channels, sessions, and TestConfig helpers.
Specs & docs
sdks/spec/**, sdks/implementations/swift/README.md, example READMEs
New SDK spec and type documentation, SDK README, example READMEs and utilities.
E2E SDK discovery test
apps/e2e/tests/general/sdk-implementations.test.ts
E2E test that discovers SDK implementations and runs their tests.
Ignore rules
sdks/implementations/swift/.gitignore
Adds Xcode/Swift/Carthage/fastlane/build ignore entries.

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
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐰 I hopped through headers, tokens, and retry,

PKCE whispers, refreshes that fly.
Examples, specs, tests in a tidy stack,
New actors and models I carried on my back.
I nudged the Swift SDK, then hopped back. 🥕

🚥 Pre-merge checks | ❌ 3
❌ Failed checks (1 warning, 2 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'SDK specs' is vague and does not clearly summarize the main change. The PR actually introduces a production-ready Swift SDK with implementations, examples, CI workflows, and testing infrastructure, but the title provides no meaningful information. Replace with a more descriptive title that captures the primary change, such as 'Add Swift SDK implementation with examples and CI workflows' or 'Introduce Stack Auth Swift SDK with macOS/iOS examples'.
Description check ❓ Inconclusive The PR description is minimal and uses auto-generated content from Cursor Bugbot without a detailed manual breakdown of changes, design decisions, or testing approach. Expand the PR description with manual context: summarize key architectural decisions for the Swift SDK (e.g., token storage strategy, error handling approach), outline testing coverage (unit tests, E2E tests), document any breaking changes or migration paths, and explain the rationale for workspace structure changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

N2D4 and others added 11 commits January 19, 2026 09:16
<!--

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 -->
@N2D4 N2D4 marked this pull request as ready for review January 19, 2026 23:08
Copilot AI review requested due to automatic review settings January 19, 2026 23:08
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-apps
Copy link
Contributor

greptile-apps bot commented Jan 19, 2026

Greptile Summary

This 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 ASWebAuthenticationSession.

Key Changes:

  • Added SDK specification in /sdks/spec/ with detailed documentation for request patterns, authentication, OAuth flows, error codes, and token refresh logic
  • Implemented complete Swift SDK with client and server components
  • Proper token refresh serialization using actor-based mutex pattern
  • Secure token storage using Keychain with fallback options (memory, explicit)
  • Platform-appropriate OAuth handling using ASWebAuthenticationSession
  • Comprehensive test suite covering authentication, OAuth, teams, contact channels, and error cases
  • Example apps for both iOS and macOS

Implementation Quality:

  • Follows Swift conventions with actors for concurrency safety
  • Proper error handling with typed errors matching API error codes
  • Token refresh uses mutex to prevent concurrent refresh requests
  • Retry logic with exponential backoff for network errors
  • All API requests include required headers per specification
  • JSON response parsing with proper error propagation

Confidence Score: 5/5

  • This PR is safe to merge with high confidence
  • The implementation is well-structured, follows Swift best practices, includes comprehensive tests, and properly implements the SDK specification. The code demonstrates good security practices with secure token storage, proper error handling, and OAuth PKCE flow. No critical issues or security vulnerabilities were identified.
  • No files require special attention

Important Files Changed

Filename Overview
sdks/spec/README.md Added SDK specification documentation explaining notation, language adaptation guidelines, and implementation patterns
sdks/spec/src/_utilities.spec.md Comprehensive specification for API request patterns, authentication flow, token refresh, error handling, and retry logic
sdks/spec/src/apps/client-app.spec.md Detailed client SDK specification including OAuth flows, authentication methods, and user management patterns
sdks/implementations/swift/Sources/StackAuth/APIClient.swift Implements HTTP client with proper token refresh serialization, retry logic, and error handling following spec
sdks/implementations/swift/Sources/StackAuth/StackClientApp.swift Main client SDK with OAuth, credential auth, magic links, MFA, and user management - properly uses ASWebAuthenticationSession
sdks/implementations/swift/Sources/StackAuth/TokenStore.swift Token storage implementations including secure Keychain storage, memory storage, explicit tokens, and null store
sdks/implementations/swift/Sources/StackAuth/Errors.swift Error types matching API error codes with proper parsing from UPPERCASE_WITH_UNDERSCORES format

Sequence Diagram

sequenceDiagram
    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
Loading

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 adding Codable, Equatable, and Hashable conformance.

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: Foundation import may be unnecessary.

String is part of the Swift standard library. Unless other Foundation types 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 id and displayName silently 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 for OAuthProviderConfig.

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-optional Data initializer.

Per SwiftLint, "{}".data(using: .utf8) returns an optional. Use Data("{}".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 for id could lead to invalid API calls.

If the JSON lacks an id field, self.id becomes "", 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 JSONSerialization fails 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.id is 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 APIClient for building query strings safely.


124-127: Inconsistent error handling: list methods silently return empty arrays on parse failure.

Methods like listTeams(), listContactChannels(), getActiveSessions(), listPermissions(), and listApiKeys() return empty arrays when JSON parsing fails, while createTeam(), createContactChannel(), and createApiKey() 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: createTeam automatically 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} like StackServerApp.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., a defer-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 defer

Also 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 nil is 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 getAuthHeaders method includes nil values 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) {

Comment on lines +398 to +404
let clientApp = StackClientApp(
projectId: testProjectId,
publishableClientKey: testPublishableClientKey,
baseUrl: baseUrl,
tokenStore: .explicit(accessToken: tokens.accessToken, refreshToken: tokens.refreshToken),
noAutomaticPrefetch: true
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "let testProjectId|var testProjectId|let testPublishableClientKey|var testPublishableClientKey|let baseUrl|var baseUrl" --type swift

Repository: stack-auth/stack-auth

Length of output: 987


🏁 Script executed:

rg -n "TestConfig\s*\.|createClientApp" --type swift

Repository: 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.

Comment on lines +308 to +334
## 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."

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +106 to +122
## 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
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
## 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")!
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in getOAuthUrl()
  • Line 239: Changed URL(string:)! force unwrap to safe guard unwrap in callOAuthCallback()
  • 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")!
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 value

Result: 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)")
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 setting isRefreshing before the first await.
The await on tokenStore.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 selectedTeam as 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: Use NSNumber.doubleValue for latitude/longitude parsing.
JSONSerialization yields NSNumber; as? Double can be brittle with integer-valued coords. Align with the created_at handling 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

Comment on lines +31 to +49
- 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +195 to +197
// Build token refresh request
let url = URL(string: "\(baseUrl)/api/v1/auth/oauth/token")!
var request = URLRequest(url: url)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd sdks/implementations/swift/Sources/StackAuth && head -200 APIClient.swift | tail -50

Repository: stack-auth/stack-auth

Length of output: 2076


🏁 Script executed:

cd sdks/implementations/swift/Sources/StackAuth && wc -l APIClient.swift

Repository: stack-auth/stack-auth

Length of output: 84


🏁 Script executed:

cd sdks/implementations/swift/Sources/StackAuth && sed -n '180,210p' APIClient.swift

Repository: stack-auth/stack-auth

Length of output: 1267


🏁 Script executed:

cd sdks/implementations/swift/Sources/StackAuth && grep -n "baseUrl" APIClient.swift | head -20

Repository: 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.

Suggested change
// 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.

Comment on lines +12 to +15
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`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

cursor[bot]

This comment was marked as outdated.

Comment on lines +515 to +530
} 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
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

cursor[bot]

This comment was marked as outdated.

Comment on lines +85 to +92
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 }}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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:

  1. In a clean target repository clone, if git add -A stages no changes (files already match what should be published), git diff --staged --quiet returns exit code 0
  2. The if condition enters the true branch and echoes "No changes to commit"
  3. However, lines 87-90 (git tag, git push origin main --tags) are outside the if/else block
  4. 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.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Comment on lines +240 to +248
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()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 throws

Result: 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 from async throws to async
  • StackClientApp.signOut(): Changed from async throws to async

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.

Comment on lines +607 to +614
public func signOut() async throws {
_ = try? await client.sendRequest(
path: "/auth/sessions/current",
method: "DELETE",
authenticated: true
)
await client.clearTokens()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Actually throw errors (using try instead of try?), OR
  2. Declare no throws capability (just async)

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.

cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 baseUrl contains invalid characters, this will crash. Apply the same safe unwrapping pattern used in sendRequest at 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: selectedTeam is immutable but setSelectedTeam() modifies server state.

selectedTeam is declared as let, so calling setSelectedTeam() updates the server but leaves the local property unchanged. This creates state divergence where currentUser.selectedTeam differs from the server's selected team.

🐛 Proposed fix
-    public let selectedTeam: Team?
+    public private(set) var selectedTeam: Team?

Then update setSelectedTeam methods to also update the local property after the server update succeeds.


73-79: setSelectedTeam methods don't update local selectedTeam property.

These methods call update(selectedTeamId:) but never refresh the local selectedTeam. The update() method only updates userData, not selectedTeam.

🐛 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() declares throws but suppresses errors with try?.

The function signature declares async throws but uses try? to silently ignore all errors. This misleads callers about error handling expectations. Either propagate errors with try or remove throws from 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 baseUrl contains 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 ASWebAuthenticationSession is created inside withCheckedThrowingContinuation but is a local variable. After session.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, if start() returns false, 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 callOAuthCallback was 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 == .throw is specified, errors and user filtering (anonymous/restricted checks) call handleNoUser() which always returns nil instead of throwing UserNotSignedInError(). The .throw case 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() declares throws but suppresses errors with try?.

Same issue as in CurrentUser.signOut(). The function signature promises it may throw, but try? silently suppresses all errors. Either propagate errors or remove throws.

🐛 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?

Comment on lines +126 to +128
if let errorCode = httpResponse.value(forHTTPHeaderField: "x-stack-known-error"),
errorCode == "invalid_access_token" {
// Try to refresh token
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check error code conventions in Errors.swift
rg -n "case \"" sdks/implementations/swift/Sources/StackAuth/Errors.swift | head -20

Repository: 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 2

Repository: 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 -10

Repository: 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/ -i

Repository: 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 -10

Repository: 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 -20

Repository: 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 -100

Repository: stack-auth/stack-auth

Length of output: 4222


🏁 Script executed:

#!/bin/bash
# Check ErrorTests.swift
cat -n sdks/implementations/swift/Tests/StackAuthTests/ErrorTests.swift

Repository: 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 -5

Repository: 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 -20

Repository: 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 -20

Repository: 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 -30

Repository: 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 -10

Repository: 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 -i

Repository: 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 -50

Repository: 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 -30

Repository: 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 -60

Repository: 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 -10

Repository: 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 -80

Repository: 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 -40

Repository: 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 -40

Repository: 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 -15

Repository: 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 -50

Repository: 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 3

Repository: 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 -10

Repository: 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 -40

Repository: 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 -100

Repository: 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 -i

Repository: 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 1

Repository: 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 -200

Repository: 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 5

Repository: 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 ts

Repository: 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 js

Repository: 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.

Comment on lines +24 to +31
- "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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
- "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.

cursor[bot]

This comment was marked as outdated.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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" {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

@promptless
Copy link
Contributor

promptless bot commented Jan 22, 2026

📝 Documentation updates detected!

New suggestion: Document Swift SDK (Preview)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants