Add CI and release workflows, docs, and example app
- Add GitHub Actions workflows for CI and release validation - Add migration and quick start guides for iOS and macOS - Add release process documentation - Add a packaged SwiftUI example app for hosted login - Update .gitignore for example app build artifacts - Refactor Package.swift for local FFI product toggling - Add basic unit tests for SmbCloudAuth - Update README, ROADMAP, and SECURITY docs for v1 MVP completion
Seto Elkahfi committed
May 17, 2026 at 10:57 UTC
4acabf487c33be1369e148a913208cd974f11a36
20 files changed
+1075
-67
.github/workflows/ci.yml
new
+46
@@ -0,0 +1,46 @@
1
+name: CI
2
+
3
+on:
4
+ push:
5
+ tags-ignore:
6
+ - "v*.*.*"
7
+ pull_request:
8
+
9
+permissions:
10
+ contents: read
11
+
12
+jobs:
13
+ verify:
14
+ name: Verify Swift package and example
15
+ runs-on: macos-latest
16
+
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v6
20
+
21
+ - name: Show Swift version
22
+ run: swift --version
23
+
24
+ - name: Verify package
25
+ run: make verify
26
+
27
+ verify-apple-destinations:
28
+ name: Verify Apple destination build (${{ matrix.platform }})
29
+ runs-on: macos-latest
30
+ strategy:
31
+ fail-fast: false
32
+ matrix:
33
+ platform:
34
+ - ios
35
+ - tvos
36
+ - visionos
37
+
38
+ steps:
39
+ - name: Checkout
40
+ uses: actions/checkout@v6
41
+
42
+ - name: Show Swift version
43
+ run: swift --version
44
+
45
+ - name: Verify destination build
46
+ run: make verify-apple-destination PLATFORM=${{ matrix.platform }}
.github/workflows/release.yml
new
+79
@@ -0,0 +1,79 @@
1
+name: Release
2
+
3
+on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+ workflow_dispatch:
8
+ inputs:
9
+ tag:
10
+ description: "Release tag (e.g. v1.0.0)"
11
+ required: true
12
+
13
+permissions:
14
+ contents: write
15
+
16
+jobs:
17
+ verify:
18
+ name: Verify release package and example
19
+ runs-on: macos-latest
20
+
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v6
24
+ with:
25
+ ref: ${{ github.event.inputs.tag || github.ref }}
26
+
27
+ - name: Show Swift version
28
+ run: swift --version
29
+
30
+ - name: Verify package
31
+ run: make verify
32
+
33
+ verify-apple-destinations:
34
+ name: Verify release Apple destination build (${{ matrix.platform }})
35
+ needs: verify
36
+ runs-on: macos-latest
37
+ strategy:
38
+ fail-fast: false
39
+ matrix:
40
+ platform:
41
+ - ios
42
+ - tvos
43
+ - visionos
44
+
45
+ steps:
46
+ - name: Checkout
47
+ uses: actions/checkout@v6
48
+ with:
49
+ ref: ${{ github.event.inputs.tag || github.ref }}
50
+
51
+ - name: Show Swift version
52
+ run: swift --version
53
+
54
+ - name: Verify destination build
55
+ run: make verify-apple-destination PLATFORM=${{ matrix.platform }}
56
+
57
+ github-release:
58
+ name: Create GitHub release
59
+ needs:
60
+ - verify
61
+ - verify-apple-destinations
62
+ runs-on: ubuntu-latest
63
+
64
+ steps:
65
+ - name: Resolve tag
66
+ id: tag
67
+ shell: bash
68
+ run: |
69
+ if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
70
+ echo "tag=${{ github.event.inputs.tag }}" >> "$GITHUB_OUTPUT"
71
+ else
72
+ echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
73
+ fi
74
+
75
+ - name: Create release
76
+ uses: softprops/action-gh-release@v2
77
+ with:
78
+ tag_name: ${{ steps.tag.outputs.tag }}
79
+ generate_release_notes: true
.gitignore
+2
@@ -1,3 +1,5 @@
1
smbcloud_authFFI.xcframework/
2
.local/
3
.build/
4
+Examples/HostedLoginExample/.build/
5
+Examples/HostedLoginExample/.swiftpm/
Docs/Migration-From-Proxy-Native-Forms.md
new
+112
@@ -0,0 +1,112 @@
1
+# Migration guide — from proxy/native-form auth to hosted auth
2
+
3
+This guide is for teams currently using:
4
+
5
+- a backend or BFF proxy for login/signup
6
+- native email/password forms in the app
7
+- confidential smbCloud Auth credentials stored on the server
8
+
9
+and who want to move Apple apps toward the hosted public-client SDK model.
10
+
11
+## When to migrate
12
+
13
+Move to hosted auth when:
14
+
15
+- your Apple app is a public client
16
+- you want to stop maintaining auth proxy glue in the app layer
17
+- you want browser/session based login with PKCE
18
+- you do not need a custom native email/password screen as the primary flow
19
+
20
+Keep the proxy/BFF model when:
21
+
22
+- your product requires native credential forms
23
+- your backend owns broader session orchestration
24
+- you need server-managed trust boundaries beyond public-client hosted auth
25
+
26
+## Old model
27
+
28
+1. user enters email and password into the app
29
+2. app sends credentials to your backend
30
+3. backend uses confidential smbCloud Auth credentials
31
+4. backend returns an app-safe response
32
+
33
+## Hosted auth model
34
+
35
+1. app calls `SmbCloudWebAuth.login(...)`
36
+2. SDK opens hosted auth in `ASWebAuthenticationSession`
37
+3. user completes login in the system browser session
38
+4. SDK validates callback `state` and exchanges the code with PKCE
39
+5. app persists the returned `SmbCloudSession`
40
+6. app loads profile data with `userInfo(...)`
41
+
42
+## Migration plan
43
+
44
+### 1. Keep server-side auth for native forms if still needed
45
+
46
+Do not remove your proxy if the product still depends on native credential entry.
47
+Hosted auth and proxy-based auth can coexist during migration.
48
+
49
+### 2. Add a public client in smbCloud Auth
50
+
51
+The Apple app should use a public client identifier and a callback URL such as:
52
+
53
+- `myapp://auth/callback`
54
+
55
+Do not copy confidential secrets into the app.
56
+
57
+### 3. Replace app-side login orchestration
58
+
59
+Replace custom login glue with:
60
+
61
+- `SmbCloudWebAuth`
62
+- `SmbCloudCredentialsManager`
63
+- `SmbCloudUserInfoClient` if you want standalone profile fetches
64
+
65
+### 4. Move local session storage to Keychain
66
+
67
+Persist `SmbCloudSession` with `SmbCloudCredentialsManager` instead of keeping access tokens in plaintext storage.
68
+
69
+### 5. Update your UX copy
70
+
71
+Hosted auth changes the user experience from a native email/password form to a system browser-backed sign-in flow.
72
+Prepare copy, onboarding, and support docs accordingly.
73
+
74
+## What changes in app code
75
+
76
+### Before
77
+
78
+- app owns email/password UI
79
+- app talks to your backend for auth
80
+- backend owns smbCloud Auth credentials
81
+
82
+### After
83
+
84
+- app launches hosted auth via `SmbCloudWebAuth.login(...)`
85
+- SDK handles PKCE and callback validation
86
+- app stores `SmbCloudSession`
87
+- app fetches user info with the access token
88
+
89
+## Security benefits
90
+
91
+- no confidential smbCloud Auth secret inside the shipped app
92
+- no embedded webview login flow
93
+- PKCE protects the public-client authorization code flow
94
+- system browser sessions align with Apple platform expectations
95
+
96
+## What does not change
97
+
98
+If you still need native forms, a BFF/proxy remains the correct architecture.
99
+This SDK does not try to replace secure server-side confidential flows for those cases.
100
+
101
+## Recommended rollout
102
+
103
+1. introduce hosted auth for new sign-ins
104
+2. keep proxy-based flows for legacy/native-form users if necessary
105
+3. migrate session restoration to `SmbCloudCredentialsManager`
106
+4. retire app-specific auth glue once the hosted path is the default
107
+
108
+## Related docs
109
+
110
+- `Docs/QuickStart-iOS.md`
111
+- `Docs/QuickStart-macOS.md`
112
+- `SECURITY.md`
Docs/QuickStart-iOS.md
new
+165
@@ -0,0 +1,165 @@
1
+# Quick Start — iOS
2
+
3
+This guide shows the hosted smbCloud Auth flow for an iPhone or iPad app using:
4
+
5
+- `SmbCloudWebAuth`
6
+- `SmbCloudCredentialsManager`
7
+- `SmbCloudUserInfoClient`
8
+
9
+## 1. Add the package
10
+
11
+Add `https://github.com/smbcloudXYZ/smbcloud-auth-swift` in Xcode or Swift Package Manager.
12
+
13
+## 2. Register a callback URL scheme
14
+
15
+Your app needs a custom callback URL such as:
16
+
17
+- `myapp://auth/callback`
18
+
19
+Add the URL scheme in your app target settings so iOS can route the hosted auth callback back into the app.
20
+
21
+## 3. Create a small auth store
22
+
23
+Use one public client ID from smbCloud Auth.
24
+Do not embed an `app_secret` in the app.
25
+
26
+```swift
27
+import SmbCloudAuth
28
+import SwiftUI
29
+
30
+@MainActor
31
+final class AuthenticationStore: ObservableObject {
32
+ @Published private(set) var session: SmbCloudSession?
33
+ @Published private(set) var email: String?
34
+ @Published private(set) var errorMessage: String?
35
+ @Published private(set) var isLoading = false
36
+
37
+ private let webAuth = try! SmbCloudWebAuth(
38
+ domain: "api.smbcloud.xyz",
39
+ clientId: "YOUR_PUBLIC_CLIENT_ID",
40
+ redirectURL: URL(string: "myapp://auth/callback")!
41
+ )
42
+ private let credentialsManager = SmbCloudCredentialsManager(
43
+ service: "com.example.myapp.smbcloud-auth"
44
+ )
45
+
46
+ func restoreSession() {
47
+ do {
48
+ session = try credentialsManager.currentValidSession()
49
+ } catch {
50
+ errorMessage = error.localizedDescription
51
+ }
52
+ }
53
+
54
+ func signIn(anchor: @escaping () -> SmbCloudPresentationAnchor) async {
55
+ isLoading = true
56
+ errorMessage = nil
57
+ defer { isLoading = false }
58
+
59
+ do {
60
+ let session = try await webAuth.login(
61
+ presentationAnchorProvider: anchor,
62
+ credentialsManager: credentialsManager
63
+ )
64
+ self.session = session
65
+
66
+ let user = try await webAuth.userInfo(session: session)
67
+ email = user.email
68
+ } catch {
69
+ errorMessage = error.localizedDescription
70
+ }
71
+ }
72
+
73
+ func signOut() {
74
+ do {
75
+ try webAuth.clearSession(credentialsManager: credentialsManager)
76
+ session = nil
77
+ email = nil
78
+ errorMessage = nil
79
+ } catch {
80
+ errorMessage = error.localizedDescription
81
+ }
82
+ }
83
+}
84
+```
85
+
86
+## 4. Provide a presentation anchor
87
+
88
+For SwiftUI on iOS, the simplest approach is to return the current key window.
89
+
90
+```swift
91
+import UIKit
92
+
93
+func currentPresentationAnchor() -> UIWindow {
94
+ UIApplication.shared.connectedScenes
95
+ .compactMap { $0 as? UIWindowScene }
96
+ .flatMap(\.windows)
97
+ .first { $0.isKeyWindow } ?? UIWindow()
98
+}
99
+```
100
+
101
+## 5. Build a simple screen
102
+
103
+```swift
104
+import SwiftUI
105
+
106
+struct ContentView: View {
107
+ @StateObject private var authStore = AuthenticationStore()
108
+
109
+ var body: some View {
110
+ VStack(spacing: 16) {
111
+ if let session = authStore.session {
112
+ Text("Signed in")
113
+ Text(session.accessToken)
114
+ .font(.footnote.monospaced())
115
+ .lineLimit(2)
116
+ if let email = authStore.email {
117
+ Text(email)
118
+ }
119
+ Button("Sign Out") {
120
+ authStore.signOut()
121
+ }
122
+ } else {
123
+ Button("Continue with smbCloud") {
124
+ Task {
125
+ await authStore.signIn {
126
+ currentPresentationAnchor()
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ if let errorMessage = authStore.errorMessage {
133
+ Text(errorMessage)
134
+ .foregroundStyle(.red)
135
+ .multilineTextAlignment(.center)
136
+ }
137
+ }
138
+ .padding()
139
+ .task {
140
+ authStore.restoreSession()
141
+ }
142
+ }
143
+}
144
+```
145
+
146
+## 6. What happens during login
147
+
148
+1. `SmbCloudWebAuth` creates a PKCE authorization request.
149
+2. `ASWebAuthenticationSession` opens the hosted smbCloud login page.
150
+3. smbCloud Auth redirects to your app callback URL.
151
+4. The SDK validates `state`, exchanges the authorization code, and returns `SmbCloudSession`.
152
+5. `SmbCloudCredentialsManager` stores the session in Keychain.
153
+
154
+## Notes
155
+
156
+- Prefer an explicit Keychain `service` value per app.
157
+- `logout()` currently clears local stored credentials. It does not revoke tokens remotely.
158
+- Hosted web login requires `ASWebAuthenticationSession`, so use iOS 16+.
159
+- If your product needs native email/password forms, send those credentials to your backend or BFF instead of embedding confidential credentials in the app.
160
+
161
+## Related docs
162
+
163
+- `Docs/QuickStart-macOS.md`
164
+- `Docs/Migration-From-Proxy-Native-Forms.md`
165
+- `SECURITY.md`
Docs/QuickStart-macOS.md
new
+132
@@ -0,0 +1,132 @@
1
+# Quick Start — macOS
2
+
3
+This guide shows hosted smbCloud Auth in a macOS app using `SmbCloudWebAuth`.
4
+
5
+## 1. Add the package
6
+
7
+Add `https://github.com/smbcloudXYZ/smbcloud-auth-swift` to your macOS app.
8
+
9
+## 2. Configure a callback URL
10
+
11
+Use a custom callback URL such as:
12
+
13
+- `mymacapp://auth/callback`
14
+
15
+Register that URL scheme for your app so the hosted auth callback returns to the application.
16
+
17
+## 3. Build a small store
18
+
19
+```swift
20
+import AppKit
21
+import SmbCloudAuth
22
+import SwiftUI
23
+
24
+@MainActor
25
+final class AuthenticationStore: ObservableObject {
26
+ @Published private(set) var session: SmbCloudSession?
27
+ @Published private(set) var email: String?
28
+ @Published private(set) var errorMessage: String?
29
+
30
+ private let webAuth = try! SmbCloudWebAuth(
31
+ domain: "api.smbcloud.xyz",
32
+ clientId: "YOUR_PUBLIC_CLIENT_ID",
33
+ redirectURL: URL(string: "mymacapp://auth/callback")!
34
+ )
35
+ private let credentialsManager = SmbCloudCredentialsManager(
36
+ service: "com.example.mymacapp.smbcloud-auth"
37
+ )
38
+
39
+ func restoreSession() {
40
+ do {
41
+ session = try credentialsManager.currentValidSession()
42
+ } catch {
43
+ errorMessage = error.localizedDescription
44
+ }
45
+ }
46
+
47
+ func signIn() async {
48
+ do {
49
+ let session = try await webAuth.login(
50
+ presentationAnchorProvider: {
51
+ NSApplication.shared.keyWindow ?? NSWindow()
52
+ },
53
+ credentialsManager: credentialsManager
54
+ )
55
+ self.session = session
56
+ let user = try await webAuth.userInfo(session: session)
57
+ email = user.email
58
+ } catch {
59
+ errorMessage = error.localizedDescription
60
+ }
61
+ }
62
+
63
+ func signOut() {
64
+ do {
65
+ try webAuth.clearSession(credentialsManager: credentialsManager)
66
+ session = nil
67
+ email = nil
68
+ } catch {
69
+ errorMessage = error.localizedDescription
70
+ }
71
+ }
72
+}
73
+```
74
+
75
+## 4. Use it from SwiftUI
76
+
77
+```swift
78
+import SwiftUI
79
+
80
+struct ContentView: View {
81
+ @StateObject private var authStore = AuthenticationStore()
82
+
83
+ var body: some View {
84
+ VStack(spacing: 12) {
85
+ if let email = authStore.email {
86
+ Text("Signed in as \(email)")
87
+ } else if authStore.session != nil {
88
+ Text("Signed in")
89
+ } else {
90
+ Text("Signed out")
91
+ }
92
+
93
+ HStack {
94
+ Button("Sign In") {
95
+ Task {
96
+ await authStore.signIn()
97
+ }
98
+ }
99
+
100
+ Button("Sign Out") {
101
+ authStore.signOut()
102
+ }
103
+ .disabled(authStore.session == nil)
104
+ }
105
+
106
+ if let errorMessage = authStore.errorMessage {
107
+ Text(errorMessage)
108
+ .foregroundStyle(.red)
109
+ .multilineTextAlignment(.center)
110
+ }
111
+ }
112
+ .padding()
113
+ .frame(width: 360)
114
+ .task {
115
+ authStore.restoreSession()
116
+ }
117
+ }
118
+}
119
+```
120
+
121
+## Notes
122
+
123
+- Supply a real public client ID from smbCloud Auth.
124
+- Do not ship an `app_secret` in the macOS app.
125
+- `clearSession()` and `logout()` currently clear local stored credentials only.
126
+- If you need native email/password forms, keep confidential auth credentials on your backend.
127
+
128
+## Related docs
129
+
130
+- `Docs/QuickStart-iOS.md`
131
+- `Docs/Migration-From-Proxy-Native-Forms.md`
132
+- `SECURITY.md`
Docs/Release.md
new
+77
@@ -0,0 +1,77 @@
1
+# Release process
2
+
3
+`smbcloud-auth-swift` now ships as a source-based Swift Package.
4
+
5
+That means the public `SmbCloudAuth` product does not need a remote XCFramework artifact to be published.
6
+The package is consumable directly from the Git tag.
7
+
8
+## What gets released
9
+
10
+Public product:
11
+
12
+- `SmbCloudAuth`
13
+
14
+Optional local development product:
15
+
16
+- `SmbCloudAuthFFI`
17
+
18
+`SmbCloudAuthFFI` is for sibling-repo development when you build the local Rust + UniFFI artifacts yourself.
19
+It is not required for normal package consumers.
20
+
21
+## CI validation
22
+
23
+GitHub Actions validates release candidates with:
24
+
25
+- `make verify`
26
+- `make verify-apple-destination PLATFORM=ios`
27
+- `make verify-apple-destination PLATFORM=tvos`
28
+- `make verify-apple-destination PLATFORM=visionos`
29
+
30
+That covers:
31
+
32
+- the main Swift package build
33
+- the main Swift package tests
34
+- the packaged macOS example app build
35
+- generic Apple destination builds for iOS, tvOS, and visionOS
36
+
37
+## Tagging a release
38
+
39
+Create and push a semver tag:
40
+
41
+```/dev/null/bash.txt#L1-L2
42
+git tag v1.0.0
43
+git push origin v1.0.0
44
+```
45
+
46
+The release workflow will:
47
+
48
+1. check out the tagged revision
49
+2. run `make verify`
50
+3. create a GitHub Release for the tag
51
+
52
+## Manual release dispatch
53
+
54
+You can also run the `Release` workflow manually and provide a tag such as `v1.0.0`.
55
+
56
+## Local pre-release verification
57
+
58
+Before tagging, run:
59
+
60
+```/dev/null/bash.txt#L1-L2
61
+make verify
62
+make verify-apple-destinations
63
+```
64
+
65
+## Local optional FFI development
66
+
67
+If you are working alongside `smbcloud-cli`, you can generate the optional local UniFFI/XCFramework layer with:
68
+
69
+```/dev/null/bash.txt#L1-L4
70
+make ios
71
+make macos
72
+make tvos
73
+make visionos
74
+```
75
+
76
+That enables the local `SmbCloudAuthFFI` product for development.
77
+The public release flow does not depend on those artifacts.
Examples/HostedLoginExample/Package.swift
new
+21
@@ -0,0 +1,21 @@
1
+// swift-tools-version:6.2
2
+import PackageDescription
3
+
4
+let package = Package(
5
+ name: "HostedLoginExample",
6
+ platforms: [
7
+ .macOS(.v14)
8
+ ],
9
+ dependencies: [
10
+ .package(path: "../..")
11
+ ],
12
+ targets: [
13
+ .executableTarget(
14
+ name: "HostedLoginExample",
15
+ dependencies: [
16
+ .product(name: "SmbCloudAuth", package: "smbcloud-auth-swift")
17
+ ],
18
+ path: "Sources/HostedLoginExample"
19
+ )
20
+ ]
21
+)
Examples/HostedLoginExample/README.md
new
+34
@@ -0,0 +1,34 @@
1
+# Hosted Login Example
2
+
3
+This folder contains a small SwiftUI-oriented example of the v1 MVP API surface:
4
+
5
+- `SmbCloudWebAuth`
6
+- `SmbCloudSession`
7
+- `SmbCloudCredentialsManager`
8
+- `SmbCloudUserInfoClient`
9
+
10
+## Files
11
+
12
+- `Package.swift` — standalone example package manifest
13
+- `Sources/HostedLoginExample/HostedLoginExampleApp.swift` — SwiftUI app entry point
14
+- `Sources/HostedLoginExample/AuthenticationStore.swift` — minimal observable auth state container
15
+- `Sources/HostedLoginExample/HostedLoginView.swift` — simple signed-in / signed-out SwiftUI view
16
+
17
+## Configuration you must replace
18
+
19
+Before using the example, replace:
20
+
21
+- `YOUR_PUBLIC_CLIENT_ID`
22
+- `hostedloginexample://auth/callback`
23
+- Keychain service name
24
+
25
+## Build locally
26
+
27
+```/dev/null/bash.txt#L1-L1
28
+swift build --package-path Examples/HostedLoginExample
29
+```
30
+
31
+## Security note
32
+
33
+Use a public client ID only.
34
+Do not embed an smbCloud Auth `app_secret` in a shipped Apple app.
Examples/HostedLoginExample/Sources/HostedLoginExample/AuthenticationStore.swift
new
+71
@@ -0,0 +1,71 @@
1
+import SmbCloudAuth
2
+import SwiftUI
3
+
4
+@MainActor
5
+final class AuthenticationStore: ObservableObject {
6
+ @Published private(set) var session: SmbCloudSession?
7
+ @Published private(set) var userEmail: String?
8
+ @Published private(set) var errorMessage: String?
9
+ @Published private(set) var isLoading = false
10
+
11
+ private let webAuth: SmbCloudWebAuth?
12
+ private let credentialsManager = SmbCloudCredentialsManager(
13
+ service: "com.example.hosted-login-example.smbcloud-auth"
14
+ )
15
+
16
+ init() {
17
+ do {
18
+ self.webAuth = try SmbCloudWebAuth(
19
+ domain: "api.smbcloud.xyz",
20
+ clientId: "YOUR_PUBLIC_CLIENT_ID",
21
+ redirectURL: URL(string: "hostedloginexample://auth/callback")!
22
+ )
23
+ } catch {
24
+ self.webAuth = nil
25
+ self.errorMessage = error.localizedDescription
26
+ }
27
+ }
28
+
29
+ func restoreSession() {
30
+ do {
31
+ session = try credentialsManager.currentValidSession()
32
+ } catch {
33
+ errorMessage = error.localizedDescription
34
+ }
35
+ }
36
+
37
+ func signIn(anchorProvider: @escaping () -> SmbCloudPresentationAnchor) async {
38
+ guard let webAuth else {
39
+ errorMessage = errorMessage ?? "The smbCloud web auth client could not be created."
40
+ return
41
+ }
42
+
43
+ isLoading = true
44
+ errorMessage = nil
45
+ defer { isLoading = false }
46
+
47
+ do {
48
+ let session = try await webAuth.login(
49
+ presentationAnchorProvider: anchorProvider,
50
+ credentialsManager: credentialsManager
51
+ )
52
+ self.session = session
53
+
54
+ let user = try await webAuth.userInfo(session: session)
55
+ userEmail = user.email
56
+ } catch {
57
+ errorMessage = error.localizedDescription
58
+ }
59
+ }
60
+
61
+ func signOut() {
62
+ do {
63
+ try webAuth?.clearSession(credentialsManager: credentialsManager)
64
+ session = nil
65
+ userEmail = nil
66
+ errorMessage = nil
67
+ } catch {
68
+ errorMessage = error.localizedDescription
69
+ }
70
+ }
71
+}
Examples/HostedLoginExample/Sources/HostedLoginExample/HostedLoginExampleApp.swift
new
+14
@@ -0,0 +1,14 @@
1
+import SwiftUI
2
+
3
+@main
4
+struct HostedLoginExampleApp: App {
5
+ var body: some Scene {
6
+ WindowGroup {
7
+ HostedLoginView(
8
+ presentationAnchorProvider: {
9
+ currentPresentationAnchor()
10
+ }
11
+ )
12
+ }
13
+ }
14
+}
Examples/HostedLoginExample/Sources/HostedLoginExample/HostedLoginView.swift
new
+64
@@ -0,0 +1,64 @@
1
+import AppKit
2
+import SmbCloudAuth
3
+import SwiftUI
4
+
5
+struct HostedLoginView: View {
6
+ @StateObject private var authenticationStore = AuthenticationStore()
7
+ let presentationAnchorProvider: () -> SmbCloudPresentationAnchor
8
+
9
+ var body: some View {
10
+ VStack(spacing: 16) {
11
+ Text("smbCloud Hosted Login Example")
12
+ .font(.title2)
13
+
14
+ if let userEmail = authenticationStore.userEmail {
15
+ Text(userEmail)
16
+ .font(.headline)
17
+ } else if authenticationStore.session != nil {
18
+ Text("Signed in")
19
+ .font(.headline)
20
+ } else {
21
+ Text("Signed out")
22
+ .font(.headline)
23
+ }
24
+
25
+ if let session = authenticationStore.session {
26
+ Text(session.accessToken)
27
+ .font(.footnote.monospaced())
28
+ .lineLimit(2)
29
+ }
30
+
31
+ HStack {
32
+ Button(authenticationStore.isLoading ? "Loading…" : "Continue with smbCloud") {
33
+ Task {
34
+ await authenticationStore.signIn(
35
+ anchorProvider: presentationAnchorProvider
36
+ )
37
+ }
38
+ }
39
+ .disabled(authenticationStore.isLoading)
40
+
41
+ Button("Sign Out") {
42
+ authenticationStore.signOut()
43
+ }
44
+ .disabled(authenticationStore.session == nil)
45
+ }
46
+
47
+ if let errorMessage = authenticationStore.errorMessage {
48
+ Text(errorMessage)
49
+ .foregroundStyle(.red)
50
+ .multilineTextAlignment(.center)
51
+ }
52
+ }
53
+ .padding()
54
+ .frame(width: 460)
55
+ .task {
56
+ authenticationStore.restoreSession()
57
+ }
58
+ }
59
+}
60
+
61
+@MainActor
62
+func currentPresentationAnchor() -> NSWindow {
63
+ NSApplication.shared.keyWindow ?? NSWindow()
64
+}
Makefile
+55
-9
@@ -21,26 +21,35 @@ FRAMEWORK_STAGING_DIR := $(LOCAL_DIR)/frameworks
21
FRAMEWORK_DIR := $(SWIFT_REPO)/smbcloud_authFFI.xcframework
22
SLICE_FRAMEWORK_NAME := smbcloud_authFFI.framework
23
FRAMEWORK_BINARY_NAME := smbcloud_authFFI
24
-SWIFT_GLUE := $(SWIFT_REPO)/Sources/SmbCloudAuth/smbcloud_auth.swift
24
+SWIFT_GLUE := $(SWIFT_REPO)/Sources/SmbCloudAuthFFI/smbcloud_auth.swift
25
+LOCAL_FFI_MARKER := $(LOCAL_DIR)/use-local-ffi
26
27
LIB_NAME := libsmbcloud_auth.a
28
SUPPORTED_PLATFORMS := ios macos tvos visionos
29
29
-.PHONY: help platform ios macos tvos visionos validate build-bindgen prepare generate-swift stage-framework clean
30
+.PHONY: help platform ios macos tvos visionos validate build-bindgen prepare generate-swift stage-framework activate-local-ffi deactivate-local-ffi clean verify verify-example verify-apple-destinations verify-apple-destination
31
32
help:
33
@printf '%s\n' \
34
'SmbCloudAuth Swift — local Apple-platform builds' \
35
'' \
36
'Usage:' \
36
- ' make ios Build smbcloud_authFFI.xcframework for iOS device + simulator' \
37
- ' make macos Build smbcloud_authFFI.xcframework for macOS (Apple silicon)' \
38
- ' make tvos Build smbcloud_authFFI.xcframework for tvOS device + simulator' \
39
- ' make visionos Build smbcloud_authFFI.xcframework for visionOS device + simulator' \
37
+ ' make ios Build smbcloud_authFFI.xcframework for iOS device + simulator' \
38
+ ' make macos Build smbcloud_authFFI.xcframework for macOS (Apple silicon)' \
39
+ ' make tvos Build smbcloud_authFFI.xcframework for tvOS device + simulator' \
40
+ ' make visionos Build smbcloud_authFFI.xcframework for visionOS device + simulator' \
41
+ ' make verify Build + test the main Swift package and build the example app package' \
42
+ ' make verify-example Build the packaged macOS example app' \
43
+ ' make verify-apple-destinations Build the public package for iOS, tvOS, and visionOS generic destinations' \
44
+ ' make deactivate-local-ffi Hide the optional local SmbCloudAuthFFI product' \
45
'' \
46
'Artifacts:' \
47
' smbcloud_authFFI.xcframework/' \
43
- ' Sources/SmbCloudAuth/smbcloud_auth.swift (regenerated from the local Rust build)' \
48
+ ' Sources/SmbCloudAuthFFI/smbcloud_auth.swift (regenerated from the local Rust build)' \
49
+ '' \
50
+ 'Notes:' \
51
+ ' The public SmbCloudAuth product is pure Swift and always available.' \
52
+ ' Local Rust/UniFFI builds enable the optional SmbCloudAuthFFI product.' \
53
'' \
54
'Optional overrides:' \
55
' CLI_REPO=/absolute/path/to/smbcloud-cli'
@@ -61,8 +70,8 @@ build-bindgen: validate
70
cargo $(RUST_STABLE) build --manifest-path "$(APPLE_CRATE_MANIFEST)" --features bindgen --bin uniffi-bindgen --release
71
72
prepare:
64
- @rm -rf "$(FRAMEWORK_DIR)" "$(GENERATED_DIR)" "$(HEADERS_DIR)" "$(FRAMEWORK_STAGING_DIR)"
65
- @mkdir -p "$(GENERATED_DIR)" "$(HEADERS_DIR)" "$(FRAMEWORK_STAGING_DIR)"
73
+ @rm -rf "$(FRAMEWORK_DIR)" "$(GENERATED_DIR)" "$(HEADERS_DIR)" "$(FRAMEWORK_STAGING_DIR)" "$(LOCAL_FFI_MARKER)"
74
+ @mkdir -p "$(GENERATED_DIR)" "$(HEADERS_DIR)" "$(FRAMEWORK_STAGING_DIR)" "$(SWIFT_REPO)/Sources/SmbCloudAuthFFI"
75
76
generate-swift:
77
@test -n "$(BINDGEN_INPUT)" || { echo "BINDGEN_INPUT is required"; exit 1; }
@@ -71,6 +80,15 @@ generate-swift:
80
cp "$(GENERATED_DIR)/smbcloud_authFFI.h" "$(HEADERS_DIR)/smbcloud_authFFI.h"
81
@echo "Updated $(SWIFT_GLUE)"
82
83
+activate-local-ffi:
84
+ @mkdir -p "$(LOCAL_DIR)"
85
+ @touch "$(LOCAL_FFI_MARKER)"
86
+ @echo "Enabled local SmbCloudAuthFFI product"
87
+
88
+deactivate-local-ffi:
89
+ @rm -f "$(LOCAL_FFI_MARKER)"
90
+ @echo "Disabled local SmbCloudAuthFFI product"
91
+
92
stage-framework:
93
@test -n "$(LIB_INPUT)" || { echo "LIB_INPUT is required"; exit 1; }
94
@test -n "$(FRAMEWORK_OUTPUT)" || { echo "FRAMEWORK_OUTPUT is required"; exit 1; }
@@ -116,6 +134,7 @@ ios: build-bindgen prepare
134
-framework "$(FRAMEWORK_STAGING_DIR)/ios/$(SLICE_FRAMEWORK_NAME)" \
135
-framework "$(FRAMEWORK_STAGING_DIR)/ios-sim/$(SLICE_FRAMEWORK_NAME)" \
136
-output "$(FRAMEWORK_DIR)"
137
+ $(MAKE) activate-local-ffi
138
@echo "Built $(FRAMEWORK_DIR) for iOS"
139
140
macos: build-bindgen prepare
@@ -125,6 +144,7 @@ macos: build-bindgen prepare
144
xcodebuild -create-xcframework \
145
-framework "$(FRAMEWORK_STAGING_DIR)/macos/$(SLICE_FRAMEWORK_NAME)" \
146
-output "$(FRAMEWORK_DIR)"
147
+ $(MAKE) activate-local-ffi
148
@echo "Built $(FRAMEWORK_DIR) for macOS"
149
150
tvos: build-bindgen prepare
@@ -137,6 +157,7 @@ tvos: build-bindgen prepare
157
-framework "$(FRAMEWORK_STAGING_DIR)/tvos/$(SLICE_FRAMEWORK_NAME)" \
158
-framework "$(FRAMEWORK_STAGING_DIR)/tvos-sim/$(SLICE_FRAMEWORK_NAME)" \
159
-output "$(FRAMEWORK_DIR)"
160
+ $(MAKE) activate-local-ffi
161
@echo "Built $(FRAMEWORK_DIR) for tvOS"
162
163
visionos: build-bindgen prepare
@@ -149,8 +170,33 @@ visionos: build-bindgen prepare
170
-framework "$(FRAMEWORK_STAGING_DIR)/visionos/$(SLICE_FRAMEWORK_NAME)" \
171
-framework "$(FRAMEWORK_STAGING_DIR)/visionos-sim/$(SLICE_FRAMEWORK_NAME)" \
172
-output "$(FRAMEWORK_DIR)"
173
+ $(MAKE) activate-local-ffi
174
@echo "Built $(FRAMEWORK_DIR) for visionOS"
175
176
+verify:
177
+ swift build
178
+ swift test
179
+ $(MAKE) verify-example
180
+
181
+verify-example:
182
+ swift build --package-path Examples/HostedLoginExample
183
+
184
+verify-apple-destinations:
185
+ $(MAKE) verify-apple-destination PLATFORM=ios
186
+ $(MAKE) verify-apple-destination PLATFORM=tvos
187
+ $(MAKE) verify-apple-destination PLATFORM=visionos
188
+
189
+verify-apple-destination:
190
+ @test -n "$(PLATFORM)" || { echo "Set PLATFORM=ios|tvos|visionos"; exit 1; }
191
+ @case "$(PLATFORM)" in \
192
+ ios) destination="generic/platform=iOS"; derived_data="$(LOCAL_DIR)/verify-ios" ;; \
193
+ tvos) destination="generic/platform=tvOS"; derived_data="$(LOCAL_DIR)/verify-tvos" ;; \
194
+ visionos) destination="generic/platform=visionOS"; derived_data="$(LOCAL_DIR)/verify-visionos" ;; \
195
+ *) echo "Unsupported PLATFORM='$(PLATFORM)'. Expected one of: ios tvos visionos"; exit 1 ;; \
196
+ esac; \
197
+ xcodebuild -quiet -scheme SmbCloudAuth -destination "$${destination}" -derivedDataPath "$${derived_data}" build
198
+
199
clean:
200
rm -rf "$(FRAMEWORK_DIR)" "$(LOCAL_DIR)"
201
+ rm -f "$(SWIFT_GLUE)"
202
@echo "Removed local SmbCloudAuth Swift build artifacts"
Package.swift
+45
-23
@@ -3,53 +3,75 @@ import Foundation
3
import PackageDescription
4
5
let packageRoot = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
6
+let fileManager = FileManager.default
7
+
8
let localFrameworkPath = "smbcloud_authFFI.xcframework"
9
let localFrameworkAbsolutePath = packageRoot.appendingPathComponent(localFrameworkPath).path
8
-let fileManager = FileManager.default
9
-let localFrameworkEntries =
10
- (try? fileManager.contentsOfDirectory(atPath: localFrameworkAbsolutePath)) ?? []
11
-let hasUsableLocalFramework =
12
- fileManager.fileExists(atPath: localFrameworkAbsolutePath)
13
- && localFrameworkEntries.contains(where: { $0.hasPrefix("macos") })
14
-
15
-let ffiDependencies: [Target.Dependency] =
16
- hasUsableLocalFramework ? [.target(name: "smbcloud_authFFI")] : []
17
-let excludedSourceFiles = hasUsableLocalFramework ? [] : ["smbcloud_auth.swift"]
18
-let swiftSettings: [SwiftSetting] =
19
- hasUsableLocalFramework ? [.define("SMBCLOUD_AUTH_FFI_AVAILABLE")] : []
10
+let localFfiMarkerPath = packageRoot.appendingPathComponent(".local/use-local-ffi").path
11
+let ffiSwiftShimPath = packageRoot.appendingPathComponent(
12
+ "Sources/SmbCloudAuthFFI/smbcloud_auth.swift"
13
+).path
14
+
15
+let shouldEnableLocalFfi =
16
+ fileManager.fileExists(atPath: localFfiMarkerPath)
17
+ && fileManager.fileExists(atPath: localFrameworkAbsolutePath)
18
+ && fileManager.fileExists(atPath: ffiSwiftShimPath)
19
20
let targets: [Target] = {
22
- var targets: [Target] = []
21
+ var targets: [Target] = [
22
+ .target(
23
+ name: "SmbCloudAuth",
24
+ path: "Sources/SmbCloudAuth"
25
+ )
26
+ ]
27
24
- if hasUsableLocalFramework {
28
+ if shouldEnableLocalFfi {
29
targets.append(
30
.binaryTarget(
31
name: "smbcloud_authFFI",
32
path: localFrameworkPath
33
)
34
)
35
+
36
+ targets.append(
37
+ .target(
38
+ name: "SmbCloudAuthFFI",
39
+ dependencies: [.target(name: "smbcloud_authFFI")],
40
+ path: "Sources/SmbCloudAuthFFI"
41
+ )
42
+ )
43
}
44
45
targets.append(
34
- .target(
35
- name: "SmbCloudAuth",
36
- dependencies: ffiDependencies,
37
- path: "Sources/SmbCloudAuth",
38
- exclude: excludedSourceFiles,
39
- swiftSettings: swiftSettings
46
+ .testTarget(
47
+ name: "SmbCloudAuthTests",
48
+ dependencies: ["SmbCloudAuth"],
49
+ path: "Tests/SmbCloudAuthTests"
50
)
51
)
52
53
return targets
54
}()
55
56
+let products: [Product] = {
57
+ var products: [Product] = [
58
+ .library(name: "SmbCloudAuth", targets: ["SmbCloudAuth"])
59
+ ]
60
+
61
+ if shouldEnableLocalFfi {
62
+ products.append(
63
+ .library(name: "SmbCloudAuthFFI", targets: ["SmbCloudAuthFFI"])
64
+ )
65
+ }
66
+
67
+ return products
68
+}()
69
+
70
let package = Package(
71
name: "SmbCloudAuth",
72
platforms: [
73
.iOS(.v16), .macOS(.v14), .tvOS(.v16), .visionOS(.v1),
74
],
51
- products: [
52
- .library(name: "SmbCloudAuth", targets: ["SmbCloudAuth"])
53
- ],
75
+ products: products,
76
targets: targets
77
)
README.md
+27
-4
@@ -31,7 +31,7 @@
31
32
## Current status
33
34
-This package is still a **developer preview**, but the v1 public-client MVP surface now exists.
34
+This package is still a **developer preview**, but the v1 public-client MVP surface, docs, tests, packaged example, and release validation workflows now exist.
35
36
Today it provides:
37
@@ -46,7 +46,8 @@ Today it provides:
46
- userinfo lookup
47
- local session clearing/logout helper
48
49
-The low-level generated UniFFI layer is still part of the repo for compatible XCFramework-based builds, but the package now has a first-class public Swift API for the main Apple login flow.
49
+The public `SmbCloudAuth` product is now a stable pure-Swift surface for hosted auth on Apple platforms.
50
+The low-level generated UniFFI layer remains available as an optional local `SmbCloudAuthFFI` product when you build the Rust/XCFramework side during sibling-repo development.
51
52
## Security model
53
@@ -222,7 +223,7 @@ Repositories/
223
└── smbcloud-auth-swift/
224
```
225
225
-Build the local XCFramework from the Rust source:
226
+Build the optional local XCFramework + UniFFI shim from the Rust source:
227
228
```/dev/null/bash.txt#L1-L4
229
make ios
@@ -231,7 +232,29 @@ make tvos
232
make visionos
233
```
234
234
-This cross-compiles the Rust Apple bindings, regenerates `Sources/SmbCloudAuth/smbcloud_auth.swift`, and writes the local XCFramework artifact used by SwiftPM.
235
+This cross-compiles the Rust Apple bindings, regenerates `Sources/SmbCloudAuthFFI/smbcloud_auth.swift`, and writes the local XCFramework artifact used by the optional local `SmbCloudAuthFFI` product.
236
+
237
+The public `SmbCloudAuth` package can still build without these local Rust artifacts.
238
+
239
+## Guides and examples
240
+
241
+- [Quick Start — iOS](Docs/QuickStart-iOS.md)
242
+- [Quick Start — macOS](Docs/QuickStart-macOS.md)
243
+- [Migration from proxy/native-form auth](Docs/Migration-From-Proxy-Native-Forms.md)
244
+- [Hosted login example](Examples/HostedLoginExample/README.md)
245
+- [Release process](Docs/Release.md)
246
+
247
+You can build the packaged macOS example app locally with:
248
+
249
+```/dev/null/bash.txt#L1-L1
250
+swift build --package-path Examples/HostedLoginExample
251
+```
252
+
253
+And you can validate generic Apple destination builds locally with:
254
+
255
+```/dev/null/bash.txt#L1-L1
256
+make verify-apple-destinations
257
+```
258
259
## Roadmap
260
ROADMAP.md
+11
-6
@@ -89,7 +89,9 @@ What is missing:
89
90
Status:
91
92
-- core API surface implemented in developer preview
92
+- v1 MVP complete in developer preview form
93
+- core API surface implemented
94
+- docs, tests, packaged example app target, CI, and release workflow implemented
95
96
Goal:
97
@@ -112,15 +114,18 @@ Ship the first sellable public-client SDK for Apple apps.
114
- local session persistence
115
- local logout/session clearing helper
116
115
-### Remaining polish before stable v1
117
+### Completed MVP docs and examples
118
119
- Quick Start for iOS
120
- Quick Start for macOS
119
-- example app
121
+- hosted login example
122
- migration guide for teams coming from proxy/native-form auth
121
-- release artifact parity across Apple platforms
122
-- logout / revocation story beyond local session clearing
123
-- security guidance refresh for the final API shape
123
+- security guidance refresh for the current API shape
124
+
125
+### Stable-v1 follow-up work
126
+
127
+- release artifact parity across Apple platforms for the optional local `SmbCloudAuthFFI` product
128
+- logout / revocation story beyond local session clearing if smbCloud Auth exposes a public-client-safe revocation flow
129
130
### MVP example shape
131
SECURITY.md
+16
@@ -23,12 +23,14 @@ Use:
23
- hosted auth
24
- system browser sessions
25
- OIDC Authorization Code + PKCE
26
+- Keychain-backed local session storage
27
28
On Apple platforms this generally means:
29
30
- `ASWebAuthenticationSession`
31
- a custom URL scheme or universal link callback
32
- token exchange after the callback returns
33
+- persisting `SmbCloudSession` with `SmbCloudCredentialsManager`
34
35
## Do not use embedded browser auth
36
@@ -64,6 +66,18 @@ not:
66
67
- confidential-client secret-based login inside the shipped app
68
69
+## Local credential storage
70
+
71
+If you persist sessions locally, prefer Keychain storage.
72
+
73
+The SDK provides `SmbCloudCredentialsManager` for that purpose.
74
+Use an explicit per-app `service` name so multiple apps or test targets do not accidentally share the same stored session namespace.
75
+
76
+## Logout semantics
77
+
78
+The current SDK `logout()` helper is a local session-clearing helper.
79
+It clears persisted credentials through `SmbCloudCredentialsManager`, but it does not yet perform remote token revocation.
80
+
81
## Operational guidance
82
83
For development:
@@ -71,9 +85,11 @@ For development:
85
- use separate dev credentials
86
- prefer local-only configuration
87
- never commit secrets into the repo
88
+- use a development-specific Keychain service name if you store sessions locally
89
90
For production:
91
92
- use public-client identifiers only in the app
93
- keep confidential secrets in server-side systems
94
- audit example code to ensure no public sample embeds secrets
95
+- review callback URL handling and Keychain service naming before shipping
Sources/SmbCloudAuth/SmbCloudFallbackModels.swift
+23
-25
@@ -1,30 +1,28 @@
1
import Foundation
2
3
-#if !SMBCLOUD_AUTH_FFI_AVAILABLE
4
- public enum SmbCloudEnvironment: Sendable {
5
- case dev
6
- case production
7
- }
3
+public enum SmbCloudEnvironment: Sendable {
4
+ case dev
5
+ case production
6
+}
7
9
- public struct SmbCloudUserInfo: Codable, Equatable, Hashable, Sendable {
10
- public let sub: String
11
- public let email: String?
12
- public let emailVerified: Bool?
13
- public let tenantId: UInt64?
14
- public let tenantSlug: String?
8
+public struct SmbCloudUserInfo: Codable, Equatable, Hashable, Sendable {
9
+ public let sub: String
10
+ public let email: String?
11
+ public let emailVerified: Bool?
12
+ public let tenantId: UInt64?
13
+ public let tenantSlug: String?
14
16
- public init(
17
- sub: String,
18
- email: String? = nil,
19
- emailVerified: Bool? = nil,
20
- tenantId: UInt64? = nil,
21
- tenantSlug: String? = nil
22
- ) {
23
- self.sub = sub
24
- self.email = email
25
- self.emailVerified = emailVerified
26
- self.tenantId = tenantId
27
- self.tenantSlug = tenantSlug
28
- }
15
+ public init(
16
+ sub: String,
17
+ email: String? = nil,
18
+ emailVerified: Bool? = nil,
19
+ tenantId: UInt64? = nil,
20
+ tenantSlug: String? = nil
21
+ ) {
22
+ self.sub = sub
23
+ self.email = email
24
+ self.emailVerified = emailVerified
25
+ self.tenantId = tenantId
26
+ self.tenantSlug = tenantSlug
27
}
30
-#endif
28
+}
Sources/SmbCloudAuthFFI/smbcloud_auth.swift
renamed
Tests/SmbCloudAuthTests/SmbCloudAuthTests.swift
new
+81
@@ -0,0 +1,81 @@
1
+import Foundation
2
+import XCTest
3
+
4
+@testable import SmbCloudAuth
5
+
6
+final class SmbCloudAuthTests: XCTestCase {
7
+ func testSessionScopeJoinsScopesWithSpaces() {
8
+ let session = SmbCloudSession(
9
+ accessToken: "token",
10
+ scopes: ["openid", "profile", "email"]
11
+ )
12
+
13
+ XCTAssertEqual(session.scope, "openid profile email")
14
+ XCTAssertEqual(session.authorizationHeaderValue, "Bearer token")
15
+ }
16
+
17
+ func testSessionValidityReflectsExpiryDate() {
18
+ let futureSession = SmbCloudSession(
19
+ accessToken: "future-token",
20
+ expiresAt: Date().addingTimeInterval(300)
21
+ )
22
+ let pastSession = SmbCloudSession(
23
+ accessToken: "past-token",
24
+ expiresAt: Date().addingTimeInterval(-300)
25
+ )
26
+
27
+ XCTAssertTrue(futureSession.isValid())
28
+ XCTAssertFalse(futureSession.isExpired)
29
+ XCTAssertFalse(pastSession.isValid(leeway: 0))
30
+ XCTAssertTrue(pastSession.isExpired)
31
+ }
32
+
33
+ func testSessionCodableRoundTripPreservesValues() throws {
34
+ let originalSession = SmbCloudSession(
35
+ accessToken: "access-token",
36
+ refreshToken: "refresh-token",
37
+ idToken: "id-token",
38
+ tokenType: "Bearer",
39
+ expiresAt: Date(timeIntervalSince1970: 1_750_000_000),
40
+ scopes: ["openid", "profile", "email"]
41
+ )
42
+
43
+ let encodedSession = try JSONEncoder().encode(originalSession)
44
+ let decodedSession = try JSONDecoder().decode(SmbCloudSession.self, from: encodedSession)
45
+
46
+ XCTAssertEqual(decodedSession, originalSession)
47
+ }
48
+
49
+ func testCredentialsManagerUsesExplicitServiceWhenProvided() {
50
+ let manager = SmbCloudCredentialsManager(service: "xyz.smbcloud.tests", account: "tester")
51
+
52
+ XCTAssertEqual(manager.service, "xyz.smbcloud.tests")
53
+ XCTAssertEqual(manager.account, "tester")
54
+ }
55
+
56
+ func testInvalidUserInfoDomainThrowsInvalidBaseUrlError() {
57
+ XCTAssertThrowsError(try SmbCloudUserInfoClient(domain: " ")) { error in
58
+ guard case SmbCloudClientError.invalidBaseURL = error else {
59
+ return XCTFail("Expected invalidBaseURL error, got: \(error)")
60
+ }
61
+ }
62
+ }
63
+
64
+ func testInvalidWebAuthDomainThrowsInvalidBaseUrlError() {
65
+ XCTAssertThrowsError(
66
+ try SmbCloudWebAuth(
67
+ domain: " ",
68
+ clientId: "public-client-id",
69
+ redirectURL: URL(string: "myapp://auth/callback")!
70
+ )
71
+ ) { error in
72
+ guard case SmbCloudClientError.invalidBaseURL = error else {
73
+ return XCTFail("Expected invalidBaseURL error, got: \(error)")
74
+ }
75
+ }
76
+ }
77
+
78
+ func testDefaultScopesMatchOidcDefaults() {
79
+ XCTAssertEqual(SmbCloudWebAuth.defaultScopes, ["openid", "profile", "email"])
80
+ }
81
+}